From ba60a44dc6b0009f0da4e51037105e48a92c8a55 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 26 Jun 2023 08:37:42 -0400 Subject: [PATCH 01/27] Expectation handlers need to be @Sendable. #454 --- .../Public/Asynchronous/XCTNSNotificationExpectation.swift | 2 +- .../XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift index 573c6c270..dc8678047 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift @@ -19,7 +19,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") diff --git a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift index 08d0cf26b..b41bca147 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift @@ -18,7 +18,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") From a7a07a241a32b351436b4e2480a18df9d740ad20 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 25 Jun 2023 11:45:51 -0400 Subject: [PATCH 02/27] Add @MainActor to waitForExpectations(timeout:handler:) #428 --- .../XCTestCase+Asynchronous.swift | 2 +- Sources/XCTest/Public/XCTestCase.swift | 2 +- .../Asynchronous/Expectations/main.swift | 43 +++++++++++++++++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift index 7b1225327..b9935ff72 100644 --- a/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift +++ b/Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift @@ -41,7 +41,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 { diff --git a/Sources/XCTest/Public/XCTestCase.swift b/Sources/XCTest/Public/XCTestCase.swift index aadfecc79..4d734cd89 100644 --- a/Sources/XCTest/Public/XCTestCase.swift +++ b/Sources/XCTest/Public/XCTestCase.swift @@ -48,7 +48,7 @@ open class XCTestCase: XCTest { return 1 } - // FIXME: Once `waitForExpectations(timeout:...handler:)` gains `@MainActor`, this may be able to add it as well. + @MainActor internal var currentWaiter: XCTWaiter? /// The set of expectations made upon this test case. diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index 6ebb3352f..f3566da01 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -551,7 +551,37 @@ class ExpectationsTestCase: XCTestCase { RunLoop.main.run(until: Date() + 1) } - static var allTests = { +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' failed \(\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' failed \(\d+\.\d+ seconds\) + @MainActor func test_waitForExpectationsFromMainActor() { + // 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) + } + +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' failed \(\d+\.\d+ seconds\) + @MainActor func test_waitForExpectationsFromMainActor_async() async { + // 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: [(String, (ExpectationsTestCase) -> () throws -> Void)] = { return [ ("test_waitingForAnUnfulfilledExpectation_fails", test_waitingForAnUnfulfilledExpectation_fails), ("test_waitingForUnfulfilledExpectations_outputsAllExpectations_andFails", test_waitingForUnfulfilledExpectations_outputsAllExpectations_andFails), @@ -603,15 +633,20 @@ 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($0) }), + ("test_waitForExpectationsFromMainActor_async", asyncTest { test_waitForExpectationsFromMainActor_async($0) }), ] }() } // 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 38 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 38 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 38 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds From 5607291b60f5af473337a0b682b45cbfa5e374e5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 26 Jun 2023 14:33:29 -0400 Subject: [PATCH 03/27] Fix typo in CHECK --- Tests/Functional/Asynchronous/Expectations/main.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index f3566da01..6c1cb8b9b 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -551,8 +551,8 @@ class ExpectationsTestCase: XCTestCase { RunLoop.main.run(until: Date() + 1) } -// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' failed \(\d+\.\d+ seconds\) +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsAsync' failed \(\d+\.\d+ seconds\) func test_waitForExpectationsAsync() async { // Basic check that waitForExpectations() is functional when used with the // await keyword in an async function. From 33f65a582db36447bd9a7120a4ec849079e5a15a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 26 Jun 2023 14:52:46 -0400 Subject: [PATCH 04/27] Fix whitespace and try asyncTest() directly again to see if the compiler is happier --- .../Asynchronous/Expectations/main.swift | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index 6c1cb8b9b..19ecb73bc 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -554,34 +554,34 @@ class ExpectationsTestCase: XCTestCase { // CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ // CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsAsync' failed \(\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) + // 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' failed \(\d+\.\d+ seconds\) @MainActor func test_waitForExpectationsFromMainActor() { - // 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) + // 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) } // CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ // CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' failed \(\d+\.\d+ seconds\) @MainActor func test_waitForExpectationsFromMainActor_async() async { - // 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) + // 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: [(String, (ExpectationsTestCase) -> () throws -> Void)] = { + static var allTests = { return [ ("test_waitingForAnUnfulfilledExpectation_fails", test_waitingForAnUnfulfilledExpectation_fails), ("test_waitingForUnfulfilledExpectations_outputsAllExpectations_andFails", test_waitingForUnfulfilledExpectations_outputsAllExpectations_andFails), @@ -636,8 +636,8 @@ class ExpectationsTestCase: XCTestCase { // waitForExpectations() + @MainActor ("test_waitForExpectationsAsync", asyncTest(test_waitForExpectationsAsync)), - ("test_waitForExpectationsFromMainActor", asyncTest { test_waitForExpectationsFromMainActor($0) }), - ("test_waitForExpectationsFromMainActor_async", asyncTest { test_waitForExpectationsFromMainActor_async($0) }), + ("test_waitForExpectationsFromMainActor", asyncTest(test_waitForExpectationsFromMainActor)), + ("test_waitForExpectationsFromMainActor_async", asyncTest(test_waitForExpectationsFromMainActor_async)), ] }() } From 6f50082f8bc44cdce7871f78a549e7f9a9473456 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 26 Jun 2023 16:18:58 -0400 Subject: [PATCH 05/27] Disable @MainActor test functions due to SILGen crash. --- Tests/Functional/Asynchronous/Expectations/main.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index 19ecb73bc..f023716b2 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -636,17 +636,18 @@ class ExpectationsTestCase: XCTestCase { // waitForExpectations() + @MainActor ("test_waitForExpectationsAsync", asyncTest(test_waitForExpectationsAsync)), - ("test_waitForExpectationsFromMainActor", asyncTest(test_waitForExpectationsFromMainActor)), - ("test_waitForExpectationsFromMainActor_async", asyncTest(test_waitForExpectationsFromMainActor_async)), + // Disabled due to a SILGen crash: + // ("test_waitForExpectationsFromMainActor", asyncTest(test_waitForExpectationsFromMainActor)), + // ("test_waitForExpectationsFromMainActor_async", asyncTest(test_waitForExpectationsFromMainActor_async)), ] }() } // CHECK: Test Suite 'ExpectationsTestCase' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: \t Executed 38 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed 36 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 38 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed 36 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 38 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed 36 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds From e889f206268fce3df9cd9886d62c50a99fc17d95 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 26 Jun 2023 17:25:33 -0400 Subject: [PATCH 06/27] Oops, wrong checks --- Tests/Functional/Asynchronous/Expectations/main.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index f023716b2..285eb7f6d 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -552,7 +552,7 @@ class ExpectationsTestCase: XCTestCase { } // CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsAsync' failed \(\d+\.\d+ seconds\) +// 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. @@ -562,7 +562,7 @@ class ExpectationsTestCase: XCTestCase { } // CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' failed \(\d+\.\d+ seconds\) +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor' passed \(\d+\.\d+ seconds\) @MainActor func test_waitForExpectationsFromMainActor() { // Basic check that waitForExpectations() is functional and does not need // the await keyword when used from a main-actor-isolated test function. @@ -572,7 +572,7 @@ class ExpectationsTestCase: XCTestCase { } // CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' failed \(\d+\.\d+ seconds\) +// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' passed \(\d+\.\d+ seconds\) @MainActor func test_waitForExpectationsFromMainActor_async() async { // Basic check that waitForExpectations() is functional and does not need // the await keyword when used from a main-actor-isolated test function. From a1bdcfe1514beb0814c5c893005a39e87872bed1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 26 Jun 2023 18:41:35 -0400 Subject: [PATCH 07/27] Reenable one of the @MainActor tests and remove the other as redundant --- .../Asynchronous/Expectations/main.swift | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index 285eb7f6d..bcf420d3a 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -563,22 +563,14 @@ class ExpectationsTestCase: XCTestCase { // 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\) - @MainActor func test_waitForExpectationsFromMainActor() { - // 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) - } - -// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: Test Case 'ExpectationsTestCase.test_waitForExpectationsFromMainActor_async' passed \(\d+\.\d+ seconds\) - @MainActor func test_waitForExpectationsFromMainActor_async() async { - // 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) + 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 = { @@ -636,9 +628,7 @@ class ExpectationsTestCase: XCTestCase { // waitForExpectations() + @MainActor ("test_waitForExpectationsAsync", asyncTest(test_waitForExpectationsAsync)), - // Disabled due to a SILGen crash: - // ("test_waitForExpectationsFromMainActor", asyncTest(test_waitForExpectationsFromMainActor)), - // ("test_waitForExpectationsFromMainActor_async", asyncTest(test_waitForExpectationsFromMainActor_async)), + ("test_waitForExpectationsFromMainActor", asyncTest(test_waitForExpectationsFromMainActor)), ] }() } From 333c1eacc29989fa75176cf16deeefb8e25ec2a5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 27 Jun 2023 09:49:11 -0400 Subject: [PATCH 08/27] Fix count in final checks. --- Tests/Functional/Asynchronous/Expectations/main.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index bcf420d3a..8d0b15998 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -633,11 +633,11 @@ class ExpectationsTestCase: XCTestCase { }() } // CHECK: Test Suite 'ExpectationsTestCase' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: \t Executed 36 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 36 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 36 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 From 543e0d878a1b78652352ff9b2b6cf362ccc5ac02 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 5 Jul 2023 11:38:21 -0400 Subject: [PATCH 09/27] Bump the macOS deployment target to 10.13 in the Xcode project. --- XCTest.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/XCTest.xcodeproj/project.pbxproj b/XCTest.xcodeproj/project.pbxproj index 71f8b1f39..27a6a5f22 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, From 035f8d4cb70781dfb5e582ac649d872bb5622d7c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 5 Oct 2023 14:17:59 -0400 Subject: [PATCH 10/27] Remove outdated documentation for LinuxMain.swift. This PR removes a section from the repo's readme that describes how to set up XCTest on Linux. SwiftPM automatically handles this these days, so the section is obsolete and can be removed. Resolves rdar://114360933. --- README.md | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/README.md b/README.md index 3216bbdde..1775ab1fe 100644 --- a/README.md +++ b/README.md @@ -19,42 +19,6 @@ The Swift Package Manager integrates directly with XCTest to provide a streamlin 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. - ### Standalone Command Line Usage When used by itself, without SwiftPM, this version of XCTest does not use the external `xctest` CLI test runner included with Xcode to run tests. Instead, you must create your own executable which links `libXCTest.so`, and in your `main.swift`, invoke the `XCTMain` function with an array of the tests from all `XCTestCase` subclasses that you wish to run, wrapped by the `testCase` helper function. For example: From 4e332248d7b545684efe809e00b2a94d9fc60397 Mon Sep 17 00:00:00 2001 From: Evan Wilde Date: Fri, 6 Oct 2023 12:35:03 -0700 Subject: [PATCH 11/27] Stop importing CF as a framework Frameworks don't exist off of Darwin resulting in link failures in this test. The CF modulemap declares CF as a framework everywhere, which is wrong, but importing CF from swift-corelibs-foundation is also equally wrong, which this test does as well, so there's a lot of wrongness. To get the rebranch working, we need to stop importing CoreFoundation as a framework in this test, or disable the test on non-darwin platforms. Given that corelibs-xctest is for non-Darwin platforms, that seems like the wrong course of action. --- Tests/Functional/Asynchronous/Expectations/main.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index 8d0b15998..da8e307b2 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -1,4 +1,4 @@ -// RUN: %{swiftc} %s -o %T/Asynchronous +// RUN: %{swiftc} -Xfrontend -disable-autolink-framework -Xfrontend CoreFoundation %s -o %T/Asynchronous // RUN: %T/Asynchronous > %t || true // RUN: %{xctest_checker} %t %s From 0a568edd68f7cd63bdff8260f70abd679b56f757 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 24 Oct 2023 16:10:19 -0400 Subject: [PATCH 12/27] Make `XCTMain()` return its exit code instead of calling `exit()`. Right now, `XCTMain()` returns `Never` and calls `exit()` to report its status. This makes it difficult (if not outright impossible) to do any post-test cleanup work like writing an event log or other cleanup. This PR adds a new overload of `XCTMain()` that instead returns the intended exit code of the process (as `CInt`, since process exit codes passed to `exit()` are always of type `int` in C.) The new overload is, for now, disfavoured until we can teach SwiftPM about it. Once we can (in a subsequent PR), we'll mark the _old_ one disfavored and deprecated. --- Sources/XCTest/Public/XCTestMain.swift | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index f95aac65b..64777d10a 100644 --- a/Sources/XCTest/Public/XCTestMain.swift +++ b/Sources/XCTest/Public/XCTestMain.swift @@ -59,19 +59,13 @@ /// /// - 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) -} - -public func XCTMain(_ testCases: [XCTestCaseEntry], arguments: [String]) -> Never { - XCTMain(testCases, arguments: arguments, observers: [PrintObserver()]) -} - +@_disfavoredOverload public func XCTMain( _ testCases: [XCTestCaseEntry], - arguments: [String], - observers: [XCTestObservation] -) -> Never { + arguments: [String] = CommandLine.arguments, + observers: [XCTestObservation]? = nil +) -> CInt { + let observers = observers ?? [PrintObserver()] let testBundle = Bundle.main let executionMode = ArgumentParser(arguments: arguments).executionMode @@ -99,10 +93,10 @@ public func XCTMain( switch executionMode { case .list(type: .humanReadable): TestListing(testSuite: rootTestSuite).printTestList() - exit(EXIT_SUCCESS) + return EXIT_SUCCESS case .list(type: .json): TestListing(testSuite: rootTestSuite).printTestJSON() - exit(EXIT_SUCCESS) + return EXIT_SUCCESS case let .help(invalidOption): if let invalid = invalidOption { let errMsg = "Error: Invalid option \"\(invalid)\"\n" @@ -133,7 +127,7 @@ public func XCTMain( > \(exeName) \(sampleTests) """) - exit(invalidOption == nil ? EXIT_SUCCESS : EXIT_FAILURE) + return invalidOption == nil ? EXIT_SUCCESS : EXIT_FAILURE case .run(selectedTestNames: _): // Add a test observer that prints test progress to stdout. let observationCenter = XCTestObservationCenter.shared @@ -145,6 +139,15 @@ public func XCTMain( rootTestSuite.run() observationCenter.testBundleDidFinish(testBundle) - exit(rootTestSuite.testRun!.totalFailureCount == 0 ? EXIT_SUCCESS : EXIT_FAILURE) + 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] = CommandLine.arguments, + observers: [XCTestObservation]? = nil +) -> Never { + exit(XCTMain(testCases, arguments: arguments, observers: observers) as CInt) +} From efdd66fb7bcd1cb211fec5344645dc2d58792adf Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 24 Oct 2023 16:47:44 -0400 Subject: [PATCH 13/27] Restore existing overloads for source compatibility's sake and clean up the documentation --- Sources/XCTest/Public/XCTestMain.swift | 34 ++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/Sources/XCTest/Public/XCTestMain.swift b/Sources/XCTest/Public/XCTestMain.swift index 64777d10a..33572e723 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,15 +49,26 @@ /// // 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` +/// - 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. @_disfavoredOverload public func XCTMain( _ testCases: [XCTestCaseEntry], @@ -143,11 +153,21 @@ public func XCTMain( } } +// @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] = CommandLine.arguments, - observers: [XCTestObservation]? = nil + arguments: [String], + observers: [XCTestObservation] ) -> Never { exit(XCTMain(testCases, arguments: arguments, observers: observers) as CInt) } From ff2e043a1e0532a21584d27d4c8e1a447618c14e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Nov 2023 12:59:42 -0500 Subject: [PATCH 14/27] Don't use Core Foundation in tests. This PR removes a dependency on Core Foundation in a couple of our tests. The dependency is unnecessary and causes linkage issues on Windows per @etcwilde. Resolves rdar://116586683. --- Tests/Functional/Asynchronous/Expectations/main.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/Functional/Asynchronous/Expectations/main.swift b/Tests/Functional/Asynchronous/Expectations/main.swift index da8e307b2..4772c382c 100644 --- a/Tests/Functional/Asynchronous/Expectations/main.swift +++ b/Tests/Functional/Asynchronous/Expectations/main.swift @@ -1,4 +1,4 @@ -// RUN: %{swiftc} -Xfrontend -disable-autolink-framework -Xfrontend CoreFoundation %s -o %T/Asynchronous +// RUN: %{swiftc} %s -o %T/Asynchronous // RUN: %T/Asynchronous > %t || true // RUN: %{xctest_checker} %t %s @@ -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 From 2bc0f37b1fc7a5da996ca0d5fe58696dc533a028 Mon Sep 17 00:00:00 2001 From: Hiroshi Yamauchi Date: Tue, 13 Feb 2024 10:25:06 -0800 Subject: [PATCH 15/27] Support Windows ARM64 builds --- cmake/modules/SwiftSupport.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 373c0fec1..3c9dbc5f0 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") From e7ea52524a519bda576b4fe8cfdd0a19f6998583 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 24 Feb 2024 11:17:15 +0000 Subject: [PATCH 16/27] 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 17/27] 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 18/27] 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 19/27] 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 20/27] 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 21/27] 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 22/27] 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 23/27] 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 From 6cb8b12d326ab9984926b5a38ebee4a24fef9752 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Mon, 24 Jun 2024 11:52:43 -0700 Subject: [PATCH 24/27] Add new include path into Foundation when building tests --- Tests/Functional/lit.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/Functional/lit.cfg b/Tests/Functional/lit.cfg index 78d6e1d97..a59de69f5 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, ]) From 1bf025542b4ec836a9fc11a894c2d7e9bb8ab855 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Mon, 24 Jun 2024 12:52:12 -0700 Subject: [PATCH 25/27] Fix build failure --- Tests/Functional/lit.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Functional/lit.cfg b/Tests/Functional/lit.cfg index a59de69f5..d48a51296 100644 --- a/Tests/Functional/lit.cfg +++ b/Tests/Functional/lit.cfg @@ -99,7 +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', + '-I', os.path.join(foundation_dir, '_CModulesForClients'), '-Xcc', '-F', '-Xcc', foundation_dir, ]) From 1c66d4d368173d32cf46a3b186d28a7bf0cb6ce3 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 25 Jun 2024 04:48:58 -0700 Subject: [PATCH 26/27] Update swift-syntax links to swiftlang/swift-syntax --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1775ab1fe..ad6a75830 100644 --- a/README.md +++ b/README.md @@ -17,7 +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). +For more information about using XCTest with SwiftPM, see its [documentation](https://github.com/swiftlang/swift-package-manager). ### Standalone Command Line Usage From 9512507b02219360f05031ad08c25b51664299de Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Wed, 26 Jun 2024 13:59:56 +0100 Subject: [PATCH 27/27] [Linux] Enable build IDs. We should use build IDs on Linux so that we can identify the built artefacts, and also so that we can match them up with debug information should we choose to separate it. rdar://130582882 --- CMakeLists.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 14ecd0fe7..7f23844ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,14 @@ 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 @@ -79,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()