From e7ea52524a519bda576b4fe8cfdd0a19f6998583 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 24 Feb 2024 11:17:15 +0000 Subject: [PATCH 1/8] WebAssembly Support WASI does not have thread spawning method yet, but the existing implementation blocks threads to wait async test cases synchronously. This commit introduced a new waiter method for running async test cases in single-threaded WASI environments, enabled by USE_SWIFT_CONCURRENCY_WAITER flag. With the new waiter, `XCTMain` is async runs the given test suites without blocking the thread by bypassing some synchronous public APIs like `XCTest.perform` and `XCTest.run`. This ignores those APIs even if they are overridden by user-defined subclasses, so it's not 100% compatible with the existing XCTest APIs. This is a trade-off to support async test execution in single-threaded environments, but it should be fine because the APIs are seldom overridden by user code. --- CMakeLists.txt | 17 +- Sources/XCTest/Private/DispatchShims.swift | 47 +++++ Sources/XCTest/Private/WaiterManager.swift | 3 + .../XCTNSNotificationExpectation.swift | 4 + .../XCTNSPredicateExpectation.swift | 3 + .../Public/Asynchronous/XCTWaiter.swift | 41 +++++ .../XCTestCase+Asynchronous.swift | 3 + Sources/XCTest/Public/XCAbstractTest.swift | 23 +++ Sources/XCTest/Public/XCTestCase.swift | 171 ++++++++++++++---- Sources/XCTest/Public/XCTestMain.swift | 95 ++++++++-- Sources/XCTest/Public/XCTestSuite.swift | 22 +++ cmake/modules/SwiftSupport.cmake | 2 + 12 files changed, 384 insertions(+), 47 deletions(-) create mode 100644 Sources/XCTest/Private/DispatchShims.swift diff --git a/CMakeLists.txt b/CMakeLists.txt index a386ef08e..6d027460c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,15 @@ project(XCTest LANGUAGES Swift) option(BUILD_SHARED_LIBS "Build shared libraries" ON) option(USE_FOUNDATION_FRAMEWORK "Use Foundation.framework on Darwin" NO) -if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin) +set(USE_SWIFT_CONCURRENCY_WAITER_default NO) + +if(CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) + set(USE_SWIFT_CONCURRENCY_WAITER_default ON) +endif() + +option(USE_SWIFT_CONCURRENCY_WAITER "Use Swift Concurrency-based waiter implementation" "${USE_SWIFT_CONCURRENCY_WAITER_default}") + +if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT USE_SWIFT_CONCURRENCY_WAITER) find_package(dispatch CONFIG REQUIRED) find_package(Foundation CONFIG REQUIRED) endif() @@ -30,6 +38,7 @@ add_library(XCTest Sources/XCTest/Private/WaiterManager.swift Sources/XCTest/Private/IgnoredErrors.swift Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift + Sources/XCTest/Private/DispatchShims.swift Sources/XCTest/Public/XCTestRun.swift Sources/XCTest/Public/XCTestMain.swift Sources/XCTest/Public/XCTestCase.swift @@ -49,6 +58,12 @@ add_library(XCTest Sources/XCTest/Public/Asynchronous/XCTWaiter.swift Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift) + +if(USE_SWIFT_CONCURRENCY_WAITER) + target_compile_definitions(XCTest PRIVATE + USE_SWIFT_CONCURRENCY_WAITER) +endif() + if(USE_FOUNDATION_FRAMEWORK) target_compile_definitions(XCTest PRIVATE USE_FOUNDATION_FRAMEWORK) diff --git a/Sources/XCTest/Private/DispatchShims.swift b/Sources/XCTest/Private/DispatchShims.swift new file mode 100644 index 000000000..55ce8b689 --- /dev/null +++ b/Sources/XCTest/Private/DispatchShims.swift @@ -0,0 +1,47 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +// +// NoThreadDispatchShims.swift +// + +// This file is a shim for platforms that don't have libdispatch and do assume a single-threaded environment. + +// NOTE: We can't use use `#if canImport(Dispatch)` because Dispatch Clang module is placed directly in the resource +// directory, and not split into target-specific directories. This means that the module is always available, even on +// platforms that don't have libdispatch. Thus, we need to check for the actual platform. +#if os(WASI) + +/// No-op shim function +func dispatchPrecondition(condition: DispatchPredicate) {} + +struct DispatchPredicate { + static func onQueue(_: X) -> Self { + return DispatchPredicate() + } + + static func notOnQueue(_: X) -> Self { + return DispatchPredicate() + } +} + +extension XCTWaiter { + /// Single-threaded queue without any actual queueing + struct DispatchQueue { + init(label: String) {} + + func sync(_ body: () -> T) -> T { + body() + } + func async(_ body: @escaping () -> Void) { + body() + } + } +} + +#endif diff --git a/Sources/XCTest/Private/WaiterManager.swift b/Sources/XCTest/Private/WaiterManager.swift index f705165fe..f5886d60b 100644 --- a/Sources/XCTest/Private/WaiterManager.swift +++ b/Sources/XCTest/Private/WaiterManager.swift @@ -9,6 +9,7 @@ // // WaiterManager.swift // +#if !USE_SWIFT_CONCURRENCY_WAITER internal protocol ManageableWaiter: AnyObject, Equatable { var isFinished: Bool { get } @@ -143,3 +144,5 @@ internal final class WaiterManager : NSObject { } } + +#endif diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift index dc8678047..03fae7c8c 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift @@ -10,6 +10,8 @@ // XCTNSNotificationExpectation.swift // +#if !USE_SWIFT_CONCURRENCY_WAITER + /// Expectation subclass for waiting on a condition defined by a Foundation Notification instance. open class XCTNSNotificationExpectation: XCTestExpectation { @@ -114,3 +116,5 @@ open class XCTNSNotificationExpectation: XCTestExpectation { /// - SeeAlso: `XCTNSNotificationExpectation.handler` @available(*, deprecated, renamed: "XCTNSNotificationExpectation.Handler") public typealias XCNotificationExpectationHandler = XCTNSNotificationExpectation.Handler + +#endif diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift index b41bca147..0164c977c 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift @@ -10,6 +10,8 @@ // XCTNSPredicateExpectation.swift // +#if !USE_SWIFT_CONCURRENCY_WAITER + /// Expectation subclass for waiting on a condition defined by an NSPredicate and an optional object. open class XCTNSPredicateExpectation: XCTestExpectation { @@ -133,3 +135,4 @@ open class XCTNSPredicateExpectation: XCTestExpectation { /// - SeeAlso: `XCTNSPredicateExpectation.handler` @available(*, deprecated, renamed: "XCTNSPredicateExpectation.Handler") public typealias XCPredicateExpectationHandler = XCTNSPredicateExpectation.Handler +#endif diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift index f19b344fd..0e820acd5 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift @@ -117,7 +117,9 @@ open class XCTWaiter { private var state = State.ready internal var timeout: TimeInterval = 0 internal var waitSourceLocation: SourceLocation? + #if !USE_SWIFT_CONCURRENCY_WAITER private weak var manager: WaiterManager? + #endif private var runLoop: RunLoop? private weak var _delegate: XCTWaiterDelegate? @@ -187,9 +189,16 @@ open class XCTWaiter { /// these environments. To ensure compatibility of tests between /// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass /// explicit values for `file` and `line`. + #if USE_SWIFT_CONCURRENCY_WAITER + @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") + #else @available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.") + #endif @discardableResult open func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { + #if USE_SWIFT_CONCURRENCY_WAITER + fatalError("This method is not available when using the Swift concurrency waiter.") + #else precondition(Set(expectations).count == expectations.count, "API violation - each expectation can appear only once in the 'expectations' parameter.") self.timeout = timeout @@ -251,6 +260,7 @@ open class XCTWaiter { } return result + #endif } /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they @@ -276,9 +286,16 @@ open class XCTWaiter { /// these environments. To ensure compatibility of tests between /// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass /// explicit values for `file` and `line`. + #if USE_SWIFT_CONCURRENCY_WAITER + @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") + #else @available(macOS 12.0, *) + #endif @discardableResult open func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result { +#if USE_SWIFT_CONCURRENCY_WAITER + fatalError("This method is not available when using the Swift concurrency waiter.") +#else return await withCheckedContinuation { continuation in // This function operates by blocking a background thread instead of one owned by libdispatch or by the // Swift runtime (as used by Swift concurrency.) To ensure we use a thread owned by neither subsystem, use @@ -288,6 +305,7 @@ open class XCTWaiter { continuation.resume(returning: result) } } +#endif } /// Convenience API to create an XCTWaiter which then waits on an array of expectations for up to the specified timeout, and optionally specify whether they @@ -306,9 +324,17 @@ open class XCTWaiter { /// expectations are not fulfilled before the given timeout. Default is the line /// number of the call to this method in the calling file. It is rare to /// provide this parameter when calling this method. + #if USE_SWIFT_CONCURRENCY_WAITER + @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") + #else @available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.") + #endif open class func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { +#if USE_SWIFT_CONCURRENCY_WAITER + fatalError("This method is not available when using the Swift concurrency waiter.") +#else return XCTWaiter().wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) +#endif } /// Convenience API to create an XCTWaiter which then waits on an array of expectations for up to the specified timeout, and optionally specify whether they @@ -327,9 +353,17 @@ open class XCTWaiter { /// expectations are not fulfilled before the given timeout. Default is the line /// number of the call to this method in the calling file. It is rare to /// provide this parameter when calling this method. + #if USE_SWIFT_CONCURRENCY_WAITER + @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") + #else @available(macOS 12.0, *) + #endif open class func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result { +#if USE_SWIFT_CONCURRENCY_WAITER + fatalError("This method is not available when using the Swift concurrency waiter.") +#else return await XCTWaiter().fulfillment(of: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) +#endif } deinit { @@ -338,6 +372,7 @@ open class XCTWaiter { } } +#if !USE_SWIFT_CONCURRENCY_WAITER private func queue_configureExpectations(_ expectations: [XCTestExpectation]) { dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) @@ -413,9 +448,11 @@ open class XCTWaiter { queue_validateExpectationFulfillment(dueToTimeout: false) } } +#endif } +#if !USE_SWIFT_CONCURRENCY_WAITER private extension XCTWaiter { func primitiveWait(using runLoop: RunLoop, duration timeout: TimeInterval) { // The contract for `primitiveWait(for:)` explicitly allows waiting for a shorter period than requested @@ -436,6 +473,7 @@ private extension XCTWaiter { #endif } } +#endif extension XCTWaiter: Equatable { public static func == (lhs: XCTWaiter, rhs: XCTWaiter) -> Bool { @@ -453,6 +491,7 @@ extension XCTWaiter: CustomStringConvertible { } } +#if !USE_SWIFT_CONCURRENCY_WAITER extension XCTWaiter: ManageableWaiter { var isFinished: Bool { return XCTWaiter.subsystemQueue.sync { @@ -479,3 +518,5 @@ extension XCTWaiter: ManageableWaiter { } } } + +#endif diff --git a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift index b9935ff72..5bb9da5cf 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift @@ -11,6 +11,8 @@ // Methods on XCTestCase for testing asynchronous operations // +#if !USE_SWIFT_CONCURRENCY_WAITER + public extension XCTestCase { /// Creates a point of synchronization in the flow of a test. Only one @@ -265,3 +267,4 @@ internal extension XCTestCase { expected: false) } } +#endif diff --git a/Sources/XCTest/Public/XCAbstractTest.swift b/Sources/XCTest/Public/XCAbstractTest.swift index cf37cba0d..58adfb4d9 100644 --- a/Sources/XCTest/Public/XCAbstractTest.swift +++ b/Sources/XCTest/Public/XCAbstractTest.swift @@ -36,20 +36,43 @@ open class XCTest { /// testRunClass. If the test has not yet been run, this will be nil. open private(set) var testRun: XCTestRun? = nil + internal var performTask: Task? + + #if USE_SWIFT_CONCURRENCY_WAITER + internal func _performAsync(_ run: XCTestRun) async { + fatalError("Must be overridden by subclasses.") + } + internal func _runAsync() async { + guard let testRunType = testRunClass as? XCTestRun.Type else { + fatalError("XCTest.testRunClass must be a kind of XCTestRun.") + } + testRun = testRunType.init(test: self) + await _performAsync(testRun!) + } + #endif + /// The method through which tests are executed. Must be overridden by /// subclasses. + #if USE_SWIFT_CONCURRENCY_WAITER + @available(*, unavailable) + #endif open func perform(_ run: XCTestRun) { fatalError("Must be overridden by subclasses.") } /// Creates an instance of the `testRunClass` and passes it as a parameter /// to `perform()`. + #if USE_SWIFT_CONCURRENCY_WAITER + @available(*, unavailable) + #endif open func run() { + #if !USE_SWIFT_CONCURRENCY_WAITER guard let testRunType = testRunClass as? XCTestRun.Type else { fatalError("XCTest.testRunClass must be a kind of XCTestRun.") } testRun = testRunType.init(test: self) perform(testRun!) + #endif } /// Async setup method called before the invocation of `setUpWithError` for each test method in the class. diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 4d734cd89..87922cdbc 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -36,6 +36,12 @@ open class XCTestCase: XCTest { private var skip: XCTSkip? +#if USE_SWIFT_CONCURRENCY_WAITER + /// A task that ends when the test closure has actually finished running. + /// This is used to ensure that all async work has completed. + fileprivate var testClosureTask: Task? +#endif + /// The name of the test case, consisting of its class name and the method /// name it will run. open override var name: String { @@ -89,6 +95,20 @@ open class XCTestCase: XCTest { return XCTestCaseRun.self } + #if USE_SWIFT_CONCURRENCY_WAITER + override func _performAsync(_ run: XCTestRun) async { + guard let testRun = run as? XCTestCaseRun else { + fatalError("Wrong XCTestRun class.") + } + + XCTCurrentTestCase = self + testRun.start() + await _invokeTestAsync() + + testRun.stop() + XCTCurrentTestCase = nil + } + #else open override func perform(_ run: XCTestRun) { guard let testRun = run as? XCTestCaseRun else { fatalError("Wrong XCTestRun class.") @@ -104,6 +124,7 @@ open class XCTestCase: XCTest { testRun.stop() XCTCurrentTestCase = nil } + #endif /// The designated initializer for SwiftXCTest's XCTestCase. /// - Note: Like the designated initializer for Apple XCTest's XCTestCase, @@ -114,9 +135,46 @@ open class XCTestCase: XCTest { self.testClosure = testClosure } + #if USE_SWIFT_CONCURRENCY_WAITER + internal func _invokeTestAsync() async { + await performSetUpSequence() + + do { + if skip == nil { + try testClosure(self) + } + if let task = testClosureTask { + _ = try await task.value + } + } catch { + if error.xct_shouldRecordAsTestFailure { + recordFailure(for: error) + } + + if error.xct_shouldRecordAsTestSkip { + if let skip = error as? XCTSkip { + self.skip = skip + } else { + self.skip = XCTSkip(error: error, message: nil, sourceLocation: nil) + } + } + } + + if let skip = skip { + testRun?.recordSkip(description: skip.summary, sourceLocation: skip.sourceLocation) + } + + await performTearDownSequence() + } + #endif + /// Invoking a test performs its setUp, invocation, and tearDown. In /// general this should not be called directly. + #if USE_SWIFT_CONCURRENCY_WAITER + @available(*, unavailable) + #endif open func invokeTest() { + #if !USE_SWIFT_CONCURRENCY_WAITER performSetUpSequence() do { @@ -142,6 +200,7 @@ open class XCTestCase: XCTest { } performTearDownSequence() + #endif } /// Records a failure in the execution of the test and is used by all test @@ -211,31 +270,21 @@ open class XCTestCase: XCTest { teardownBlocksState.appendAsync(block) } - private func performSetUpSequence() { - func handleErrorDuringSetUp(_ error: Error) { - if error.xct_shouldRecordAsTestFailure { - recordFailure(for: error) - } - - if error.xct_shouldSkipTestInvocation { - if let skip = error as? XCTSkip { - self.skip = skip - } else { - self.skip = XCTSkip(error: error, message: nil, sourceLocation: nil) - } - } + private func handleErrorDuringSetUp(_ error: Error) { + if error.xct_shouldRecordAsTestFailure { + recordFailure(for: error) } - do { - if #available(macOS 12.0, *) { - try awaitUsingExpectation { - try await self.setUp() - } + if error.xct_shouldSkipTestInvocation { + if let skip = error as? XCTSkip { + self.skip = skip + } else { + self.skip = XCTSkip(error: error, message: nil, sourceLocation: nil) } - } catch { - handleErrorDuringSetUp(error) } + } + private func performPostSetup() { do { try setUpWithError() } catch { @@ -245,32 +294,73 @@ open class XCTestCase: XCTest { setUp() } - private func performTearDownSequence() { - func handleErrorDuringTearDown(_ error: Error) { - if error.xct_shouldRecordAsTestFailure { - recordFailure(for: error) - } + private func handleErrorDuringTearDown(_ error: Error) { + if error.xct_shouldRecordAsTestFailure { + recordFailure(for: error) } + } - func runTeardownBlocks() { - for block in self.teardownBlocksState.finalize().reversed() { - do { - try block() - } catch { - handleErrorDuringTearDown(error) - } + private func runTeardownBlocks() { + for block in self.teardownBlocksState.finalize().reversed() { + do { + try block() + } catch { + handleErrorDuringTearDown(error) } } + } + private func performPreTearDown() { runTeardownBlocks() - tearDown() + func syncTearDown() { tearDown() } + syncTearDown() do { try tearDownWithError() } catch { handleErrorDuringTearDown(error) } + } + + #if USE_SWIFT_CONCURRENCY_WAITER + private func performSetUpSequence() async { + do { + if #available(macOS 12.0, *) { + try await self.setUp() + } + } catch { + handleErrorDuringSetUp(error) + } + + performPostSetup() + } + + private func performTearDownSequence() async { + performPreTearDown() + + do { + try await self.tearDown() + } catch { + handleErrorDuringTearDown(error) + } + } + #else + private func performSetUpSequence() { + do { + if #available(macOS 12.0, *) { + try awaitUsingExpectation { + try await self.setUp() + } + } + } catch { + handleErrorDuringSetUp(error) + } + performPostSetup() + } + + private func performTearDownSequence() { + performPreTearDown() do { if #available(macOS 12.0, *) { @@ -282,6 +372,7 @@ open class XCTestCase: XCTest { handleErrorDuringTearDown(error) } } + #endif open var continueAfterFailure: Bool { get { @@ -325,18 +416,31 @@ private func test(_ testFunc: @escaping (T) -> () throws -> Void) public func asyncTest( _ testClosureGenerator: @escaping (T) -> () async throws -> Void ) -> (T) -> () throws -> Void { +#if USE_SWIFT_CONCURRENCY_WAITER + return { (testType: T) in + let testClosure = testClosureGenerator(testType) + return { + assert(testType.testClosureTask == nil, "Async test case \(testType) cannot be run more than once") + testType.testClosureTask = Task { + try await testClosure() + } + } + } +#else return { (testType: T) in let testClosure = testClosureGenerator(testType) return { try awaitUsingExpectation(testClosure) } } +#endif } @available(macOS 12.0, *) func awaitUsingExpectation( _ closure: @escaping () async throws -> Void ) throws -> Void { +#if !USE_SWIFT_CONCURRENCY_WAITER let expectation = XCTestExpectation(description: "async test completion") let thrownErrorWrapper = ThrownErrorWrapper() @@ -355,6 +459,7 @@ func awaitUsingExpectation( if let error = thrownErrorWrapper.error { throw error } +#endif } private final class ThrownErrorWrapper: @unchecked Sendable { diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 33572e723..7b67ba223 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -69,12 +69,70 @@ /// - Returns: The exit code to use when the process terminates. `EXIT_SUCCESS` /// indicates success, while any other value (including `EXIT_FAILURE`) /// indicates failure. +#if USE_SWIFT_CONCURRENCY_WAITER +@_disfavoredOverload +public func XCTMain( + _ testCases: [XCTestCaseEntry], + arguments: [String] = CommandLine.arguments, + observers: [XCTestObservation]? = nil +) async -> CInt { + // Async-version of XCTMain() + switch XCTMainMisc(testCases, arguments: arguments, observers: observers) { + case .exitCode(let code): + return code + case .testSuite(let rootTestSuite, let testBundle, let observers): + // Add a test observer that prints test progress to stdout. + let observationCenter = XCTestObservationCenter.shared + for observer in observers { + observationCenter.addTestObserver(observer) + } + + observationCenter.testBundleWillStart(testBundle) + await rootTestSuite._runAsync() + observationCenter.testBundleDidFinish(testBundle) + + return rootTestSuite.testRun!.totalFailureCount == 0 ? EXIT_SUCCESS : EXIT_FAILURE + } +} +#else @_disfavoredOverload public func XCTMain( _ testCases: [XCTestCaseEntry], arguments: [String] = CommandLine.arguments, observers: [XCTestObservation]? = nil ) -> CInt { + // Sync-version of XCTMain() + switch XCTMainMisc(testCases, arguments: arguments, observers: observers) { + case .exitCode(let code): + return code + case .testSuite(let rootTestSuite, let testBundle, let observers): + // Add a test observer that prints test progress to stdout. + let observationCenter = XCTestObservationCenter.shared + for observer in observers { + observationCenter.addTestObserver(observer) + } + + observationCenter.testBundleWillStart(testBundle) + rootTestSuite.run() + observationCenter.testBundleDidFinish(testBundle) + + return rootTestSuite.testRun!.totalFailureCount == 0 ? EXIT_SUCCESS : EXIT_FAILURE + } +} +#endif + +internal enum TestSuiteOrExitCode { + case testSuite(rootTestSuite: XCTestSuite, testBundle: Bundle, observers: [XCTestObservation]) + case exitCode(CInt) +} + +/// Returns a test suite to be run or an exit code for the specified test cases and +/// command-line arguments. +internal func XCTMainMisc( + _ testCases: [XCTestCaseEntry], + arguments: [String] = CommandLine.arguments, + observers: [XCTestObservation]? +) -> TestSuiteOrExitCode { let observers = observers ?? [PrintObserver()] let testBundle = Bundle.main @@ -103,10 +161,10 @@ public func XCTMain( switch executionMode { case .list(type: .humanReadable): TestListing(testSuite: rootTestSuite).printTestList() - return EXIT_SUCCESS + return .exitCode(EXIT_SUCCESS) case .list(type: .json): TestListing(testSuite: rootTestSuite).printTestJSON() - return EXIT_SUCCESS + return .exitCode(EXIT_SUCCESS) case let .help(invalidOption): if let invalid = invalidOption { let errMsg = "Error: Invalid option \"\(invalid)\"\n" @@ -137,22 +195,32 @@ public func XCTMain( > \(exeName) \(sampleTests) """) - return invalidOption == nil ? EXIT_SUCCESS : EXIT_FAILURE + return .exitCode(invalidOption == nil ? EXIT_SUCCESS : EXIT_FAILURE) case .run(selectedTestNames: _): - // Add a test observer that prints test progress to stdout. - let observationCenter = XCTestObservationCenter.shared - for observer in observers { - observationCenter.addTestObserver(observer) - } + return .testSuite(rootTestSuite: rootTestSuite, testBundle: testBundle, observers: observers) + } +} - observationCenter.testBundleWillStart(testBundle) - rootTestSuite.run() - observationCenter.testBundleDidFinish(testBundle) +#if USE_SWIFT_CONCURRENCY_WAITER +// @available(*, deprecated, message: "Call the overload of XCTMain() that returns an exit code instead.") +public func XCTMain(_ testCases: [XCTestCaseEntry]) async -> Never { + exit(await XCTMain(testCases, arguments: CommandLine.arguments, observers: nil) as CInt) +} - return rootTestSuite.testRun!.totalFailureCount == 0 ? EXIT_SUCCESS : EXIT_FAILURE - } +// @available(*, deprecated, message: "Call the overload of XCTMain() that returns an exit code instead.") +public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) async -> Never { + exit(await XCTMain(testCases, arguments: arguments, observers: nil) as CInt) } +// @available(*, deprecated, message: "Call the overload of XCTMain() that returns an exit code instead.") +public func XCTMain( + _ testCases: [XCTestCaseEntry], + arguments: [String], + observers: [XCTestObservation] +) async -> Never { + exit(await XCTMain(testCases, arguments: arguments, observers: observers) as CInt) +} +#else // @available(*, deprecated, message: "Call the overload of XCTMain() that returns an exit code instead.") public func XCTMain(_ testCases: [XCTestCaseEntry]) -> Never { exit(XCTMain(testCases, arguments: CommandLine.arguments, observers: nil) as CInt) @@ -171,3 +239,4 @@ public func XCTMain( ) -> Never { exit(XCTMain(testCases, arguments: arguments, observers: observers) as CInt) } +#endif diff --git a/Sources/XCTest/Public/XCTestSuite.swift b/Sources/XCTest/Public/XCTestSuite.swift index 177dd1cb7..b7b9d12f9 100644 --- a/Sources/XCTest/Public/XCTestSuite.swift +++ b/Sources/XCTest/Public/XCTestSuite.swift @@ -38,6 +38,27 @@ open class XCTestSuite: XCTest { return XCTestSuiteRun.self } + #if USE_SWIFT_CONCURRENCY_WAITER + override func _performAsync(_ run: XCTestRun) async { + guard let testRun = run as? XCTestSuiteRun else { + fatalError("Wrong XCTestRun class.") + } + + run.start() + func syncSetUp() { setUp() } + syncSetUp() + for test in tests { + await test._runAsync() + if let childPerformTask = test.performTask { + _ = await childPerformTask.value + } + testRun.addTestRun(test.testRun!) + } + func syncTearDown() { tearDown() } + syncTearDown() + run.stop() + } + #else open override func perform(_ run: XCTestRun) { guard let testRun = run as? XCTestSuiteRun else { fatalError("Wrong XCTestRun class.") @@ -52,6 +73,7 @@ open class XCTestSuite: XCTest { tearDown() run.stop() } + #endif public init(name: String) { _name = name diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 3c9dbc5f0..cabd5d119 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -36,6 +36,8 @@ function(get_swift_host_arch result_var_name) set("${result_var_name}" "i686" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "i686") set("${result_var_name}" "i686" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "wasm32") + set("${result_var_name}" "wasm32" PARENT_SCOPE) else() message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}") endif() From ae2cb672d1b6de6b1f2a5b442cf661ce36c8b14c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 12 Mar 2024 18:09:59 +0000 Subject: [PATCH 2/8] Guard out `awaitUsingExpectation` for SWIFT_CONCURRENCY_WAITER mode Also this revealed teardown blocks were not being run in the mode, so fix that as well. --- .../XCTestCase.TearDownBlocksState.swift | 17 ++++++- Sources/XCTest/Public/XCTestCase.swift | 47 +++++++++++-------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index 83f43fe47..b4fcd7782 100644 --- a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift +++ b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift @@ -12,8 +12,14 @@ extension XCTestCase { /// Supports async and sync throwing methods. final class TeardownBlocksState { + #if USE_SWIFT_CONCURRENCY_WAITER + typealias TeardownBlock = @Sendable () async throws -> Void + #else + typealias TeardownBlock = () throws -> Void + #endif + private var wasFinalized = false - private var blocks: [() throws -> Void] = [] + private var blocks: [TeardownBlock] = [] // We don't want to overload append(_:) below because of how Swift will implicitly promote sync closures to async closures, // which can unexpectedly change their semantics in difficult to track down ways. @@ -21,9 +27,16 @@ extension XCTestCase { // Because of this, we chose the unusual decision to forgo overloading (which is a super sweet language feature <3) to prevent this issue from surprising any contributors to corelibs-xctest @available(macOS 12.0, *) func appendAsync(_ block: @Sendable @escaping () async throws -> Void) { + #if USE_SWIFT_CONCURRENCY_WAITER + XCTWaiter.subsystemQueue.sync { + precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") + blocks.append(block) + } + #else self.append { try awaitUsingExpectation { try await block() } } + #endif } func append(_ block: @escaping () throws -> Void) { @@ -33,7 +46,7 @@ extension XCTestCase { } } - func finalize() -> [() throws -> Void] { + func finalize() -> [TeardownBlock] { XCTWaiter.subsystemQueue.sync { precondition(wasFinalized == false, "API violation -- attempting to run teardown blocks after they've already run") wasFinalized = true diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 87922cdbc..70ab65d88 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -300,21 +300,8 @@ open class XCTestCase: XCTest { } } - private func runTeardownBlocks() { - for block in self.teardownBlocksState.finalize().reversed() { - do { - try block() - } catch { - handleErrorDuringTearDown(error) - } - } - } - - private func performPreTearDown() { - runTeardownBlocks() - - func syncTearDown() { tearDown() } - syncTearDown() + private func performSyncTearDown() { + tearDown() do { try tearDownWithError() @@ -324,6 +311,16 @@ open class XCTestCase: XCTest { } #if USE_SWIFT_CONCURRENCY_WAITER + private func runTeardownBlocks() async { + for block in self.teardownBlocksState.finalize().reversed() { + do { + try await block() + } catch { + handleErrorDuringTearDown(error) + } + } + } + private func performSetUpSequence() async { do { if #available(macOS 12.0, *) { @@ -337,7 +334,8 @@ open class XCTestCase: XCTest { } private func performTearDownSequence() async { - performPreTearDown() + await runTeardownBlocks() + performSyncTearDown() do { try await self.tearDown() @@ -346,6 +344,16 @@ open class XCTestCase: XCTest { } } #else + private func runTeardownBlocks() { + for block in self.teardownBlocksState.finalize().reversed() { + do { + try block() + } catch { + handleErrorDuringTearDown(error) + } + } + } + private func performSetUpSequence() { do { if #available(macOS 12.0, *) { @@ -360,7 +368,8 @@ open class XCTestCase: XCTest { } private func performTearDownSequence() { - performPreTearDown() + runTeardownBlocks() + performSyncTearDown() do { if #available(macOS 12.0, *) { @@ -436,11 +445,11 @@ public func asyncTest( #endif } +#if !USE_SWIFT_CONCURRENCY_WAITER @available(macOS 12.0, *) func awaitUsingExpectation( _ closure: @escaping () async throws -> Void ) throws -> Void { -#if !USE_SWIFT_CONCURRENCY_WAITER let expectation = XCTestExpectation(description: "async test completion") let thrownErrorWrapper = ThrownErrorWrapper() @@ -459,8 +468,8 @@ func awaitUsingExpectation( if let error = thrownErrorWrapper.error { throw error } -#endif } +#endif private final class ThrownErrorWrapper: @unchecked Sendable { From e428651271485f03ed5e3cccd4a5edfbb9a82cf9 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 12 Mar 2024 18:12:10 +0000 Subject: [PATCH 3/8] Guard out `SWIFT_CONCURRENCY_WAITER` specific member field --- Sources/XCTest/Public/XCAbstractTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCTest/Public/XCAbstractTest.swift b/Sources/XCTest/Public/XCAbstractTest.swift index 58adfb4d9..f4d0e3971 100644 --- a/Sources/XCTest/Public/XCAbstractTest.swift +++ b/Sources/XCTest/Public/XCAbstractTest.swift @@ -36,9 +36,9 @@ open class XCTest { /// testRunClass. If the test has not yet been run, this will be nil. open private(set) var testRun: XCTestRun? = nil + #if USE_SWIFT_CONCURRENCY_WAITER internal var performTask: Task? - #if USE_SWIFT_CONCURRENCY_WAITER internal func _performAsync(_ run: XCTestRun) async { fatalError("Must be overridden by subclasses.") } From 99a5c706cdc59e01150681f0f6dc141d2dcacade Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 12 Mar 2024 18:13:07 +0000 Subject: [PATCH 4/8] Run test suite, setUp, and tearDown in the main actor To keep consistency with the regular mode. --- Sources/XCTest/Public/XCTestCase.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 70ab65d88..17239a320 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -136,7 +136,7 @@ open class XCTestCase: XCTest { } #if USE_SWIFT_CONCURRENCY_WAITER - internal func _invokeTestAsync() async { + @MainActor internal func _invokeTestAsync() async { await performSetUpSequence() do { @@ -311,7 +311,7 @@ open class XCTestCase: XCTest { } #if USE_SWIFT_CONCURRENCY_WAITER - private func runTeardownBlocks() async { + @MainActor private func runTeardownBlocks() async { for block in self.teardownBlocksState.finalize().reversed() { do { try await block() @@ -321,7 +321,7 @@ open class XCTestCase: XCTest { } } - private func performSetUpSequence() async { + @MainActor private func performSetUpSequence() async { do { if #available(macOS 12.0, *) { try await self.setUp() @@ -333,7 +333,7 @@ open class XCTestCase: XCTest { performPostSetup() } - private func performTearDownSequence() async { + @MainActor private func performTearDownSequence() async { await runTeardownBlocks() performSyncTearDown() From c63a84fe9616d2276def1b185702adae9374b9f0 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 12 Mar 2024 20:31:20 +0000 Subject: [PATCH 5/8] Rename `USE_SWIFT_CONCURRENCY_WAITER` to `DISABLE_XCTWAITER` To make the semantics of the option clearer. --- CMakeLists.txt | 12 +++++----- Sources/XCTest/Private/WaiterManager.swift | 2 +- .../XCTestCase.TearDownBlocksState.swift | 4 ++-- .../XCTNSNotificationExpectation.swift | 2 +- .../XCTNSPredicateExpectation.swift | 2 +- .../Public/Asynchronous/XCTWaiter.swift | 24 +++++++++---------- .../XCTestCase+Asynchronous.swift | 2 +- Sources/XCTest/Public/XCAbstractTest.swift | 8 +++---- Sources/XCTest/Public/XCTestCase.swift | 16 ++++++------- Sources/XCTest/Public/XCTestMain.swift | 4 ++-- Sources/XCTest/Public/XCTestSuite.swift | 2 +- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d027460c..75fd3da4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,15 +8,15 @@ project(XCTest LANGUAGES Swift) option(BUILD_SHARED_LIBS "Build shared libraries" ON) option(USE_FOUNDATION_FRAMEWORK "Use Foundation.framework on Darwin" NO) -set(USE_SWIFT_CONCURRENCY_WAITER_default NO) +set(DISABLE_XCTWAITER_default NO) if(CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) - set(USE_SWIFT_CONCURRENCY_WAITER_default ON) + set(DISABLE_XCTWAITER_default ON) endif() -option(USE_SWIFT_CONCURRENCY_WAITER "Use Swift Concurrency-based waiter implementation" "${USE_SWIFT_CONCURRENCY_WAITER_default}") +option(DISABLE_XCTWAITER "Disable XCTWaiter" "${DISABLE_XCTWAITER_default}") -if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT USE_SWIFT_CONCURRENCY_WAITER) +if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT DISABLE_XCTWAITER) find_package(dispatch CONFIG REQUIRED) find_package(Foundation CONFIG REQUIRED) endif() @@ -59,9 +59,9 @@ add_library(XCTest Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift) -if(USE_SWIFT_CONCURRENCY_WAITER) +if(DISABLE_XCTWAITER) target_compile_definitions(XCTest PRIVATE - USE_SWIFT_CONCURRENCY_WAITER) + DISABLE_XCTWAITER) endif() if(USE_FOUNDATION_FRAMEWORK) diff --git a/Sources/XCTest/Private/WaiterManager.swift b/Sources/XCTest/Private/WaiterManager.swift index f5886d60b..2366e0ed4 100644 --- a/Sources/XCTest/Private/WaiterManager.swift +++ b/Sources/XCTest/Private/WaiterManager.swift @@ -9,7 +9,7 @@ // // WaiterManager.swift // -#if !USE_SWIFT_CONCURRENCY_WAITER +#if !DISABLE_XCTWAITER internal protocol ManageableWaiter: AnyObject, Equatable { var isFinished: Bool { get } diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index b4fcd7782..36962d766 100644 --- a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift +++ b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift @@ -12,7 +12,7 @@ extension XCTestCase { /// Supports async and sync throwing methods. final class TeardownBlocksState { - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER typealias TeardownBlock = @Sendable () async throws -> Void #else typealias TeardownBlock = () throws -> Void @@ -27,7 +27,7 @@ extension XCTestCase { // Because of this, we chose the unusual decision to forgo overloading (which is a super sweet language feature <3) to prevent this issue from surprising any contributors to corelibs-xctest @available(macOS 12.0, *) func appendAsync(_ block: @Sendable @escaping () async throws -> Void) { - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER XCTWaiter.subsystemQueue.sync { precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") blocks.append(block) diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift index 03fae7c8c..7d66e64d4 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift @@ -10,7 +10,7 @@ // XCTNSNotificationExpectation.swift // -#if !USE_SWIFT_CONCURRENCY_WAITER +#if !DISABLE_XCTWAITER /// Expectation subclass for waiting on a condition defined by a Foundation Notification instance. open class XCTNSNotificationExpectation: XCTestExpectation { diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift index 0164c977c..70fa2b96e 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift @@ -10,7 +10,7 @@ // XCTNSPredicateExpectation.swift // -#if !USE_SWIFT_CONCURRENCY_WAITER +#if !DISABLE_XCTWAITER /// Expectation subclass for waiting on a condition defined by an NSPredicate and an optional object. open class XCTNSPredicateExpectation: XCTestExpectation { diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift index 0e820acd5..c1146fa12 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift @@ -117,7 +117,7 @@ open class XCTWaiter { private var state = State.ready internal var timeout: TimeInterval = 0 internal var waitSourceLocation: SourceLocation? - #if !USE_SWIFT_CONCURRENCY_WAITER + #if !DISABLE_XCTWAITER private weak var manager: WaiterManager? #endif private var runLoop: RunLoop? @@ -189,14 +189,14 @@ open class XCTWaiter { /// these environments. To ensure compatibility of tests between /// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass /// explicit values for `file` and `line`. - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") #else @available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.") #endif @discardableResult open func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER fatalError("This method is not available when using the Swift concurrency waiter.") #else precondition(Set(expectations).count == expectations.count, "API violation - each expectation can appear only once in the 'expectations' parameter.") @@ -286,14 +286,14 @@ open class XCTWaiter { /// these environments. To ensure compatibility of tests between /// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass /// explicit values for `file` and `line`. - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") #else @available(macOS 12.0, *) #endif @discardableResult open func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result { -#if USE_SWIFT_CONCURRENCY_WAITER +#if DISABLE_XCTWAITER fatalError("This method is not available when using the Swift concurrency waiter.") #else return await withCheckedContinuation { continuation in @@ -324,13 +324,13 @@ open class XCTWaiter { /// expectations are not fulfilled before the given timeout. Default is the line /// number of the call to this method in the calling file. It is rare to /// provide this parameter when calling this method. - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") #else @available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.") #endif open class func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { -#if USE_SWIFT_CONCURRENCY_WAITER +#if DISABLE_XCTWAITER fatalError("This method is not available when using the Swift concurrency waiter.") #else return XCTWaiter().wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) @@ -353,13 +353,13 @@ open class XCTWaiter { /// expectations are not fulfilled before the given timeout. Default is the line /// number of the call to this method in the calling file. It is rare to /// provide this parameter when calling this method. - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") #else @available(macOS 12.0, *) #endif open class func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result { -#if USE_SWIFT_CONCURRENCY_WAITER +#if DISABLE_XCTWAITER fatalError("This method is not available when using the Swift concurrency waiter.") #else return await XCTWaiter().fulfillment(of: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) @@ -372,7 +372,7 @@ open class XCTWaiter { } } -#if !USE_SWIFT_CONCURRENCY_WAITER +#if !DISABLE_XCTWAITER private func queue_configureExpectations(_ expectations: [XCTestExpectation]) { dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) @@ -452,7 +452,7 @@ open class XCTWaiter { } -#if !USE_SWIFT_CONCURRENCY_WAITER +#if !DISABLE_XCTWAITER private extension XCTWaiter { func primitiveWait(using runLoop: RunLoop, duration timeout: TimeInterval) { // The contract for `primitiveWait(for:)` explicitly allows waiting for a shorter period than requested @@ -491,7 +491,7 @@ extension XCTWaiter: CustomStringConvertible { } } -#if !USE_SWIFT_CONCURRENCY_WAITER +#if !DISABLE_XCTWAITER extension XCTWaiter: ManageableWaiter { var isFinished: Bool { return XCTWaiter.subsystemQueue.sync { diff --git a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift index 5bb9da5cf..34d43e940 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift @@ -11,7 +11,7 @@ // Methods on XCTestCase for testing asynchronous operations // -#if !USE_SWIFT_CONCURRENCY_WAITER +#if !DISABLE_XCTWAITER public extension XCTestCase { diff --git a/Sources/XCTest/Public/XCAbstractTest.swift b/Sources/XCTest/Public/XCAbstractTest.swift index f4d0e3971..f1f06b3b3 100644 --- a/Sources/XCTest/Public/XCAbstractTest.swift +++ b/Sources/XCTest/Public/XCAbstractTest.swift @@ -36,7 +36,7 @@ open class XCTest { /// testRunClass. If the test has not yet been run, this will be nil. open private(set) var testRun: XCTestRun? = nil - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER internal var performTask: Task? internal func _performAsync(_ run: XCTestRun) async { @@ -53,7 +53,7 @@ open class XCTest { /// The method through which tests are executed. Must be overridden by /// subclasses. - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @available(*, unavailable) #endif open func perform(_ run: XCTestRun) { @@ -62,11 +62,11 @@ open class XCTest { /// Creates an instance of the `testRunClass` and passes it as a parameter /// to `perform()`. - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @available(*, unavailable) #endif open func run() { - #if !USE_SWIFT_CONCURRENCY_WAITER + #if !DISABLE_XCTWAITER guard let testRunType = testRunClass as? XCTestRun.Type else { fatalError("XCTest.testRunClass must be a kind of XCTestRun.") } diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 17239a320..d66795298 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -36,7 +36,7 @@ open class XCTestCase: XCTest { private var skip: XCTSkip? -#if USE_SWIFT_CONCURRENCY_WAITER +#if DISABLE_XCTWAITER /// A task that ends when the test closure has actually finished running. /// This is used to ensure that all async work has completed. fileprivate var testClosureTask: Task? @@ -95,7 +95,7 @@ open class XCTestCase: XCTest { return XCTestCaseRun.self } - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER override func _performAsync(_ run: XCTestRun) async { guard let testRun = run as? XCTestCaseRun else { fatalError("Wrong XCTestRun class.") @@ -135,7 +135,7 @@ open class XCTestCase: XCTest { self.testClosure = testClosure } - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @MainActor internal func _invokeTestAsync() async { await performSetUpSequence() @@ -170,11 +170,11 @@ open class XCTestCase: XCTest { /// Invoking a test performs its setUp, invocation, and tearDown. In /// general this should not be called directly. - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @available(*, unavailable) #endif open func invokeTest() { - #if !USE_SWIFT_CONCURRENCY_WAITER + #if !DISABLE_XCTWAITER performSetUpSequence() do { @@ -310,7 +310,7 @@ open class XCTestCase: XCTest { } } - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER @MainActor private func runTeardownBlocks() async { for block in self.teardownBlocksState.finalize().reversed() { do { @@ -425,7 +425,7 @@ private func test(_ testFunc: @escaping (T) -> () throws -> Void) public func asyncTest( _ testClosureGenerator: @escaping (T) -> () async throws -> Void ) -> (T) -> () throws -> Void { -#if USE_SWIFT_CONCURRENCY_WAITER +#if DISABLE_XCTWAITER return { (testType: T) in let testClosure = testClosureGenerator(testType) return { @@ -445,7 +445,7 @@ public func asyncTest( #endif } -#if !USE_SWIFT_CONCURRENCY_WAITER +#if !DISABLE_XCTWAITER @available(macOS 12.0, *) func awaitUsingExpectation( _ closure: @escaping () async throws -> Void diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 7b67ba223..1bbd65380 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -69,7 +69,7 @@ /// - Returns: The exit code to use when the process terminates. `EXIT_SUCCESS` /// indicates success, while any other value (including `EXIT_FAILURE`) /// indicates failure. -#if USE_SWIFT_CONCURRENCY_WAITER +#if DISABLE_XCTWAITER @_disfavoredOverload public func XCTMain( _ testCases: [XCTestCaseEntry], @@ -201,7 +201,7 @@ internal func XCTMainMisc( } } -#if USE_SWIFT_CONCURRENCY_WAITER +#if DISABLE_XCTWAITER // @available(*, deprecated, message: "Call the overload of XCTMain() that returns an exit code instead.") public func XCTMain(_ testCases: [XCTestCaseEntry]) async -> Never { exit(await XCTMain(testCases, arguments: CommandLine.arguments, observers: nil) as CInt) diff --git a/Sources/XCTest/Public/XCTestSuite.swift b/Sources/XCTest/Public/XCTestSuite.swift index b7b9d12f9..7ee39b65d 100644 --- a/Sources/XCTest/Public/XCTestSuite.swift +++ b/Sources/XCTest/Public/XCTestSuite.swift @@ -38,7 +38,7 @@ open class XCTestSuite: XCTest { return XCTestSuiteRun.self } - #if USE_SWIFT_CONCURRENCY_WAITER + #if DISABLE_XCTWAITER override func _performAsync(_ run: XCTestRun) async { guard let testRun = run as? XCTestSuiteRun else { fatalError("Wrong XCTestRun class.") From 9decbc4897b720a71c8c2aa9dbad3f7f90f2cdd7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 12 Mar 2024 20:44:25 +0000 Subject: [PATCH 6/8] Make entire `XCTWaiter` and `XCTestExpectation` unavailable for WASI --- .../XCTestCase.TearDownBlocksState.swift | 6 +-- .../Asynchronous/XCTWaiter+Validation.swift | 3 ++ .../Public/Asynchronous/XCTWaiter.swift | 40 +------------------ .../Asynchronous/XCTestExpectation.swift | 3 ++ Sources/XCTest/Public/XCTestCase.swift | 8 +++- 5 files changed, 16 insertions(+), 44 deletions(-) diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index 36962d766..321f06b1e 100644 --- a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift +++ b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift @@ -28,7 +28,7 @@ extension XCTestCase { @available(macOS 12.0, *) func appendAsync(_ block: @Sendable @escaping () async throws -> Void) { #if DISABLE_XCTWAITER - XCTWaiter.subsystemQueue.sync { + XCTestCase.subsystemQueue.sync { precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") blocks.append(block) } @@ -40,14 +40,14 @@ extension XCTestCase { } func append(_ block: @escaping () throws -> Void) { - XCTWaiter.subsystemQueue.sync { + XCTestCase.subsystemQueue.sync { precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") blocks.append(block) } } func finalize() -> [TeardownBlock] { - XCTWaiter.subsystemQueue.sync { + XCTestCase.subsystemQueue.sync { precondition(wasFinalized == false, "API violation -- attempting to run teardown blocks after they've already run") wasFinalized = true return blocks diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift index 5ff4643c7..c9dd621e8 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift @@ -9,6 +9,7 @@ // // XCTWaiter+Validation.swift // +#if !DISABLE_XCTWAITER protocol XCTWaiterValidatableExpectation: Equatable { var isFulfilled: Bool { get } @@ -87,3 +88,5 @@ extension XCTWaiter { return .incomplete } } + +#endif diff --git a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift index c1146fa12..ac114578d 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift @@ -9,6 +9,7 @@ // // XCTWaiter.swift // +#if !DISABLE_XCTWAITER #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) import CoreFoundation @@ -117,9 +118,7 @@ open class XCTWaiter { private var state = State.ready internal var timeout: TimeInterval = 0 internal var waitSourceLocation: SourceLocation? - #if !DISABLE_XCTWAITER private weak var manager: WaiterManager? - #endif private var runLoop: RunLoop? private weak var _delegate: XCTWaiterDelegate? @@ -189,16 +188,9 @@ open class XCTWaiter { /// these environments. To ensure compatibility of tests between /// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass /// explicit values for `file` and `line`. - #if DISABLE_XCTWAITER - @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") - #else @available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.") - #endif @discardableResult open func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { - #if DISABLE_XCTWAITER - fatalError("This method is not available when using the Swift concurrency waiter.") - #else precondition(Set(expectations).count == expectations.count, "API violation - each expectation can appear only once in the 'expectations' parameter.") self.timeout = timeout @@ -260,7 +252,6 @@ open class XCTWaiter { } return result - #endif } /// Wait on an array of expectations for up to the specified timeout, and optionally specify whether they @@ -286,16 +277,9 @@ open class XCTWaiter { /// these environments. To ensure compatibility of tests between /// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass /// explicit values for `file` and `line`. - #if DISABLE_XCTWAITER - @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") - #else @available(macOS 12.0, *) - #endif @discardableResult open func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result { -#if DISABLE_XCTWAITER - fatalError("This method is not available when using the Swift concurrency waiter.") -#else return await withCheckedContinuation { continuation in // This function operates by blocking a background thread instead of one owned by libdispatch or by the // Swift runtime (as used by Swift concurrency.) To ensure we use a thread owned by neither subsystem, use @@ -305,7 +289,6 @@ open class XCTWaiter { continuation.resume(returning: result) } } -#endif } /// Convenience API to create an XCTWaiter which then waits on an array of expectations for up to the specified timeout, and optionally specify whether they @@ -324,17 +307,9 @@ open class XCTWaiter { /// expectations are not fulfilled before the given timeout. Default is the line /// number of the call to this method in the calling file. It is rare to /// provide this parameter when calling this method. - #if DISABLE_XCTWAITER - @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") - #else @available(*, noasync, message: "Use await fulfillment(of:timeout:enforceOrder:) instead.") - #endif open class func wait(for expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) -> Result { -#if DISABLE_XCTWAITER - fatalError("This method is not available when using the Swift concurrency waiter.") -#else return XCTWaiter().wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) -#endif } /// Convenience API to create an XCTWaiter which then waits on an array of expectations for up to the specified timeout, and optionally specify whether they @@ -353,17 +328,9 @@ open class XCTWaiter { /// expectations are not fulfilled before the given timeout. Default is the line /// number of the call to this method in the calling file. It is rare to /// provide this parameter when calling this method. - #if DISABLE_XCTWAITER - @available(*, unavailable, message: "Expectation-based waiting is not available when using the Swift concurrency waiter.") - #else @available(macOS 12.0, *) - #endif open class func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async -> Result { -#if DISABLE_XCTWAITER - fatalError("This method is not available when using the Swift concurrency waiter.") -#else return await XCTWaiter().fulfillment(of: expectations, timeout: timeout, enforceOrder: enforceOrder, file: file, line: line) -#endif } deinit { @@ -372,7 +339,6 @@ open class XCTWaiter { } } -#if !DISABLE_XCTWAITER private func queue_configureExpectations(_ expectations: [XCTestExpectation]) { dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue)) @@ -448,11 +414,9 @@ open class XCTWaiter { queue_validateExpectationFulfillment(dueToTimeout: false) } } -#endif } -#if !DISABLE_XCTWAITER private extension XCTWaiter { func primitiveWait(using runLoop: RunLoop, duration timeout: TimeInterval) { // The contract for `primitiveWait(for:)` explicitly allows waiting for a shorter period than requested @@ -473,7 +437,6 @@ private extension XCTWaiter { #endif } } -#endif extension XCTWaiter: Equatable { public static func == (lhs: XCTWaiter, rhs: XCTWaiter) -> Bool { @@ -491,7 +454,6 @@ extension XCTWaiter: CustomStringConvertible { } } -#if !DISABLE_XCTWAITER extension XCTWaiter: ManageableWaiter { var isFinished: Bool { return XCTWaiter.subsystemQueue.sync { diff --git a/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift index 16564dd9c..829ba7987 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift @@ -9,6 +9,7 @@ // // XCTestExpectation.swift // +#if !DISABLE_XCTWAITER /// Expectations represent specific conditions in asynchronous testing. open class XCTestExpectation: @unchecked Sendable { @@ -320,3 +321,5 @@ extension XCTestExpectation: CustomStringConvertible { return expectationDescription } } + +#endif diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index d66795298..1f2ab76a8 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -54,6 +54,9 @@ open class XCTestCase: XCTest { return 1 } + internal static let subsystemQueue = DispatchQueue(label: "org.swift.XCTestCase") + + #if !DISABLE_XCTWAITER @MainActor internal var currentWaiter: XCTWaiter? @@ -87,6 +90,7 @@ open class XCTestCase: XCTest { } } } + #endif /// An internal object implementing performance measurements. internal var _performanceMeter: PerformanceMeter? @@ -477,10 +481,10 @@ private final class ThrownErrorWrapper: @unchecked Sendable { var error: Error? { get { - XCTWaiter.subsystemQueue.sync { _error } + XCTestCase.subsystemQueue.sync { _error } } set { - XCTWaiter.subsystemQueue.sync { _error = newValue } + XCTestCase.subsystemQueue.sync { _error = newValue } } } } From 2a4682263d45c7615a53e84694da9189de5d19ba Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 12 Mar 2024 20:50:48 +0000 Subject: [PATCH 7/8] Remove DispatchShims.swift and replace it with a conditional typealias The use of DispatchQueue in XCTest is now very limited, and it's only used in a single place in XCTestCase.swift. --- CMakeLists.txt | 1 - Sources/XCTest/Private/DispatchShims.swift | 47 ---------------------- Sources/XCTest/Public/XCTestCase.swift | 18 ++++++++- 3 files changed, 17 insertions(+), 49 deletions(-) delete mode 100644 Sources/XCTest/Private/DispatchShims.swift diff --git a/CMakeLists.txt b/CMakeLists.txt index 75fd3da4f..14ecd0fe7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,6 @@ add_library(XCTest Sources/XCTest/Private/WaiterManager.swift Sources/XCTest/Private/IgnoredErrors.swift Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift - Sources/XCTest/Private/DispatchShims.swift Sources/XCTest/Public/XCTestRun.swift Sources/XCTest/Public/XCTestMain.swift Sources/XCTest/Public/XCTestCase.swift diff --git a/Sources/XCTest/Private/DispatchShims.swift b/Sources/XCTest/Private/DispatchShims.swift deleted file mode 100644 index 55ce8b689..000000000 --- a/Sources/XCTest/Private/DispatchShims.swift +++ /dev/null @@ -1,47 +0,0 @@ -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -// -// NoThreadDispatchShims.swift -// - -// This file is a shim for platforms that don't have libdispatch and do assume a single-threaded environment. - -// NOTE: We can't use use `#if canImport(Dispatch)` because Dispatch Clang module is placed directly in the resource -// directory, and not split into target-specific directories. This means that the module is always available, even on -// platforms that don't have libdispatch. Thus, we need to check for the actual platform. -#if os(WASI) - -/// No-op shim function -func dispatchPrecondition(condition: DispatchPredicate) {} - -struct DispatchPredicate { - static func onQueue(_: X) -> Self { - return DispatchPredicate() - } - - static func notOnQueue(_: X) -> Self { - return DispatchPredicate() - } -} - -extension XCTWaiter { - /// Single-threaded queue without any actual queueing - struct DispatchQueue { - init(label: String) {} - - func sync(_ body: () -> T) -> T { - body() - } - func async(_ body: @escaping () -> Void) { - body() - } - } -} - -#endif diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index 1f2ab76a8..2d899b910 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -54,7 +54,23 @@ open class XCTestCase: XCTest { return 1 } - internal static let subsystemQueue = DispatchQueue(label: "org.swift.XCTestCase") + #if DISABLE_XCTWAITER && os(WASI) + /// Single-threaded queue without any actual queueing + struct SubsystemQueue { + init(label: String) {} + + func sync(_ body: () -> T) -> T { + body() + } + func async(_ body: @escaping () -> Void) { + body() + } + } + #else + typealias SubsystemQueue = DispatchQueue + #endif + + internal static let subsystemQueue = SubsystemQueue(label: "org.swift.XCTestCase") #if !DISABLE_XCTWAITER @MainActor From da82bb90e2fb5e7a4bf438099acc35101500dc84 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 12 Mar 2024 21:57:56 +0000 Subject: [PATCH 8/8] Keep running tearDown blocks on the main actor consistently --- Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index 321f06b1e..db45f88a9 100644 --- a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift +++ b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift @@ -13,7 +13,7 @@ extension XCTestCase { final class TeardownBlocksState { #if DISABLE_XCTWAITER - typealias TeardownBlock = @Sendable () async throws -> Void + typealias TeardownBlock = @Sendable @MainActor () async throws -> Void #else typealias TeardownBlock = () throws -> Void #endif