-
Notifications
You must be signed in to change notification settings - Fork 267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[REVIEW] Add Async Support to XCTest #326
Changes from all commits
4aa611b
b61c8bc
a94963c
c1d8e0f
cb41e55
5e1ca37
55f88c5
eaf8439
b8926d4
c385668
9c2ca2b
ae1e843
bdafb49
a11d359
ff0974c
8b5e9f3
8794e0c
1f92e0e
fe41749
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,4 @@ xcuserdata | |
*.xcscmblueprint | ||
.build/ | ||
Output/ | ||
Tests/Functional/.lit_test_times.txt |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2014 - 2021 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 | ||
|
||
extension XCTestCase { | ||
|
||
/// A class which encapsulates teardown blocks which are registered via the `addTeardownBlock(_:)` method. | ||
/// Supports async and sync throwing methods. | ||
final class TeardownBlocksState { | ||
|
||
private var wasFinalized = false | ||
private var blocks: [() throws -> Void] = [] | ||
|
||
// 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. | ||
// | ||
// 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) { | ||
self.append { | ||
try awaitUsingExpectation { try await block() } | ||
} | ||
} | ||
|
||
func append(_ block: @escaping () throws -> Void) { | ||
XCTWaiter.subsystemQueue.sync { | ||
precondition(wasFinalized == false, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") | ||
blocks.append(block) | ||
} | ||
} | ||
|
||
func finalize() -> [() throws -> Void] { | ||
XCTWaiter.subsystemQueue.sync { | ||
precondition(wasFinalized == false, "API violation -- attempting to run teardown blocks after they've already run") | ||
wasFinalized = true | ||
return blocks | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -195,23 +195,23 @@ open class XCTestCase: XCTest { | |
/// class. | ||
open class func tearDown() {} | ||
|
||
private var teardownBlocks: [() -> Void] = [] | ||
private var teardownBlocksDequeued: Bool = false | ||
private let teardownBlocksQueue: DispatchQueue = DispatchQueue(label: "org.swift.XCTest.XCTestCase.teardownBlocks") | ||
private let teardownBlocksState = TeardownBlocksState() | ||
|
||
/// Registers a block of teardown code to be run after the current test | ||
/// method ends. | ||
open func addTeardownBlock(_ block: @escaping () -> Void) { | ||
teardownBlocksQueue.sync { | ||
precondition(!self.teardownBlocksDequeued, "API violation -- attempting to add a teardown block after teardown blocks have been dequeued") | ||
self.teardownBlocks.append(block) | ||
} | ||
teardownBlocksState.append(block) | ||
SeanROlszewski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// Registers a block of teardown code to be run after the current test | ||
/// method ends. | ||
@available(macOS 12.0, *) | ||
public func addTeardownBlock(_ block: @Sendable @escaping () async throws -> Void) { | ||
teardownBlocksState.appendAsync(block) | ||
} | ||
|
||
private func performSetUpSequence() { | ||
do { | ||
try setUpWithError() | ||
} catch { | ||
func handleErrorDuringSetUp(_ error: Error) { | ||
if error.xct_shouldRecordAsTestFailure { | ||
recordFailure(for: error) | ||
} | ||
|
@@ -225,33 +225,60 @@ open class XCTestCase: XCTest { | |
} | ||
} | ||
|
||
do { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is our behavior here matching Xcode XCTest, in terms of what additional steps of the test we still run and what we report if an async setUp override:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe it should be handling these correctly, due to the following two guards above in
|
||
if #available(macOS 12.0, *) { | ||
try awaitUsingExpectation { | ||
try await self.setUp() | ||
} | ||
} | ||
} catch { | ||
handleErrorDuringSetUp(error) | ||
} | ||
|
||
do { | ||
try setUpWithError() | ||
} catch { | ||
handleErrorDuringSetUp(error) | ||
} | ||
|
||
setUp() | ||
} | ||
|
||
private func performTearDownSequence() { | ||
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) | ||
} | ||
} | ||
} | ||
|
||
runTeardownBlocks() | ||
|
||
tearDown() | ||
|
||
do { | ||
try tearDownWithError() | ||
} catch { | ||
if error.xct_shouldRecordAsTestFailure { | ||
recordFailure(for: error) | ||
} | ||
} | ||
} | ||
|
||
private func runTeardownBlocks() { | ||
let blocks = teardownBlocksQueue.sync { () -> [() -> Void] in | ||
self.teardownBlocksDequeued = true | ||
let blocks = self.teardownBlocks | ||
self.teardownBlocks = [] | ||
return blocks | ||
handleErrorDuringTearDown(error) | ||
} | ||
|
||
for block in blocks.reversed() { | ||
block() | ||
do { | ||
if #available(macOS 12.0, *) { | ||
try awaitUsingExpectation { | ||
try await self.tearDown() | ||
} | ||
} | ||
} catch { | ||
handleErrorDuringTearDown(error) | ||
} | ||
} | ||
|
||
|
@@ -292,3 +319,59 @@ private func test<T: XCTestCase>(_ testFunc: @escaping (T) -> () throws -> Void) | |
try testFunc(testCase)() | ||
} | ||
} | ||
|
||
@available(macOS 12.0, *) | ||
public func asyncTest<T: XCTestCase>( | ||
_ testClosureGenerator: @escaping (T) -> () async throws -> Void | ||
) -> (T) -> () throws -> Void { | ||
return { (testType: T) in | ||
let testClosure = testClosureGenerator(testType) | ||
return { | ||
try awaitUsingExpectation(testClosure) | ||
} | ||
} | ||
} | ||
|
||
@available(macOS 12.0, *) | ||
func awaitUsingExpectation( | ||
_ closure: @escaping () async throws -> Void | ||
) throws -> Void { | ||
let expectation = XCTestExpectation(description: "async test completion") | ||
let thrownErrorWrapper = ThrownErrorWrapper() | ||
|
||
Task { | ||
defer { expectation.fulfill() } | ||
|
||
do { | ||
try await closure() | ||
} catch { | ||
thrownErrorWrapper.error = error | ||
} | ||
} | ||
|
||
_ = XCTWaiter.wait(for: [expectation], timeout: asyncTestTimeout) | ||
|
||
if let error = thrownErrorWrapper.error { | ||
throw error | ||
} | ||
} | ||
|
||
private final class ThrownErrorWrapper: @unchecked Sendable { | ||
|
||
private var _error: Error? | ||
|
||
var error: Error? { | ||
get { | ||
XCTWaiter.subsystemQueue.sync { _error } | ||
} | ||
set { | ||
XCTWaiter.subsystemQueue.sync { _error = newValue } | ||
} | ||
} | ||
} | ||
|
||
|
||
// This time interval is set to a very large value due to their being no real native timeout functionality within corelibs-xctest. | ||
// 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. | ||
// This time interval is the timeout corelibs-xctest uses with XCTestExpectations. | ||
private let asyncTestTimeout: TimeInterval = 60 * 60 * 24 * 30 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to have better encapsulation of this now!