From f4764ad761784d46b3be8f6a47b51d1f37f79e67 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 14:43:06 -0800 Subject: [PATCH 1/5] Add `.default` reporter, deprecate `.runtimeWarning` This patch moves the test reporting logic into a `default` reporter so that changing the reporters can influence tests. This is an alternate solution to #146. --- .../Internal/Deprecations.swift | 12 +- Sources/IssueReporting/IsTesting.swift | 1 + Sources/IssueReporting/IssueReporter.swift | 2 +- ...ngReporter.swift => DefaultReporter.swift} | 104 +++++++++++---- Sources/IssueReporting/ReportIssue.swift | 125 ++++++------------ 5 files changed, 138 insertions(+), 106 deletions(-) rename Sources/IssueReporting/IssueReporters/{RuntimeWarningReporter.swift => DefaultReporter.swift} (68%) diff --git a/Sources/IssueReporting/Internal/Deprecations.swift b/Sources/IssueReporting/Internal/Deprecations.swift index 11aa07f..c405b25 100644 --- a/Sources/IssueReporting/Internal/Deprecations.swift +++ b/Sources/IssueReporting/Internal/Deprecations.swift @@ -1,3 +1,13 @@ +// NB: Deprecated after 1.7.0 + +extension IssueReporter where Self == _DefaultReporter { + @available(*, deprecated, renamed: "default") + #if canImport(Darwin) + @_transparent + #endif + public static var runtimeWarning: Self { Self() } +} + // NB: Deprecated after 1.2.2 #if canImport(Darwin) @@ -9,4 +19,4 @@ public typealias FatalErrorReporter = _FatalErrorReporter @available(*, unavailable, renamed: "_RuntimeWarningReporter") -public typealias RuntimeWarningReporter = _RuntimeWarningReporter +public typealias RuntimeWarningReporter = _DefaultReporter diff --git a/Sources/IssueReporting/IsTesting.swift b/Sources/IssueReporting/IsTesting.swift index e709534..b0b27e5 100644 --- a/Sources/IssueReporting/IsTesting.swift +++ b/Sources/IssueReporting/IsTesting.swift @@ -28,6 +28,7 @@ extension ProcessInfo { fileprivate var isTesting: Bool { if environment.keys.contains("XCTestBundlePath") { return true } + if environment.keys.contains("XCTestBundleInjectPath") { return true } if environment.keys.contains("XCTestConfigurationFilePath") { return true } if environment.keys.contains("XCTestSessionIdentifier") { return true } diff --git a/Sources/IssueReporting/IssueReporter.swift b/Sources/IssueReporting/IssueReporter.swift index 29672d9..aa96edd 100644 --- a/Sources/IssueReporting/IssueReporter.swift +++ b/Sources/IssueReporting/IssueReporter.swift @@ -152,7 +152,7 @@ public enum IssueReporters { set { _current.withLock { $0 = newValue } } } - @TaskLocal fileprivate static var _current = LockIsolated<[any IssueReporter]>([.runtimeWarning]) + @TaskLocal fileprivate static var _current = LockIsolated<[any IssueReporter]>([.default]) } /// Overrides the task's issue reporters for the duration of the synchronous operation. diff --git a/Sources/IssueReporting/IssueReporters/RuntimeWarningReporter.swift b/Sources/IssueReporting/IssueReporters/DefaultReporter.swift similarity index 68% rename from Sources/IssueReporting/IssueReporters/RuntimeWarningReporter.swift rename to Sources/IssueReporting/IssueReporters/DefaultReporter.swift index 2ef970e..06684fe 100644 --- a/Sources/IssueReporting/IssueReporters/RuntimeWarningReporter.swift +++ b/Sources/IssueReporting/IssueReporters/DefaultReporter.swift @@ -4,25 +4,25 @@ import Foundation import os #endif -extension IssueReporter where Self == _RuntimeWarningReporter { +extension IssueReporter where Self == _DefaultReporter { /// An issue reporter that emits "purple" runtime warnings to Xcode and logs fault-level messages /// to the console. /// /// This is the default issue reporter. On non-Apple platforms it logs messages to `stderr`. + /// During test runs it emits test failures, instead. /// /// If this issue reporter receives an expected issue, it will log an info-level message to the /// console, instead. #if canImport(Darwin) @_transparent #endif - public static var runtimeWarning: Self { Self() } + public static var `default`: Self { Self() } } -/// A type representing an issue reporter that emits "purple" runtime warnings to Xcode and logs -/// fault-level messages to the console. +/// A type representing an issue reporter that emits "purple" runtime warnings and test failures. /// /// Use ``IssueReporter/runtimeWarning`` to create one of these values. -public struct _RuntimeWarningReporter: IssueReporter { +public struct _DefaultReporter: IssueReporter { #if canImport(os) @UncheckedSendable #if canImport(Darwin) @@ -64,55 +64,113 @@ public struct _RuntimeWarningReporter: IssueReporter { filePath: StaticString, line: UInt, column: UInt + ) { + guard !isTesting else { + _recordIssue( + message: message(), + fileID: "\(fileID)", + filePath: "\(filePath)", + line: Int(line), + column: Int(column) + ) + _XCTFail( + message().withAppHostWarningIfNeeded() ?? "", + file: filePath, + line: line + ) + return + } + runtimeWarn(message(), fileID: fileID, line: line) + } + + @_transparent + public func reportIssue( + _ error: any Error, + _ message: @autoclosure () -> String?, + fileID: StaticString, + filePath: StaticString, + line: UInt, + column: UInt + ) { + guard !isTesting else { + _recordError( + error: error, + message: message(), + fileID: "\(fileID)", + filePath: "\(filePath)", + line: Int(line), + column: Int(column) + ) + _XCTFail( + "Caught error: \(error)\(message().map { ": \($0)" } ?? "")".withAppHostWarningIfNeeded(), + file: filePath, + line: line + ) + return + } + runtimeWarn( + "Caught error: \(error)\(message().map { ": \($0)" } ?? "")", + fileID: fileID, + line: line + ) + } + + public func expectIssue( + _ message: @autoclosure () -> String?, + fileID: StaticString, + filePath: StaticString, + line: UInt, + column: UInt ) { #if canImport(os) - guard ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" - else { - print("🟣 \(fileID):\(line): \(message() ?? "")") - return - } let moduleName = String( Substring("\(fileID)".utf8.prefix(while: { $0 != UTF8.CodeUnit(ascii: "/") })) ) var message = message() ?? "" if message.isEmpty { - message = "Issue reported" + message = "Issue expected" } os_log( - .fault, - dso: dso, - log: OSLog(subsystem: "com.apple.runtime-issues", category: moduleName), + .info, + log: OSLog(subsystem: "co.pointfree.expected-issues", category: moduleName), "%@", "\(isTesting ? "\(fileID):\(line): " : "")\(message)" ) #else - printError("\(fileID):\(line): \(message() ?? "")") + print("\(fileID):\(line): \(message() ?? "")") #endif } - public func expectIssue( + @_transparent + @inlinable + func runtimeWarn( _ message: @autoclosure () -> String?, fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt + line: UInt ) { #if canImport(os) + guard ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" + else { + print("🟣 \(fileID):\(line): \(message() ?? "")") + return + } let moduleName = String( Substring("\(fileID)".utf8.prefix(while: { $0 != UTF8.CodeUnit(ascii: "/") })) ) var message = message() ?? "" if message.isEmpty { - message = "Issue expected" + message = "Issue reported" } os_log( - .info, - log: OSLog(subsystem: "co.pointfree.expected-issues", category: moduleName), + .fault, + dso: dso, + log: OSLog(subsystem: "com.apple.runtime-issues", category: moduleName), "%@", "\(isTesting ? "\(fileID):\(line): " : "")\(message)" ) #else - print("\(fileID):\(line): \(message() ?? "")") + printError("\(fileID):\(line): \(message() ?? "")") #endif + } } diff --git a/Sources/IssueReporting/ReportIssue.swift b/Sources/IssueReporting/ReportIssue.swift index 81ac120..d572815 100644 --- a/Sources/IssueReporting/ReportIssue.swift +++ b/Sources/IssueReporting/ReportIssue.swift @@ -34,50 +34,35 @@ public func reportIssue( line: UInt = #line, column: UInt = #column ) { + guard !IssueReporters.current.isEmpty else { return } let (fileID, filePath, line, column) = ( IssueContext.current?.fileID ?? fileID, IssueContext.current?.filePath ?? filePath, IssueContext.current?.line ?? line, IssueContext.current?.column ?? column ) - guard TestContext.current != nil else { - guard !isTesting else { return } - if let observer = FailureObserver.current { - observer.withLock { $0 += 1 } - for reporter in IssueReporters.current { - reporter.expectIssue( - message(), - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } - } else { - for reporter in IssueReporters.current { - reporter.reportIssue( - message(), - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } + if let observer = FailureObserver.current { + observer.withLock { $0 += 1 } + for reporter in IssueReporters.current { + reporter.expectIssue( + message(), + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + } + } else { + for reporter in IssueReporters.current { + reporter.reportIssue( + message(), + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) } - return } - _recordIssue( - message: message(), - fileID: "\(fileID)", - filePath: "\(filePath)", - line: Int(line), - column: Int(column) - ) - _XCTFail( - message().withAppHostWarningIfNeeded() ?? "", - file: filePath, - line: line - ) } /// Report a caught error. @@ -101,57 +86,35 @@ public func reportIssue( line: UInt = #line, column: UInt = #column ) { + guard !IssueReporters.current.isEmpty else { return } let (fileID, filePath, line, column) = ( IssueContext.current?.fileID ?? fileID, IssueContext.current?.filePath ?? filePath, IssueContext.current?.line ?? line, IssueContext.current?.column ?? column ) - guard let context = TestContext.current else { - guard !isTesting else { return } - if let observer = FailureObserver.current { - observer.withLock { $0 += 1 } - for reporter in IssueReporters.current { - reporter.expectIssue( - error, - message(), - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } - } else { - for reporter in IssueReporters.current { - reporter.reportIssue( - error, - message(), - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } + if let observer = FailureObserver.current { + observer.withLock { $0 += 1 } + for reporter in IssueReporters.current { + reporter.expectIssue( + error, + message(), + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + } + } else { + for reporter in IssueReporters.current { + reporter.reportIssue( + error, + message(), + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) } - return - } - - switch context { - case .swiftTesting: - _recordError( - error: error, - message: message(), - fileID: "\(fileID)", - filePath: "\(filePath)", - line: Int(line), - column: Int(column) - ) - case .xcTest: - _XCTFail( - "Caught error: \(error)\(message().map { ": \($0)" } ?? "")".withAppHostWarningIfNeeded(), - file: filePath, - line: line - ) - @unknown default: break } } From 141026853167b99cd165d8627b6ddb162ffc1655 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 14:49:32 -0800 Subject: [PATCH 2/5] wip --- Tests/IssueReportingTests/SwiftTestingTests.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/IssueReportingTests/SwiftTestingTests.swift b/Tests/IssueReportingTests/SwiftTestingTests.swift index 88f5d43..9b31167 100644 --- a/Tests/IssueReportingTests/SwiftTestingTests.swift +++ b/Tests/IssueReportingTests/SwiftTestingTests.swift @@ -162,6 +162,13 @@ reportIssue("") } } + + @Test + func emptyReporters() async throws { + withIssueReporters([]) { + reportIssue("This should not fail") + } + } } private struct Failure: Error {} From 6d52c9b9c237d74edacabc4df8289093b8a6450a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 14:52:21 -0800 Subject: [PATCH 3/5] wip --- Sources/IssueReporting/Internal/Deprecations.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/IssueReporting/Internal/Deprecations.swift b/Sources/IssueReporting/Internal/Deprecations.swift index c405b25..17d0345 100644 --- a/Sources/IssueReporting/Internal/Deprecations.swift +++ b/Sources/IssueReporting/Internal/Deprecations.swift @@ -8,6 +8,9 @@ extension IssueReporter where Self == _DefaultReporter { public static var runtimeWarning: Self { Self() } } +@available(*, unavailable, renamed: "_DefaultReporter") +public typealias _RuntimeWarningReporter = _DefaultReporter + // NB: Deprecated after 1.2.2 #if canImport(Darwin) From c0510d945ea3d0555f3e72c116d4909db5c58f2d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 15:32:48 -0800 Subject: [PATCH 4/5] wip --- Examples/ExamplesTests/SwiftTestingTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/ExamplesTests/SwiftTestingTests.swift b/Examples/ExamplesTests/SwiftTestingTests.swift index e19f7e6..efbde3f 100644 --- a/Examples/ExamplesTests/SwiftTestingTests.swift +++ b/Examples/ExamplesTests/SwiftTestingTests.swift @@ -18,7 +18,7 @@ withKnownIssue { reportIssue() } matching: { issue in - issue.description == "Issue recorded" + issue.description.hasPrefix("Issue recorded") } } @@ -26,7 +26,7 @@ withKnownIssue { reportIssue("Something went wrong") } matching: { issue in - issue.description == "Issue recorded: Something went wrong" + issue.description.hasSuffix("Something went wrong") } } @@ -51,7 +51,7 @@ withExpectedIssue { } } matching: { issue in - issue.description == "Known issue was not recorded" + issue.description.hasPrefix("Known issue was not recorded") } } @@ -60,7 +60,7 @@ withExpectedIssue("This didn't fail") { } } matching: { issue in - issue.description == "Known issue was not recorded: This didn't fail" + issue.description.hasSuffix("This didn't fail") } } From 69685601aeff1b82aa4778ac9dcb5e04ff36b99a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 14 Nov 2025 23:53:27 -0800 Subject: [PATCH 5/5] wip --- .github/workflows/ci.yml | 26 +++++++++++++------------- Makefile | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index badb85a..cd629a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,18 +14,18 @@ concurrency: cancel-in-progress: true jobs: - macos-15: + macos-26: strategy: matrix: config: - debug - release xcode: - - '16.4' - name: macOS 15 - runs-on: macos-15 + - '26.1' + name: macOS 26 + runs-on: macos-26 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Run tests @@ -35,7 +35,7 @@ jobs: name: Library evolution runs-on: macos-15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: Run tests @@ -48,11 +48,11 @@ jobs: - Debug - Release xcode: - - '16.4' + - '26.1' name: Examples - runs-on: macos-15 + runs-on: macos-26 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Run tests @@ -68,7 +68,7 @@ jobs: runs-on: ubuntu-latest container: swift:6.0.3 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install dependencies run: apt-get update && apt-get install -y build-essential libcurl4-openssl-dev - name: Run tests @@ -80,7 +80,7 @@ jobs: name: Wasm runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: bytecodealliance/actions/wasmtime/setup@v1 - name: Install Swift and Swift SDK for WebAssembly run: | @@ -111,7 +111,7 @@ jobs: # tag: 6.0.3-RELEASE # - name: Set long paths # run: git config --system core.longpaths true - # - uses: actions/checkout@v4 + # - uses: actions/checkout@v5 # - name: Build # run: swift build -c ${{ matrix.config }} # - name: Run tests (debug only) @@ -121,6 +121,6 @@ jobs: # name: Android # runs-on: ubuntu-latest # steps: - # - uses: actions/checkout@v4 + # - uses: actions/checkout@v5 # - name: "Test Swift Package on Android" # uses: skiptools/swift-android-action@v2 diff --git a/Makefile b/Makefile index ba50097..bbcef5b 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ test-examples: -configuration $(CONFIG) \ -workspace IssueReporting.xcworkspace \ -scheme Examples \ - -destination platform="iOS Simulator,name=iPhone 16" + -destination platform="iOS Simulator,name=iPhone 17" test-wasm: echo wasm-DEVELOPMENT-SNAPSHOT-2024-07-16-a > .swift-version