Skip to content

Commit 256f54b

Browse files
Revert "Revert "[REVIEW] Add Async Support to XCTest""
This reverts commit 6569394. # Conflicts: # Sources/XCTest/Public/XCAbstractTest.swift
1 parent ad8f592 commit 256f54b

File tree

9 files changed

+370
-26
lines changed

9 files changed

+370
-26
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ xcuserdata
44
*.xcscmblueprint
55
.build/
66
Output/
7+
Tests/Functional/.lit_test_times.txt

CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ add_library(XCTest
2929
Sources/XCTest/Private/SourceLocation.swift
3030
Sources/XCTest/Private/WaiterManager.swift
3131
Sources/XCTest/Private/IgnoredErrors.swift
32+
Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift
3233
Sources/XCTest/Public/XCTestRun.swift
3334
Sources/XCTest/Public/XCTestMain.swift
3435
Sources/XCTest/Public/XCTestCase.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2021 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+
extension XCTestCase {
10+
11+
/// A class which encapsulates teardown blocks which are registered via the `addTeardownBlock(_:)` method.
12+
/// Supports async and sync throwing methods.
13+
final class TeardownBlocksState {
14+
15+
private var wasFinalized = false
16+
private var blocks: [() throws -> Void] = []
17+
18+
// We don't want to overload append(_:) below because of how Swift will implicitly promote sync closures to async closures,
19+
// which can unexpectedly change their semantics in difficult to track down ways.
20+
//
21+
// 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
22+
@available(macOS 12.0, *)
23+
func appendAsync(_ block: @Sendable @escaping () async throws -> Void) {
24+
self.append {
25+
try awaitUsingExpectation { try await block() }
26+
}
27+
}
28+
29+
func append(_ block: @escaping () throws -> Void) {
30+
XCTWaiter.subsystemQueue.sync {
31+
precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued")
32+
blocks.append(block)
33+
}
34+
}
35+
36+
func finalize() -> [() throws -> Void] {
37+
XCTWaiter.subsystemQueue.sync {
38+
precondition(wasFinalized == false, "API violation -- attempting to run teardown blocks after they've already run")
39+
wasFinalized = true
40+
return blocks
41+
}
42+
}
43+
}
44+
}

Sources/XCTest/Public/XCAbstractTest.swift

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ open class XCTest {
5252
perform(testRun!)
5353
}
5454

55+
/// Async setup method called before the invocation of `setUpWithError` for each test method in the class.
56+
@available(macOS 12.0, *)
57+
open func setUp() async throws {}
5558
/// Setup method called before the invocation of `setUp` and the test method
5659
/// for each test method in the class.
5760
open func setUpWithError() throws {}
@@ -68,6 +71,10 @@ open class XCTest {
6871
/// for each test method in the class.
6972
open func tearDownWithError() throws {}
7073

74+
/// Async teardown method which is called after the invocation of `tearDownWithError`
75+
/// for each test method in the class.
76+
@available(macOS 12.0, *)
77+
open func tearDown() async throws {}
7178
// FIXME: This initializer is required due to a Swift compiler bug on Linux.
7279
// It should be removed once the bug is fixed.
7380
public init() {}

Sources/XCTest/Public/XCTestCase.swift

+107-24
Original file line numberDiff line numberDiff line change
@@ -195,23 +195,23 @@ open class XCTestCase: XCTest {
195195
/// class.
196196
open class func tearDown() {}
197197

198-
private var teardownBlocks: [() -> Void] = []
199-
private var teardownBlocksDequeued: Bool = false
200-
private let teardownBlocksQueue: DispatchQueue = DispatchQueue(label: "org.swift.XCTest.XCTestCase.teardownBlocks")
198+
private let teardownBlocksState = TeardownBlocksState()
201199

202200
/// Registers a block of teardown code to be run after the current test
203201
/// method ends.
204202
open func addTeardownBlock(_ block: @escaping () -> Void) {
205-
teardownBlocksQueue.sync {
206-
precondition(!self.teardownBlocksDequeued, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued")
207-
self.teardownBlocks.append(block)
208-
}
203+
teardownBlocksState.append(block)
204+
}
205+
206+
/// Registers a block of teardown code to be run after the current test
207+
/// method ends.
208+
@available(macOS 12.0, *)
209+
public func addTeardownBlock(_ block: @Sendable @escaping () async throws -> Void) {
210+
teardownBlocksState.appendAsync(block)
209211
}
210212

211213
private func performSetUpSequence() {
212-
do {
213-
try setUpWithError()
214-
} catch {
214+
func handleErrorDuringSetUp(_ error: Error) {
215215
if error.xct_shouldRecordAsTestFailure {
216216
recordFailure(for: error)
217217
}
@@ -225,33 +225,60 @@ open class XCTestCase: XCTest {
225225
}
226226
}
227227

228+
do {
229+
if #available(macOS 12.0, *) {
230+
try awaitUsingExpectation {
231+
try await self.setUp()
232+
}
233+
}
234+
} catch {
235+
handleErrorDuringSetUp(error)
236+
}
237+
238+
do {
239+
try setUpWithError()
240+
} catch {
241+
handleErrorDuringSetUp(error)
242+
}
243+
228244
setUp()
229245
}
230246

231247
private func performTearDownSequence() {
248+
func handleErrorDuringTearDown(_ error: Error) {
249+
if error.xct_shouldRecordAsTestFailure {
250+
recordFailure(for: error)
251+
}
252+
}
253+
254+
func runTeardownBlocks() {
255+
for block in self.teardownBlocksState.finalize().reversed() {
256+
do {
257+
try block()
258+
} catch {
259+
handleErrorDuringTearDown(error)
260+
}
261+
}
262+
}
263+
232264
runTeardownBlocks()
233265

234266
tearDown()
235267

236268
do {
237269
try tearDownWithError()
238270
} catch {
239-
if error.xct_shouldRecordAsTestFailure {
240-
recordFailure(for: error)
241-
}
242-
}
243-
}
244-
245-
private func runTeardownBlocks() {
246-
let blocks = teardownBlocksQueue.sync { () -> [() -> Void] in
247-
self.teardownBlocksDequeued = true
248-
let blocks = self.teardownBlocks
249-
self.teardownBlocks = []
250-
return blocks
271+
handleErrorDuringTearDown(error)
251272
}
252273

253-
for block in blocks.reversed() {
254-
block()
274+
do {
275+
if #available(macOS 12.0, *) {
276+
try awaitUsingExpectation {
277+
try await self.tearDown()
278+
}
279+
}
280+
} catch {
281+
handleErrorDuringTearDown(error)
255282
}
256283
}
257284

@@ -292,3 +319,59 @@ private func test<T: XCTestCase>(_ testFunc: @escaping (T) -> () throws -> Void)
292319
try testFunc(testCase)()
293320
}
294321
}
322+
323+
@available(macOS 12.0, *)
324+
public func asyncTest<T: XCTestCase>(
325+
_ testClosureGenerator: @escaping (T) -> () async throws -> Void
326+
) -> (T) -> () throws -> Void {
327+
return { (testType: T) in
328+
let testClosure = testClosureGenerator(testType)
329+
return {
330+
try awaitUsingExpectation(testClosure)
331+
}
332+
}
333+
}
334+
335+
@available(macOS 12.0, *)
336+
func awaitUsingExpectation(
337+
_ closure: @escaping () async throws -> Void
338+
) throws -> Void {
339+
let expectation = XCTestExpectation(description: "async test completion")
340+
let thrownErrorWrapper = ThrownErrorWrapper()
341+
342+
Task {
343+
defer { expectation.fulfill() }
344+
345+
do {
346+
try await closure()
347+
} catch {
348+
thrownErrorWrapper.error = error
349+
}
350+
}
351+
352+
_ = XCTWaiter.wait(for: [expectation], timeout: asyncTestTimeout)
353+
354+
if let error = thrownErrorWrapper.error {
355+
throw error
356+
}
357+
}
358+
359+
private final class ThrownErrorWrapper: @unchecked Sendable {
360+
361+
private var _error: Error?
362+
363+
var error: Error? {
364+
get {
365+
XCTWaiter.subsystemQueue.sync { _error }
366+
}
367+
set {
368+
XCTWaiter.subsystemQueue.sync { _error = newValue }
369+
}
370+
}
371+
}
372+
373+
374+
// This time interval is set to a very large value due to their being no real native timeout functionality within corelibs-xctest.
375+
// With the introduction of async/await support, the framework now relies on XCTestExpectations internally to coordinate the addition async portions of setup and tear down.
376+
// This time interval is the timeout corelibs-xctest uses with XCTestExpectations.
377+
private let asyncTestTimeout: TimeInterval = 60 * 60 * 24 * 30

0 commit comments

Comments
 (0)