Skip to content
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

Merged
merged 19 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ xcuserdata
*.xcscmblueprint
.build/
Output/
Tests/Functional/.lit_test_times.txt
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ add_library(XCTest
Sources/XCTest/Private/SourceLocation.swift
Sources/XCTest/Private/WaiterManager.swift
Sources/XCTest/Private/IgnoredErrors.swift
Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift
Sources/XCTest/Public/XCTestRun.swift
Sources/XCTest/Public/XCTestMain.swift
Sources/XCTest/Public/XCTestCase.swift
Expand Down
44 changes: 44 additions & 0 deletions Sources/XCTest/Private/XCTestCase.TearDownBlocksState.swift
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
}
}
}
}
9 changes: 9 additions & 0 deletions Sources/XCTest/Public/XCAbstractTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ open class XCTest {
perform(testRun!)
}

/// Async setup method called before the invocation of `setUpWithError` for each test method in the class.
@available(macOS 12.0, *)
open func setUp() async throws {}

/// Setup method called before the invocation of `setUp` and the test method
/// for each test method in the class.
open func setUpWithError() throws {}
Expand All @@ -68,6 +72,11 @@ open class XCTest {
/// for each test method in the class.
open func tearDownWithError() throws {}

/// Async teardown method which is called after the invocation of `tearDownWithError`
/// for each test method in the class.
@available(macOS 12.0, *)
open func tearDown() async throws {}

// FIXME: This initializer is required due to a Swift compiler bug on Linux.
// It should be removed once the bug is fixed.
public init() {}
Expand Down
131 changes: 107 additions & 24 deletions Sources/XCTest/Public/XCTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,23 +195,23 @@ open class XCTestCase: XCTest {
/// class.
open class func tearDown() {}

private var teardownBlocks: [() -> Void] = []
Copy link
Contributor

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!

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)
}
Expand All @@ -225,33 +225,60 @@ open class XCTestCase: XCTest {
}
}

do {
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

  • throws XCTSkip?
  • throws some other error?

Copy link
Contributor

Choose a reason for hiding this comment

The 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 handleErrorDuringSetUp(_:):

if error.xct_shouldRecordAsTestFailure { and
if error.xct_shouldSkipTestInvocation {

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)
}
}

Expand Down Expand Up @@ -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
Loading