Skip to content

Commit 56501e7

Browse files
authored
Merge pull request #340 from stmontgomery/montgomery/expectation-the-unexpected
Support XCTestExpectation creation APIs on XCTestCase from non-main threads
2 parents a787343 + 04e05b5 commit 56501e7

File tree

3 files changed

+51
-8
lines changed

3 files changed

+51
-8
lines changed

Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public extension XCTestCase {
4141
/// these environments. To ensure compatibility of tests between
4242
/// swift-corelibs-xctest and Apple XCTest, it is not recommended to pass
4343
/// explicit values for `file` and `line`.
44+
// 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`.
4445
func waitForExpectations(timeout: TimeInterval, file: StaticString = #file, line: Int = #line, handler: XCWaitCompletionHandler? = nil) {
4546
precondition(Thread.isMainThread, "\(#function) must be called on the main thread")
4647
if currentWaiter != nil {
@@ -58,7 +59,7 @@ public extension XCTestCase {
5859

5960
currentWaiter = nil
6061

61-
cleanUpExpectations()
62+
cleanUpExpectations(expectations)
6263

6364
// The handler is invoked regardless of whether the test passed.
6465
if let handler = handler {

Sources/XCTest/Public/XCTestCase.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ open class XCTestCase: XCTest {
4848
return 1
4949
}
5050

51+
// FIXME: Once `waitForExpectations(timeout:...handler:)` gains `@MainActor`, this may be able to add it as well.
5152
internal var currentWaiter: XCTWaiter?
5253

5354
/// The set of expectations made upon this test case.
@@ -60,9 +61,6 @@ open class XCTestCase: XCTest {
6061
}
6162

6263
internal func addExpectation(_ expectation: XCTestExpectation) {
63-
precondition(Thread.isMainThread, "\(#function) must be called on the main thread")
64-
precondition(currentWaiter == nil, "API violation - creating an expectation while already in waiting mode.")
65-
6664
XCTWaiter.subsystemQueue.sync {
6765
_allExpectations.append(expectation)
6866
}
@@ -99,7 +97,10 @@ open class XCTestCase: XCTest {
9997
XCTCurrentTestCase = self
10098
testRun.start()
10199
invokeTest()
102-
failIfExpectationsNotWaitedFor(_allExpectations)
100+
101+
let allExpectations = XCTWaiter.subsystemQueue.sync { _allExpectations }
102+
failIfExpectationsNotWaitedFor(allExpectations)
103+
103104
testRun.stop()
104105
XCTCurrentTestCase = nil
105106
}

Tests/Functional/Asynchronous/Expectations/main.swift

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,45 @@ class ExpectationsTestCase: XCTestCase {
482482
waitForExpectations(timeout: 1)
483483
}
484484

485+
// CHECK: Test Case 'ExpectationsTestCase.test_expectationCreationOnSecondaryThread' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
486+
// CHECK: Test Case 'ExpectationsTestCase.test_expectationCreationOnSecondaryThread' passed \(\d+\.\d+ seconds\)
487+
func test_expectationCreationOnSecondaryThread() {
488+
DispatchQueue.global().sync {
489+
let foo = self.expectation(description: "foo")
490+
foo.fulfill()
491+
}
492+
waitForExpectations(timeout: 1)
493+
494+
// Also test using an "explicit" wait API (which accepts an array of expectations) to wait on an
495+
// expectation created on a non-main thread.
496+
let foo: XCTestExpectation = DispatchQueue.global().sync {
497+
let foo = self.expectation(description: "foo")
498+
foo.fulfill()
499+
return foo
500+
}
501+
wait(for: [foo], timeout: 1)
502+
}
503+
504+
// CHECK: Test Case 'ExpectationsTestCase.test_expectationCreationWhileWaiting' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
505+
// CHECK: Test Case 'ExpectationsTestCase.test_expectationCreationWhileWaiting' passed \(\d+\.\d+ seconds\)
506+
func test_expectationCreationWhileWaiting() {
507+
let foo = expectation(description: "foo")
508+
509+
DispatchQueue.main.async {
510+
let bar = self.expectation(description: "bar")
511+
bar.fulfill()
512+
513+
foo.fulfill()
514+
}
515+
516+
// Waits on `foo` with a generous timeout, since we want to ensure we don't proceed until the dispatched block has completed.
517+
waitForExpectations(timeout: 100)
518+
519+
// Waits on `bar` with a zero timeout, since by this point we expect the dispatched block to have completed and thus
520+
// the second expectation should already be fulfilled.
521+
waitForExpectations(timeout: 0)
522+
}
523+
485524
// CHECK: Test Case 'ExpectationsTestCase.test_runLoopInsideDispatch' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
486525
// CHECK: .*[/\\]Tests[/\\]Functional[/\\]Asynchronous[/\\]Expectations[/\\]main.swift:[[@LINE+8]]: error: ExpectationsTestCase.test_runLoopInsideDispatch : Asynchronous wait failed - Exceeded timeout of 0.5 seconds, with unfulfilled expectations: foo
487526
// CHECK: Test Case 'ExpectationsTestCase.test_runLoopInsideDispatch' failed \(\d+\.\d+ seconds\)
@@ -545,16 +584,18 @@ class ExpectationsTestCase: XCTestCase {
545584

546585
// Regressions
547586
("test_fulfillmentOnSecondaryThread", test_fulfillmentOnSecondaryThread),
587+
("test_expectationCreationOnSecondaryThread", test_expectationCreationOnSecondaryThread),
588+
("test_expectationCreationWhileWaiting", test_expectationCreationWhileWaiting),
548589
("test_runLoopInsideDispatch", test_runLoopInsideDispatch),
549590
]
550591
}()
551592
}
552593
// CHECK: Test Suite 'ExpectationsTestCase' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
553-
// CHECK: \t Executed 32 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
594+
// CHECK: \t Executed 34 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
554595

555596
XCTMain([testCase(ExpectationsTestCase.allTests)])
556597

557598
// CHECK: Test Suite '.*\.xctest' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
558-
// CHECK: \t Executed 32 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
599+
// CHECK: \t Executed 34 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
559600
// CHECK: Test Suite 'All tests' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
560-
// CHECK: \t Executed 32 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
601+
// CHECK: \t Executed 34 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds

0 commit comments

Comments
 (0)