Skip to content

[6.0] Add isolation argument to functions taking non-sendable async closures. #643

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

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
15 changes: 11 additions & 4 deletions Sources/Testing/Expectations/ExpectationChecking+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -835,10 +835,11 @@ public func __checkClosureCall<E>(
/// `#require()` macros. Do not call it directly.
public func __checkClosureCall<E>(
throws errorType: E.Type,
performing body: () async throws -> some Any,
performing body: () async throws -> sending some Any,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation
) async -> Result<Void, any Error> where E: Error {
if errorType == Never.self {
Expand All @@ -848,6 +849,7 @@ public func __checkClosureCall<E>(
expression: expression,
comments: comments(),
isRequired: isRequired,
isolation: isolation,
sourceLocation: sourceLocation
)
} else {
Expand All @@ -858,6 +860,7 @@ public func __checkClosureCall<E>(
expression: expression,
comments: comments(),
isRequired: isRequired,
isolation: isolation,
sourceLocation: sourceLocation
)
}
Expand Down Expand Up @@ -911,10 +914,11 @@ public func __checkClosureCall(
/// `#require()` macros. Do not call it directly.
public func __checkClosureCall(
throws _: Never.Type,
performing body: () async throws -> some Any,
performing body: () async throws -> sending some Any,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation
) async -> Result<Void, any Error> {
var success = true
Expand Down Expand Up @@ -973,10 +977,11 @@ public func __checkClosureCall<E>(
/// `#require()` macros. Do not call it directly.
public func __checkClosureCall<E>(
throws error: E,
performing body: () async throws -> some Any,
performing body: () async throws -> sending some Any,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation
) async -> Result<Void, any Error> where E: Error & Equatable {
await __checkClosureCall(
Expand All @@ -986,6 +991,7 @@ public func __checkClosureCall<E>(
expression: expression,
comments: comments(),
isRequired: isRequired,
isolation: isolation,
sourceLocation: sourceLocation
)
}
Expand Down Expand Up @@ -1047,12 +1053,13 @@ public func __checkClosureCall<R>(
/// - Warning: This function is used to implement the `#expect()` and
/// `#require()` macros. Do not call it directly.
public func __checkClosureCall<R>(
performing body: () async throws -> R,
performing body: () async throws -> sending R,
throws errorMatcher: (any Error) async throws -> Bool,
mismatchExplanation: ((any Error) -> String)? = nil,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation
) async -> Result<Void, any Error> {
var errorMatches = false
Expand Down
13 changes: 9 additions & 4 deletions Sources/Testing/Issues/Confirmation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ extension Confirmation {
/// `body` is invoked. The default value of this argument is `1`, indicating
/// that the event should occur exactly once. Pass `0` if the event should
/// _never_ occur when `body` is invoked.
/// - isolation: The actor to which `body` is isolated, if any.
/// - sourceLocation: The source location to which any recorded issues should
/// be attributed.
/// - body: The function to invoke.
Expand Down Expand Up @@ -94,12 +95,14 @@ extension Confirmation {
public func confirmation<R>(
_ comment: Comment? = nil,
expectedCount: Int = 1,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation = #_sourceLocation,
_ body: (Confirmation) async throws -> R
_ body: (Confirmation) async throws -> sending R
) async rethrows -> R {
try await confirmation(
comment,
expectedCount: expectedCount ... expectedCount,
isolation: isolation,
sourceLocation: sourceLocation,
body
)
Expand All @@ -114,6 +117,7 @@ public func confirmation<R>(
/// function.
/// - expectedCount: A range of integers indicating the number of times the
/// expected event should occur when `body` is invoked.
/// - isolation: The actor to which `body` is isolated, if any.
/// - sourceLocation: The source location to which any recorded issues should
/// be attributed.
/// - body: The function to invoke.
Expand Down Expand Up @@ -156,13 +160,14 @@ public func confirmation<R>(
/// preconditions have been met, and records an issue if they have not.
///
/// If an exact count is expected, use
/// ``confirmation(_:expectedCount:sourceLocation:_:)-7kfko`` instead.
/// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)`` instead.
@_spi(Experimental)
public func confirmation<R>(
_ comment: Comment? = nil,
expectedCount: some Confirmation.ExpectedCount,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation = #_sourceLocation,
_ body: (Confirmation) async throws -> R
_ body: (Confirmation) async throws -> sending R
) async rethrows -> R {
let confirmation = Confirmation()
defer {
Expand All @@ -182,7 +187,7 @@ public func confirmation<R>(
@_spi(Experimental)
extension Confirmation {
/// A protocol that describes a range expression that can be used with
/// ``confirmation(_:expectedCount:sourceLocation:_:)-41gmd``.
/// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)-9rt6m``.
///
/// This protocol represents any expression that describes a range of
/// confirmation counts. For example, the expression `1 ..< 10` automatically
Expand Down
2 changes: 2 additions & 0 deletions Sources/Testing/Issues/Issue+Recording.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ extension Issue {
/// - sourceLocation: The source location to attribute any caught error to.
/// - configuration: The test configuration to use when recording an issue.
/// The default value is ``Configuration/current``.
/// - isolation: The actor to which `body` is isolated, if any.
/// - body: An asynchronous closure that might throw an error.
///
/// - Returns: The issue representing the caught error, if any error was
Expand All @@ -204,6 +205,7 @@ extension Issue {
static func withErrorRecording(
at sourceLocation: SourceLocation,
configuration: Configuration? = nil,
isolation: isolated (any Actor)? = #isolation,
_ body: () async throws -> Void
) async -> (any Error)? {
// Ensure that we are capturing backtraces for errors before we start
Expand Down
8 changes: 4 additions & 4 deletions Sources/Testing/Issues/Issue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public struct Issue: Sendable {
/// ``Confirmation/confirm(count:)`` should have been called.
///
/// This issue can occur when calling
/// ``confirmation(_:expectedCount:sourceLocation:_:)`` when the
/// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)`` when the
/// confirmation passed to these functions' `body` closures is confirmed too
/// few or too many times.
indirect case confirmationMiscounted(actual: Int, expected: Int)
Expand All @@ -48,9 +48,9 @@ public struct Issue: Sendable {
/// ``Confirmation/confirm(count:)`` should have been called.
///
/// This issue can occur when calling
/// ``confirmation(_:expectedCount:sourceLocation:_:)-41gmd`` when the
/// confirmation passed to these functions' `body` closures is confirmed too
/// few or too many times.
/// ``confirmation(_:expectedCount:isolation:sourceLocation:_:)-9rt6m`` when
/// the confirmation passed to these functions' `body` closures is confirmed
/// too few or too many times.
@_spi(Experimental)
indirect case confirmationOutOfRange(actual: Int, expected: any Confirmation.ExpectedCount)

Expand Down
14 changes: 9 additions & 5 deletions Sources/Testing/Issues/KnownIssue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public typealias KnownIssueMatcher = @Sendable (_ issue: Issue) -> Bool
/// Because all errors thrown by `body` are caught as known issues, this
/// function is not throwing. If only some errors or issues are known to occur
/// while others should continue to cause test failures, use
/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-5vi5n``
/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)``
/// instead.
public func withKnownIssue(
_ comment: Comment? = nil,
Expand Down Expand Up @@ -161,7 +161,7 @@ public func withKnownIssue(
///
/// It is not necessary to specify both `precondition` and `issueMatcher` if
/// only one is relevant. If all errors and issues should be considered known
/// issues, use ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-95r6o``
/// issues, use ``withKnownIssue(_:isIntermittent:sourceLocation:_:)``
/// instead.
///
/// - Note: `issueMatcher` may be invoked more than once for the same issue.
Expand Down Expand Up @@ -200,6 +200,7 @@ public func withKnownIssue(
/// - isIntermittent: Whether or not the known issue occurs intermittently. If
/// this argument is `true` and the known issue does not occur, no secondary
/// issue is recorded.
/// - isolation: The actor to which `body` is isolated, if any.
/// - sourceLocation: The source location to which any recorded issues should
/// be attributed.
/// - body: The function to invoke.
Expand All @@ -218,15 +219,16 @@ public func withKnownIssue(
/// Because all errors thrown by `body` are caught as known issues, this
/// function is not throwing. If only some errors or issues are known to occur
/// while others should continue to cause test failures, use
/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-47y3z``
/// ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)``
/// instead.
public func withKnownIssue(
_ comment: Comment? = nil,
isIntermittent: Bool = false,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation = #_sourceLocation,
_ body: () async throws -> Void
) async {
try? await withKnownIssue(comment, isIntermittent: isIntermittent, sourceLocation: sourceLocation, body, matching: { _ in true })
try? await withKnownIssue(comment, isIntermittent: isIntermittent, isolation: isolation, sourceLocation: sourceLocation, body, matching: { _ in true })
}

/// Invoke a function that has a known issue that is expected to occur during
Expand All @@ -237,6 +239,7 @@ public func withKnownIssue(
/// - isIntermittent: Whether or not the known issue occurs intermittently. If
/// this argument is `true` and the known issue does not occur, no secondary
/// issue is recorded.
/// - isolation: The actor to which `body` is isolated, if any.
/// - sourceLocation: The source location to which any recorded issues should
/// be attributed.
/// - body: The function to invoke.
Expand Down Expand Up @@ -269,13 +272,14 @@ public func withKnownIssue(
///
/// It is not necessary to specify both `precondition` and `issueMatcher` if
/// only one is relevant. If all errors and issues should be considered known
/// issues, use ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-3g6b7``
/// issues, use ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)``
/// instead.
///
/// - Note: `issueMatcher` may be invoked more than once for the same issue.
public func withKnownIssue(
_ comment: Comment? = nil,
isIntermittent: Bool = false,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation = #_sourceLocation,
_ body: () async throws -> Void,
when precondition: () async -> Bool = { true },
Expand Down
2 changes: 1 addition & 1 deletion Sources/Testing/Testing.docc/Expectations.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ the test when the code doesn't satisfy a requirement, use
### Confirming that asynchronous events occur

- <doc:testing-asynchronous-code>
- ``confirmation(_:expectedCount:sourceLocation:_:)``
- ``confirmation(_:expectedCount:isolation:sourceLocation:_:)``
- ``Confirmation``

### Retrieving information about checked expectations
Expand Down
10 changes: 5 additions & 5 deletions Sources/Testing/Testing.docc/MigratingFromXCTest.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ Some tests, especially those that test asynchronously-delivered events, cannot
be readily converted to use Swift concurrency. The testing library offers
functionality called _confirmations_ which can be used to implement these tests.
Instances of ``Confirmation`` are created and used within the scope of the
function ``confirmation(_:expectedCount:sourceLocation:_:)``.
function ``confirmation(_:expectedCount:isolation:sourceLocation:_:)``.

Confirmations function similarly to the expectations API of XCTest, however, they don't
block or suspend the caller while waiting for a condition to be fulfilled.
Expand Down Expand Up @@ -531,8 +531,8 @@ to tell XCTest and its infrastructure that the issue shouldn't cause the test
to fail. The testing library has an equivalent function with synchronous and
asynchronous variants:

- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-95r6o``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-3g6b7``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)``
- ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:)``

This function can be used to annotate a section of a test as having a known
issue:
Expand Down Expand Up @@ -627,8 +627,8 @@ Additional options can be specified when calling `XCTExpectFailure()`:
The testing library includes overloads of `withKnownIssue()` that take
additional arguments with similar behavior:

- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-5vi5n``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-47y3z``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)``
- ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)``

To conditionally enable known-issue matching or to match only certain kinds
of issues:
Expand Down
8 changes: 4 additions & 4 deletions Sources/Testing/Testing.docc/known-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ at runtime not to mark the test as failing when issues occur.

### Recording known issues in tests

- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-95r6o``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)-3g6b7``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-5vi5n``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)-47y3z``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:)``
- ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:)``
- ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)``
- ``withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)``
- ``KnownIssueMatcher``

### Describing a failure or warning
Expand Down
6 changes: 3 additions & 3 deletions Sources/Testing/Testing.docc/testing-asynchronous-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ expected event happens.

### Confirm that an event happens

Call ``confirmation(_:expectedCount:sourceLocation:_:)`` in your asynchronous
test function to create a `Confirmation` for the expected event. In the trailing
closure parameter, call the code under test. Swift Testing passes a
Call ``confirmation(_:expectedCount:isolation:sourceLocation:_:)`` in your
asynchronous test function to create a `Confirmation` for the expected event. In
the trailing closure parameter, call the code under test. Swift Testing passes a
`Confirmation` as the parameter to the closure, which you call as a function in
the event handler for the code under test when the event you're testing for
occurs:
Expand Down
17 changes: 16 additions & 1 deletion Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,20 @@ public import SwiftSyntaxMacros
///
/// The `__check()` function that implements expansions of these macros must
/// take any developer-supplied arguments _before_ the ones inserted during
/// macro expansion (starting with the `"expression"` argument.)
/// macro expansion (starting with the `"expression"` argument.) The `isolation`
/// argument (if present) and `sourceLocation` argument are placed at the end of
/// the generated function call's argument list.
public protocol ConditionMacro: ExpressionMacro, Sendable {
/// Whether or not the macro's expansion may throw an error.
static var isThrowing: Bool { get }
}

// MARK: -

/// The token used as the label of the argument passed to `#expect()` and
/// `#require()` and used for actor isolation.
private var _isolationLabel: TokenSyntax { .identifier("isolation") }

/// The token used as the label of the source location argument passed to
/// `#expect()` and `#require()`.
private var _sourceLocationLabel: TokenSyntax { .identifier("sourceLocation") }
Expand Down Expand Up @@ -89,6 +95,9 @@ extension ConditionMacro {
// never the first argument.)
commentIndex = macroArguments.dropFirst().lastIndex { $0.label == nil }
}
let isolationArgumentIndex = macroArguments.lazy
.compactMap(\.label)
.firstIndex { $0.tokenKind == _isolationLabel.tokenKind }
let sourceLocationArgumentIndex = macroArguments.lazy
.compactMap(\.label)
.firstIndex { $0.tokenKind == _sourceLocationLabel.tokenKind }
Expand All @@ -103,6 +112,7 @@ extension ConditionMacro {
// arguments here.
checkArguments += macroArguments.indices.lazy
.filter { $0 != commentIndex }
.filter { $0 != isolationArgumentIndex }
.filter { $0 != sourceLocationArgumentIndex }
.map { macroArguments[$0] }

Expand All @@ -124,6 +134,7 @@ extension ConditionMacro {
// "sourceLocation" arguments here.
checkArguments += macroArguments.dropFirst().indices.lazy
.filter { $0 != commentIndex }
.filter { $0 != isolationArgumentIndex }
.filter { $0 != sourceLocationArgumentIndex }
.map { macroArguments[$0] }

Expand Down Expand Up @@ -160,6 +171,10 @@ extension ConditionMacro {

checkArguments.append(Argument(label: "isRequired", expression: BooleanLiteralExprSyntax(isThrowing)))

if let isolationArgumentIndex {
checkArguments.append(macroArguments[isolationArgumentIndex])
}

if let sourceLocationArgumentIndex {
checkArguments.append(macroArguments[sourceLocationArgumentIndex])
} else {
Expand Down
4 changes: 4 additions & 0 deletions Tests/TestingMacrosTests/ConditionMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ struct ConditionMacroTests {
##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
##"#expect(a?.b.isB)"##:
##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
##"#expect(isolation: somewhere) {}"##:
##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: false, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
]
)
func expectMacro(input: String, expectedOutput: String) throws {
Expand Down Expand Up @@ -164,6 +166,8 @@ struct ConditionMacroTests {
##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
##"#require(a?.b.isB)"##:
##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
##"#require(isolation: somewhere) {}"##:
##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: true, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
]
)
func requireMacro(input: String, expectedOutput: String) throws {
Expand Down
Loading