Skip to content

Commit

Permalink
Lie to the Swift compiler about isolation
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Jul 15, 2024
1 parent 2ebbbb4 commit d00ef0f
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 6 deletions.
27 changes: 21 additions & 6 deletions Sources/XCTRuntimeAssertions/XCTRuntimePrecondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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<Value>: @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:
Expand Down
16 changes: 16 additions & 0 deletions Tests/XCTRuntimeAssertionsTests/XCTRuntimeAssertionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
14 changes: 14 additions & 0 deletions Tests/XCTRuntimeAssertionsTests/XCTRuntimePreconditionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}
}
}

0 comments on commit d00ef0f

Please sign in to comment.