Skip to content

Commit dcaa306

Browse files
committed
Over-fulfilling XCTestExpectation records test failure even when assertForOverFulfill is disabled
This fixes an issue where calling `XCTestExpectation.fulfill()` more times than expected will still record a test failure, even if its `assertForOverFulfill` property is set to `false` (the default). In this Corelibs version of XCTest, `assertForOverFulfill` is only guarding a `fatalError`, but we still unconditionally record a test failure which causes this symptom. This means the behavior is different than in the Xcode copy of XCTest, where if `assertForOverFulfill` is false, neither an NSAssert exception nor a test failure occurs. Swift doesn't support exceptions, so I believe the unconditional test failure was added here in the past in an attempt to emulate that behavior. The fix here removes the `fatalError()` entirely, and leaves the test failure-recording logic but makes it conditional on `assertForOverFulfill` being true. The behavior will still differ from Xcode's copy of XCTest, but I think it's justifiable and closer to what users expect. The use of NSAssert in the (ObjC) Xcode copy of XCTest was largely an attempt to get better diagnostics when an over-fulfill occurred by capturing the backtrace of that fulfillment. This is especially important with XCTestExpectations, since they are meant for testing async conditions, and an over-fulfillment could occur after their test has completed, so without a backtrace or some better state tracking the test failure could be misattributed to a subsequent test once it becomes "current". Corelibs XCTest doesn't suffer from this diagnostics problem as much since it already captures `file:line:` in `fulfill()`. Moreover, there have been requests to change that behavior in ObjC XCTest and use a "plain" test failure everywhere instead of NSAssert. So this PR gets us closer to that goal, starting here in Corelibs, by removing the fatalError and only leaving the existing test failure-recording behavior. To fully realize it and ensure these test failures are attributed to the correct test, we'll need to make further internal state tracking enhancements (63874139), but for now this seems like a good incremental step, considering that Corelibs already performed this test failure recording. I've also added a new test and modified an existing one. rdar://problem/62202297
1 parent c21ac84 commit dcaa306

File tree

3 files changed

+49
-41
lines changed

3 files changed

