Skip to content

Commit 237d97c

Browse files
authored
Merge pull request #228 from stmontgomery/XCTWaiter
[SR-7615] Implement XCTWaiter and missing XCTestExpectation APIs
2 parents 52d876b + f9217b9 commit 237d97c

26 files changed

+2027
-425
lines changed

CMakeLists.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ add_swift_library(XCTest
6060
Sources/XCTest/Private/PerformanceMeter.swift
6161
Sources/XCTest/Private/PrintObserver.swift
6262
Sources/XCTest/Private/ArgumentParser.swift
63-
Sources/XCTest/Private/XCPredicateExpectation.swift
63+
Sources/XCTest/Private/SourceLocation.swift
64+
Sources/XCTest/Private/WaiterManager.swift
6465
Sources/XCTest/Public/XCTestRun.swift
6566
Sources/XCTest/Public/XCTestMain.swift
6667
Sources/XCTest/Public/XCTestCase.swift
@@ -73,11 +74,10 @@ add_swift_library(XCTest
7374
Sources/XCTest/Public/XCTestObservationCenter.swift
7475
Sources/XCTest/Public/XCTestCase+Performance.swift
7576
Sources/XCTest/Public/XCTAssert.swift
76-
Sources/XCTest/Public/Asynchronous/XCTestCase+NotificationExpectation.swift
77-
Sources/XCTest/Public/Asynchronous/XCTestCase+PredicateExpectation.swift
78-
Sources/XCTest/Public/Asynchronous/XCPredicateExpectationHandler.swift
79-
Sources/XCTest/Public/Asynchronous/XCWaitCompletionHandler.swift
80-
Sources/XCTest/Public/Asynchronous/XCNotificationExpectationHandler.swift
77+
Sources/XCTest/Public/Asynchronous/XCTNSNotificationExpectation.swift
78+
Sources/XCTest/Public/Asynchronous/XCTNSPredicateExpectation.swift
79+
Sources/XCTest/Public/Asynchronous/XCTWaiter+Validation.swift
80+
Sources/XCTest/Public/Asynchronous/XCTWaiter.swift
8181
Sources/XCTest/Public/Asynchronous/XCTestCase+Asynchronous.swift
8282
Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift
8383
SWIFT_FLAGS
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2018 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
//
10+
// SourceLocation.swift
11+
//
12+
13+
internal struct SourceLocation {
14+
15+
typealias LineNumber = UInt
16+
17+
/// Represents an "unknown" source location, with default values, which may be used as a fallback
18+
/// when a real source location may not be known.
19+
static var unknown: SourceLocation = {
20+
return SourceLocation(file: "<unknown>", line: 0)
21+
}()
22+
23+
let file: String
24+
let line: LineNumber
25+
26+
init(file: String, line: LineNumber) {
27+
self.file = file
28+
self.line = line
29+
}
30+
31+
init(file: StaticString, line: LineNumber) {
32+
self.init(file: String(describing: file), line: line)
33+
}
34+
35+
init(file: String, line: Int) {
36+
self.init(file: file, line: LineNumber(line))
37+
}
38+
39+
init(file: StaticString, line: Int) {
40+
self.init(file: String(describing: file), line: LineNumber(line))
41+
}
42+
43+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2018 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
//
10+
// WaiterManager.swift
11+
//
12+
13+
internal protocol ManageableWaiter: AnyObject, Equatable {
14+
var isFinished: Bool { get }
15+
16+
// Invoked on `XCTWaiter.subsystemQueue`
17+
func queue_handleWatchdogTimeout()
18+
func queue_interrupt(for interruptingWaiter: Self)
19+
}
20+
21+
private protocol ManageableWaiterWatchdog {
22+
func cancel()
23+
}
24+
extension DispatchWorkItem: ManageableWaiterWatchdog {}
25+
26+
/// This class manages the XCTWaiter instances which are currently waiting on a particular thread.
27+
/// It facilitates "nested" waiters, allowing an outer waiter to interrupt inner waiters if it times
28+
/// out.
29+
internal final class WaiterManager<WaiterType: ManageableWaiter> {
30+
31+
/// The current thread's waiter manager. This is the only supported way to access an instance of
32+
/// this class, since each instance is bound to a particular thread and is only concerned with
33+
/// the XCTWaiters waiting on that thread.
34+
static var current: WaiterManager {
35+
let threadKey = "org.swift.XCTest.WaiterManager"
36+
37+
if let existing = Thread.current.threadDictionary[threadKey] as? WaiterManager {
38+
return existing
39+
} else {
40+
let manager = WaiterManager()
41+
Thread.current.threadDictionary[threadKey] = manager
42+
return manager
43+
}
44+
}
45+
46+
private struct ManagedWaiterDetails {
47+
let waiter: WaiterType
48+
let watchdog: ManageableWaiterWatchdog?
49+
}
50+
51+
private var managedWaiterStack = [ManagedWaiterDetails]()
52+
private weak var thread = Thread.current
53+
private let queue = DispatchQueue(label: "org.swift.XCTest.WaiterManager")
54+
55+
// Use `WaiterManager.current` to access the thread-specific instance
56+
private init() {}
57+
58+
deinit {
59+
assert(managedWaiterStack.isEmpty, "Waiters still registered when WaiterManager is deallocating.")
60+
}
61+
62+
func startManaging(_ waiter: WaiterType, timeout: TimeInterval) {
63+
guard let thread = thread else { fatalError("\(self) no longer belongs to a thread") }
64+
precondition(thread === Thread.current, "\(#function) called on wrong thread, must be called on \(thread)")
65+
66+
var alreadyFinishedOuterWaiter: WaiterType?
67+
68+
queue.sync {
69+
// To start managing `waiter`, first see if any existing, outer waiters have already finished,
70+
// because if one has, then `waiter` will be immediately interrupted before it begins waiting.
71+
alreadyFinishedOuterWaiter = managedWaiterStack.first(where: { $0.waiter.isFinished })?.waiter
72+
73+
let watchdog: ManageableWaiterWatchdog?
74+
if alreadyFinishedOuterWaiter == nil {
75+
// If there is no already-finished outer waiter, install a watchdog for `waiter`, and store it
76+
// alongside `waiter` so that it may be canceled if `waiter` finishes waiting within its allotted timeout.
77+
watchdog = WaiterManager.installWatchdog(for: waiter, timeout: timeout)
78+
} else {
79+
// If there is an already-finished outer waiter, no watchdog is needed for `waiter` because it will
80+
// be interrupted before it begins waiting.
81+
watchdog = nil
82+
}
83+
84+
// Add the waiter even if it's going to immediately be interrupted below to simplify the stack management
85+
let details = ManagedWaiterDetails(waiter: waiter, watchdog: watchdog)
86+
managedWaiterStack.append(details)
87+
}
88+
89+
if let alreadyFinishedOuterWaiter = alreadyFinishedOuterWaiter {
90+
XCTWaiter.subsystemQueue.async {
91+
waiter.queue_interrupt(for: alreadyFinishedOuterWaiter)
92+
}
93+
}
94+
}
95+
96+
func stopManaging(_ waiter: WaiterType) {
97+
guard let thread = thread else { fatalError("\(self) no longer belongs to a thread") }
98+
precondition(thread === Thread.current, "\(#function) called on wrong thread, must be called on \(thread)")
99+
100+
queue.sync {
101+
precondition(!managedWaiterStack.isEmpty, "Waiter stack was empty when requesting to stop managing: \(waiter)")
102+
103+
let expectedIndex = managedWaiterStack.index(before: managedWaiterStack.endIndex)
104+
let waiterDetails = managedWaiterStack[expectedIndex]
105+
guard waiter == waiterDetails.waiter else {
106+
fatalError("Top waiter on stack \(waiterDetails.waiter) is not equal to waiter to stop managing: \(waiter)")
107+
}
108+
109+
waiterDetails.watchdog?.cancel()
110+
managedWaiterStack.remove(at: expectedIndex)
111+
}
112+
}
113+
114+
private static func installWatchdog(for waiter: WaiterType, timeout: TimeInterval) -> ManageableWaiterWatchdog {
115+
// Use DispatchWorkItem instead of a basic closure since it can be canceled.
116+
let watchdog = DispatchWorkItem { [weak waiter] in
117+
waiter?.queue_handleWatchdogTimeout()
118+
}
119+
120+
let outerTimeoutSlop = TimeInterval(0.25)
121+
let deadline = DispatchTime.now() + timeout + outerTimeoutSlop
122+
XCTWaiter.subsystemQueue.asyncAfter(deadline: deadline, execute: watchdog)
123+
124+
return watchdog
125+
}
126+
127+
func queue_handleWatchdogTimeout(of waiter: WaiterType) {
128+
dispatchPrecondition(condition: .onQueue(XCTWaiter.subsystemQueue))
129+
130+
var waitersToInterrupt = [WaiterType]()
131+
132+
queue.sync {
133+
guard let indexOfWaiter = managedWaiterStack.firstIndex(where: { $0.waiter == waiter }) else {
134+
preconditionFailure("Waiter \(waiter) reported timed out but is not in the waiter stack \(managedWaiterStack)")
135+
}
136+
137+
waitersToInterrupt += managedWaiterStack[managedWaiterStack.index(after: indexOfWaiter)...].map { $0.waiter }
138+
}
139+
140+
for waiterToInterrupt in waitersToInterrupt.reversed() {
141+
waiterToInterrupt.queue_interrupt(for: waiter)
142+
}
143+
}
144+
145+
}

Sources/XCTest/Private/XCPredicateExpectation.swift

Lines changed: 0 additions & 52 deletions
This file was deleted.

Sources/XCTest/Public/Asynchronous/XCNotificationExpectationHandler.swift

Lines changed: 0 additions & 20 deletions
This file was deleted.

Sources/XCTest/Public/Asynchronous/XCPredicateExpectationHandler.swift

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)