diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift index 80c75b5e9..29728d26d 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @_spi(Experimental) public import Testing public import Foundation diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift index cfae97ca7..53506811d 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @_spi(Experimental) public import Testing private import Foundation diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift index c6916ec39..ea304982d 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @_spi(Experimental) public import Testing public import Foundation diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index dbf7e2688..ca7120073 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @_spi(Experimental) public import Testing public import Foundation diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift index f931e5824..bc72eba03 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @_spi(Experimental) public import Testing public import Foundation diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift index bbbe934ab..79c016a34 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @_spi(Experimental) import Testing import Foundation diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift index 38f21d4d3..858a54407 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/_AttachableURLContainer.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @_spi(Experimental) public import Testing public import Foundation diff --git a/Sources/Overlays/_Testing_Foundation/Events/Clock+Date.swift b/Sources/Overlays/_Testing_Foundation/Events/Clock+Date.swift index df9f178c7..1e6a792d1 100644 --- a/Sources/Overlays/_Testing_Foundation/Events/Clock+Date.swift +++ b/Sources/Overlays/_Testing_Foundation/Events/Clock+Date.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) && !SWT_NO_UTC_CLOCK +#if !SWT_NO_FOUNDATION && canImport(Foundation) && !SWT_NO_UTC_CLOCK @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing public import Foundation diff --git a/Sources/Testing/ABI/ABI.Record+Streaming.swift b/Sources/Testing/ABI/ABI.Record+Streaming.swift index 7b86cb438..4df0799da 100644 --- a/Sources/Testing/ABI/ABI.Record+Streaming.swift +++ b/Sources/Testing/ABI/ABI.Record+Streaming.swift @@ -8,9 +8,6 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) && (!SWT_NO_FILE_IO || !SWT_NO_ABI_ENTRY_POINT) -private import Foundation - extension ABI.Version { /// Post-process encoded JSON and write it to a file. /// @@ -58,13 +55,13 @@ extension ABI.Version { let humanReadableOutputRecorder = Event.HumanReadableOutputRecorder() return { [eventHandler = eventHandlerCopy] event, context in if case .testDiscovered = event.kind, let test = context.test { - try? JSON.withEncoding(of: ABI.Record(encoding: test)) { testJSON in + JSON.withEncoding(of: ABI.Record(encoding: test)) { testJSON in eventHandler(testJSON) } } else { let messages = humanReadableOutputRecorder.record(event, in: context, verbosity: 0) if let eventRecord = ABI.Record(encoding: event, in: context, messages: messages) { - try? JSON.withEncoding(of: eventRecord, eventHandler) + JSON.withEncoding(of: eventRecord, eventHandler) } } } @@ -96,12 +93,9 @@ extension ABI.Xcode16 { eventContext: Event.Context.Snapshot(snapshotting: context) ) try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in - eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in - eventHandler(eventAndContextJSON) - } + eventHandler(eventAndContextJSON) } } } } #endif -#endif diff --git a/Sources/Testing/ABI/ABI.Record.swift b/Sources/Testing/ABI/ABI.Record.swift index 74ac7f9aa..73379d3d5 100644 --- a/Sources/Testing/ABI/ABI.Record.swift +++ b/Sources/Testing/ABI/ABI.Record.swift @@ -41,28 +41,15 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable -extension ABI.Record: Codable { +extension ABI.Record: Decodable { private enum CodingKeys: String, CodingKey { case version case kind case payload } - func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(V.versionNumber, forKey: .version) - switch kind { - case let .test(test): - try container.encode("test", forKey: .kind) - try container.encode(test, forKey: .payload) - case let .event(event): - try container.encode("event", forKey: .kind) - try container.encode(event, forKey: .payload) - } - } - init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -93,3 +80,22 @@ extension ABI.Record: Codable { } } } + +extension ABI.Record: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + var dict = [ + "version": V.versionNumber.makeJSONValue() + ] + + switch kind { + case let .test(test): + dict["kind"] = "test".makeJSONValue() + dict["payload"] = test.makeJSONValue() + case let .event(event): + dict["kind"] = "event".makeJSONValue() + dict["payload"] = event.makeJSONValue() + } + + return dict.makeJSONValue() + } +} diff --git a/Sources/Testing/ABI/ABI.swift b/Sources/Testing/ABI/ABI.swift index 3106d2a72..2d85f1f4c 100644 --- a/Sources/Testing/ABI/ABI.swift +++ b/Sources/Testing/ABI/ABI.swift @@ -20,7 +20,6 @@ extension ABI { /// The numeric representation of this ABI version. static var versionNumber: Int { get } -#if canImport(Foundation) && (!SWT_NO_FILE_IO || !SWT_NO_ABI_ENTRY_POINT) /// Create an event handler that encodes events as JSON and forwards them to /// an ABI-friendly event handler. /// @@ -39,7 +38,6 @@ extension ABI { encodeAsJSONLines: Bool, forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void ) -> Event.Handler -#endif } /// The current supported ABI version (ignoring any experimental versions.) @@ -50,6 +48,10 @@ extension ABI { extension ABI { #if !SWT_NO_SNAPSHOT_TYPES +#if SWT_NO_FOUNDATION || !canImport(Foundation) +#error("Platform-specific misconfiguration: Foundation is required for snapshot type support") +#endif + /// A namespace and version type for Xcode 16 compatibility. /// /// - Warning: This type will be removed in a future update. diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index 7668f778a..c6e3c9692 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -27,6 +27,18 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable -extension ABI.EncodedAttachment: Codable {} +extension ABI.EncodedAttachment: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedAttachment: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + var dict = [String: JSON.Value]() + if let path { + dict["path"] = path.makeJSONValue() + } + return dict.makeJSONValue() + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift index fcfa5cc37..a6798d9d7 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift @@ -31,14 +31,35 @@ extension ABI { } } -// MARK: - Codable - -extension ABI.EncodedBacktrace: Codable { - func encode(to encoder: any Encoder) throws { - try symbolicatedAddresses.encode(to: encoder) - } +// MARK: - Decodable +extension ABI.EncodedBacktrace: Decodable { init(from decoder: any Decoder) throws { self.symbolicatedAddresses = try [Backtrace.SymbolicatedAddress](from: decoder) } } + +// MARK: - JSON.Serializable + +extension ABI.EncodedBacktrace: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + symbolicatedAddresses.makeJSONValue() + } +} + +extension Backtrace.SymbolicatedAddress: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + var dict = [ + "address": address.makeJSONValue() + ] + + if let offset { + dict["offset"] = offset.makeJSONValue() + } + if let symbolName { + dict["symbolName"] = symbolName.makeJSONValue() + } + + return dict.makeJSONValue() + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift index 3a299cd3f..3c6d5e365 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift @@ -54,9 +54,22 @@ extension ABI.EncodedError: Error { } } -// MARK: - Codable +// MARK: - Decodable -extension ABI.EncodedError: Codable {} +extension ABI.EncodedError: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedError: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + let dict = [ + "description": description.makeJSONValue(), + "domain": domain.makeJSONValue(), + "code": code.makeJSONValue() + ] + return dict.makeJSONValue() + } +} // MARK: - CustomTestStringConvertible diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index b8bafdde1..0b6bb821e 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -107,7 +107,36 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable -extension ABI.EncodedEvent: Codable {} -extension ABI.EncodedEvent.Kind: Codable {} +extension ABI.EncodedEvent: Decodable {} +extension ABI.EncodedEvent.Kind: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedEvent: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + var dict = [ + "kind": kind.makeJSONValue(), + "instant": instant.makeJSONValue(), + "messages": messages.makeJSONValue(), + ] + + if let issue { + dict["issue"] = issue.makeJSONValue() + } + if let _attachment { + dict["_attachment"] = _attachment.makeJSONValue() + } + if let testID { + dict["testID"] = testID.makeJSONValue() + } + if let _testCase { + dict["_testCase"] = _testCase.makeJSONValue() + } + + return dict.makeJSONValue() + } +} + +extension ABI.EncodedEvent.Kind: JSON.Serializable {} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift index 9a71ddbfe..33e7c3e4a 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift @@ -35,6 +35,18 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable -extension ABI.EncodedInstant: Codable {} +extension ABI.EncodedInstant: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedInstant: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + let dict = [ + "absolute": absolute.makeJSONValue(), + "since1970": since1970.makeJSONValue() + ] + return dict.makeJSONValue() + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 0ea218cc8..82bbd0b5a 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -33,7 +33,7 @@ extension ABI { var isKnown: Bool /// The location in source where this issue occurred, if available. - var sourceLocation: SourceLocation? + var sourceLocation: EncodedSourceLocation? /// The backtrace where this issue occurred, if available. /// @@ -51,7 +51,9 @@ extension ABI { case .error: .error } isKnown = issue.isKnown - sourceLocation = issue.sourceLocation + if let sourceLocation = issue.sourceLocation { + self.sourceLocation = EncodedSourceLocation(encoding: sourceLocation) + } if let backtrace = issue.sourceContext.backtrace { _backtrace = EncodedBacktrace(encoding: backtrace, in: eventContext) } @@ -62,7 +64,32 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable + +extension ABI.EncodedIssue: Decodable {} +extension ABI.EncodedIssue.Severity: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedIssue: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + var dict = [ + "_severity": _severity.makeJSONValue(), + "isKnown": isKnown.makeJSONValue() + ] + + if let sourceLocation { + dict["sourceLocation"] = sourceLocation.makeJSONValue() + } + if let _backtrace { + dict["_backtrace"] = _backtrace.makeJSONValue() + } + if let _error { + dict["_error"] = _error.makeJSONValue() + } + + return dict.makeJSONValue() + } +} -extension ABI.EncodedIssue: Codable {} -extension ABI.EncodedIssue.Severity: Codable {} +extension ABI.EncodedIssue.Severity: JSON.Serializable {} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift index 8f993ecb7..9e4511ae3 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift @@ -74,7 +74,20 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable -extension ABI.EncodedMessage: Codable {} -extension ABI.EncodedMessage.Symbol: Codable {} +extension ABI.EncodedMessage: Decodable {} +extension ABI.EncodedMessage.Symbol: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedMessage: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + let dict = [ + "symbol": symbol.makeJSONValue(), + "text": text.makeJSONValue(), + ] + return dict.makeJSONValue() + } +} +extension ABI.EncodedMessage.Symbol: JSON.Serializable {} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift new file mode 100644 index 000000000..24b66e209 --- /dev/null +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift @@ -0,0 +1,47 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// + +extension ABI { + /// A type implementing the JSON encoding of ``SourceLocation`` for the ABI + /// entry point and event stream output. + /// + /// This type is not part of the public interface of the testing library. It + /// assists in converting values to JSON; clients that consume this JSON are + /// expected to write their own decoders. + struct EncodedSourceLocation: Sendable where V: ABI.Version { + var sourceLocation: SourceLocation + + init(encoding sourceLocation: borrowing SourceLocation) { + self.sourceLocation = copy sourceLocation + } + } +} + +// MARK: - Decodable + +extension ABI.EncodedSourceLocation: Decodable { + init(from decoder: any Decoder) throws { + self.sourceLocation = try SourceLocation(from: decoder) + } +} + +// MARK: - JSON.Serializable + +extension ABI.EncodedSourceLocation: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + let dict = [ + "_filePath": sourceLocation._filePath.makeJSONValue(), + "fileID": sourceLocation.fileID.makeJSONValue(), + "line": sourceLocation.line.makeJSONValue(), + "column": sourceLocation.column.makeJSONValue(), + ] + return dict.makeJSONValue() + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift index 51d01781d..b86f4cf51 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift @@ -38,25 +38,17 @@ extension ABI { var displayName: String? /// The source location of this test. - var sourceLocation: SourceLocation + var sourceLocation: EncodedSourceLocation /// A type implementing the JSON encoding of ``Test/ID`` for the ABI entry /// point and event stream output. - struct ID: Codable { + struct ID: Sendable { /// The string value representing the corresponding test ID. var stringValue: String init(encoding testID: borrowing Test.ID) { stringValue = String(describing: copy testID) } - - func encode(to encoder: any Encoder) throws { - try stringValue.encode(to: encoder) - } - - init(from decoder: any Decoder) throws { - stringValue = try String(from: decoder) - } } /// The unique identifier of this test. @@ -95,7 +87,7 @@ extension ABI { } name = test.name displayName = test.displayName - sourceLocation = test.sourceLocation + sourceLocation = EncodedSourceLocation(encoding: test.sourceLocation) id = ID(encoding: test.id) if V.versionNumber >= ABI.v1.versionNumber { @@ -134,8 +126,58 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable + +extension ABI.EncodedTest: Decodable {} +extension ABI.EncodedTest.Kind: Decodable {} +extension ABI.EncodedTest.ID: Decodable { + init(from decoder: any Decoder) throws { + stringValue = try String(from: decoder) + } +} +extension ABI.EncodedTestCase: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedTest: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + var dict = [ + "kind": kind.makeJSONValue(), + "name": name.makeJSONValue(), + "sourceLocation": sourceLocation.makeJSONValue(), + "id": id.makeJSONValue(), + ] + + if let displayName { + dict["displayName"] = displayName.makeJSONValue() + } + if let _testCases { + dict["_testCases"] = _testCases.makeJSONValue() + } + if let isParameterized { + dict["isParameterized"] = isParameterized.makeJSONValue() + } + if let _tags { + dict["_tags"] = _tags.makeJSONValue() + } + + return dict.makeJSONValue() + } +} +extension ABI.EncodedTest.Kind: JSON.Serializable {} + +extension ABI.EncodedTest.ID: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + stringValue.makeJSONValue() + } +} -extension ABI.EncodedTest: Codable {} -extension ABI.EncodedTest.Kind: Codable {} -extension ABI.EncodedTestCase: Codable {} +extension ABI.EncodedTestCase: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + let dict = [ + "id": id.makeJSONValue(), + "displayName": displayName.makeJSONValue(), + ] + return dict.makeJSONValue() + } +} diff --git a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift index f3f50a1be..a210071e3 100644 --- a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) && !SWT_NO_ABI_ENTRY_POINT +#if !SWT_NO_ABI_ENTRY_POINT private import _TestingInternals extension ABI.v0 { diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index c72542d65..8249a7e4a 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -337,7 +337,6 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum } #if !SWT_NO_FILE_IO -#if canImport(Foundation) // Configuration for the test run passed in as a JSON file (experimental) // // This argument should always be the first one we parse. @@ -388,7 +387,6 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum } } } -#endif // XML output if let xunitOutputIndex = args.firstIndex(of: "--xunit-output"), !isLastArgument(at: xunitOutputIndex) { @@ -516,7 +514,6 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr configuration.attachmentsPath = attachmentsPath } -#if canImport(Foundation) // Event stream output (experimental) if let eventStreamOutputPath = args.eventStreamOutputPath { let file = try FileHandle(forWritingAtPath: eventStreamOutputPath) @@ -531,7 +528,6 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr oldEventHandler(event, context) } } -#endif #endif // Filtering @@ -604,7 +600,7 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr return configuration } -#if canImport(Foundation) && (!SWT_NO_FILE_IO || !SWT_NO_ABI_ENTRY_POINT) +#if !SWT_NO_FILE_IO || !SWT_NO_ABI_ENTRY_POINT /// Create an event handler that streams events to the given file using the /// specified ABI version. /// diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 7e07636d5..6b207d85a 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(Testing ABI/Encoded/ABI.EncodedInstant.swift ABI/Encoded/ABI.EncodedIssue.swift ABI/Encoded/ABI.EncodedMessage.swift + ABI/Encoded/ABI.EncodedSourceLocation.swift ABI/Encoded/ABI.EncodedTest.swift Attachments/Attachable.swift Attachments/AttachableContainer.swift @@ -80,6 +81,8 @@ add_library(Testing Support/GetSymbol.swift Support/Graph.swift Support/JSON.swift + Support/JSON.Serializable.swift + Support/JSON.Value.swift Support/Locked.swift Support/Locked+Platform.swift Support/Versions.swift diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 69346b74e..bffc5d2f2 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -36,7 +36,7 @@ private import _TestingInternals public struct ExitTest: Sendable, ~Copyable { /// A type whose instances uniquely identify instances of ``ExitTest``. @_spi(ForToolsIntegrationOnly) - public struct ID: Sendable, Equatable, Codable { + public struct ID: Sendable, Equatable, Codable, JSON.Serializable { /// An underlying UUID (stored as two `UInt64` values to avoid relying on /// `UUID` from Foundation or any platform-specific interfaces.) private var _lo: UInt64 @@ -46,6 +46,14 @@ public struct ExitTest: Sendable, ~Copyable { self._lo = uuid.0 self._hi = uuid.1 } + + func makeJSONValue() -> JSON.Value { + let dict = [ + "_lo": _lo.makeJSONValue(), + "_hi": _hi.makeJSONValue() + ] + return dict.makeJSONValue() + } } /// A value that uniquely identifies this instance. @@ -618,7 +626,7 @@ extension ExitTest { // Insert a specific variable that tells the child process which exit test // to run. - try JSON.withEncoding(of: exitTest.id) { json in + JSON.withEncoding(of: exitTest.id) { json in childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_ID"] = String(decoding: json, as: UTF8.self) } @@ -747,6 +755,10 @@ extension ExitTest { } catch { // NOTE: an error caught here indicates a decoding problem. // TODO: should we record these issues as systemic instead? + FileHandle.stdout.withLock { + try! FileHandle.stdout.write(recordJSON) + try! FileHandle.stdout.write("\n") + } Issue(for: error).record() } } @@ -778,7 +790,7 @@ extension ExitTest { } let sourceContext = SourceContext( backtrace: nil, // `issue._backtrace` will have the wrong address space. - sourceLocation: issue.sourceLocation + sourceLocation: issue.sourceLocation?.sourceLocation ) var issueCopy = Issue(kind: issueKind, comments: comments, sourceContext: sourceContext) issueCopy.isKnown = issue.isKnown diff --git a/Sources/Testing/Parameterization/CustomTestArgumentEncodable.swift b/Sources/Testing/Parameterization/CustomTestArgumentEncodable.swift index 58d738f11..9d54eaccb 100644 --- a/Sources/Testing/Parameterization/CustomTestArgumentEncodable.swift +++ b/Sources/Testing/Parameterization/CustomTestArgumentEncodable.swift @@ -62,7 +62,7 @@ extension Test.Case.Argument.ID { /// /// - ``CustomTestArgumentEncodable`` init?(identifying value: some Sendable, parameter: Test.Parameter) throws { -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) func customArgumentWrapper(for value: some CustomTestArgumentEncodable) -> some Encodable { _CustomArgumentWrapper(rawValue: value) } @@ -89,7 +89,7 @@ extension Test.Case.Argument.ID { #endif } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) /// Encode the specified test argument value and store its encoded /// representation as an array of bytes suitable for storing in an instance of /// ``Test/Case/Argument/ID-swift.struct``. diff --git a/Sources/Testing/Support/JSON.Serializable.swift b/Sources/Testing/Support/JSON.Serializable.swift new file mode 100644 index 000000000..71e42c168 --- /dev/null +++ b/Sources/Testing/Support/JSON.Serializable.swift @@ -0,0 +1,110 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// + +extension JSON { + /// A protocol describing a value that can be serialized as JSON. + protocol Serializable { + /// Serialize this instance as a JSON value. + /// + /// - Returns: A JSON value representing this instance. + func makeJSONValue() -> Value + } +} + +extension JSON.Value: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + self + } +} + +// MARK: - Scalars + +extension Bool: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + .bool(self) + } +} + +extension SignedInteger where Self: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + .int64(Int64(self)) + } +} + +extension Int8: JSON.Serializable {} +extension Int16: JSON.Serializable {} +extension Int32: JSON.Serializable {} +extension Int64: JSON.Serializable {} +@available(*, unavailable) +extension Int128: JSON.Serializable {} +extension Int: JSON.Serializable {} + +extension UnsignedInteger where Self: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + .uint64(UInt64(self)) + } +} + +extension UInt8: JSON.Serializable {} +extension UInt16: JSON.Serializable {} +extension UInt32: JSON.Serializable {} +extension UInt64: JSON.Serializable {} +@available(*, unavailable) +extension UInt128: JSON.Serializable {} +extension UInt: JSON.Serializable {} + +extension Double: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + .double(self) + } +} + +extension String: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + .string(self) + } +} + +// MARK: - Arrays + +extension Array: JSON.Serializable where Element: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + let selfCopy = self.map { $0.makeJSONValue() } + return .array(selfCopy) + } +} + +// MARK: - Dictionaries + +extension Dictionary: JSON.Serializable where Key == String, Value: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + let selfCopy = self.mapValues { $0.makeJSONValue() } + return .object(selfCopy) + } +} + +// MARK: - Optional and RawRepresentable + +#if SWT_ENCODE_JSON_NULL_VALUES +extension Optional: JSON.Serializable where Wrapped: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + guard let value = self else { + return .null + } + return value.makeJSONValue() + } +} +#endif + +extension RawRepresentable where Self: JSON.Serializable, RawValue: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + rawValue.makeJSONValue() + } +} diff --git a/Sources/Testing/Support/JSON.Value.swift b/Sources/Testing/Support/JSON.Value.swift new file mode 100644 index 000000000..9009684c4 --- /dev/null +++ b/Sources/Testing/Support/JSON.Value.swift @@ -0,0 +1,236 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// + +extension JSON { + /// An enumeration representing the different kinds of value that can be + /// encoded directly as JSON. + enum Value: Sendable { +#if SWT_ENCODE_JSON_NULL_VALUES + /// The `null` constant (`nil` in Swift.) + case null +#endif + + /// A boolean value. + case bool(Bool) + + /// A signed integer value. + case int64(Int64) + + /// An unsigned integer value. + case uint64(UInt64) + + /// A floating point value. + case double(Double) + + /// A string. + case string(String) + + /// An array of values. + case array([JSON.Value]) + + /// An object (a dictionary in Swift.) + case object([String: JSON.Value]) + } +} + +extension JSON.Value { + /// Call a function and pass it the JSON representation of a JSON keyword. + /// + /// - Parameters: + /// - value: A string representing the keyword. `StaticString` is used so + /// that the bytes representing the keyword can be acquired cheaply. + /// - body: The function to invoke. A buffer containing the JSON + /// representation of this keyword is passed. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + private static func _withUnsafeBytesForKeyword(_ value: StaticString, _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + // NOTE: StaticString.withUTF8Buffer does not rethrow. + try withExtendedLifetime(value) { () throws(E) in + let buffer = UnsafeBufferPointer(start: value.utf8Start, count: value.utf8CodeUnitCount) + return try body(.init(buffer)) + } + } + + /// Call a function and pass it the JSON representation of a numeric value. + /// + /// - Parameters: + /// - value: A numeric value. + /// - body: The function to invoke. A buffer containing the JSON + /// representation of `value` is passed. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + private static func _withUnsafeBytesForNumericValue(_ value: V, _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R where V: Numeric { + var string = String(describing: value) +#if hasFeature(Embedded) + // Embedded Swift requires typed throws, but withUTF8 is still rethrows, so + // copy to an array first. + try Array(string.utf8).withUnsafeBufferPointer { buffer throws(E) in + try body(.init(buffer)) + } +#else + do { + return try string.withUTF8 { utf8 in + try body(.init(utf8)) + } + } catch { + throw error as! E + } +#endif + } + + /// Call a function and pass it the JSON representation of a string. + /// + /// - Parameters: + /// - value: A string. + /// - body: The function to invoke. A buffer containing the JSON + /// representation of `value` is passed. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + private static func _withUnsafeBytesForString(_ value: String, _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + var result = [UInt8]() + + let scalars = value.unicodeScalars + result.reserveCapacity(scalars.underestimatedCount + 2) + + do { + result.append(UInt8(ascii: #"""#)) + defer { + result.append(UInt8(ascii: #"""#)) + } + + for scalar in scalars { + switch scalar { + case Unicode.Scalar(0x0000) ..< Unicode.Scalar(0x0020): + let hexValue = String(scalar.value, radix: 16) + let leadingZeroes = repeatElement(UInt8(ascii: "0"), count: 4 - hexValue.count) + result += #"\u"#.utf8 + result += leadingZeroes + result += hexValue.utf8 + case #"""#, #"\"#: + result += #"\\#(scalar)"#.utf8 + default: + result += scalar.utf8 + } + } + } + + return try result.withUnsafeBufferPointer { buffer throws(E) in + try body(.init(buffer)) + } + } + + /// Call a function and pass it the JSON representation of an array. + /// + /// - Parameters: + /// - value: An array of JSON values. + /// - body: The function to invoke. A buffer containing the JSON + /// representation of `value` is passed. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + private static func _withUnsafeBytesForArray(_ value: [JSON.Value], _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + var result = [UInt8]() + + do { + result.append(UInt8(ascii: "[")) + defer { + result.append(UInt8(ascii: "]")) + } + + result += value.lazy.map { element in + element.withUnsafeBytes { bytes in + Array(bytes) + } + }.joined(separator: CollectionOfOne(UInt8(ascii: ","))) + } + + return try result.withUnsafeBufferPointer { buffer throws(E) in + try body(.init(buffer)) + } + } + + /// Call a function and pass it the JSON representation of an object (a + /// dictionary in Swift). + /// + /// - Parameters: + /// - value: A JSON object. + /// - body: The function to invoke. A buffer containing the JSON + /// representation of `value` is passed. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + private static func _withUnsafeBytesForObject(_ value: [String: JSON.Value], _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + var result = [UInt8]() + + do { + result.append(UInt8(ascii: "{")) + defer { + result.append(UInt8(ascii: "}")) + } + + result += value.sorted { lhs, rhs in + lhs.key < rhs.key + }.map { key, value in + key.makeJSONValue().withUnsafeBytes { serializedKey in + value.withUnsafeBytes { serializedValue in + var result = Array(serializedKey) + result.append(UInt8(ascii: ":")) + result += serializedValue + return result + } + } + }.joined(separator: CollectionOfOne(UInt8(ascii: ","))) + } + + return try result.withUnsafeBufferPointer { buffer throws(E) in + try body(.init(buffer)) + } + } + + /// Call a function and pass it the JSON representation of this JSON value. + /// + /// - Parameters: + /// - body: The function to invoke. A buffer containing the JSON + /// representation of this value is passed. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + switch self { +#if SWT_ENCODE_JSON_NULL_VALUES + case .null: + return try Self._withUnsafeBytesForKeyword("null", body) +#endif + case let .bool(value): + return try Self._withUnsafeBytesForKeyword(value ? "true" : "false", body) + case let .int64(value): + return try Self._withUnsafeBytesForNumericValue(value, body) + case let .uint64(value): + return try Self._withUnsafeBytesForNumericValue(value, body) + case let .double(value): + return try Self._withUnsafeBytesForNumericValue(value, body) + case let .string(value): + return try Self._withUnsafeBytesForString(value, body) + case let .array(value): + return try Self._withUnsafeBytesForArray(value, body) + case let .object(value): + return try Self._withUnsafeBytesForObject(value, body) + } + } +} diff --git a/Sources/Testing/Support/JSON.swift b/Sources/Testing/Support/JSON.swift index 76c7b7f07..0e1ce909a 100644 --- a/Sources/Testing/Support/JSON.swift +++ b/Sources/Testing/Support/JSON.swift @@ -8,15 +8,38 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) private import Foundation #endif enum JSON { + /// Encode a value as JSON. + /// + /// - Parameters: + /// - value: The value to encode. + /// - userInfo: Any user info to pass into the encoder during encoding. + /// - body: A function to call. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body` or by the encoding process. + static func withEncoding(of value: J, _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R where J: JSON.Serializable { + try value.makeJSONValue().withUnsafeBytes { json throws(E) in + try body(json) + } + } +} + +// MARK: - Foundation-based JSON support + +extension JSON { /// Whether or not pretty-printed JSON is enabled for this process. /// /// This is a debugging tool that can be used by developers working on the /// testing library to improve the readability of JSON output. + /// + /// This property is only used by the Foundation-based overload of + /// `withEncoding()`. It is ignored when using ``JSON/Serializable``. private static let _prettyPrintingEnabled = Environment.flag(named: "SWT_PRETTY_PRINT_JSON") == true /// Encode a value as JSON. @@ -29,8 +52,9 @@ enum JSON { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body` or by the encoding process. + @_disfavoredOverload static func withEncoding(of value: some Encodable, userInfo: [CodingUserInfoKey: any Sendable] = [:], _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) let encoder = JSONEncoder() // Keys must be sorted to ensure deterministic matching of encoded data. @@ -60,7 +84,7 @@ enum JSON { /// /// - Throws: Whatever is thrown by the decoding process. static func decode(_ type: T.Type, from jsonRepresentation: UnsafeRawBufferPointer) throws -> T where T: Decodable { -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) try withExtendedLifetime(jsonRepresentation) { let byteCount = jsonRepresentation.count let data = if byteCount > 0 { diff --git a/Tests/TestingTests/ABIEntryPointTests.swift b/Tests/TestingTests/ABIEntryPointTests.swift index 9fcda9223..9a973c9bf 100644 --- a/Tests/TestingTests/ABIEntryPointTests.swift +++ b/Tests/TestingTests/ABIEntryPointTests.swift @@ -8,12 +8,13 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) && !SWT_NO_ABI_ENTRY_POINT -@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing - -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) private import Foundation #endif + +#if !SWT_NO_ABI_ENTRY_POINT +@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing + private import _TestingInternals @Suite("ABI entry point tests") @@ -27,8 +28,10 @@ struct ABIEntryPointTests { arguments.verbosity = .min let result = try await _invokeEntryPointV0Experimental(passing: arguments) { recordJSON in +#if !SWT_NO_FOUNDATION && canImport(Foundation) let record = try! JSON.decode(ABI.Record.self, from: recordJSON) _ = record.kind +#endif } #expect(result == EXIT_SUCCESS) @@ -160,7 +163,7 @@ struct ABIEntryPointTests { return try await abiEntryPoint(.init(argumentsJSON), recordHandler) } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test func decodeEmptyConfiguration() throws { let emptyBuffer = UnsafeRawBufferPointer(start: nil, count: 0) #expect(throws: DecodingError.self) { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 5a36fd4b6..6e3cdda2c 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -10,7 +10,7 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) import Foundation @_spi(Experimental) import _Testing_Foundation #endif @@ -246,7 +246,7 @@ struct AttachmentTests { } } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) #if !SWT_NO_FILE_IO @Test func attachContentsOfFileURL() async throws { let data = try #require("".data(using: .utf8)) @@ -468,7 +468,7 @@ extension AttachmentTests { try test(value) } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test func data() throws { let value = try #require("abc123".data(using: .utf8)) try test(value) @@ -607,7 +607,7 @@ struct MySendableAttachableWithDefaultByteCount: Attachable, Sendable { } } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) struct MyCodableAttachable: Codable, Attachable, Sendable { var string: String } diff --git a/Tests/TestingTests/BacktraceTests.swift b/Tests/TestingTests/BacktraceTests.swift index c04b05c15..457f3b8a5 100644 --- a/Tests/TestingTests/BacktraceTests.swift +++ b/Tests/TestingTests/BacktraceTests.swift @@ -9,7 +9,7 @@ // @testable @_spi(ForToolsIntegrationOnly) import Testing -#if SWT_TARGET_OS_APPLE && canImport(Foundation) +#if SWT_TARGET_OS_APPLE && !SWT_NO_FOUNDATION && canImport(Foundation) import Foundation #endif @@ -72,7 +72,7 @@ struct BacktraceTests { } } -#if SWT_TARGET_OS_APPLE && canImport(Foundation) +#if SWT_TARGET_OS_APPLE && !SWT_NO_FOUNDATION && canImport(Foundation) @available(_typedThrowsAPI, *) @Test("Thrown NSError captures backtrace") func thrownNSErrorCapturesBacktrace() async throws { @@ -141,7 +141,7 @@ struct BacktraceTests { #expect(Backtrace(forFirstThrowOf: BacktracedError()) == nil) } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test("Encoding/decoding") func encodingAndDecoding() throws { let original = Backtrace.current() diff --git a/Tests/TestingTests/ClockTests.swift b/Tests/TestingTests/ClockTests.swift index e4aabfe22..2b3ac8457 100644 --- a/Tests/TestingTests/ClockTests.swift +++ b/Tests/TestingTests/ClockTests.swift @@ -121,7 +121,7 @@ struct ClockTests { #expect(duration == .nanoseconds(offsetNanoseconds)) } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @available(_clockAPI, *) @Test("Codable") func codable() async throws { diff --git a/Tests/TestingTests/DiscoveryTests.swift b/Tests/TestingTests/DiscoveryTests.swift index a730f8b53..1d093e0e1 100644 --- a/Tests/TestingTests/DiscoveryTests.swift +++ b/Tests/TestingTests/DiscoveryTests.swift @@ -10,7 +10,7 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import _TestDiscovery -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) private import Foundation #endif @@ -30,7 +30,7 @@ struct DiscoveryTests { #expect(String(describing: kind3).lowercased() == "0xff123456") } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test func testContentKindCodableConformance() throws { let kind1: TestContentKind = "moof" let data = try JSONEncoder().encode(kind1) diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index 8ac7f6728..9c5294135 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -12,10 +12,10 @@ #if !os(Windows) import RegexBuilder #endif -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) import Foundation #endif -#if canImport(FoundationXML) +#if !SWT_NO_FOUNDATION && canImport(FoundationXML) import FoundationXML #endif @@ -370,7 +370,7 @@ struct EventRecorderTests { } #endif -#if canImport(Foundation) || canImport(FoundationXML) +#if !SWT_NO_FOUNDATION && (canImport(Foundation) || canImport(FoundationXML)) @Test( "JUnitXMLRecorder outputs valid XML", .bug("https://github.com/swiftlang/swift-testing/issues/254") diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index 941dcadb9..f63778abb 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -14,7 +14,7 @@ private import _TestingInternals @Suite("Event Tests") struct EventTests { -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test("Event's and Event.Kinds's Codable Conformances", arguments: [ Event.Kind.expectationChecked( diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index d22bf9fba..9fabe0664 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -1596,7 +1596,7 @@ final class IssueTests: XCTestCase { } #endif -#if canImport(Foundation) && !SWT_NO_SNAPSHOT_TYPES +#if !SWT_NO_FOUNDATION && canImport(Foundation) && !SWT_NO_SNAPSHOT_TYPES import Foundation @Suite("Issue Codable Conformance Tests") diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 1f18f20a9..d5e8b10f5 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -580,4 +580,13 @@ struct MiscellaneousTests { } #expect(duration < .seconds(1)) } + +#if !SWT_NO_FOUNDATION && canImport(Foundation) + @Test("JSON string escaping") + func escapeJSONStrings() throws { + let value1 = "abc\t123äßç" + let value2 = try JSON.encodeAndDecode(value1) + #expect(value1 == value2) + } +#endif } diff --git a/Tests/TestingTests/Runner.Plan.SnapshotTests.swift b/Tests/TestingTests/Runner.Plan.SnapshotTests.swift index e193de219..ed35ab3bf 100644 --- a/Tests/TestingTests/Runner.Plan.SnapshotTests.swift +++ b/Tests/TestingTests/Runner.Plan.SnapshotTests.swift @@ -13,7 +13,7 @@ @Suite("Runner.Plan.Snapshot tests") struct Runner_Plan_SnapshotTests { -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test("Codable") func codable() async throws { let suite = try #require(await test(for: Runner_Plan_SnapshotFixtures.self)) diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 6e7be0f15..6e9286adc 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -195,7 +195,7 @@ struct SwiftPMTests { #expect(fileContents.contains(UInt8(ascii: ">"))) } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test("--configuration-path argument", arguments: [ "--configuration-path", "--experimental-configuration-path", ]) diff --git a/Tests/TestingTests/Test.Case.Argument.IDTests.swift b/Tests/TestingTests/Test.Case.Argument.IDTests.swift index ced76adac..99f6be310 100644 --- a/Tests/TestingTests/Test.Case.Argument.IDTests.swift +++ b/Tests/TestingTests/Test.Case.Argument.IDTests.swift @@ -37,7 +37,7 @@ struct Test_Case_Argument_IDTests { #expect(testCase.arguments.count == 1) let argument = try #require(testCase.arguments.first) let argumentID = try #require(argument.id) -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) let decodedArgument = try argumentID.bytes.withUnsafeBufferPointer { argumentID in try JSON.decode(MyCustomTestArgument.self, from: .init(argumentID)) } diff --git a/Tests/TestingTests/Test.SnapshotTests.swift b/Tests/TestingTests/Test.SnapshotTests.swift index 12a3f2467..dfa1cda9e 100644 --- a/Tests/TestingTests/Test.SnapshotTests.swift +++ b/Tests/TestingTests/Test.SnapshotTests.swift @@ -13,7 +13,6 @@ @Suite("Test.Snapshot tests") struct Test_SnapshotTests { -#if canImport(Foundation) @Test("Codable") func codable() throws { let test = try #require(Test.current) @@ -27,7 +26,6 @@ struct Test_SnapshotTests { // FIXME: Compare traits as well, once they are included. #expect(decoded.parameters == snapshot.parameters) } -#endif @Test("isParameterized property") func isParameterized() async throws { diff --git a/Tests/TestingTests/TestSupport/TestingAdditions.swift b/Tests/TestingTests/TestSupport/TestingAdditions.swift index 5a0121444..eeae96174 100644 --- a/Tests/TestingTests/TestSupport/TestingAdditions.swift +++ b/Tests/TestingTests/TestSupport/TestingAdditions.swift @@ -343,9 +343,26 @@ extension JSON { /// - Returns: A copy of `value` after encoding and decoding. /// /// - Throws: Any error encountered encoding or decoding `value`. + @_disfavoredOverload static func encodeAndDecode(_ value: T) throws -> T where T: Codable { try JSON.withEncoding(of: value) { data in - try JSON.decode(T.self, from: data) + try FileHandle.stdout.write(data) + return try JSON.decode(T.self, from: data) + } + } + + /// Round-trip a value through JSON encoding/decoding. + /// + /// - Parameters: + /// - value: The value to round-trip. + /// + /// - Returns: A copy of `value` after encoding and decoding. + /// + /// - Throws: Any error encountered encoding or decoding `value`. + static func encodeAndDecode(_ value: T) throws -> T where T: JSON.Serializable & Codable { + try JSON.withEncoding(of: value) { data in + try FileHandle.stdout.write(data) + return try JSON.decode(T.self, from: data) } } } diff --git a/Tests/TestingTests/Traits/BugTests.swift b/Tests/TestingTests/Traits/BugTests.swift index 739acb672..42c3bfdf0 100644 --- a/Tests/TestingTests/Traits/BugTests.swift +++ b/Tests/TestingTests/Traits/BugTests.swift @@ -84,7 +84,7 @@ struct BugTests { #expect(traits.count == 3) } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test("Encoding/decoding") func encodingAndDecoding() throws { let original = Bug.bug(id: 12345, "Lorem ipsum") diff --git a/Tests/TestingTests/Traits/TagListTests.swift b/Tests/TestingTests/Traits/TagListTests.swift index 1ec8d1248..b7b21fee5 100644 --- a/Tests/TestingTests/Traits/TagListTests.swift +++ b/Tests/TestingTests/Traits/TagListTests.swift @@ -98,7 +98,7 @@ struct TagListTests { #expect(Tag(userProvidedStringValue: ".red") == .red) } -#if canImport(Foundation) +#if !SWT_NO_FOUNDATION && canImport(Foundation) @Test("Encoding/decoding tags") func encodeAndDecodeTags() throws { let array: [Tag] = [.red, .orange, Tag("abc123"), Tag(".abc123")] diff --git a/Tests/TestingTests/_Testing_Foundation/FoundationTests.swift b/Tests/TestingTests/_Testing_Foundation/FoundationTests.swift index 04c0f57dd..e5a7cefc2 100644 --- a/Tests/TestingTests/_Testing_Foundation/FoundationTests.swift +++ b/Tests/TestingTests/_Testing_Foundation/FoundationTests.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) && !SWT_NO_UTC_CLOCK +#if !SWT_NO_FOUNDATION && canImport(Foundation) && !SWT_NO_UTC_CLOCK @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import _Testing_Foundation @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing import Foundation