+49
-41
lines changed

Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -259,19 +259,12 @@ open class XCTestExpectation {
259259
// expectations have completed. Similarly, this should cause an
260260
// error as well.
261261

262-
if queue_isFulfilled {
263-
// FIXME: No regression tests exist for this feature. We may break it
264-
// without ever realizing (similar to `continueAfterFailure`).
265-
if _assertForOverFulfill {
266-
fatalError("API violation - multiple calls made to fulfill() for \(queue_expectationDescription)")
267-
}
268-
269-
if let testCase = XCTCurrentTestCase {
270-
testCase.recordFailure(
271-
description: "API violation - multiple calls made to XCTestExpectation.fulfill() for \(queue_expectationDescription).",
272-
at: sourceLocation,
273-
expected: false)
274-
}
262+
if queue_isFulfilled, _assertForOverFulfill, let testCase = XCTCurrentTestCase {
263+
testCase.recordFailure(
264+
description: "API violation - multiple calls made to XCTestExpectation.fulfill() for \(queue_expectationDescription).",
265+
at: sourceLocation,
266+
expected: false)
267+
275268
return nil
276269
}
277270

Tests/Functional/Asynchronous/Expectations/main.swift

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,39 @@ class ExpectationsTestCase: XCTestCase {
338338
waitForExpectations(timeout: 0.2)
339339
}
340340

341+
// PRAGMA MARK: - assertForOverFulfill
342+
343+
// CHECK: Test Case 'ExpectationsTestCase.test_assertForOverfulfill_disabled' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
344+
// CHECK: Test Case 'ExpectationsTestCase.test_assertForOverfulfill_disabled' passed \(\d+\.\d+ seconds\)
345+
func test_assertForOverfulfill_disabled() {
346+
let foo = XCTestExpectation(description: "foo")
347+
XCTAssertFalse(foo.assertForOverFulfill, "assertForOverFulfill should be disabled by default")
348+
foo.fulfill()
349+
foo.fulfill()
350+
}
351+
352+
// CHECK: Test Case 'ExpectationsTestCase.test_assertForOverfulfill_failure' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
353+
// CHECK: .*[/\\]Tests[/\\]Functional[/\\]Asynchronous[/\\]Expectations[/\\]main.swift:[[@LINE+7]]: error: ExpectationsTestCase.test_assertForOverfulfill_failure : API violation - multiple calls made to XCTestExpectation.fulfill\(\) for rob.
354+
// CHECK: .*[/\\]Tests[/\\]Functional[/\\]Asynchronous[/\\]Expectations[/\\]main.swift:[[@LINE+16]]: error: ExpectationsTestCase.test_assertForOverfulfill_failure : API violation - multiple calls made to XCTestExpectation.fulfill\(\) for rob.
355+
// CHECK: Test Case 'ExpectationsTestCase.test_assertForOverfulfill_failure' failed \(\d+\.\d+ seconds\)
356+
func test_assertForOverfulfill_failure() {
357+
let expectation = self.expectation(description: "rob")
358+
expectation.assertForOverFulfill = true
359+
expectation.fulfill()
360+
expectation.fulfill()
361+
// FIXME: The behavior here is subtly different from Objective-C XCTest.
362+
// Objective-C XCTest would stop executing the test on the line
363+
// above, and so would not report a failure for this line below.
364+
// In total, it would highlight one line as a failure in this
365+
// test.
366+
//
367+
// swift-corelibs-xctest continues to execute the test, and so
368+
// highlights both the lines above and below as failures.
369+
// This should be fixed such that the behavior is identical.
370+
expectation.fulfill()
371+
self.waitForExpectations(timeout: 0.1)
372+
}
373+
341374
// PRAGMA MARK: - Interrupted Waiters
342375

343376
// Disabled due to non-deterministic ordering of XCTWaiterDelegate callbacks, see [SR-10034] and <rdar://problem/49123061>
@@ -497,6 +530,10 @@ class ExpectationsTestCase: XCTestCase {
497530
("test_countedConditionPassBeforeWaiting", test_countedConditionPassBeforeWaiting),
498531
("test_countedConditionFail", test_countedConditionFail),
499532

533+
// assertForOverFulfill
534+
("test_assertForOverfulfill_disabled", test_assertForOverfulfill_disabled),
535+
("test_assertForOverfulfill_failure", test_assertForOverfulfill_failure),
536+
500537
// Interrupted Waiters
501538
// ("test_outerWaiterTimesOut_InnerWaitersAreInterrupted", test_outerWaiterTimesOut_InnerWaitersAreInterrupted),
502539
("test_outerWaiterCompletes_InnerWaiterTimesOut", test_outerWaiterCompletes_InnerWaiterTimesOut),
@@ -513,11 +550,11 @@ class ExpectationsTestCase: XCTestCase {
513550
}()
514551
}
515552
// CHECK: Test Suite 'ExpectationsTestCase' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
516-
// CHECK: \t Executed 30 tests, with 14 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
553+
// CHECK: \t Executed 32 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
517554

518555
XCTMain([testCase(ExpectationsTestCase.allTests)])
519556

520557
// CHECK: Test Suite '.*\.xctest' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
521-
// CHECK: \t Executed 30 tests, with 14 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
558+
// CHECK: \t Executed 32 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
522559
// CHECK: Test Suite 'All tests' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
523-
// CHECK: \t Executed 30 tests, with 14 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
560+
// CHECK: \t Executed 32 tests, with 16 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds

Tests/Functional/Asynchronous/Misuse/main.swift

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,41 +28,19 @@ class MisuseTestCase: XCTestCase {
2828
self.waitForExpectations(timeout: 0.1)
2929
}
3030

31-
// CHECK: Test Case 'MisuseTestCase.test_whenExpectationIsFulfilledMultipleTimes_fails' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
32-
// CHECK: .*[/\\]Tests[/\\]Functional[/\\]Asynchronous[/\\]Misuse[/\\]main.swift:[[@LINE+6]]: error: MisuseTestCase.test_whenExpectationIsFulfilledMultipleTimes_fails : API violation - multiple calls made to XCTestExpectation.fulfill\(\) for rob.
33-
// CHECK: .*[/\\]Tests[/\\]Functional[/\\]Asynchronous[/\\]Misuse[/\\]main.swift:[[@LINE+15]]: error: MisuseTestCase.test_whenExpectationIsFulfilledMultipleTimes_fails : API violation - multiple calls made to XCTestExpectation.fulfill\(\) for rob.
34-
// CHECK: Test Case 'MisuseTestCase.test_whenExpectationIsFulfilledMultipleTimes_fails' failed \(\d+\.\d+ seconds\)
35-
func test_whenExpectationIsFulfilledMultipleTimes_fails() {
36-
let expectation = self.expectation(description: "rob")
37-
expectation.fulfill()
38-
expectation.fulfill()
39-
// FIXME: The behavior here is subtly different from Objective-C XCTest.
40-
// Objective-C XCTest would stop executing the test on the line
41-
// above, and so would not report a failure for this line below.
42-
// In total, it would highlight one line as a failure in this
43-
// test.
44-
//
45-
// swift-corelibs-xctest continues to execute the test, and so
46-
// highlights both the lines above and below as failures.
47-
// This should be fixed such that the behavior is identical.
48-
expectation.fulfill()
49-
self.waitForExpectations(timeout: 0.1)
50-
}
51-
5231
static var allTests = {
5332
return [
5433
("test_whenExpectationsAreMade_butNotWaitedFor_fails", test_whenExpectationsAreMade_butNotWaitedFor_fails),
5534
("test_whenNoExpectationsAreMade_butTheyAreWaitedFor_fails", test_whenNoExpectationsAreMade_butTheyAreWaitedFor_fails),
56-
("test_whenExpectationIsFulfilledMultipleTimes_fails", test_whenExpectationIsFulfilledMultipleTimes_fails),
5735
]
5836
}()
5937
}
6038
// CHECK: Test Suite 'MisuseTestCase' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
61-
// CHECK: \t Executed 3 tests, with 4 failures \(4 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
39+
// CHECK: \t Executed 2 tests, with 2 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
6240

6341
XCTMain([testCase(MisuseTestCase.allTests)])
6442

6543
// CHECK: Test Suite '.*\.xctest' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
66-
// CHECK: \t Executed 3 tests, with 4 failures \(4 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
44+
// CHECK: \t Executed 2 tests, with 2 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
6745
// CHECK: Test Suite 'All tests' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+
68-
// CHECK: \t Executed 3 tests, with 4 failures \(4 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
46+
// CHECK: \t Executed 2 tests, with 2 failures \(2 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds

0 commit comments

Comments
 (0)