diff --git a/Sources/XCTRuntimeAssertions/XCTRuntimePrecondition.swift b/Sources/XCTRuntimeAssertions/XCTRuntimePrecondition.swift index 1cd91ef..3bad9a2 100644 --- a/Sources/XCTRuntimeAssertions/XCTRuntimePrecondition.swift +++ b/Sources/XCTRuntimeAssertions/XCTRuntimePrecondition.swift @@ -11,6 +11,11 @@ import Foundation /// `XCTRuntimePrecondition` allows you to test assertions of types that use the `precondition` and `preconditionFailure` functions of the `XCTRuntimeAssertions` target. +/// +/// - Important: The `expression` is executed on a background thread, even though it is not annotated as `@Sendable`. This is by design. Preconditions return `Never` and, therefore, +/// need to be run on a separate thread that can block forever. Without this workaround, testing preconditions that are isolated to `@MainActor` would be impossible. +/// Make sure to only run isolated parts of your code that don't suffer from concurrency issues in such a scenario. +/// /// - Parameters: /// - validateRuntimeAssertion: An optional closure that can be used to further validate the messages passed to the /// `precondition` and `preconditionFailure` functions of the `XCTRuntimeAssertions` target. @@ -22,11 +27,11 @@ import Foundation /// - Throws: Throws an `XCTFail` error if the expression does not trigger a runtime assertion with the parameters defined above. public func XCTRuntimePrecondition( validateRuntimeAssertion: ((String) -> Void)? = nil, - timeout: Double = 0.01, + timeout: TimeInterval = 0.01, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, - _ expression: @escaping @Sendable () -> Void + _ expression: @escaping () -> Void ) throws { let fulfillmentCount = Counter() let xctRuntimeAssertionId = setupXCTRuntimeAssertionInjector( @@ -59,6 +64,11 @@ public func XCTRuntimePrecondition( } /// `XCTRuntimePrecondition` allows you to test async assertions of types that use the `precondition` and `preconditionFailure` functions of the `XCTRuntimeAssertions` target. +/// +/// - Important: The `expression` is executed on a background thread, even though it is not annotated as `@Sendable`. This is by design. Preconditions return `Never` and, therefore, +/// need to be run on a separate thread that can block forever. Without this workaround, testing preconditions that are isolated to `@MainActor` would be impossible. +/// Make sure to only run isolated parts of your code that don't suffer from concurrency issues in such a scenario. +/// /// - Parameters: /// - validateRuntimeAssertion: An optional closure that can be used to further validate the messages passed to the /// `precondition` and `preconditionFailure` functions of the `XCTRuntimeAssertions` target. @@ -70,20 +80,25 @@ public func XCTRuntimePrecondition( /// - Throws: Throws an `XCTFail` error if the expression does not trigger a runtime assertion with the parameters defined above. public func XCTRuntimePrecondition( validateRuntimeAssertion: ((String) -> Void)? = nil, - timeout: Double = 0.01, + timeout: TimeInterval = 0.01, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, - _ expression: @escaping @Sendable () async -> Void + _ expression: @escaping () async -> Void ) throws { + struct HackySendable: @unchecked Sendable { + let value: Value + } + let fulfillmentCount = Counter() let xctRuntimeAssertionId = setupXCTRuntimeAssertionInjector( fulfillmentCount: fulfillmentCount, validateRuntimeAssertion: validateRuntimeAssertion ) - + + let expressionClosure = HackySendable(value: expression) let task = Task { - await expression() + await expressionClosure.value() } // We don't use: diff --git a/Tests/XCTRuntimeAssertionsTests/XCTRuntimeAssertionsTests.swift b/Tests/XCTRuntimeAssertionsTests/XCTRuntimeAssertionsTests.swift index 0d12567..7441519 100644 --- a/Tests/XCTRuntimeAssertionsTests/XCTRuntimeAssertionsTests.swift +++ b/Tests/XCTRuntimeAssertionsTests/XCTRuntimeAssertionsTests.swift @@ -106,4 +106,20 @@ final class XCTRuntimeAssertionsTests: XCTestCase { XCTAssertTrue(called, "assert was never called!") } + + @MainActor + func testActorAnnotatedClosure() throws { + @MainActor + class Test { + var property = "Hello World" + + nonisolated init() {} + } + + let test = Test() + + try XCTRuntimeAssertion { + assert(test.property != "Hello World", "Failed successfully") + } + } } diff --git a/Tests/XCTRuntimeAssertionsTests/XCTRuntimePreconditionsTests.swift b/Tests/XCTRuntimeAssertionsTests/XCTRuntimePreconditionsTests.swift index 533ae7d..b6e0ee0 100644 --- a/Tests/XCTRuntimeAssertionsTests/XCTRuntimePreconditionsTests.swift +++ b/Tests/XCTRuntimeAssertionsTests/XCTRuntimePreconditionsTests.swift @@ -92,4 +92,18 @@ final class XCTRuntimePreconditionsTests: XCTestCase { XCTAssertTrue(called, "precondition was never called!") } + + @MainActor + func testAsyncInvocationOnMainActor() throws { + @MainActor + class Test { + var property = "Hello World" + } + + let test = Test() + + try XCTRuntimePrecondition { + precondition(test.property != "Hello World", "Failed successfully.") + } + } }