diff --git a/CMakeLists.txt b/CMakeLists.txt index a386ef08..7f23844c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,13 +8,29 @@ 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(DISABLE_XCTWAITER_default NO) + +if(CMAKE_SYSTEM_PROCESSOR STREQUAL wasm32) + set(DISABLE_XCTWAITER_default ON) +endif() + +option(DISABLE_XCTWAITER "Disable XCTWaiter" "${DISABLE_XCTWAITER_default}") + +if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin AND NOT DISABLE_XCTWAITER) find_package(dispatch CONFIG REQUIRED) find_package(Foundation CONFIG REQUIRED) endif() include(SwiftSupport) include(GNUInstallDirs) +include(CheckLinkerFlag) + +if(CMAKE_SYSTEM_NAME STREQUAL Linux + OR CMAKE_SYSTEM_NAME STREQUAL FreeBSD + OR CMAKE_SYSTEM_NAME STREQUAL OpenBSD) + enable_language(C) + check_linker_flag(C "LINKER:--build-id=sha1" LINKER_SUPPORTS_BUILD_ID) +endif() add_library(XCTest Sources/XCTest/Private/WallClockTimeMetric.swift @@ -49,6 +65,12 @@ add_library(XCTest Sources/XCTest/Public/Asynchronous/XCTWaiter.swift Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift) + +if(DISABLE_XCTWAITER) + target_compile_definitions(XCTest PRIVATE + DISABLE_XCTWAITER) +endif() + if(USE_FOUNDATION_FRAMEWORK) target_compile_definitions(XCTest PRIVATE USE_FOUNDATION_FRAMEWORK) @@ -65,6 +87,10 @@ set_target_properties(XCTest PROPERTIES Swift_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/swift INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}/swift) +if(LINKER_SUPPORTS_BUILD_ID) + target_link_options(XCTest PRIVATE "LINKER:--build-id=sha1") +endif() + if(ENABLE_TESTING) enable_testing() diff --git a/README.md b/README.md index 3216bbdd..ad6a7583 100644 --- a/README.md +++ b/README.md @@ -17,43 +17,7 @@ For general information about using XCTest, see: The Swift Package Manager integrates directly with XCTest to provide a streamlined experience for unit testing SwiftPM packages. If you are using XCTest within a SwiftPM package, unit test files are located within the package's `Tests` subdirectory, and you can build and run the full test suite in one step by running `swift test`. -For more information about using XCTest with SwiftPM, see its [documentation](https://github.com/apple/swift-package-manager). - -### Test Method Discovery - -Unlike the version of XCTest included with Xcode, this version does not use the Objective-C runtime to automatically discover test methods because that runtime is not available on all platforms Swift supports. This means that in certain configurations, the full set of test methods must be explicitly provided to XCTest. - -When using XCTest via SwiftPM on macOS, this is not necessary because SwiftPM uses the version of XCTest included with Xcode to run tests. But when using this version of XCTest _without_ SwiftPM, or _with_ SwiftPM on a platform other than macOS (including Linux), the full set of test methods cannot be discovered automatically, and your test target must tell XCTest about them explicitly. - -The recommended way to do this is to create a static property in each of your `XCTestCase` subclasses. By convention, this property is named `allTests`, and should contain all of the tests in the class. For example: - -```swift -class TestNSURL : XCTestCase { - static var allTests = { - return [ - ("test_bestNumber", test_bestNumber), - ("test_URLStrings", test_URLStrings), - ("test_fileURLWithPath", test_fileURLWithPath), - // Other tests go here - ] - }() - - func test_bestNumber() { - // Write your test here. Most of the XCTAssert functions you are familiar with are available. - XCTAssertTrue(theBestNumber == 42, "The number is wrong") - } - - // Other tests go here -} -``` - -After creating an `allTests` property in each `XCTestCase` subclass, you must tell XCTest about those classes' tests. - -If the project is a SwiftPM package which supports macOS, the easiest way to do this is to run `swift test --generate-linuxmain` from a macOS machine. This command generates files within the package's `Tests` subdirectory which contains the necessary source code for passing all test classes and methods to XCTest. These files should be committed to source control and re-generated whenever `XCTestCase` subclasses or test methods are added to or removed from your package's test suite. - -If the project is a SwiftPM package but does not support macOS, you may edit the package's default `LinuxMain.swift` file manually to add all `XCTestCase` subclasses. - -If the project is not a SwiftPM package, follow the steps in the next section to create an executable which calls the `XCTMain` function manually. +For more information about using XCTest with SwiftPM, see its [documentation](https://github.com/swiftlang/swift-package-manager). ### Standalone Command Line Usage diff --git a/Sources/XCTest/Private/WaiterManager.swift b/Sources/XCTest/Private/WaiterManager.swift index f705165f..2366e0ed 100644 --- a/Sources/XCTest/Private/WaiterManager.swift +++ b/Sources/XCTest/Private/WaiterManager.swift @@ -9,6 +9,7 @@ // // WaiterManager.swift // +#if !DISABLE_XCTWAITER internal protocol ManageableWaiter: AnyObject, Equatable { var isFinished: Bool { get } @@ -143,3 +144,5 @@ internal final class WaiterManager : NSObject { } } + +#endif diff --git a/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift b/Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift index 83f43fe4..db45f88a 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 DISABLE_XCTWAITER + typealias TeardownBlock = @Sendable @MainActor () 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,20 +27,27 @@ 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 DISABLE_XCTWAITER + XCTestCase.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) { - 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() -> [() throws -> Void] { - XCTWaiter.subsystemQueue.sync { + func finalize() -> [TeardownBlock] { + 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/XCTNSNotificationExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift index 573c6c27..7d66e64d 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift @@ -10,6 +10,8 @@ // XCTNSNotificationExpectation.swift // +#if !DISABLE_XCTWAITER + /// Expectation subclass for waiting on a condition defined by a Foundation Notification instance. open class XCTNSNotificationExpectation: XCTestExpectation { @@ -19,7 +21,7 @@ open class XCTNSNotificationExpectation: XCTestExpectation { /// - Returns: `true` if the expectation should be fulfilled, `false` if it should not. /// /// - SeeAlso: `XCTNSNotificationExpectation.handler` - public typealias Handler = (Notification) -> Bool + public typealias Handler = @Sendable (Notification) -> Bool private let queue = DispatchQueue(label: "org.swift.XCTest.XCTNSNotificationExpectation") @@ -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 08d0cf26..70fa2b96 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift @@ -10,6 +10,8 @@ // XCTNSPredicateExpectation.swift // +#if !DISABLE_XCTWAITER + /// Expectation subclass for waiting on a condition defined by an NSPredicate and an optional object. open class XCTNSPredicateExpectation: XCTestExpectation { @@ -18,7 +20,7 @@ open class XCTNSPredicateExpectation: XCTestExpectation { /// - Returns: `true` if the expectation should be fulfilled, `false` if it should not. /// /// - SeeAlso: `XCTNSPredicateExpectation.handler` - public typealias Handler = () -> Bool + public typealias Handler = @Sendable () -> Bool private let queue = DispatchQueue(label: "org.swift.XCTest.XCTNSPredicateExpectation") @@ -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+Validation.swift b/Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift index 5ff4643c..c9dd621e 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 f19b344f..ac114578 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 @@ -479,3 +480,5 @@ extension XCTWaiter: ManageableWaiter { } } } + +#endif diff --git a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift index 7b122532..34d43e94 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 !DISABLE_XCTWAITER + public extension XCTestCase { /// Creates a point of synchronization in the flow of a test. Only one @@ -41,7 +43,7 @@ public extension XCTestCase { /// 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`. - // FIXME: This should have `@MainActor` to match Xcode XCTest, but adding it causes errors in tests authored pre-Swift Concurrency which don't typically have `@MainActor`. + @preconcurrency @MainActor func waitForExpectations(timeout: TimeInterval, file: StaticString = #file, line: Int = #line, handler: XCWaitCompletionHandler? = nil) { precondition(Thread.isMainThread, "\(#function) must be called on the main thread") if currentWaiter != nil { @@ -265,3 +267,4 @@ internal extension XCTestCase { expected: false) } } +#endif diff --git a/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift index 16564dd9..829ba798 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/XCAbstractTest.swift b/Sources/XCTest/Public/XCAbstractTest.swift index cf37cba0..f1f06b3b 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 + #if DISABLE_XCTWAITER + internal var performTask: Task? + + 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 DISABLE_XCTWAITER + @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 DISABLE_XCTWAITER + @available(*, unavailable) + #endif open func run() { + #if !DISABLE_XCTWAITER 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 aadfecc7..2d899b91 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 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? +#endif + /// The name of the test case, consisting of its class name and the method /// name it will run. open override var name: String { @@ -48,7 +54,26 @@ open class XCTestCase: XCTest { return 1 } - // FIXME: Once `waitForExpectations(timeout:...handler:)` gains `@MainActor`, this may be able to add it as well. + #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 internal var currentWaiter: XCTWaiter? /// The set of expectations made upon this test case. @@ -81,6 +106,7 @@ open class XCTestCase: XCTest { } } } + #endif /// An internal object implementing performance measurements. internal var _performanceMeter: PerformanceMeter? @@ -89,6 +115,20 @@ open class XCTestCase: XCTest { return XCTestCaseRun.self } + #if DISABLE_XCTWAITER + 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 +144,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 +155,46 @@ open class XCTestCase: XCTest { self.testClosure = testClosure } + #if DISABLE_XCTWAITER + @MainActor 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 DISABLE_XCTWAITER + @available(*, unavailable) + #endif open func invokeTest() { + #if !DISABLE_XCTWAITER performSetUpSequence() do { @@ -142,6 +220,7 @@ open class XCTestCase: XCTest { } performTearDownSequence() + #endif } /// Records a failure in the execution of the test and is used by all test @@ -211,31 +290,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 +314,82 @@ 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) + } + } + + private func performSyncTearDown() { + tearDown() + + do { + try tearDownWithError() + } catch { + handleErrorDuringTearDown(error) + } + } + + #if DISABLE_XCTWAITER + @MainActor private func runTeardownBlocks() async { + for block in self.teardownBlocksState.finalize().reversed() { + do { + try await block() + } catch { + handleErrorDuringTearDown(error) } } + } - func runTeardownBlocks() { - for block in self.teardownBlocksState.finalize().reversed() { - do { - try block() - } catch { - handleErrorDuringTearDown(error) - } + @MainActor private func performSetUpSequence() async { + do { + if #available(macOS 12.0, *) { + try await self.setUp() } + } catch { + handleErrorDuringSetUp(error) } - runTeardownBlocks() + performPostSetup() + } - tearDown() + @MainActor private func performTearDownSequence() async { + await runTeardownBlocks() + performSyncTearDown() do { - try tearDownWithError() + try await self.tearDown() } catch { handleErrorDuringTearDown(error) } + } + #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, *) { + try awaitUsingExpectation { + try await self.setUp() + } + } + } catch { + handleErrorDuringSetUp(error) + } + performPostSetup() + } + + private func performTearDownSequence() { + runTeardownBlocks() + performSyncTearDown() do { if #available(macOS 12.0, *) { @@ -282,6 +401,7 @@ open class XCTestCase: XCTest { handleErrorDuringTearDown(error) } } + #endif open var continueAfterFailure: Bool { get { @@ -325,14 +445,27 @@ private func test(_ testFunc: @escaping (T) -> () throws -> Void) public func asyncTest( _ testClosureGenerator: @escaping (T) -> () async throws -> Void ) -> (T) -> () throws -> Void { +#if DISABLE_XCTWAITER + 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 } +#if !DISABLE_XCTWAITER @available(macOS 12.0, *) func awaitUsingExpectation( _ closure: @escaping () async throws -> Void @@ -356,6 +489,7 @@ func awaitUsingExpectation( throw error } } +#endif private final class ThrownErrorWrapper: @unchecked Sendable { @@ -363,10 +497,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 } } } } diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index f95aac65..1bbd6538 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -32,7 +32,6 @@ /// Starts a test run for the specified test cases. /// -/// This function will not return. If the test cases pass, then it will call `exit(EXIT_SUCCESS)`. If there is a failure, then it will call `exit(EXIT_FAILURE)`. /// Example usage: /// /// class TestFoo: XCTestCase { @@ -50,28 +49,91 @@ /// // etc... /// } /// -/// XCTMain([ testCase(TestFoo.allTests) ]) +/// let exitCode = XCTMain([ testCase(TestFoo.allTests) ]) /// -/// Command line arguments can be used to select a particular test case or class to execute. For example: +/// Command line arguments can be used to select a particular test case or class +/// to execute. For example: /// /// ./FooTests FooTestCase/testFoo # Run a single test case /// ./FooTests FooTestCase # Run all the tests in FooTestCase /// -/// - Parameter testCases: An array of test cases run, each produced by a call to the `testCase` function -/// - seealso: `testCase` -public func XCTMain(_ testCases: [XCTestCaseEntry]) -> Never { - XCTMain(testCases, arguments: CommandLine.arguments) +/// - Parameters: +/// - testCases: An array of test cases run, each produced by a call to the +/// `testCase` function. +/// - arguments: Command-line arguments to pass to XCTest. By default, the +/// arguments passed to the process are used. +/// - observers: Zero or more observers that should observe events that +/// occur while testing. If `nil` (the default), events are written to +/// the console. +/// +/// - Returns: The exit code to use when the process terminates. `EXIT_SUCCESS` +/// indicates success, while any other value (including `EXIT_FAILURE`) +/// indicates failure. +#if DISABLE_XCTWAITER +@_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 -public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Never { - XCTMain(testCases, arguments: arguments, observers: [PrintObserver()]) +internal enum TestSuiteOrExitCode { + case testSuite(rootTestSuite: XCTestSuite, testBundle: Bundle, observers: [XCTestObservation]) + case exitCode(CInt) } -public func XCTMain( +/// 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], - observers: [XCTestObservation] -) -> Never { + arguments: [String] = CommandLine.arguments, + observers: [XCTestObservation]? +) -> TestSuiteOrExitCode { + let observers = observers ?? [PrintObserver()] let testBundle = Bundle.main let executionMode = ArgumentParser(arguments: arguments).executionMode @@ -99,10 +161,10 @@ public func XCTMain( switch executionMode { case .list(type: .humanReadable): TestListing(testSuite: rootTestSuite).printTestList() - exit(EXIT_SUCCESS) + return .exitCode(EXIT_SUCCESS) case .list(type: .json): TestListing(testSuite: rootTestSuite).printTestJSON() - exit(EXIT_SUCCESS) + return .exitCode(EXIT_SUCCESS) case let .help(invalidOption): if let invalid = invalidOption { let errMsg = "Error: Invalid option \"\(invalid)\"\n" @@ -133,18 +195,48 @@ public func XCTMain( > \(exeName) \(sampleTests) """) - exit(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 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) +} - exit(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) +} + +// @available(*, deprecated, message: "Call the overload of XCTMain() that returns an exit code instead.") +public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Never { + exit(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] +) -> 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 177dd1cb..7ee39b65 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 DISABLE_XCTWAITER + 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/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index 6ebb3352..4772c382 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -8,8 +8,6 @@ import XCTest #endif -import CoreFoundation - // CHECK: Test Suite 'All tests' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ // CHECK: Test Suite '.*\.xctest' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ @@ -273,7 +271,7 @@ class ExpectationsTestCase: XCTestCase { // CHECK: Test Case 'ExpectationsTestCase.test_combiningInverseAndStandardExpectationsWithOrderingEnforcement' passed \(\d+\.\d+ seconds\) func test_combiningInverseAndStandardExpectationsWithOrderingEnforcement() { var a, b, c: XCTestExpectation - var start: CFAbsoluteTime + var start: TimeInterval a = XCTestExpectation(description: "a") a.isInverted = true @@ -431,7 +429,7 @@ class ExpectationsTestCase: XCTestCase { let outerWaiter = XCTWaiter(delegate: self) let outerExpectation = XCTestExpectation(description: "outer") - var outerExpectationFulfillTime = CFAbsoluteTime(0) + var outerExpectationFulfillTime = TimeInterval(0) RunLoop.main.perform { RunLoop.main.perform { outerExpectationFulfillTime = Date.timeIntervalSinceReferenceDate @@ -551,6 +549,28 @@ class ExpectationsTestCase: XCTestCase { RunLoop.main.run(until: Date() + 1) } +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsAsync' passed \(\d+\.\d+ seconds\) + func test_waitForExpectationsAsync() async { + // Basic check that waitForExpectations() is functional when used with the + // await keyword in an async function. + let expectation = self.expectation(description: "foo") + expectation.fulfill() + await self.waitForExpectations(timeout: 0.0) + } + +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' passed \(\d+\.\d+ seconds\) + func test_waitForExpectationsFromMainActor() async { + await MainActor.run { + // Basic check that waitForExpectations() is functional and does not need + // the await keyword when used from a main-actor-isolated test function. + let expectation = self.expectation(description: "foo") + expectation.fulfill() + self.waitForExpectations(timeout: 0.0) + } + } + static var allTests = { return [ ("test_waitingForAnUnfulfilledExpectation_fails", test_waitingForAnUnfulfilledExpectation_fails), @@ -603,15 +623,19 @@ class ExpectationsTestCase: XCTestCase { ("test_expectationCreationOnSecondaryThread", test_expectationCreationOnSecondaryThread), ("test_expectationCreationWhileWaiting", test_expectationCreationWhileWaiting), ("test_runLoopInsideDispatch", test_runLoopInsideDispatch), + + // waitForExpectations() + @MainActor + ("test_waitForExpectationsAsync", asyncTest(test_waitForExpectationsAsync)), + ("test_waitForExpectationsFromMainActor", asyncTest(test_waitForExpectationsFromMainActor)), ] }() } // CHECK: Test Suite 'ExpectationsTestCase' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: \t Executed 35 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed 37 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds XCTMain([testCase(ExpectationsTestCase.allTests)]) // CHECK: Test Suite '.*\.xctest' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: \t Executed 35 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed 37 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds // CHECK: Test Suite 'All tests' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: \t Executed 35 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed 37 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds diff --git a/Tests/Functional/lit.cfg b/Tests/Functional/lit.cfg index 78d6e1d9..d48a5129 100644 --- a/Tests/Functional/lit.cfg +++ b/Tests/Functional/lit.cfg @@ -99,6 +99,7 @@ else: '-L', os.path.join(foundation_dir, 'lib'), '-I', foundation_dir, '-I', os.path.join(foundation_dir, 'swift'), + '-I', os.path.join(foundation_dir, '_CModulesForClients'), '-Xcc', '-F', '-Xcc', foundation_dir, ]) diff --git a/XCTest.xcodeproj/project.pbxproj b/XCTest.xcodeproj/project.pbxproj index 71f8b1f3..27a6a5f2 100644 --- a/XCTest.xcodeproj/project.pbxproj +++ b/XCTest.xcodeproj/project.pbxproj @@ -513,7 +513,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 10.13; "OTHER_LDFLAGS[sdk=macosx*]" = ( "-framework", SwiftFoundation, @@ -538,7 +538,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 10.13; "OTHER_LDFLAGS[sdk=macosx*]" = ( "-framework", SwiftFoundation, diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 373c0fec..cabd5d11 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -12,6 +12,8 @@ function(get_swift_host_arch result_var_name) set("${result_var_name}" "arm64" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "aarch64") set("${result_var_name}" "aarch64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ARM64") + set("${result_var_name}" "aarch64" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64") set("${result_var_name}" "powerpc64" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64le") @@ -34,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()