diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift index 1203618af..5d497f95c 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLWrapper.swift @@ -53,7 +53,7 @@ public struct _AttachableURLWrapper: Sendable { /// thrown if a file descriptor to `url` or `copyURL` cannot be created. init(url: URL, copiedToFileAt copyURL: URL? = nil, isCompressedDirectory: Bool) throws { if isCompressedDirectory && copyURL == nil { - preconditionFailure("When attaching a directory to a test, the URL to its compressed copy must be supplied. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + preconditionFailure(reportBugMessage("When attaching a directory to a test, the URL to its compressed copy must be supplied.")) } self.url = url self.data = try Data(contentsOf: copyURL ?? url, options: [.mappedIfSafe]) diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift index d5d30340c..6743002a4 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift @@ -135,7 +135,7 @@ extension ABI { init(encoding testCase: borrowing Test.Case) { guard let arguments = testCase.arguments else { - preconditionFailure("Attempted to initialize an EncodedTestCase encoding a test case which is not parameterized: \(testCase). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + preconditionFailure(reportBugMessage("Attempted to initialize an EncodedTestCase encoding a test case which is not parameterized: \(testCase).")) } // TODO: define an encodable form of Test.Case.ID diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 8cb049cd4..05534ef95 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -584,7 +584,7 @@ extension Configuration { } guard case let .valueAttached(attachment) = event.kind else { - preconditionFailure("Passed the wrong kind of event to \(#function) (expected valueAttached, got \(event.kind)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + preconditionFailure(reportBugMessage("Passed the wrong kind of event to \(#function) (expected valueAttached, got \(event.kind)).")) } if attachment.fileSystemPath != nil { // Somebody already saved this attachment. This isn't necessarily a logic diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 6cde72716..eccd684e4 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -91,6 +91,7 @@ add_library(Testing Support/Additions/TaskAdditions.swift Support/Additions/WinSDKAdditions.swift Support/Allocated.swift + Support/BugReporting.swift Support/CartesianProduct.swift Support/CError.swift Support/CustomIssueRepresentable.swift diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 724b85168..aa9b1d4b0 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -294,7 +294,7 @@ extension ExitTest { // Set ExitTest.current before the test body runs. Self._current.withLock { current in - precondition(current == nil, "Set the current exit test twice in the same process. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + precondition(current == nil, reportBugMessage("Set the current exit test twice in the same process.")) current = self.unsafeCopy() } @@ -1095,7 +1095,7 @@ extension ExitTest { } let capturedValuesJSON = try fileHandle.readToEnd() let capturedValuesJSONLines = capturedValuesJSON.split(whereSeparator: \.isASCIINewline) - assert(capturedValues.count == capturedValuesJSONLines.count, "Expected to decode \(capturedValues.count) captured value(s) for the current exit test, but received \(capturedValuesJSONLines.count). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + assert(capturedValues.count == capturedValuesJSONLines.count, reportBugMessage("Expected to decode \(capturedValues.count) captured value(s) for the current exit test, but received \(capturedValuesJSONLines.count).")) // Walk the list of captured values' types, map them to their JSON blobs, // and decode them. diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 6114566f1..2aeeda638 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -129,7 +129,7 @@ func spawnExecutable( func inherit(_ fileHandle: borrowing FileHandle, as standardFD: CInt? = nil) throws { try fileHandle.withUnsafePOSIXFileDescriptor { fd in guard let fd else { - throw SystemError(description: "A child process cannot inherit a file handle without an associated file descriptor. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + throw SystemError(description: reportBugMessage("A child process cannot inherit a file handle without an associated file descriptor.")) } if let standardFD, standardFD != fd { _ = posix_spawn_file_actions_adddup2(fileActions, fd, standardFD) @@ -241,7 +241,7 @@ func spawnExecutable( func inherit(_ fileHandle: borrowing FileHandle) throws -> HANDLE? { try fileHandle.withUnsafeWindowsHANDLE { windowsHANDLE in guard let windowsHANDLE else { - throw SystemError(description: "A child process cannot inherit a file handle without an associated Windows handle. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + throw SystemError(description: reportBugMessage("A child process cannot inherit a file handle without an associated Windows handle.")) } // Ensure the file handle can be inherited by the child process. diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index f0326ff3c..b91923614 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -33,7 +33,7 @@ private func _blockAndWait(for pid: consuming pid_t) throws -> ExitStatus { case .init(CLD_KILLED), .init(CLD_DUMPED): return .signal(siginfo.si_status) default: - throw SystemError(description: "Unexpected siginfo_t value. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new and include this information: \(String(reflecting: siginfo))") + throw SystemError(description: reportBugMessage("Unexpected siginfo_t value.", context: String(reflecting: siginfo))) } } else if case let errorCode = swt_errno(), errorCode != EINTR { throw CError(rawValue: errorCode) @@ -247,7 +247,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { // we add this continuation to the dictionary, then it will simply loop // and report the status again. let oldContinuation = childProcessContinuations.updateValue(continuation, forKey: pid) - assert(oldContinuation == nil, "Unexpected continuation found for PID \(pid). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + assert(oldContinuation == nil, reportBugMessage("Unexpected continuation found for PID \(pid).")) // Wake up the waiter thread if it is waiting for more child processes. _ = pthread_cond_signal(_waitThreadNoChildrenCondition) diff --git a/Sources/Testing/Support/Additions/TaskAdditions.swift b/Sources/Testing/Support/Additions/TaskAdditions.swift index 1ec0c3079..b79e9cac5 100644 --- a/Sources/Testing/Support/Additions/TaskAdditions.swift +++ b/Sources/Testing/Support/Additions/TaskAdditions.swift @@ -19,7 +19,7 @@ func decorateTaskName(_ taskName: String?, withAction action: String?) -> String let prefix = "[Swift Testing]" return taskName.map { taskName in #if DEBUG - precondition(!taskName.hasPrefix(prefix), "Applied prefix '\(prefix)' to task name '\(taskName)' twice. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + precondition(!taskName.hasPrefix(prefix), reportBugMessage("Applied prefix '\(prefix)' to task name '\(taskName)' twice.")) #endif let action = action.map { " - \($0)" } ?? "" return "\(prefix) \(taskName)\(action)" diff --git a/Sources/Testing/Support/Additions/WinSDKAdditions.swift b/Sources/Testing/Support/Additions/WinSDKAdditions.swift index 488d52dd6..a97a91fdb 100644 --- a/Sources/Testing/Support/Additions/WinSDKAdditions.swift +++ b/Sources/Testing/Support/Additions/WinSDKAdditions.swift @@ -44,7 +44,7 @@ let STATUS_SIGNAL_CAUGHT_BITS = { #if DEBUG assert( (result & STATUS_CODE_MASK) == 0, - "Constructed NTSTATUS mask \(String(result, radix: 16)) encroached on code bits. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new" + reportBugMessage("Constructed NTSTATUS mask \(String(result, radix: 16)) encroached on code bits.") ) #endif diff --git a/Sources/Testing/Support/BugReporting.swift b/Sources/Testing/Support/BugReporting.swift new file mode 100644 index 000000000..f18fae56e --- /dev/null +++ b/Sources/Testing/Support/BugReporting.swift @@ -0,0 +1,29 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +/// Construct a message describing a problem and inviting a user to file a bug +/// report. +/// +/// - Parameters: +/// - message: A description of the problem encountered. +/// - context: Optional additional diagnostic information to include with the +/// bug report request. +/// +/// - Returns: A string combining `message` with a standard request to file a +/// bug report (with a URL provided), optionally followed by `context`. +/// +/// This function is not part of the public interface of the testing library. +package func reportBugMessage(_ message: String, context: String? = nil) -> String { + var result = "\(message) Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new" + if let context { + result += " and include this information: \(context)" + } + return result +} diff --git a/Sources/Testing/Test.swift b/Sources/Testing/Test.swift index 4e077f7e6..89bbf0184 100644 --- a/Sources/Testing/Test.swift +++ b/Sources/Testing/Test.swift @@ -207,7 +207,7 @@ public struct Test: Sendable { // error (because the test cannot be run.) If an error was thrown, a // `Runner.Plan` is expected to record issue for the test, rather than // attempt to run it, and thus never access this property. - preconditionFailure("Attempting to access test cases with invalid state. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new and include this information: \(String(reflecting: testCasesState))") + preconditionFailure(reportBugMessage("Attempting to access test cases with invalid state.", context: String(reflecting: testCasesState))) } return AnySequence(testCases) } diff --git a/Sources/Testing/Traits/AttachmentSavingTrait.swift b/Sources/Testing/Traits/AttachmentSavingTrait.swift index 3bae9f03a..e5ca02dc0 100644 --- a/Sources/Testing/Traits/AttachmentSavingTrait.swift +++ b/Sources/Testing/Traits/AttachmentSavingTrait.swift @@ -114,7 +114,7 @@ extension AttachmentSavingTrait: TestScoping { public func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { guard var configuration = Configuration.current else { - throw SystemError(description: "There is no current Configuration when attempting to provide scope for test '\(test.name)'. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + throw SystemError(description: reportBugMessage("There is no current Configuration when attempting to provide scope for test '\(test.name)'.")) } let oldConfiguration = configuration diff --git a/Sources/Testing/Traits/IssueHandlingTrait.swift b/Sources/Testing/Traits/IssueHandlingTrait.swift index 21a9adaac..74ab36902 100644 --- a/Sources/Testing/Traits/IssueHandlingTrait.swift +++ b/Sources/Testing/Traits/IssueHandlingTrait.swift @@ -100,7 +100,7 @@ extension IssueHandlingTrait: TestScoping { /// issue. func provideScope(performing function: @Sendable () async throws -> Void) async throws { guard var configuration = Configuration.current else { - preconditionFailure("Configuration.current is nil when calling \(#function). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + preconditionFailure(reportBugMessage("Configuration.current is nil when calling \(#function).")) } configuration.eventHandler = { [oldConfiguration = configuration] event, context in diff --git a/Sources/Testing/Traits/ParallelizationTrait.swift b/Sources/Testing/Traits/ParallelizationTrait.swift index c91e01761..0f8671fd5 100644 --- a/Sources/Testing/Traits/ParallelizationTrait.swift +++ b/Sources/Testing/Traits/ParallelizationTrait.swift @@ -41,7 +41,7 @@ extension ParallelizationTrait: TestScoping { public func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { guard var configuration = Configuration.current else { - throw SystemError(description: "There is no current Configuration when attempting to provide scope for test '\(test.name)'. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + throw SystemError(description: reportBugMessage("There is no current Configuration when attempting to provide scope for test '\(test.name)'.")) } configuration.isParallelizationEnabled = false diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index 9908b05f4..babcbd995 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -100,6 +100,7 @@ target_sources(TestingMacros PRIVATE Support/Argument.swift Support/AttributeDiscovery.swift Support/AvailabilityGuards.swift + Support/BugReporting.swift Support/ClosureCaptureListParsing.swift Support/CommentParsing.swift Support/ConditionArgumentParsing.swift diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index a4e50ba29..3a666fe7f 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -460,7 +460,7 @@ extension ExitTestConditionMacro { var arguments = argumentList(of: macro, in: context) let trailingClosureIndex = arguments.firstIndex { $0.label?.tokenKind == _trailingClosureLabel.tokenKind } guard let trailingClosureIndex else { - fatalError("Could not find the body argument to this exit test. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + fatalError(reportBugMessage("Could not find the body argument to this exit test.")) } let conditionExpr = arguments.first { $0.label?.tokenKind == .identifier("processExitsWith") }?.expression diff --git a/Sources/TestingMacros/Support/Additions/DeclGroupSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/DeclGroupSyntaxAdditions.swift index b5e8f83c0..af50f098c 100644 --- a/Sources/TestingMacros/Support/Additions/DeclGroupSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/DeclGroupSyntaxAdditions.swift @@ -18,7 +18,7 @@ extension DeclGroupSyntax { } else if let extensionDecl = `as`(ExtensionDeclSyntax.self) { return extensionDecl.extendedType } - fatalError("Unexpected DeclGroupSyntax type \(Swift.type(of: self)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + fatalError(reportBugMessage("Unexpected DeclGroupSyntax type \(Swift.type(of: self)).")) } /// Check whether or not this instance includes a given type name in its diff --git a/Sources/TestingMacros/Support/AvailabilityGuards.swift b/Sources/TestingMacros/Support/AvailabilityGuards.swift index 93451170c..7c1d123d5 100644 --- a/Sources/TestingMacros/Support/AvailabilityGuards.swift +++ b/Sources/TestingMacros/Support/AvailabilityGuards.swift @@ -141,7 +141,7 @@ private func _createAvailabilityTraitExpr( } default: - fatalError("Unsupported keyword \(whenKeyword) passed to \(#function). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + fatalError(reportBugMessage("Unsupported keyword \(whenKeyword) passed to \(#function).")) } } diff --git a/Sources/TestingMacros/Support/BugReporting.swift b/Sources/TestingMacros/Support/BugReporting.swift new file mode 100644 index 000000000..94633775a --- /dev/null +++ b/Sources/TestingMacros/Support/BugReporting.swift @@ -0,0 +1,27 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +/// Construct a message describing a problem and inviting a user to file a bug +/// report. +/// +/// - Parameters: +/// - message: A description of the problem encountered. +/// - context: Optional additional diagnostic information to include with the +/// bug report request. +/// +/// - Returns: A string combining `message` with a standard request to file a +/// bug report (with a URL provided), optionally followed by `context`. +func reportBugMessage(_ message: String, context: String? = nil) -> String { + var result = "\(message) Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new" + if let context { + result += " and include this information: \(context)" + } + return result +}