diff --git a/Examples/ExamplesTests/SwiftTestingTests.swift b/Examples/ExamplesTests/SwiftTestingTests.swift index d8f21fd..e19f7e6 100644 --- a/Examples/ExamplesTests/SwiftTestingTests.swift +++ b/Examples/ExamplesTests/SwiftTestingTests.swift @@ -88,9 +88,14 @@ @Test func withExpectedIssueDoesNotFail() { withExpectedIssue {} } + + @Test func withExpectedIssueDoesNotFailAsync() async { + await withExpectedIssue { + await Task.yield() + } + } } #endif - #endif private struct Failure: Error {} diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/withExpectedIssue.md b/Sources/IssueReporting/Documentation.docc/Extensions/withExpectedIssue.md index 6f57328..c6ffb4a 100644 --- a/Sources/IssueReporting/Documentation.docc/Extensions/withExpectedIssue.md +++ b/Sources/IssueReporting/Documentation.docc/Extensions/withExpectedIssue.md @@ -1,7 +1,7 @@ -# ``IssueReporting/withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)-9pinm`` +# ``IssueReporting/withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)`` ## Topics ### Overloads -- ``IssueReporting/withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)-7noz2`` +- ``IssueReporting/withExpectedIssue(_:isIntermittent:isolation:fileID:filePath:line:column:_:)`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/withIssueContext.md b/Sources/IssueReporting/Documentation.docc/Extensions/withIssueContext.md index 5bee801..e641720 100644 --- a/Sources/IssueReporting/Documentation.docc/Extensions/withIssueContext.md +++ b/Sources/IssueReporting/Documentation.docc/Extensions/withIssueContext.md @@ -1,7 +1,7 @@ -# ``IssueReporting/withIssueContext(fileID:filePath:line:column:operation:)-97lux`` +# ``IssueReporting/withIssueContext(fileID:filePath:line:column:operation:)`` ## Topics ### Overloads -- ``withIssueContext(fileID:filePath:line:column:operation:)-6o3dr`` +- ``withIssueContext(fileID:filePath:line:column:isolation:operation:)`` diff --git a/Sources/IssueReporting/Internal/AppHostWarning.swift b/Sources/IssueReporting/Internal/AppHostWarning.swift index 941f9af..4aa378d 100644 --- a/Sources/IssueReporting/Internal/AppHostWarning.swift +++ b/Sources/IssueReporting/Internal/AppHostWarning.swift @@ -38,7 +38,7 @@ extension String { For more information (and workarounds), see "Testing gotchas": - https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/testing#Testing-gotchas + https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependencies/testing#Testing-gotchas """ return isEmpty diff --git a/Sources/IssueReporting/Internal/SwiftTesting.swift b/Sources/IssueReporting/Internal/SwiftTesting.swift index c43b2f9..4dbe056 100644 --- a/Sources/IssueReporting/Internal/SwiftTesting.swift +++ b/Sources/IssueReporting/Internal/SwiftTesting.swift @@ -148,6 +148,7 @@ func _withKnownIssue( To fix this, add "IssueReportingTestSupport" as a dependency to your test target. """ ) + try? body() #endif return } @@ -166,73 +167,151 @@ func _withKnownIssue( withKnownIssue(message, isIntermittent, fileID, filePath, line, column, body) } -@usableFromInline -func _withKnownIssue( - _ message: String? = nil, - isIntermittent: Bool = false, - fileID: String = #fileID, - filePath: String = #filePath, - line: Int = #line, - column: Int = #column, - _ body: () async throws -> Void -) async { - guard let function = function(for: "$s25IssueReportingTestSupport010_withKnownA5AsyncypyF") - else { - #if DEBUG - guard - let withKnownIssue = unsafeBitCast( - symbol: """ - $s7Testing14withKnownIssue_14isIntermittent14sourceLocation_yAA7CommentVSg_SbAA06Source\ - H0VyyYaKXEtYaFTu - """, - in: "Testing", - to: (@convention(thin) ( - Any?, - Bool, - SourceLocation, - () async throws -> Void - ) async -> Void) - .self +#if compiler(>=6.0.2) + @usableFromInline + func _withKnownIssue( + _ message: String?, + isIntermittent: Bool, + isolation: isolated (any Actor)?, + fileID: String, + filePath: String, + line: Int, + column: Int, + _ body: () async throws -> Void + ) async { + guard + let function = function(for: "$s25IssueReportingTestSupport010_withKnownA13AsyncIsolatedypyF") + else { + #if DEBUG + guard + let withKnownIssue = unsafeBitCast( + symbol: """ + $s7Testing14withKnownIssue_14isIntermittent9isolation14sourceLocation_yAA7CommentVSg_\ + SbScA_pSgYiAA06SourceI0VyyYaKXEtYaF + """, + in: "Testing", + to: (@convention(thin) ( + Any?, + Bool, + isolated (any Actor)?, + SourceLocation, + () async throws -> Void + ) async -> Void) + .self + ) + else { return } + + var comment: Any? + if let message { + var c = UnsafeMutablePointer.allocate(capacity: 1).pointee + c.rawValue = message + comment = c + } + await withKnownIssue( + comment, + isIntermittent, + isolation, + SourceLocation(fileID: fileID, _filePath: filePath, line: line, column: column), + body ) - else { return } + #else + printError( + """ + \(fileID):\(line): A known issue was recorded without linking the Testing framework. - var comment: Any? - if let message { - var c = UnsafeMutablePointer.allocate(capacity: 1).pointee - c.rawValue = message - comment = c - } - await withKnownIssue( - comment, - isIntermittent, - SourceLocation(fileID: fileID, _filePath: filePath, line: line, column: column), - body - ) - #else - printError( - """ - \(fileID):\(line): A known issue was recorded without linking the Testing framework. + To fix this, add "IssueReportingTestSupport" as a dependency to your test target. + """ + ) + try? await body() + #endif + return + } - To fix this, add "IssueReportingTestSupport" as a dependency to your test target. - """ - ) - #endif - return + let withKnownIssue = + function + as! @Sendable ( + String?, + Bool, + isolated (any Actor)?, + String, + String, + Int, + Int, + () async throws -> Void + ) async -> Void + await withKnownIssue(message, isIntermittent, isolation, fileID, filePath, line, column, body) } +#else + @usableFromInline + func _withKnownIssue( + _ message: String?, + isIntermittent: Bool, + fileID: String, + filePath: String, + line: Int, + column: Int, + _ body: () async throws -> Void + ) async { + guard let function = function(for: "$s25IssueReportingTestSupport010_withKnownA5AsyncypyF") + else { + #if DEBUG + guard + let withKnownIssue = unsafeBitCast( + symbol: """ + $s7Testing14withKnownIssue_14isIntermittent14sourceLocation_yAA7CommentVSg_SbAA06Sour\ + ceH0VyyYaKXEtYaFTu + """, + in: "Testing", + to: (@convention(thin) ( + Any?, + Bool, + SourceLocation, + () async throws -> Void + ) async -> Void) + .self + ) + else { return } + + var comment: Any? + if let message { + var c = UnsafeMutablePointer.allocate(capacity: 1).pointee + c.rawValue = message + comment = c + } + await withKnownIssue( + comment, + isIntermittent, + SourceLocation(fileID: fileID, _filePath: filePath, line: line, column: column), + body + ) + #else + printError( + """ + \(fileID):\(line): A known issue was recorded without linking the Testing framework. + + To fix this, add "IssueReportingTestSupport" as a dependency to your test target. + """ + ) + #endif + return + } + + let withKnownIssue = + function + as! @Sendable ( + String?, + Bool, + String, + String, + Int, + Int, + () async throws -> Void + ) async -> Void + await withKnownIssue(message, isIntermittent, fileID, filePath, line, column, body) + } + +#endif - let withKnownIssue = - function - as! @Sendable ( - String?, - Bool, - String, - String, - Int, - Int, - () async throws -> Void - ) async -> Void - await withKnownIssue(message, isIntermittent, fileID, filePath, line, column, body) -} @usableFromInline func _currentTestID() -> AnyHashable? { guard let function = function(for: "$s25IssueReportingTestSupport08_currentC2IDypyF") diff --git a/Sources/IssueReporting/Internal/XCTest.swift b/Sources/IssueReporting/Internal/XCTest.swift index b9d9816..d6a88bb 100644 --- a/Sources/IssueReporting/Internal/XCTest.swift +++ b/Sources/IssueReporting/Internal/XCTest.swift @@ -33,7 +33,8 @@ func _XCTFail( #endif printError( """ - \(file):\(line): A failure was recorded without linking the XCTest framework. + \(file):\(line): A failure was recorded without linking the XCTest framework\ + \(message.isEmpty ? "" : ": \(message)") To fix this, add "IssueReportingTestSupport" as a dependency to your test target. """ diff --git a/Sources/IssueReporting/WithExpectedIssue.swift b/Sources/IssueReporting/WithExpectedIssue.swift index 718948a..7bdd607 100644 --- a/Sources/IssueReporting/WithExpectedIssue.swift +++ b/Sources/IssueReporting/WithExpectedIssue.swift @@ -112,45 +112,58 @@ public func withExpectedIssue( } } -/// Invoke an asynchronous function that has an issue that is expected to occur during its -/// execution. -/// -/// An asynchronous version of -/// ``withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)-9pinm``. -/// -/// > Warning: The asynchronous version of this function is incompatible with XCTest and will -/// > unconditionally report an issue when used, instead. -/// -/// - Parameters: -/// - message: An optional message describing the expected issue. -/// - isIntermittent: Whether or not the known expected occurs intermittently. If this argument is -/// `true` and the expected issue does not occur, no secondary issue is recorded. -/// - fileID: The source `#fileID` associated with the issue. -/// - filePath: The source `#filePath` associated with the issue. -/// - line: The source `#line` associated with the issue. -/// - column: The source `#column` associated with the issue. -/// - body: The asynchronous function to invoke. -@_transparent -public func withExpectedIssue( - _ message: String? = nil, - isIntermittent: Bool = false, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column, - _ body: () async throws -> Void -) async { - - guard let context = TestContext.current else { - guard !isTesting else { return } - let observer = FailureObserver() - await FailureObserver.$current.withValue(observer) { - do { - try await body() - if observer.withLock({ $0 == 0 }), !isIntermittent { +#if compiler(>=6.0.2) + /// Invoke an asynchronous function that has an issue that is expected to occur during its + /// execution. + /// + /// An asynchronous version of + /// ``withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)-9pinm``. + /// + /// > Warning: The asynchronous version of this function is incompatible with XCTest and will + /// > unconditionally report an issue when used, instead. + /// + /// - Parameters: + /// - message: An optional message describing the expected issue. + /// - isIntermittent: Whether or not the known expected occurs intermittently. If this argument is + /// `true` and the expected issue does not occur, no secondary issue is recorded. + /// - fileID: The source `#fileID` associated with the issue. + /// - filePath: The source `#filePath` associated with the issue. + /// - line: The source `#line` associated with the issue. + /// - column: The source `#column` associated with the issue. + /// - body: The asynchronous function to invoke. + @_transparent + public func withExpectedIssue( + _ message: String? = nil, + isIntermittent: Bool = false, + isolation: isolated (any Actor)? = #isolation, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column, + _ body: () async throws -> Void + ) async { + guard let context = TestContext.current else { + guard !isTesting else { return } + let observer = FailureObserver() + await FailureObserver.$current.withValue(observer) { + do { + try await body() + if observer.withLock({ $0 == 0 }), !isIntermittent { + for reporter in IssueReporters.current { + reporter.reportIssue( + "Known issue was not recorded\(message.map { ": \($0)" } ?? "")", + fileID: IssueContext.current?.fileID ?? fileID, + filePath: IssueContext.current?.filePath ?? filePath, + line: IssueContext.current?.line ?? line, + column: IssueContext.current?.column ?? column + ) + } + } + } catch { for reporter in IssueReporters.current { - reporter.reportIssue( - "Known issue was not recorded\(message.map { ": \($0)" } ?? "")", + reporter.expectIssue( + error, + message, fileID: IssueContext.current?.fileID ?? fileID, filePath: IssueContext.current?.filePath ?? filePath, line: IssueContext.current?.line ?? line, @@ -158,46 +171,107 @@ public func withExpectedIssue( ) } } - } catch { - for reporter in IssueReporters.current { - reporter.expectIssue( - error, - message, - fileID: IssueContext.current?.fileID ?? fileID, - filePath: IssueContext.current?.filePath ?? filePath, - line: IssueContext.current?.line ?? line, - column: IssueContext.current?.column ?? column - ) - } } + return + } + + switch context { + case .swiftTesting: + await _withKnownIssue( + message, + isIntermittent: isIntermittent, + isolation: isolation, + fileID: fileID.description, + filePath: filePath.description, + line: Int(line), + column: Int(column), + body + ) + case .xcTest: + reportIssue( + """ + Asynchronously expecting failures is unavailable in XCTest. + + Omit this test from your XCTest suite, or consider using Swift Testing, instead. + """, + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + try? await body() + @unknown default: break } - return } +#else + @_transparent + public func withExpectedIssue( + _ message: String? = nil, + isIntermittent: Bool = false, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column, + _ body: () async throws -> Void + ) async { + guard let context = TestContext.current else { + guard !isTesting else { return } + let observer = FailureObserver() + await FailureObserver.$current.withValue(observer) { + do { + try await body() + if observer.withLock({ $0 == 0 }), !isIntermittent { + for reporter in IssueReporters.current { + reporter.reportIssue( + "Known issue was not recorded\(message.map { ": \($0)" } ?? "")", + fileID: IssueContext.current?.fileID ?? fileID, + filePath: IssueContext.current?.filePath ?? filePath, + line: IssueContext.current?.line ?? line, + column: IssueContext.current?.column ?? column + ) + } + } + } catch { + for reporter in IssueReporters.current { + reporter.expectIssue( + error, + message, + fileID: IssueContext.current?.fileID ?? fileID, + filePath: IssueContext.current?.filePath ?? filePath, + line: IssueContext.current?.line ?? line, + column: IssueContext.current?.column ?? column + ) + } + } + } + return + } - switch context { - case .swiftTesting: - await _withKnownIssue( - message, - isIntermittent: isIntermittent, - fileID: fileID.description, - filePath: filePath.description, - line: Int(line), - column: Int(column), - body - ) - case .xcTest: - reportIssue( - """ - Asynchronously expecting failures is unavailable in XCTest. + switch context { + case .swiftTesting: + await _withKnownIssue( + message, + isIntermittent: isIntermittent, + fileID: fileID.description, + filePath: filePath.description, + line: Int(line), + column: Int(column), + body + ) + case .xcTest: + reportIssue( + """ + Asynchronously expecting failures is unavailable in XCTest. - Omit this test from your XCTest suite, or consider using Swift Testing, instead. - """, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - try? await body() - @unknown default: break + Omit this test from your XCTest suite, or consider using Swift Testing, instead. + """, + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + try? await body() + @unknown default: break + } } -} +#endif diff --git a/Sources/IssueReporting/WithIssueContext.swift b/Sources/IssueReporting/WithIssueContext.swift index 1bca408..7311af0 100644 --- a/Sources/IssueReporting/WithIssueContext.swift +++ b/Sources/IssueReporting/WithIssueContext.swift @@ -25,28 +25,45 @@ public func withIssueContext( ) } -/// Sets the context for issues reported for the duration of the asynchronous operation. -/// -/// An asynchronous version of ``withIssueContext(fileID:filePath:line:column:operation:)-97lux``. -/// -/// - Parameters: -/// - fileID: The source `#fileID` to associate with issues reported during the operation. -/// - filePath: The source `#filePath` to associate with issues reported during the operation. -/// - line: The source `#line` to associate with issues reported during the operation. -/// - column: The source `#column` to associate with issues reported during the operation. -/// - operation: An asynchronous operation. -public func withIssueContext( - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt, - operation: () async throws -> R -) async rethrows -> R { - try await IssueContext.$current.withValue( - IssueContext(fileID: fileID, filePath: filePath, line: line, column: column), - operation: operation - ) -} +#if compiler(>=6) + /// Sets the context for issues reported for the duration of the asynchronous operation. + /// + /// An asynchronous version of ``withIssueContext(fileID:filePath:line:column:operation:)-97lux``. + /// + /// - Parameters: + /// - fileID: The source `#fileID` to associate with issues reported during the operation. + /// - filePath: The source `#filePath` to associate with issues reported during the operation. + /// - line: The source `#line` to associate with issues reported during the operation. + /// - column: The source `#column` to associate with issues reported during the operation. + /// - operation: An asynchronous operation. + public func withIssueContext( + fileID: StaticString, + filePath: StaticString, + line: UInt, + column: UInt, + isolation: isolated (any Actor)? = #isolation, + operation: () async throws -> R + ) async rethrows -> R { + try await IssueContext.$current.withValue( + IssueContext(fileID: fileID, filePath: filePath, line: line, column: column), + operation: operation, + isolation: isolation + ) + } +#else + public func withIssueContext( + fileID: StaticString, + filePath: StaticString, + line: UInt, + column: UInt, + operation: () async throws -> R + ) async rethrows -> R { + try await IssueContext.$current.withValue( + IssueContext(fileID: fileID, filePath: filePath, line: line, column: column), + operation: operation + ) + } +#endif @usableFromInline struct IssueContext: Sendable { diff --git a/Sources/IssueReportingTestSupport/SwiftTesting.swift b/Sources/IssueReportingTestSupport/SwiftTesting.swift index e59b6ed..030631b 100644 --- a/Sources/IssueReportingTestSupport/SwiftTesting.swift +++ b/Sources/IssueReportingTestSupport/SwiftTesting.swift @@ -74,7 +74,9 @@ private func __withKnownIssue( #endif } -public func _withKnownIssueAsync() -> Any { __withKnownIssueAsync } +public func _withKnownIssueAsync() -> Any { + __withKnownIssueAsync(_:isIntermittent:fileID:filePath:line:column:_:) +} @Sendable private func __withKnownIssueAsync( _ message: String?, @@ -100,6 +102,38 @@ private func __withKnownIssueAsync( #endif } +#if compiler(>=6.0.2) + public func _withKnownIssueAsyncIsolated() -> Any { + __withKnownIssueAsync(_:isIntermittent:isolation:fileID:filePath:line:column:_:) + } + @Sendable + private func __withKnownIssueAsync( + _ message: String?, + isIntermittent: Bool, + isolation: isolated (any Actor)?, + fileID: String, + filePath: String, + line: Int, + column: Int, + _ body: () async throws -> Void + ) async { + #if canImport(Testing) + await withKnownIssue( + message.map(Comment.init(rawValue:)), + isIntermittent: isIntermittent, + isolation: isolation, + sourceLocation: SourceLocation( + fileID: fileID, + filePath: filePath, + line: line, + column: column + ), + body + ) + #endif + } +#endif + public func _currentTestID() -> Any { __currentTestID } @Sendable private func __currentTestID() -> AnyHashable? {