diff --git a/.spi.yml b/.spi.yml index 975cc15b..fee6e676 100644 --- a/.spi.yml +++ b/.spi.yml @@ -2,5 +2,4 @@ version: 1 builder: configs: - documentation_targets: - - IssueReporting - XCTestDynamicOverlay diff --git a/Package.resolved b/Package.resolved index 5b815747..22304495 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,41 +1,5 @@ { "pins" : [ - { - "identity" : "carton", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftwasm/carton", - "state" : { - "revision" : "d3f1da61faa05283e46a05698ac9bea46fd1035f", - "version" : "1.1.2" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "46989693916f56d1186bd59ac15124caef896560", - "version" : "1.3.1" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", - "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", - "version" : "1.1.2" - } - }, { "identity" : "swift-docc-plugin", "kind" : "remoteSourceControl", @@ -46,39 +10,12 @@ } }, { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", - "version" : "1.6.1" - } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "fc79798d5a150d61361a27ce0c51169b889e23de", - "version" : "2.68.0" - } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system.git", - "state" : { - "revision" : "6a9e38e7bd22a3b8ba80bddf395623cf68f57807", - "version" : "1.3.1" - } - }, - { - "identity" : "wasmtransformer", + "identity" : "swift-issue-reporting-preview", "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftwasm/WasmTransformer", + "location" : "https://github.com/pointfreeco/swift-issue-reporting-preview", "state" : { - "revision" : "d04b31f61b6f528a9a96ebfe4fa4275e333eba82", - "version" : "0.5.0" + "branch" : "main", + "revision" : "5d8dcd8c27f58f1280762cc4af49fc6be0181032" } } ], diff --git a/Package.swift b/Package.swift index 88a6c544..e723b176 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,6 @@ import PackageDescription let package = Package( - // NB: Keep this for backwards compatibility. Will rename to 'swift-issue-reporting' in 2.0. name: "xctest-dynamic-overlay", platforms: [ .iOS(.v13), @@ -12,32 +11,22 @@ let package = Package( .watchOS(.v6), ], products: [ - .library(name: "IssueReporting", targets: ["IssueReporting"]), - .library(name: "IssueReportingTestSupport", targets: ["IssueReportingTestSupport"]), .library(name: "XCTestDynamicOverlay", targets: ["XCTestDynamicOverlay"]), ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-issue-reporting-preview", branch: "main"), + ], targets: [ .target( - name: "IssueReporting" - ), - .testTarget( - name: "IssueReportingTests", + name: "XCTestDynamicOverlay", dependencies: [ - "IssueReporting", - "IssueReportingTestSupport", + .product(name: "IssueReporting", package: "swift-issue-reporting-preview"), ] ), - .target( - name: "IssueReportingTestSupport" - ), - .target( - name: "XCTestDynamicOverlay", - dependencies: ["IssueReporting"] - ), .testTarget( name: "XCTestDynamicOverlayTests", dependencies: [ - "IssueReportingTestSupport", + .product(name: "IssueReportingTestSupport", package: "swift-issue-reporting-preview"), "XCTestDynamicOverlay", ] ), @@ -47,14 +36,5 @@ let package = Package( #if os(macOS) package.dependencies.append(contentsOf: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftwasm/carton", from: "1.0.0"), ]) - package.targets.append( - .executableTarget( - name: "WasmTests", - dependencies: [ - "IssueReporting" - ] - ) - ) #endif diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index e8833a57..ac261c7b 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -11,32 +11,22 @@ let package = Package( .watchOS(.v6), ], products: [ - .library(name: "IssueReporting", targets: ["IssueReporting"]), - .library(name: "IssueReportingTestSupport", targets: ["IssueReportingTestSupport"]), .library(name: "XCTestDynamicOverlay", targets: ["XCTestDynamicOverlay"]), ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-issue-reporting-preview", branch: "main"), + ], targets: [ .target( - name: "IssueReporting" - ), - .testTarget( - name: "IssueReportingTests", + name: "XCTestDynamicOverlay", dependencies: [ - "IssueReporting", - "IssueReportingTestSupport", + .product(name: "IssueReporting", package: "swift-issue-reporting-preview"), ] ), - .target( - name: "IssueReportingTestSupport" - ), - .target( - name: "XCTestDynamicOverlay", - dependencies: ["IssueReporting"] - ), .testTarget( name: "XCTestDynamicOverlayTests", dependencies: [ - "IssueReportingTestSupport", + .product(name: "IssueReportingTestSupport", package: "swift-issue-reporting-preview"), "XCTestDynamicOverlay", ] ), @@ -53,14 +43,5 @@ let package = Package( #if os(macOS) package.dependencies.append(contentsOf: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftwasm/carton", from: "1.0.0"), ]) - package.targets.append( - .executableTarget( - name: "WasmTests", - dependencies: [ - "IssueReporting" - ] - ) - ) #endif diff --git a/Sources/IssueReporting/Documentation.docc/Articles/CreatingTestHelpers.md b/Sources/IssueReporting/Documentation.docc/Articles/CreatingTestHelpers.md deleted file mode 100644 index 6d136087..00000000 --- a/Sources/IssueReporting/Documentation.docc/Articles/CreatingTestHelpers.md +++ /dev/null @@ -1,47 +0,0 @@ -# Creating testing tools - -Learn how to build testing tools in your libraries using Issue Reporting. - -## Overview - -It is common for libraries to provide a set of tools that help one test code that is using said -library. One can use this library to power those testing tools, and that comes with two big -benefits: - - * Your testing tools will simultaneously work in both XCTest and [swift-testing][testing-gh] with - no further work from you. The Issue Reporting library detects which testing framework is being - used, and correctly invokes either `XCTFail` or `Issue.record`. - - * You can put your testing tools in the same library as the core tools, without the need of a - dedicated "test support" library. Typically testing tools need their own "test support" library - because you cannot invoke `XCTFail` (or `Issue.record`) from app targets. Our library - dynamically loads the symbols necessary to invoke those functions, and this makes it simpler for - people to use your library since they only have one single target to think about. - -## Case studies - -We have two main uses cases for Issue Reporting in our libraries: - - * In the [Composable Architecture][tca-gh] we provide a `TestStore` tool that allows one to test - their features. It allows you to send actions to the store and assert on how state changes, and - further assert how effects feed data back into the system. These testing tools need to invoke - `XCTFail` (or `Issue.record`), but instead they can simply invoke - [`reportIssue`](). This will trigger a test - failure in tests, all without needing a dedicated "ComposableArchitectureTestSupport" library. - - * In our [Dependencies][deps-gh] library, we trigger a test failure when dependencies are accessed - in a test environment that have not been explicitly overridden. That helps to make sure people - do not accidentally access live dependencies in tests. However, that does mean we must invoke - `XCTFail` (or `Issue.record`) from within library to support this functionality, and generally - speaking that is not possible. That is why we instead use `reportIssue` in Dependencies, and - then everything works just fine. - -## Your own libraries - -To build more robust testing tools for your libraries, or to be able to report issues from your -library that are very customizable, simply depend on "IssueReporting" and use the - [`reportIssue`]() tool. - -[tca-gh]: https://github.com/pointfreeco/swift-composable-architecture -[deps-gh]: https://github.com/pointfreeco/swift-dependencies -[testing-gh]: https://github.com/apple/swift-testing diff --git a/Sources/IssueReporting/Documentation.docc/Articles/GettingStarted.md b/Sources/IssueReporting/Documentation.docc/Articles/GettingStarted.md deleted file mode 100644 index 60841945..00000000 --- a/Sources/IssueReporting/Documentation.docc/Articles/GettingStarted.md +++ /dev/null @@ -1,194 +0,0 @@ -# Getting started - -Learn how to report issues in your application code, and how to customize how issues are reported. - -## Reporting issues - -The primary tool for reporting an issue in your application code is the -[`reportIssue`]() function. You can invoke it from -anywhere in your features' code to signal that something happened that should not have: - -```swift -guard let lastItem = items.last -else { - reportIssue("'items' should never be empty.") - return -} -// ... -``` - -By default, this will trigger an unobtrusive, purple runtime warning when running your app in Xcode -(simulator and device): - -![A purple runtime warning in Xcode showing that an issue has been reported.](runtime-warning) - -This provides a very visual way to see when an issue has occurred in your application without -stopping the app's execution or interrupting your workflow. - -The [`reportIssue`]() tool can also be customized -to allow for other ways of reporting issues. It can be configured to trigger a breakpoint if you -want to do some debugging when an issue is reported, or a precondition or fatal error if you want -to truly stop execution. And you can create your own custom issue reporter to send issues to OSLog -or an external server. - -Further, when running your code in a testing context (both Swift's native Testing framework as well -as XCTest), all reported issues become _test failures_. This helps you get test coverage that -problematic code paths are not executed, and makes it possible to build testing tools for libraries -that ship in the same target as the library itself. - -![A test failure in Xcode where an issue has been reported.](test-failure) - -## Issue reporters - -The library comes with a variety of issue reporters that can be used right away: - - * ``IssueReporter/runtimeWarning``: Issues are reported as purple runtime warnings in Xcode and - printed to the console on all other platforms. This is the default reporter. - * ``IssueReporter/breakpoint``: A breakpoint is triggered, stopping execution of your app. This - gives you the ability to debug the issue. - * ``IssueReporter/fatalError``: A fatal error is raised and execution of your app is permanently - stopped. - -You an also create your own custom issue reporter by defining a type that conforms to the -``IssueReporter`` protocol. It has one requirement, -``IssueReporter/reportIssue(_:fileID:filePath:line:column:)``, which you can implement to report -issues in any way you want. - -## Overriding issue reporters - -By default the library uses the ``IssueReporter/runtimeWarning`` reporter, but it is possible to -override the reporters used. There are two primary ways: - - * You can temporarily override reporters for a lexical scope using - ``withIssueReporters(_:operation:)-91179``. For example, to turn off reporting entirely you can - do: - - ```swift - withIssueReporters([]) { - // Any issues raised here will not be reported. - } - ``` - - …or to temporarily add a new issue reporter: - - ```swift - withIssueReporters(IssueReporters.current + [.breakpoint]) { - // Any issues reported here will trigger a breakpoint - } - ``` - - * You can also override the issue reporters globally by setting the ``IssueReporters/current`` - variable. This is typically best done at the entry point of your application: - - ```swift - import IssueReporting - import SwiftUI - - @main - struct MyApp: App { - init() { - IssueReporters.current = [.fatalError] - } - var body: some Scene { - // ... - } - } - ``` - -## Unimplemented closures - -The library also comes with a tool for marking a closure as "unimplemented" so that if it is ever -invoked it will report an issue. This can be useful for a common pattern of defining callback -closures that allow a child domain to communicate to the parent domain. - -For example, suppose you have a child feature that has a delete button to delete the data associated -with the feature. However, the child feature can't actually perform the deletion itself, and -instead needs to communicate to the parent to perform the deletion. One way to do this is to -have the child model hold onto a `onDelete` callback closure: - -```swift -@Observable -class ChildModel { - var onDelete: () -> Void - - func deleteButtonTapped() { - onDelete() - } -} -``` - -Then when the parent model creates the child model it will need to provide this closure and -perform the actual deletion logic: - -```swift -class ParentModel { - var child: ChildModel? - - func presentChildButtonTapped() { - child = ChildModel(onDelete: { - // Parent feature performs deletion logic - }) - } -} -``` - -However, requiring the `onDelete` closure at the time of creating a `ChildModel` is too restrictive. -Sometimes you need to create the `ChildModel` in situations where it is not appropriate to -provide the `onDelete` closure. For example, when deep linking into the child feature: - -```swift -import SwiftUI - -@main -struct MyApp: App { - var body: some Scene { - ParentView( - model: ParentModel( - child: ChildModel(onDelete: { /* ??? */ }) - ) - ) - } -} -``` - -One way to fix this is to provide a default for the closure so that it does not have to be provided -upon initializing of `ChildModel`: - -```swift -@Observable -class ChildModel { - var onDelete: () -> Void = {} - // ... -} -``` - -And instead you will override the closure after creating the model: - -```swift -func presentChildButtonTapped() { - child = ChildModel() - child.onDelete = { - // Parent feature performs deletion logic - } -} -``` - -But now this is to lax. It is not possible to create a `ChildModel` without ever overriding -the `onDelete` closure, which will subtly break your feature. - -The fix is to strike a balance between the restrictiveness of requiring the closure and the -laxness of making it fully optional. By using the library's -[`unimplemented`]() tool we can -mark the closure as unimplemented: - -```swift -@Observable -class ChildModel { - var onDelete: () -> Void = unimplemented("onDelete") - // ... -} -``` - -This means it is not required to provide this closure when creating the `ChildModel`, but if -the closure is not overridden and then invoked, it will report an issue. This will make it obvious -when you forget to override the `onDelete` closure, and allow you to fix it. diff --git a/Sources/IssueReporting/Documentation.docc/Articles/ReleaseMode.md b/Sources/IssueReporting/Documentation.docc/Articles/ReleaseMode.md deleted file mode 100644 index 79c0a18b..00000000 --- a/Sources/IssueReporting/Documentation.docc/Articles/ReleaseMode.md +++ /dev/null @@ -1,37 +0,0 @@ -# Testing in release mode - -Learn about extra steps that must be taken when writing tests in release mode. - -## Overview - -Running your test suite in release mode can be helpful in tracking down even more potential issues -in your app since it exercises the exact code that will run on your users' devices. However, many of -the techniques employed by this package to allow for triggering test failures from app code are not -safe for release builds (or App Store submissions), and so they are hidden behind an `#if DEBUG` -flag. This means that test failures will not be triggered properly when running your tests in -release mode. - -To fix this, you can have your _test targets_ depend on the "IssueReportingTestSupport" library that -comes with this package: - -```swift -.testTarget( - name: "FeatureTests", - dependencies: [ - "Feature", - .product(name: "IssueReportingTestSupport", package: "swift-issue-reporting"), - ] -) -``` - -With that library linked the "IssueReporting" library will now be able to trigger test failures even -in release mode. - -> Important: **Do not** link the "IssueReportingTestSupport" product to any target that is intended -> to be used in an app target. This will result in a variety of linker errors such as: -> -> ``` -> Undefined symbol: __swift_FORCE_LOAD_$_XCTestSwiftSupport -> Undefined symbol: XCTest.XCTExpectFailure(_: Swift.String?, enabled: Swift.Bool?, strict: Swift.Bool?, failingBlock: () throws -> A, issueMatcher: ((XCTest.XCTIssue) -> Swift.Bool)?) throws -> A -> Undefined symbol: XCTest.XCTFail(_: Swift.String, file: Swift.StaticString, line: Swift.UInt) -> () -> ``` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.breakpoint.md b/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.breakpoint.md deleted file mode 100644 index ea031570..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.breakpoint.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``IssueReporting/IssueReporter/breakpoint`` - -## Topics - -### Reporter - -- ``BreakpointReporter`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.expectIssue.md b/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.expectIssue.md deleted file mode 100644 index e4852d22..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.expectIssue.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``IssueReporting/IssueReporter/expectIssue(_:fileID:filePath:line:column:)`` - -## Topics - -### Protocol method - -- ``IssueReporter/expectIssue(_:fileID:filePath:line:column:)-8ooca`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.fatalError.md b/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.fatalError.md deleted file mode 100644 index 0358cf95..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.fatalError.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``IssueReporting/IssueReporter/fatalError`` - -## Topics - -### Reporter - -- ``FatalErrorReporter`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.md b/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.md deleted file mode 100644 index 7221f27b..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.md +++ /dev/null @@ -1,14 +0,0 @@ -# ``IssueReporting/IssueReporter`` - -## Topics - -### Conforming to report issues - -- ``reportIssue(_:fileID:filePath:line:column:)`` -- ``expectIssue(_:fileID:filePath:line:column:)-8ooca`` - -### Available reporters - -- ``runtimeWarning`` -- ``breakpoint`` -- ``fatalError`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.runtimeWarning.md b/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.runtimeWarning.md deleted file mode 100644 index ab3acf4c..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/IssueReporter.runtimeWarning.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``IssueReporting/IssueReporter/runtimeWarning`` - -## Topics - -### Reporter - -- ``RuntimeWarningReporter`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/Unimplemented.md b/Sources/IssueReporting/Documentation.docc/Extensions/Unimplemented.md deleted file mode 100644 index 7cb2b395..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/Unimplemented.md +++ /dev/null @@ -1,14 +0,0 @@ -# ``IssueReporting/unimplemented(_:placeholder:fileID:filePath:function:line:column:)-3hygi`` - -## Topics - -### Overloads - -- ``unimplemented(_:fileID:filePath:function:line:column:)-2ae22`` -- ``unimplemented(_:fileID:filePath:function:line:column:)-1hsov`` -- ``unimplemented(_:placeholder:fileID:filePath:function:line:column:)-6ts5j`` -- ``unimplemented(_:placeholder:fileID:filePath:function:line:column:)-34tpp`` - -### Structures - -- ``UnimplementedFailure`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/reportIssue.md b/Sources/IssueReporting/Documentation.docc/Extensions/reportIssue.md deleted file mode 100644 index 9100c5e8..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/reportIssue.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``IssueReporting/reportIssue(_:fileID:filePath:line:column:)`` - -## Topics - -### Reporting errors - -- ``reportIssue(_:_:fileID:filePath:line:column:)`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/withExpectedIssue.md b/Sources/IssueReporting/Documentation.docc/Extensions/withExpectedIssue.md deleted file mode 100644 index 6f57328c..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/withExpectedIssue.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``IssueReporting/withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)-9pinm`` - -## Topics - -### Overloads - -- ``IssueReporting/withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)-7noz2`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/withIssueContext.md b/Sources/IssueReporting/Documentation.docc/Extensions/withIssueContext.md deleted file mode 100644 index 5bee8019..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/withIssueContext.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``IssueReporting/withIssueContext(fileID:filePath:line:column:operation:)-97lux`` - -## Topics - -### Overloads - -- ``withIssueContext(fileID:filePath:line:column:operation:)-6o3dr`` diff --git a/Sources/IssueReporting/Documentation.docc/Extensions/withIssueReporters.md b/Sources/IssueReporting/Documentation.docc/Extensions/withIssueReporters.md deleted file mode 100644 index e15fcd5c..00000000 --- a/Sources/IssueReporting/Documentation.docc/Extensions/withIssueReporters.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``IssueReporting/withIssueReporters(_:operation:)-91179`` - -## Topics - -### Overloads - -- ``withIssueReporters(_:operation:)-6xjha`` diff --git a/Sources/IssueReporting/Documentation.docc/IssueReporting.md b/Sources/IssueReporting/Documentation.docc/IssueReporting.md deleted file mode 100644 index 4d4edd1c..00000000 --- a/Sources/IssueReporting/Documentation.docc/IssueReporting.md +++ /dev/null @@ -1,77 +0,0 @@ -# ``IssueReporting`` - -Report issues in your application and library code as Xcode runtime warnings, breakpoints, -assertions, and do so in a testable manner. - -## Overview - -This library provides robust tools for reporting issues in your application with a customizable -degree of granularity and severity. In its most basic form you use the unified -[`reportIssue`]() function anywhere in your -application to flag an issue in your code, such as a code path that you think should never be -executed: - -```swift -guard let lastItem = items.last -else { - reportIssue("'items' should never be empty.") - return -} -// ... -``` - -By default, [`reportIssue`]() will trigger an -unobtrusive, purple runtime warning when running your app in Xcode (simulator and device): - -![A purple runtime warning in Xcode showing that an issue has been reported.](runtime-warning) - -This provides a very visible way to see when an issue has occurred in your application without -stopping the app's execution and interrupting your workflow. - -The [`reportIssue`]() tool can also be customized -to allow for other ways of reporting issues. It can be configured to trigger a breakpoint if you -want to do some debugging when an issue is reported, or a precondition or fatal error if you want -to truly stop execution. And you can create your own custom issue reporter to send issues to OSLog -or an external server. - -Further, when running your code in a testing context (both XCTest and Swift's native Testing -framework), all reported issues become _test failures_. This helps you get test coverage that -problematic code paths are not executed, and makes it possible to build testing tools for libraries -that ship in the same target as the library itself. - -![A test failure in Xcode where an issue has been reported.](test-failure) - -## Topics - -### Essentials - -- -- -- - -### Reporting issues - -- ``reportIssue(_:fileID:filePath:line:column:)`` -- ``withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)-9pinm`` - -### Issue reporters - -- ``IssueReporter/breakpoint`` -- ``IssueReporter/fatalError`` -- ``IssueReporter/runtimeWarning`` - -### Custom reporting - -- ``IssueReporter`` -- ``withIssueReporters(_:operation:)-91179`` -- ``withIssueContext(fileID:filePath:line:column:operation:)-97lux`` -- ``IssueReporters`` - -### Testing - -- ``isTesting`` -- ``TestContext`` - -### Unimplemented - -- ``unimplemented(_:placeholder:fileID:filePath:function:line:column:)-3hygi`` diff --git a/Sources/IssueReporting/Documentation.docc/Resources/runtime-warning.png b/Sources/IssueReporting/Documentation.docc/Resources/runtime-warning.png deleted file mode 100644 index 3f64de6f..00000000 Binary files a/Sources/IssueReporting/Documentation.docc/Resources/runtime-warning.png and /dev/null differ diff --git a/Sources/IssueReporting/Documentation.docc/Resources/runtime-warning~dark.png b/Sources/IssueReporting/Documentation.docc/Resources/runtime-warning~dark.png deleted file mode 100644 index f30fdf5f..00000000 Binary files a/Sources/IssueReporting/Documentation.docc/Resources/runtime-warning~dark.png and /dev/null differ diff --git a/Sources/IssueReporting/Documentation.docc/Resources/test-failure.png b/Sources/IssueReporting/Documentation.docc/Resources/test-failure.png deleted file mode 100644 index 36716d7d..00000000 Binary files a/Sources/IssueReporting/Documentation.docc/Resources/test-failure.png and /dev/null differ diff --git a/Sources/IssueReporting/Documentation.docc/Resources/test-failure~dark.png b/Sources/IssueReporting/Documentation.docc/Resources/test-failure~dark.png deleted file mode 100644 index 2b6fedfa..00000000 Binary files a/Sources/IssueReporting/Documentation.docc/Resources/test-failure~dark.png and /dev/null differ diff --git a/Sources/IssueReporting/Internal/AppHostWarning.swift b/Sources/IssueReporting/Internal/AppHostWarning.swift deleted file mode 100644 index 941f9af8..00000000 --- a/Sources/IssueReporting/Internal/AppHostWarning.swift +++ /dev/null @@ -1,79 +0,0 @@ -import Foundation - -extension String? { - @usableFromInline - func withAppHostWarningIfNeeded() -> String? { - guard let self - else { - let warning = "".withAppHostWarningIfNeeded() - return warning.isEmpty ? nil : warning - } - return self.withAppHostWarningIfNeeded() - } -} - -extension String { - @usableFromInline - func withAppHostWarningIfNeeded() -> String { - #if os(WASI) - return self - #else - guard - isTesting, - Bundle.main.bundleIdentifier != "com.apple.dt.xctest.tool" - else { return self } - - let callStack = Thread.callStackSymbols - guard - callStack.allSatisfy({ !$0.contains(" XCTestCore ") }), - callStack.allSatisfy({ !$0.isTestFrame }) - else { return self } - - let warning = """ - This issue was emitted from tests running in a host application\ - \(Bundle.main.bundleIdentifier.map { " (\($0))" } ?? ""). - - This can lead to false positives, where failures could have emitted from live application \ - code at launch time, and not from the current test. - - For more information (and workarounds), see "Testing gotchas": - - https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/testing#Testing-gotchas - """ - - return isEmpty - ? warning - : """ - \(self) - - ━━┉┅ - \(warning) - """ - #endif - } - - #if !os(WASI) - @usableFromInline - var isTestFrame: Bool { - guard let xcTestCase = NSClassFromString("XCTestCase") - else { return false } - - // Regular expression to detect and demangle an XCTest case frame: - // - // 1. `(?<=\$s)`: Starts with "$s" (stable mangling) - // 2. `\d{1,3}`: Some numbers (the class name length or the module name length) - // 3. `.*`: The class name, or module name + class name length + class name - // 4. `C`: The class type identifier - // 5. `(?=\d{1,3}test.*yy(Ya)?K?F)`: The function name length, a function that starts with - // `test`, has no arguments (`y`), returns Void (`y`), and is a function (`F`), - // potentially async (`Ya`), throwing (`K`), or both. - return range( - of: #"(?<=\$s)\d{1,3}.*C(?=\d{1,3}test.*yy(Ya)?K?F)"#, options: .regularExpression - ) - .map { - (_typeByName(String(self[$0])) as? NSObject.Type)?.isSubclass(of: xcTestCase) ?? false - } - ?? false - } - #endif -} diff --git a/Sources/IssueReporting/Internal/FailureObserver.swift b/Sources/IssueReporting/Internal/FailureObserver.swift deleted file mode 100644 index eaa633d0..00000000 --- a/Sources/IssueReporting/Internal/FailureObserver.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -@usableFromInline -final class FailureObserver: @unchecked Sendable { - @TaskLocal public static var current: FailureObserver? - - private let lock = NSRecursiveLock() - private var count = 0 - - @usableFromInline - init(count: Int = 0) { - self.count = count - } - - @usableFromInline - func withLock(_ body: (inout Int) -> R) -> R { - lock.lock() - defer { lock.unlock() } - return body(&count) - } -} diff --git a/Sources/IssueReporting/Internal/LockIsolated.swift b/Sources/IssueReporting/Internal/LockIsolated.swift deleted file mode 100644 index 59e56606..00000000 --- a/Sources/IssueReporting/Internal/LockIsolated.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -@usableFromInline -final class LockIsolated: @unchecked Sendable { - private var _value: Value - private let lock = NSRecursiveLock() - @usableFromInline - init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { - self._value = try value() - } - @usableFromInline - func withLock( - _ operation: @Sendable (inout Value) throws -> T - ) rethrows -> T { - lock.lock() - defer { lock.unlock() } - var value = _value - defer { _value = value } - return try operation(&value) - } -} diff --git a/Sources/IssueReporting/Internal/Rethrows.swift b/Sources/IssueReporting/Internal/Rethrows.swift deleted file mode 100644 index 58e6626f..00000000 --- a/Sources/IssueReporting/Internal/Rethrows.swift +++ /dev/null @@ -1,17 +0,0 @@ -@rethrows -@usableFromInline -protocol _ErrorMechanism { - associatedtype Output - func get() throws -> Output -} -extension _ErrorMechanism { - func _rethrowError() rethrows -> Never { - _ = try _rethrowGet() - fatalError() - } - @usableFromInline - func _rethrowGet() rethrows -> Output { - return try get() - } -} -extension Result: _ErrorMechanism {} diff --git a/Sources/IssueReporting/Internal/SwiftTesting.swift b/Sources/IssueReporting/Internal/SwiftTesting.swift deleted file mode 100644 index 02053583..00000000 --- a/Sources/IssueReporting/Internal/SwiftTesting.swift +++ /dev/null @@ -1,416 +0,0 @@ -import Foundation - -#if canImport(WinSDK) - import WinSDK -#endif - -@usableFromInline -func _recordIssue( - message: String?, - fileID: String = #fileID, - filePath: String = #filePath, - line: Int = #line, - column: Int = #column -) { - guard let function = function(for: "$s25IssueReportingTestSupport07_recordA0ypyF") - else { - #if DEBUG - guard - let fromSyntaxNode = unsafeBitCast( - symbol: "$s7Testing12__ExpressionV16__fromSyntaxNodeyACSSFZ", - in: "Testing", - to: (@convention(thin) (String) -> __Expression).self - ), - let checkValue = unsafeBitCast( - symbol: """ - $s7Testing12__checkValue_10expression0D25WithCapturedRuntimeValues26mismatchedErrorDesc\ - ription10difference8comments10isRequired14sourceLocations6ResultOyyts0J0_pGSb_AA12__Exp\ - ressionVAOSgyXKSSSgyXKAQyXKSayAA7CommentVGyXKSbAA06SourceQ0VtF - """, - in: "Testing", - to: (@convention(thin) ( - Bool, - __Expression, - @autoclosure () -> __Expression?, - @autoclosure () -> String?, - @autoclosure () -> String?, - @autoclosure () -> [Any], - Bool, - SourceLocation - ) -> Result) - .self - ) - else { return } - - let syntaxNode = fromSyntaxNode(message ?? "") - _ = checkValue( - false, - syntaxNode, - nil, - nil, - nil, - [], - false, - SourceLocation(fileID: fileID, _filePath: filePath, line: line, column: column) - ) - #else - printError( - """ - \(fileID):\(line): An issue was recorded without linking the Testing framework. - - To fix this, add "IssueReportingTestSupport" as a dependency to your test target. - """ - ) - #endif - return - } - - let recordIssue = function as! @Sendable (String?, String, String, Int, Int) -> Void - recordIssue(message, fileID, filePath, line, column) -} - -@usableFromInline -func _recordError( - error: any Error, - message: String?, - fileID: String = #fileID, - filePath: String = #filePath, - line: Int = #line, - column: Int = #column -) { - guard let function = function(for: "$s25IssueReportingTestSupport12_recordErrorypyF") - else { - #if DEBUG - guard - let record = unsafeBitCast( - symbol: """ - $s7Testing5IssueV6record__14sourceLocationACs5Error_p_AA7CommentVSgAA06SourceE0VtFZ - """, - in: "Testing", - to: (@convention(thin) (any Error, Any?, SourceLocation) -> Any).self - ) - else { return } - - var comment: Any? - if let message { - var c = UnsafeMutablePointer.allocate(capacity: 1).pointee - c.rawValue = message - comment = c - } - _ = record( - error, - comment, - SourceLocation(fileID: fileID, _filePath: filePath, line: line, column: column) - ) - #else - printError( - """ - \(fileID):\(line): An issue was recorded without linking the Testing framework. - - To fix this, add "IssueReportingTestSupport" as a dependency to your test target. - """ - ) - #endif - return - } - - let recordError = function as! @Sendable (any Error, String?, String, String, Int, Int) -> Void - recordError(error, message, fileID, filePath, line, column) -} - -@usableFromInline -func _withKnownIssue( - _ message: String? = nil, - isIntermittent: Bool = false, - fileID: String = #fileID, - filePath: String = #filePath, - line: Int = #line, - column: Int = #column, - _ body: () throws -> Void -) { - guard let function = function(for: "$s25IssueReportingTestSupport010_withKnownA0ypyF") - else { - #if DEBUG - guard - let withKnownIssue = unsafeBitCast( - symbol: """ - $s7Testing14withKnownIssue_14isIntermittent14sourceLocation_yAA7CommentVSg_SbAA06Source\ - H0VyyKXEtF - """, - in: "Testing", - to: (@convention(thin) ( - Any?, - Bool, - SourceLocation, - () throws -> Void - ) -> Void) - .self - ) - else { return } - - var comment: Any? - if let message { - var c = UnsafeMutablePointer.allocate(capacity: 1).pointee - c.rawValue = message - comment = c - } - 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, - () throws -> Void - ) -> Void - 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 - ) - 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) -} -@usableFromInline -func _currentTestIsNotNil() -> Bool { - guard let function = function(for: "$s25IssueReportingTestSupport08_currentC8IsNotNilypyF") - else { - #if DEBUG - return Test.current != nil - #else - printError( - """ - 'Test.current' was accessed without linking the Testing framework. - - To fix this, add "IssueReportingTestSupport" as a dependency to your test target. - """ - ) - return false - #endif - } - - return (function as! @Sendable () -> Bool)() -} - -#if DEBUG - #if _runtime(_ObjC) - import ObjectiveC - - private typealias __XCTestCompatibleSelector = Selector - #else - private typealias __XCTestCompatibleSelector = Never - #endif - - private struct __Expression: Sendable { - enum Kind: Sendable { - case generic(_ sourceCode: String) - case stringLiteral(sourceCode: String, stringValue: String) - indirect case binaryOperation(lhs: __Expression, `operator`: String, rhs: __Expression) - struct FunctionCallArgument: Sendable { - var label: String? - var value: __Expression - } - indirect case functionCall( - value: __Expression?, functionName: String, arguments: [FunctionCallArgument] - ) - indirect case propertyAccess(value: __Expression, keyPath: __Expression) - indirect case negation(_ expression: __Expression, isParenthetical: Bool) - } - var kind: Kind - struct Value: Sendable { - var description: String - var debugDescription: String - var typeInfo: TypeInfo - var label: String? - var isCollection: Bool - var children: [Self]? - } - var runtimeValue: Value? - } - - private struct TypeInfo: Sendable { - enum _Kind: Sendable { - case type(_ type: Any.Type) - case nameOnly(fullyQualifiedComponents: [String], unqualified: String, mangled: String?) - } - var _kind: _Kind - } - - private struct SourceLocation: Sendable { - var fileID: String - var _filePath: String - var line: Int - var column: Int - } - - private struct Comment: RawRepresentable, Sendable { - var rawValue: String - init(rawValue: String) { - self.rawValue = rawValue - self.kind = nil - } - enum Kind: Sendable { - case line - case block - case documentationLine - case documentationBlock - case trait - case stringLiteral - } - var kind: Kind? - } - - private protocol Trait: Sendable {} - - struct Test: @unchecked Sendable { - static var current: Self? { - guard - let current = unsafeBitCast( - symbol: "$s7Testing4TestV7currentACSgvgZ", - in: "Testing", - to: (@convention(thin) () -> Test?).self - ) - else { return nil } - return current() - } - - struct Case {} - private var name: String - private var displayName: String? - private var traits: [any Trait] - private var sourceLocation: SourceLocation - private var containingTypeInfo: TypeInfo? - private var xcTestCompatibleSelector: __XCTestCompatibleSelector? - fileprivate enum TestCasesState: @unchecked Sendable { - case unevaluated(_ function: @Sendable () async throws -> AnySequence) - case evaluated(_ testCases: AnySequence) - case failed(_ error: any Error) - } - fileprivate var testCasesState: TestCasesState? - private var parameters: [Parameter]? - private struct Parameter: Sendable { - var index: Int - var firstName: String - var secondName: String? - var typeInfo: TypeInfo - } - private var isSynthesized = false - } -#endif - -@usableFromInline -func function(for symbol: String) -> Any? { - let function = unsafeBitCast( - symbol: symbol, - in: "IssueReportingTestSupport", - to: (@convention(thin) () -> Any).self - ) - return function?() -} - -@usableFromInline -func unsafeBitCast(symbol: String, in library: String, to function: F.Type) -> F? { - #if os(Linux) - guard - let handle = dlopen("lib\(library).so", RTLD_LAZY), - let pointer = dlsym(handle, symbol) - else { return nil } - return unsafeBitCast(pointer, to: F.self) - #elseif canImport(Darwin) - guard - let handle = dlopen(nil, RTLD_LAZY), - let pointer = dlsym(handle, symbol) - else { return nil } - return unsafeBitCast(pointer, to: F.self) - #elseif os(Windows) - guard - let handle = LoadLibraryA("\(library).dll"), - let pointer = GetProcAddress(handle, symbol) - else { return nil } - return unsafeBitCast(pointer, to: F.self) - #else - return nil - #endif -} diff --git a/Sources/IssueReporting/Internal/UncheckedSendable.swift b/Sources/IssueReporting/Internal/UncheckedSendable.swift deleted file mode 100644 index c602ccdd..00000000 --- a/Sources/IssueReporting/Internal/UncheckedSendable.swift +++ /dev/null @@ -1,9 +0,0 @@ -@propertyWrapper -@usableFromInline -struct UncheckedSendable: @unchecked Sendable { - @usableFromInline - var wrappedValue: Value - init(wrappedValue value: Value) { - self.wrappedValue = value - } -} diff --git a/Sources/IssueReporting/Internal/Warn.swift b/Sources/IssueReporting/Internal/Warn.swift deleted file mode 100644 index 489ee3f6..00000000 --- a/Sources/IssueReporting/Internal/Warn.swift +++ /dev/null @@ -1,14 +0,0 @@ -#if os(Linux) - @preconcurrency import Foundation -#else - import Foundation -#endif - -#if canImport(WinSDK) - import WinSDK -#endif - -@usableFromInline -func printError(_ message: String) { - fputs("\(message)\n", stderr) -} diff --git a/Sources/IssueReporting/Internal/XCTest.swift b/Sources/IssueReporting/Internal/XCTest.swift deleted file mode 100644 index b9d9816c..00000000 --- a/Sources/IssueReporting/Internal/XCTest.swift +++ /dev/null @@ -1,126 +0,0 @@ -#if _runtime(_ObjC) - import Foundation -#endif - -#if canImport(Darwin) - import Darwin -#elseif canImport(Glibc) - import Glibc -#elseif canImport(WinSDK) - import WinSDK -#endif - -@usableFromInline -func _XCTFail( - _ message: String = "", - file: StaticString = #filePath, - line: UInt = #line -) { - #if !_runtime(_ObjC) - guard !_XCTExpectedFailure.isInFailingBlock else { return } - #endif - guard let function = function(for: "$s25IssueReportingTestSupport8_XCTFailypyF") - else { - #if DEBUG - if let XCTFail = unsafeBitCast( - symbol: "$s6XCTest7XCTFail_4file4lineySS_s12StaticStringVSutF", - in: "XCTest", - to: (@convention(thin) (String, StaticString, UInt) -> Void).self - ) { - XCTFail(message, file, line) - return - } - #endif - printError( - """ - \(file):\(line): A failure was recorded without linking the XCTest framework. - - To fix this, add "IssueReportingTestSupport" as a dependency to your test target. - """ - ) - return - } - let XCTFail = function as! @Sendable (String, StaticString, UInt) -> Void - XCTFail(message, file, line) -} - -@_transparent -@usableFromInline -func _XCTExpectFailure( - _ failureReason: String? = nil, - enabled: Bool? = nil, - strict: Bool? = nil, - file: StaticString, - line: UInt, - failingBlock: () throws -> R -) rethrows -> R { - #if _runtime(_ObjC) - guard let function = function(for: "$s25IssueReportingTestSupport17_XCTExpectFailureypyF") - else { - #if DEBUG - guard enabled != false - else { return try failingBlock() } - if let pointer = dlsym(dlopen(nil, RTLD_NOW), "XCTExpectFailureWithOptionsInBlock"), - let XCTExpectedFailureOptions = NSClassFromString("XCTExpectedFailureOptions") - as Any as? NSObjectProtocol, - let options = strict ?? true - ? XCTExpectedFailureOptions - .perform(NSSelectorFromString("alloc"))?.takeUnretainedValue() - .perform(NSSelectorFromString("init"))?.takeUnretainedValue() - : XCTExpectedFailureOptions - .perform(NSSelectorFromString("nonStrictOptions"))?.takeUnretainedValue() - { - let XCTExpectFailureInBlock = unsafeBitCast( - pointer, - to: (@convention(c) (String?, AnyObject, () -> Void) -> Void).self - ) - var result: Result? - XCTExpectFailureInBlock(failureReason, options) { - result = Result { try failingBlock() } - } - return try result!._rethrowGet() - } - #else - printError( - """ - \(file):\(line): An expected failure was recorded without linking the XCTest framework. - - To fix this, add "IssueReportingTestSupport" as a dependency to your test target. - """ - ) - #endif - return try failingBlock() - } - let XCTExpectFailure = - function - as! @Sendable (String?, Bool?, Bool?, () throws -> Void) throws -> Void - var result: Result! - do { - try XCTExpectFailure(failureReason, enabled, strict) { - result = Result { try failingBlock() } - } - } catch { - fatalError() - } - return try result._rethrowGet() - #else - _XCTFail( - """ - Expecting failures is unavailable in XCTest on this platform. - - Omit this test from your suite by wrapping it in '#if canImport(Darwin)', or consider using \ - Swift Testing and 'withKnownIssue', instead. - """ - ) - return try _XCTExpectedFailure.$isInFailingBlock.withValue(true) { - try failingBlock() - } - #endif -} - -#if !_runtime(_ObjC) - @usableFromInline - enum _XCTExpectedFailure { - @TaskLocal public static var isInFailingBlock = false - } -#endif diff --git a/Sources/IssueReporting/IsTesting.swift b/Sources/IssueReporting/IsTesting.swift deleted file mode 100644 index 756c87e2..00000000 --- a/Sources/IssueReporting/IsTesting.swift +++ /dev/null @@ -1,39 +0,0 @@ -#if os(WASI) - public let isTesting = false -#else - import Foundation - - /// Whether or not the current process is running tests. - /// - /// You can use this information to prevent application code from running when hosting tests. For - /// example, you can wrap your app entry point: - /// - /// ```swift - /// import IssueReporting - /// - /// @main - /// struct MyApp: App { - /// var body: some Scene { - /// WindowGroup { - /// if !isTesting { - /// MyRootView() - /// } - /// } - /// } - /// } - /// - /// To detect if the current task is running inside a test, use ``TestContext/current``, instead. - public let isTesting = ProcessInfo.processInfo.isTesting - - extension ProcessInfo { - fileprivate var isTesting: Bool { - if environment.keys.contains("XCTestBundlePath") { return true } - if environment.keys.contains("XCTestConfigurationFilePath") { return true } - if environment.keys.contains("XCTestSessionIdentifier") { return true } - return arguments.contains { argument in - let path = URL(fileURLWithPath: argument) - return path.lastPathComponent == "xctest" || path.pathExtension == "xctest" - } - } - } -#endif diff --git a/Sources/IssueReporting/IssueReporter.swift b/Sources/IssueReporting/IssueReporter.swift deleted file mode 100644 index 711fbd12..00000000 --- a/Sources/IssueReporting/IssueReporter.swift +++ /dev/null @@ -1,197 +0,0 @@ -/// A type that can report issues. -public protocol IssueReporter: Sendable { - /// Called when an issue is reported. - /// - /// - Parameters: - /// - message: A message describing the issue. - /// - 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. - func reportIssue( - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) - - /// Called when an error is caught. - /// - /// The default implementation of this conformance simply calls - /// ``reportIssue(_:fileID:filePath:line:column:)`` with a description of the error. - /// - /// - Parameters: - /// - error: An error. - /// - message: A message describing the issue. - /// - 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. - func reportIssue( - _ error: any Error, - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) - - /// Called when an expected issue is reported. - /// - /// The default implementation of this conformance simply ignores the issue. - /// - /// - Parameters: - /// - message: A message describing the issue. - /// - 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. - func expectIssue( - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) - - /// Called when an expected error is reported. - /// - /// The default implementation of this conformance simply ignores the error. - /// - /// - Parameters: - /// - error: An error. - /// - message: A message describing the issue. - /// - 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. - func expectIssue( - _ error: any Error, - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) -} - -extension IssueReporter { - public func reportIssue( - _ error: any Error, - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) { - reportIssue( - "Caught error: \(error)\(message().map { ": \($0)" } ?? "")", - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } - - public func expectIssue( - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) {} - - public func expectIssue( - _ error: any Error, - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) { - expectIssue( - "Caught error: \(error)\(message().map { ": \($0)" } ?? "")", - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } -} - -public enum IssueReporters { - /// The task's current issue reporters. - /// - /// Assigning this directly will override the which issue reporters are notified in the current - /// task. This is generally useful at the entry point of your application, should you want to - /// replace the default reporting: - /// - /// ```swift - /// import IssueReporting - /// - /// @main - /// struct MyApp: App { - /// init() { - /// IssueReporters.current = [.fatalError] - /// } - /// - /// var body: some Scene { - /// // ... - /// } - /// } - /// ``` - /// - /// Issue reporters are fed issues in order. - /// - /// To override the task's issue reporters for a scoped operation, prefer - /// ``withIssueReporters(_:operation:)-91179``. - public static var current: [any IssueReporter] { - get { _current.withLock { $0 } } - set { _current.withLock { $0 = newValue } } - } - - @TaskLocal fileprivate static var _current = LockIsolated<[any IssueReporter]>([.runtimeWarning]) -} - -/// Overrides the task's issue reporters for the duration of the synchronous operation. -/// -/// For example, you can ignore all reported issues by passing an empty array of reporters: -/// -/// ```swift -/// withIssueReporters([]) { -/// // Reported issues will be ignored here... -/// } -/// ``` -/// -/// Or, to temporarily add a custom reporter, you can append it to ``IssueReporters/current``: -/// -/// ```swift -/// withIssueReporters(IssueReporters.current + [MyCustomReporter()]) { -/// // Reported issues will be fed to the -/// } -/// ``` -/// -/// - Parameters: -/// - reporters: Issue reporters to notify during the operation. -/// - operation: A synchronous operation. -public func withIssueReporters( - _ reporters: [any IssueReporter], - operation: () throws -> R -) rethrows -> R { - try IssueReporters.$_current.withValue(LockIsolated(reporters), operation: operation) -} - -/// Overrides the task's issue reporters for the duration of the asynchronous operation. -/// -/// An asynchronous version of ``withIssueReporters(_:operation:)-91179``. -/// -/// - Parameters: -/// - reporters: Issue reporters to notify during the operation. -/// - operation: An asynchronous operation. -public func withIssueReporters( - _ reporters: [any IssueReporter], - operation: () async throws -> R -) async rethrows -> R { - try await IssueReporters.$_current.withValue(LockIsolated(reporters), operation: operation) -} diff --git a/Sources/IssueReporting/IssueReporters/BreakpointReporter.swift b/Sources/IssueReporting/IssueReporters/BreakpointReporter.swift deleted file mode 100644 index d752ed88..00000000 --- a/Sources/IssueReporting/IssueReporters/BreakpointReporter.swift +++ /dev/null @@ -1,52 +0,0 @@ -#if canImport(Darwin) - import Darwin - - extension IssueReporter where Self == BreakpointReporter { - /// An issue reporter that pauses program execution when a debugger is attached. - /// - /// Logs a warning to the console and raises `SIGTRAP` when an issue is received. - public static var breakpoint: Self { Self() } - } - - /// A type representing an issue reporter that pauses program execution when a debugger is - /// attached. - /// - /// Use ``IssueReporter/breakpoint`` to create one of these values. - public struct BreakpointReporter: IssueReporter { - public func reportIssue( - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) { - var message = message() ?? "" - if message.isEmpty { - message = "Issue reported" - } - printError("\(fileID):\(line): \(message)") - guard isDebuggerAttached else { return } - printError( - """ - - Caught debug breakpoint. Type "continue" ("c") to resume execution. - """ - ) - raise(SIGTRAP) - } - - var isDebuggerAttached: Bool { - var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] - var info: kinfo_proc = kinfo_proc() - var info_size = MemoryLayout.size - - return name.withUnsafeMutableBytes { - $0.bindMemory(to: Int32.self).baseAddress - .map { - sysctl($0, 4, &info, &info_size, nil, 0) != -1 && info.kp_proc.p_flag & P_TRACED != 0 - } - ?? false - } - } - } -#endif diff --git a/Sources/IssueReporting/IssueReporters/FatalErrorReporter.swift b/Sources/IssueReporting/IssueReporters/FatalErrorReporter.swift deleted file mode 100644 index d9778b74..00000000 --- a/Sources/IssueReporting/IssueReporters/FatalErrorReporter.swift +++ /dev/null @@ -1,25 +0,0 @@ -extension IssueReporter where Self == FatalErrorReporter { - /// An issue reporter that terminates program execution. - /// - /// Calls Swift's `fatalError` function when an issue is received. - public static var fatalError: Self { Self() } -} - -/// A type representing an issue reporter that terminates program execution. -/// -/// Use ``IssueReporter/fatalError`` to create one of these values. -public struct FatalErrorReporter: IssueReporter { - public func reportIssue( - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) { - var message = message() ?? "" - if message.isEmpty { - message = "Issue reported" - } - Swift.fatalError(message, file: filePath, line: line) - } -} diff --git a/Sources/IssueReporting/IssueReporters/RuntimeWarningReporter.swift b/Sources/IssueReporting/IssueReporters/RuntimeWarningReporter.swift deleted file mode 100644 index ec0f3881..00000000 --- a/Sources/IssueReporting/IssueReporters/RuntimeWarningReporter.swift +++ /dev/null @@ -1,113 +0,0 @@ -import Foundation - -#if canImport(os) - import os -#endif - -extension IssueReporter where Self == RuntimeWarningReporter { - /// 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`. - /// - /// 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() } -} - -/// A type representing an issue reporter that emits "purple" runtime warnings to Xcode and logs -/// fault-level messages to the console. -/// -/// Use ``IssueReporter/runtimeWarning`` to create one of these values. -public struct RuntimeWarningReporter: IssueReporter { - #if canImport(os) - @UncheckedSendable - #if canImport(Darwin) - @_transparent - #endif - @usableFromInline var dso: UnsafeRawPointer - - init(dso: UnsafeRawPointer) { - self.dso = dso - } - - @usableFromInline - init() { - // NB: Xcode runtime warnings offer a much better experience than traditional assertions and - // breakpoints, but Apple provides no means of creating custom runtime warnings ourselves. - // To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead. - // - // Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc - let count = _dyld_image_count() - for i in 0.. String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) { - #if canImport(os) - let moduleName = String( - Substring("\(fileID)".utf8.prefix(while: { $0 != UTF8.CodeUnit(ascii: "/") })) - ) - var message = message() ?? "" - if message.isEmpty { - message = "Issue reported" - } - os_log( - .fault, - dso: dso, - log: OSLog(subsystem: "com.apple.runtime-issues", category: moduleName), - "%@", - "\(isTesting ? "\(fileID):\(line): " : "")\(message)" - ) - #else - printError("\(fileID):\(line): \(message() ?? "")") - #endif - } - - public func expectIssue( - _ message: @autoclosure () -> String?, - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt - ) { - #if canImport(os) - let moduleName = String( - Substring("\(fileID)".utf8.prefix(while: { $0 != UTF8.CodeUnit(ascii: "/") })) - ) - var message = message() ?? "" - if message.isEmpty { - message = "Issue expected" - } - os_log( - .info, - log: OSLog(subsystem: "co.pointfree.expected-issues", category: moduleName), - "%@", - "\(isTesting ? "\(fileID):\(line): " : "")\(message)" - ) - #else - print("\(fileID):\(line): \(message() ?? "")") - #endif - } -} diff --git a/Sources/IssueReporting/ReportIssue.swift b/Sources/IssueReporting/ReportIssue.swift deleted file mode 100644 index f92646e5..00000000 --- a/Sources/IssueReporting/ReportIssue.swift +++ /dev/null @@ -1,143 +0,0 @@ -/// Report an issue. -/// -/// Invoking this function has two different behaviors depending on the context: -/// -/// * When running your code in a non-testing context, this method will loop over the -/// collection of issue reports registered and invoke them. The default issue reporter for the -/// library is ``IssueReporter/runtimeWarning``, which emits a purple, runtime warning in Xcode: -/// -/// ![A purple runtime warning in Xcode showing that an issue has been reported.](runtime-warning) -/// -/// But you can there are also [other issue reports]() you -/// can use, and you can create your own. -/// -/// * When running your app in tests (both XCTest and Swift's native Testing framework), it will -/// emit a test failure. This allows you to get test coverage on your reported issues, both expected -/// and unexpected ones. -/// -/// ![A test failure in Xcode where an issue has been reported.](test-failure) -/// -/// [Issue.record]: https://developer.apple.com/documentation/testing/issue/record(_:sourcelocation:) -/// [XCTFail]: https://developer.apple.com/documentation/xctest/1500970-xctfail/ -/// -/// - Parameters: -/// - message: A message describing the issue. -/// - 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. -@_transparent -public func reportIssue( - _ message: @autoclosure () -> String? = nil, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column -) { - switch TestContext.current { - case .swiftTesting: - _recordIssue( - message: message(), - fileID: "\(IssueContext.current?.fileID ?? fileID)", - filePath: "\(IssueContext.current?.filePath ?? filePath)", - line: Int(IssueContext.current?.line ?? line), - column: Int(IssueContext.current?.column ?? column) - ) - case .xcTest: - _XCTFail( - message().withAppHostWarningIfNeeded() ?? "", - file: IssueContext.current?.filePath ?? filePath, - line: IssueContext.current?.line ?? line - ) - case nil: - guard !isTesting else { return } - if let observer = FailureObserver.current { - observer.withLock { $0 += 1 } - for reporter in IssueReporters.current { - reporter.expectIssue( - message(), - fileID: IssueContext.current?.fileID ?? fileID, - filePath: IssueContext.current?.filePath ?? filePath, - line: IssueContext.current?.line ?? line, - column: IssueContext.current?.column ?? column - ) - } - } else { - for reporter in IssueReporters.current { - reporter.reportIssue( - message(), - fileID: IssueContext.current?.fileID ?? fileID, - filePath: IssueContext.current?.filePath ?? filePath, - line: IssueContext.current?.line ?? line, - column: IssueContext.current?.column ?? column - ) - } - } - } -} - -/// Report a caught error. -/// -/// This function behaves similarly to ``reportIssue(_:fileID:filePath:line:column:)``, but for -/// reporting errors. -/// -/// - Parameters: -/// - error: The error that caused the issue. -/// - message: A message describing the expectation. -/// - 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. -@_transparent -public func reportIssue( - _ error: any Error, - _ message: @autoclosure () -> String? = nil, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column -) { - switch TestContext.current { - case .swiftTesting: - _recordError( - error: error, - message: message(), - fileID: "\(IssueContext.current?.fileID ?? fileID)", - filePath: "\(IssueContext.current?.filePath ?? filePath)", - line: Int(IssueContext.current?.line ?? line), - column: Int(IssueContext.current?.column ?? column) - ) - case .xcTest: - _XCTFail( - "Caught error: \(error)\(message().map { ": \($0)" } ?? "")".withAppHostWarningIfNeeded(), - file: IssueContext.current?.filePath ?? filePath, - line: IssueContext.current?.line ?? line - ) - case nil: - guard !isTesting else { return } - if let observer = FailureObserver.current { - observer.withLock { $0 += 1 } - 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 - ) - } - } else { - for reporter in IssueReporters.current { - reporter.reportIssue( - error, - message(), - fileID: IssueContext.current?.fileID ?? fileID, - filePath: IssueContext.current?.filePath ?? filePath, - line: IssueContext.current?.line ?? line, - column: IssueContext.current?.column ?? column - ) - } - } - } -} diff --git a/Sources/IssueReporting/TestContext.swift b/Sources/IssueReporting/TestContext.swift deleted file mode 100644 index ad3f4e26..00000000 --- a/Sources/IssueReporting/TestContext.swift +++ /dev/null @@ -1,30 +0,0 @@ -/// A type representing the context in which a test is being run, i.e. either in Swift's native -/// Testing framework, or Xcode's XCTest framework. -public enum TestContext { - /// The Swift Testing framework. - case swiftTesting - - /// The XCTest framework. - case xcTest - - /// The context associated with current test. - /// - /// How the test context is detected depends on the framework: - /// - /// * If Swift Testing is running, _and_ this is called from the current test's task, this will - /// return ``swiftTesting``. In this way, `TestContext.current == .swiftTesting` is equivalent - /// to checking `Test.current != nil`, but safe to do from library and application code. - /// - /// * If XCTest is running, _and_ this is called during the execution of a test _regardless_ of - /// task, this will return ``xcTest``. - /// - /// If executed outside of a test process, this will return `nil`. - public static var current: Self? { - guard isTesting else { return nil } - if _currentTestIsNotNil() { - return .swiftTesting - } else { - return .xcTest - } - } -} diff --git a/Sources/IssueReporting/Unimplemented.swift b/Sources/IssueReporting/Unimplemented.swift deleted file mode 100644 index 312a4b88..00000000 --- a/Sources/IssueReporting/Unimplemented.swift +++ /dev/null @@ -1,215 +0,0 @@ -/// Returns a closure that reports an issue when invoked. -/// -/// Useful for creating closures that need to be overridden by users of your API, and if it is -/// ever invoked without being overridden an issue will be reported. See -/// for more information. -/// -/// - Parameters: -/// - description: An optional description of the unimplemented closure. -/// - placeholder: A placeholder value returned from the closure when left unimplemented. -/// - fileID: The fileID. -/// - filePath: The filePath. -/// - function: The function. -/// - line: The line. -/// - column: The column. -/// - Returns: A closure that reports an issue when invoked. -public func unimplemented( - _ description: @autoclosure @escaping @Sendable () -> String = "", - placeholder: @autoclosure @escaping @Sendable () -> Result = (), - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - column: UInt = #column -) -> @Sendable (repeat each Argument) -> Result { - return { (argument: repeat each Argument) in - _fail( - description(), - (repeat each argument), - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: column - ) - return placeholder() - } -} - -/// Returns a throwing closure that reports an issue and throws an error when invoked. -/// -/// Useful for creating closures that need to be overridden by users of your API, and if it is -/// ever invoked without being overridden an issue will be reported. See -/// for more information. -/// -/// - Parameters: -/// - description: An optional description of the unimplemented closure. -/// - fileID: The fileID. -/// - filePath: The filePath. -/// - function: The function. -/// - line: The line. -/// - column: The column. -/// - Returns: A throwing closure that reports an issue and throws an error when invoked. -public func unimplemented( - _ description: @autoclosure @escaping @Sendable () -> String = "", - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - column: UInt = #column -) -> @Sendable (repeat each Argument) throws -> Result { - return { (argument: repeat each Argument) in - let description = description() - _fail( - description, - (repeat each argument), - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: column - ) - throw UnimplementedFailure(description: description) - } -} - -/// Returns an asynchronous closure that reports an issue when invoked. -/// -/// Useful for creating closures that need to be overridden by users of your API, and if it is -/// ever invoked without being overridden an issue will be reported. See -/// for more information. -/// -/// - Parameters: -/// - description: An optional description of the unimplemented closure. -/// - placeholder: A placeholder value returned from the closure when left unimplemented. -/// - fileID: The fileID. -/// - filePath: The filePath. -/// - function: The function. -/// - line: The line. -/// - column: The column. -/// - Returns: An asynchronous closure that reports an issue when invoked. -public func unimplemented( - _ description: @autoclosure @escaping @Sendable () -> String = "", - placeholder: @autoclosure @escaping @Sendable () -> Result = (), - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - column: UInt = #column -) -> @Sendable (repeat each Argument) async -> Result { - return { (argument: repeat each Argument) in - _fail( - description(), - (repeat each argument), - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: column - ) - return placeholder() - } -} - -/// Returns a throwing, asynchronous closure that reports an issue and throws an error when invoked. -/// -/// Useful for creating closures that need to be overridden by users of your API, and if it is -/// ever invoked without being overridden an issue will be reported. See -/// for more information. -/// -/// - Parameters: -/// - description: An optional description of the unimplemented closure. -/// - fileID: The fileID. -/// - filePath: The filePath. -/// - function: The function. -/// - line: The line. -/// - column: The column. -/// - Returns: A throwing, asynchronous closure that reports an issue and throws an error when -/// invoked. -public func unimplemented( - _ description: @autoclosure @escaping @Sendable () -> String = "", - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - column: UInt = #column -) -> @Sendable (repeat each Argument) async throws -> Result { - return { (argument: repeat each Argument) in - let description = description() - _fail( - description, - (repeat each argument), - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: column - ) - throw UnimplementedFailure(description: description) - } -} - -@_disfavoredOverload -public func unimplemented( - _ description: @autoclosure @escaping @Sendable () -> String = "", - placeholder: @autoclosure @escaping @Sendable () -> Result = (), - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - column: UInt = #column -) -> Result { - _fail( - description(), - nil, - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: column - ) - return placeholder() -} - -/// An error thrown from throwing `unimplemented` closures. -public struct UnimplementedFailure: Error { - public let description: String -} - -package func _fail( - _ description: String, - _ parameters: Any?, - fileID: StaticString, - filePath: StaticString, - function: StaticString, - line: UInt, - column: UInt -) { - var debugDescription = """ - … - - Defined in '\(function)' at: - \(fileID):\(line) - """ - if let parameters { - var parametersDescription = "" - debugPrint(parameters, terminator: "", to: ¶metersDescription) - debugDescription.append( - """ - - - Invoked with: - \(parametersDescription) - """ - ) - } - reportIssue( - """ - Unimplemented\(description.isEmpty ? "" : ": \(description)")\(debugDescription) - """, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) -} diff --git a/Sources/IssueReporting/WithExpectedIssue.swift b/Sources/IssueReporting/WithExpectedIssue.swift deleted file mode 100644 index 91345ae1..00000000 --- a/Sources/IssueReporting/WithExpectedIssue.swift +++ /dev/null @@ -1,196 +0,0 @@ -/// Invoke a function that has an issue that is expected to occur during its execution. -/// -/// A generalized version of Swift Testing's [`withKnownIssue`][withKnownIssue] that works with this -/// library's [`reportIssue`]() instead of just -/// Swift Testing's tools. -/// -/// At runtime it can be used to lower the log level of reported issues: -/// -/// ```swift -/// // Emits a "purple" warning to Xcode and logs a fault-level message to console -/// reportIssue("Failed") -/// -/// withExpectedIssue { -/// // Simply logs an info-level message -/// reportIssue("Failed") -/// } -/// ``` -/// -/// During test runs, the issue will be sent to Swift Testing's [`withKnownIssue`][withKnownIssue] -/// _or_ XCTest's [`XCTExpectFailure`][XCTExpectFailure] accordingly, which means you can use it to -/// drive custom assertion helpers that you want to work in both Swift Testing and XCTest. -/// -/// Errors thrown from the function are automatically caught and reported as issues: -/// -/// ```swift -/// withExpectedIssue { -/// // If this function throws an error, it will be caught and reported as an issue -/// try functionThatCanFail() -/// } -/// ``` -/// -/// [withKnownIssue]: https://developer.apple.com/documentation/testing/withknownissue(_:isintermittent:fileid:filepath:line:column:_:)-30kgk -/// [XCTExpectFailure]: https://developer.apple.com/documentation/xctest/3727246-xctexpectfailure/ -/// -/// - Parameters: -/// - message: An optional message describing the expected issue. -/// - isIntermittent: Whether or not the expected issue 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 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: () throws -> Void -) { - switch TestContext.current { - case .swiftTesting: - _withKnownIssue( - message, - isIntermittent: isIntermittent, - fileID: fileID.description, - filePath: filePath.description, - line: Int(line), - column: Int(column), - body - ) - case .xcTest: - _XCTExpectFailure( - message.withAppHostWarningIfNeeded(), - strict: !isIntermittent, - file: filePath, - line: line - ) { - do { - try body() - } catch { - reportIssue(error, fileID: fileID, filePath: filePath, line: line, column: column) - } - } - case nil: - guard !isTesting else { return } - let observer = FailureObserver() - FailureObserver.$current.withValue(observer) { - do { - try 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 - } -} - -/// 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 { - switch TestContext.current { - 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() - case nil: - 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 - } -} diff --git a/Sources/IssueReporting/WithIssueContext.swift b/Sources/IssueReporting/WithIssueContext.swift deleted file mode 100644 index 1bca4086..00000000 --- a/Sources/IssueReporting/WithIssueContext.swift +++ /dev/null @@ -1,62 +0,0 @@ -/// Sets the context for issues reported for the duration of the synchronous operation. -/// -/// This context will override the implicit context from the call sites of -/// ``reportIssue(_:fileID:filePath:line:column:)`` and -/// ``withExpectedIssue(_:isIntermittent:fileID:filePath:line:column:_:)-9pinm``, and can be -/// leveraged by custom test helpers that want to associate reported issues with specific source -/// code. -/// -/// - 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: A synchronous operation. -public func withIssueContext( - fileID: StaticString, - filePath: StaticString, - line: UInt, - column: UInt, - operation: () throws -> R -) rethrows -> R { - try IssueContext.$current.withValue( - IssueContext(fileID: fileID, filePath: filePath, line: line, column: column), - operation: operation - ) -} - -/// 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 - ) -} - -@usableFromInline -struct IssueContext: Sendable { - @TaskLocal public static var current: Self? - @usableFromInline - let fileID: StaticString - @usableFromInline - let filePath: StaticString - @usableFromInline - let line: UInt - @usableFromInline - let column: UInt -} diff --git a/Sources/IssueReportingTestSupport/SwiftTesting.swift b/Sources/IssueReportingTestSupport/SwiftTesting.swift deleted file mode 100644 index 93c5604d..00000000 --- a/Sources/IssueReportingTestSupport/SwiftTesting.swift +++ /dev/null @@ -1,125 +0,0 @@ -#if canImport(Testing) - import Testing -#endif - -public func _recordIssue() -> Any { __recordIssue } -@Sendable -private func __recordIssue( - message: String?, - fileID: String, - filePath: String, - line: Int, - column: Int -) { - #if canImport(Testing) - // NB: https://github.com/apple/swift-testing/issues/490 - // Issue.record( - // message.map(Comment.init(rawValue:)), - // sourceLocation: SourceLocation( - // fileID: fileID, - // filePath: filePath, - // line: line, - // column: column - // ) - // ) - __checkValue( - false, - expression: .__fromSyntaxNode(message ?? ""), - comments: [], - isRequired: false, - sourceLocation: SourceLocation( - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - ) - .__expected() - #endif -} - -public func _recordError() -> Any { __recordError } -@Sendable -private func __recordError( - error: any Error, - message: String?, - fileID: String, - filePath: String, - line: Int, - column: Int -) { - #if canImport(Testing) - Issue.record( - error, - message.map(Comment.init(rawValue:)), - sourceLocation: SourceLocation( - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - ) - #endif -} - -public func _withKnownIssue() -> Any { __withKnownIssue } -@Sendable -private func __withKnownIssue( - _ message: String?, - isIntermittent: Bool, - fileID: String, - filePath: String, - line: Int, - column: Int, - _ body: () throws -> Void -) { - #if canImport(Testing) - withKnownIssue( - message.map(Comment.init(rawValue:)), - isIntermittent: isIntermittent, - sourceLocation: SourceLocation( - fileID: fileID, - filePath: filePath, - line: line, - column: column - ), - body - ) - #endif -} - -public func _withKnownIssueAsync() -> Any { __withKnownIssueAsync } -@Sendable -private func __withKnownIssueAsync( - _ message: String?, - isIntermittent: Bool, - 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, - sourceLocation: SourceLocation( - fileID: fileID, - filePath: filePath, - line: line, - column: column - ), - body - ) - #endif -} - -public func _currentTestIsNotNil() -> Any { __currentTestIsNotNil } -@Sendable -private func __currentTestIsNotNil() -> Bool { - #if canImport(Testing) - return Test.current != nil - #else - return false - #endif -} diff --git a/Sources/IssueReportingTestSupport/XCTest.swift b/Sources/IssueReportingTestSupport/XCTest.swift deleted file mode 100644 index c8e7c905..00000000 --- a/Sources/IssueReportingTestSupport/XCTest.swift +++ /dev/null @@ -1,34 +0,0 @@ -#if canImport(XCTest) - import XCTest -#endif - -public func _XCTFail() -> Any { __XCTFail } -@Sendable -private func __XCTFail(_ message: String, file: StaticString, line: UInt) { - #if canImport(XCTest) - XCTFail(message, file: file, line: line) - #endif -} - -public func _XCTExpectFailure() -> Any { __XCTExpectFailure } -@_transparent -@Sendable -private func __XCTExpectFailure( - _ failureReason: String?, - enabled: Bool?, - strict: Bool?, - failingBlock: () throws -> Void -) rethrows { - #if canImport(XCTest) - #if _runtime(_ObjC) - try XCTExpectFailure( - failureReason, - enabled: enabled, - strict: strict, - failingBlock: failingBlock - ) - #else - try failingBlock() - #endif - #endif -} diff --git a/Sources/WasmTests/main.swift b/Sources/WasmTests/main.swift deleted file mode 100644 index 797de46f..00000000 --- a/Sources/WasmTests/main.swift +++ /dev/null @@ -1,3 +0,0 @@ -import IssueReporting - -reportIssue() diff --git a/Sources/XCTestDynamicOverlay/Internal/Deprecations.swift b/Sources/XCTestDynamicOverlay/Internal/Deprecations.swift index 965df0bc..0ea41503 100644 --- a/Sources/XCTestDynamicOverlay/Internal/Deprecations.swift +++ b/Sources/XCTestDynamicOverlay/Internal/Deprecations.swift @@ -260,21 +260,21 @@ public func unimplemented( line: UInt = #line, column: UInt = #column ) -> Result { - let description = description() - _fail( - description, - nil, + unimplemented( + description(), + placeholder: { + do { + return try _generatePlaceholder() + } catch { + _unimplementedFatalError(description(), file: filePath, line: line) + } + }(), fileID: fileID, filePath: filePath, function: function, line: line, column: column ) - do { - return try _generatePlaceholder() - } catch { - _unimplementedFatalError(description, file: filePath, line: line) - } } @_disfavoredOverload @@ -286,23 +286,21 @@ public func unimplemented( function: StaticString = #function, line: UInt = #line ) -> @Sendable (repeat each Argument) -> Result { - return { (argument: repeat each Argument) in - let description = description() - _fail( - description, - (repeat each argument), - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: 0 - ) - do { - return try _generatePlaceholder() - } catch { - _unimplementedFatalError(description, file: filePath, line: line) - } - } + unimplemented( + description(), + placeholder: { + do { + return try _generatePlaceholder() + } catch { + _unimplementedFatalError(description(), file: filePath, line: line) + } + }(), + fileID: fileID, + filePath: filePath, + function: function, + line: line, + column: 0 + ) } @_disfavoredOverload @@ -314,23 +312,21 @@ public func unimplemented( function: StaticString = #function, line: UInt = #line ) -> @Sendable (repeat each Argument) async -> Result { - return { (argument: repeat each Argument) in - let description = description() - _fail( - description, - (repeat each argument), - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: 0 - ) - do { - return try _generatePlaceholder() - } catch { - _unimplementedFatalError(description, file: filePath, line: line) - } - } + unimplemented( + description(), + placeholder: { + do { + return try _generatePlaceholder() + } catch { + _unimplementedFatalError(description(), file: filePath, line: line) + } + }(), + fileID: fileID, + filePath: filePath, + function: function, + line: line, + column: 0 + ) } @available(*, deprecated) diff --git a/Tests/IssueReporting.xctestplan b/Tests/IssueReporting.xctestplan deleted file mode 100644 index b07d2dcd..00000000 --- a/Tests/IssueReporting.xctestplan +++ /dev/null @@ -1,31 +0,0 @@ -{ - "configurations" : [ - { - "id" : "A4B6699D-624A-42BB-A98E-F210B2D25DAC", - "name" : "Test Scheme Action", - "options" : { - - } - } - ], - "defaultOptions" : { - - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:", - "identifier" : "IssueReportingTests", - "name" : "IssueReportingTests" - } - }, - { - "target" : { - "containerPath" : "container:", - "identifier" : "XCTestDynamicOverlayTests", - "name" : "XCTestDynamicOverlayTests" - } - } - ], - "version" : 1 -} diff --git a/Tests/IssueReportingTests/HostAppDetectionTests.swift b/Tests/IssueReportingTests/HostAppDetectionTests.swift deleted file mode 100644 index 436063b2..00000000 --- a/Tests/IssueReportingTests/HostAppDetectionTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -#if DEBUG && canImport(ObjectiveC) - import XCTest - - @testable import IssueReporting - - final class HostAppCallStackTests: XCTestCase { - func testIsAbleToDetectTest() { - XCTAssert(Thread.callStackSymbols.contains(where: \.isTestFrame)) - } - - func testIsAbleToDetectAsyncTest() async { - XCTAssert(Thread.callStackSymbols.contains(where: \.isTestFrame)) - } - - func testIsAbleToDetectThrowingTest() throws { - XCTAssert(Thread.callStackSymbols.contains(where: \.isTestFrame)) - } - - func testIsAbleToDetectAsyncThrowingTest() async throws { - XCTAssert(Thread.callStackSymbols.contains(where: \.isTestFrame)) - } - } -#endif diff --git a/Tests/IssueReportingTests/SwiftTestingTests.swift b/Tests/IssueReportingTests/SwiftTestingTests.swift deleted file mode 100644 index 9dafda8c..00000000 --- a/Tests/IssueReportingTests/SwiftTestingTests.swift +++ /dev/null @@ -1,118 +0,0 @@ -#if canImport(Testing) - import Testing - import IssueReporting - - @Suite - struct SwiftTestingTests { - @Test func context() { - #expect(TestContext.current == .swiftTesting) - } - - @Test func reportIssue_NoMessage() { - withKnownIssue { - reportIssue() - } matching: { issue in - issue.description == "Expectation failed: " - } - } - - @Test func reportError_NoMessage() { - struct MyError: Error {} - withKnownIssue { - reportIssue(Failure()) - } matching: { issue in - issue.description == "Caught error: Failure()" - } - } - - @Test func reportIssue_CustomMessage() { - withKnownIssue { - reportIssue("Something went wrong") - } matching: { issue in - issue.description == "Expectation failed: Something went wrong" - } - } - - @Test func reportError_CustomMessage() { - withKnownIssue { - reportIssue(Failure(), "Something went wrong") - } matching: { issue in - issue.description == "Caught error: Failure(): Something went wrong" - } - } - - @Test func withExpectedIssue_reportIssue() { - withExpectedIssue { - reportIssue() - } - } - - @Test func withExpectedIssue_reportIssue_Async() async { - await withExpectedIssue { - await Task.yield() - reportIssue() - } - } - - @Test func withExpectedIssue_issueRecord() { - withExpectedIssue { - Issue.record() - } - } - - @Test func withExpectedIssue_throw() { - withExpectedIssue { throw Failure() } - } - - @Test func withExpectedIssue_NoMessage_NoIssue() { - withKnownIssue { - withExpectedIssue { - } - } matching: { issue in - issue.description == "Known issue was not recorded" - } - } - - @Test func withExpectedIssue_NoMessage_NoIssue_Async() async { - await withKnownIssue { - await withExpectedIssue { - await Task.yield() - } - } matching: { issue in - issue.description == "Known issue was not recorded" - } - } - - @Test func withExpectedIssue_CustomMessage_NoIssue() { - withKnownIssue { - withExpectedIssue("This didn't fail") { - } - } matching: { issue in - issue.description == "Known issue was not recorded: This didn't fail" - } - } - - @Test func withExpectedIssue_CustomMessage_NoIssue_Async() async { - await withKnownIssue { - await withExpectedIssue("This didn't fail") { - await Task.yield() - } - } matching: { issue in - issue.description == "Known issue was not recorded: This didn't fail" - } - } - - @Test func withExpectedIssue_IsIntermittent() { - withExpectedIssue(isIntermittent: true) { - } - } - - @Test func withExpectedIssue_IsIntermittent_Async() async { - await withExpectedIssue(isIntermittent: true) { - await Task.yield() - } - } - } - - private struct Failure: Error {} -#endif diff --git a/Tests/IssueReportingTests/UnimplementedTests.swift b/Tests/IssueReportingTests/UnimplementedTests.swift deleted file mode 100644 index 71ed8571..00000000 --- a/Tests/IssueReportingTests/UnimplementedTests.swift +++ /dev/null @@ -1,126 +0,0 @@ -#if canImport(Testing) - import IssueReporting - import Testing - - @Suite - struct UnimplementedTests { - @Test func unimplemented_ReturningVoid() { - final class Model: Sendable { - let line = #line + 1 - let callback: @Sendable (Int) -> Void = unimplemented() - } - - let model = Model() - withKnownIssue { - model.callback(42) - } matching: { issue in - issue.description == """ - Expectation failed: Unimplemented … - - Defined in 'Model' at: - IssueReportingTests/UnimplementedTests.swift:\(model.line) - - Invoked with: - 42 - """ - } - } - - @Test func unimplemented_VoidToVoid() { - final class Model: Sendable { - let line = #line + 1 - let callback: @Sendable () -> Void = unimplemented() - } - - let model = Model() - withKnownIssue { - model.callback() - } matching: { issue in - issue.description == """ - Expectation failed: Unimplemented … - - Defined in 'Model' at: - IssueReportingTests/UnimplementedTests.swift:\(model.line) - - Invoked with: - () - """ - } - } - - @Test func unimplemented_NonVoidReturning() { - final class Model: Sendable { - let line = #line + 1 - let callback: @Sendable () -> Int = unimplemented(placeholder: 42) - } - - let model = Model() - withKnownIssue { - _ = model.callback() - } matching: { issue in - issue.description == """ - Expectation failed: Unimplemented … - - Defined in 'Model' at: - IssueReportingTests/UnimplementedTests.swift:\(model.line) - - Invoked with: - () - """ - } - } - - @Test func unimplemented_ThrowingFunction() throws { - final class Model: Sendable { - let line = #line + 1 - let callback: @Sendable () throws -> Void = unimplemented() - } - - let model = Model() - try withKnownIssue { - _ = try model.callback() - } matching: { issue in - issue.description == """ - Expectation failed: Unimplemented … - - Defined in 'Model' at: - IssueReportingTests/UnimplementedTests.swift:\(model.line) - - Invoked with: - () - """ - || issue.description == """ - Caught error: UnimplementedFailure(description: "") - """ - } - } - - @Test func throwing() throws { - final class Model: Sendable { - let line = #line + 1 - let callback: @Sendable () throws -> Void = IssueReporting.unimplemented() - } - - let model = Model() - try withKnownIssue { - try withKnownIssue { - _ = try model.callback() - } matching: { issue in - issue.description == """ - Expectation failed: Unimplemented … - - Defined in 'Model' at: - IssueReportingTests/UnimplementedTests.swift:\(model.line) - - Invoked with: - () - """ - } - } matching: { issue in - issue.description == """ - Caught error: UnimplementedFailure(description: "") - """ - } - } - } -#endif diff --git a/Tests/IssueReportingTests/XCTestTests.swift b/Tests/IssueReportingTests/XCTestTests.swift deleted file mode 100644 index 9937dee5..00000000 --- a/Tests/IssueReportingTests/XCTestTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -import IssueReporting -import XCTest - -final class XCTestTests: XCTestCase { - #if !os(WASI) - func testIsTesting() { - XCTAssertTrue(isTesting) - } - - func testTestContext() { - XCTAssertEqual(TestContext.current, .xcTest) - } - #endif - - #if _runtime(_ObjC) - func testReportIssue_NoMessage() { - XCTExpectFailure { - reportIssue() - } issueMatcher: { - $0.compactDescription == "failed" - } - } - - func testReportIssue_CustomMessage() { - XCTExpectFailure { - reportIssue("Something went wrong") - } issueMatcher: { - $0.compactDescription == "failed - Something went wrong" - } - } - - func testWithExpectedIssue() { - withExpectedIssue { - reportIssue("Something went wrong") - } - } - - func testWithExpectedIssue_XCTFail() { - withExpectedIssue { - XCTFail() - } - } - - func testWithExpectedIssue_Throwing() { - withExpectedIssue { throw Failure() } - } - #endif -} - -private struct Failure: Error {}