From 97245a16437afb5233adec475a604130cefd4563 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 13 Mar 2025 11:19:56 -0400 Subject: [PATCH 1/3] Implement JSON coding without using Foundation or `Codable`. This PR replaces most of our uses of `JSONEncoder` with a home-grown implementation. Types conform to the internal `JSON.Serializable` protocol and emit strings, numbers, dictionaries, arrays, etc. Unlike with `Codable`, there is no synthesized implementation here. Pros: - Less reliance on Foundation (ideally we drop the dependency entirely at some point); - No need to use existentials to encode a value (allowing us to eventually support Embedded Swift); and - The potential for more inlining as all the encoding work is done in-module. Cons: - Don't Repeat Yourself; - This implementation doesn't come with a decoder (which is harder to write); - The implementation of `JSON.Serializable` cannot be supplied by the compiler; - More code means more technical debt and a higher maintenance burden; - The implementation is less configurable/customizable than Foundation's; and - The implementation is not a full `Encoder` conformance, so it cannot be used to reimplement `CustomTestArgumentEncodable`. Resolves rdar://146964016. --- .../Attachable+Encodable+NSSecureCoding.swift | 2 +- .../Attachments/Attachable+Encodable.swift | 2 +- .../Attachable+NSSecureCoding.swift | 2 +- .../Attachments/Attachment+URL.swift | 2 +- .../Attachments/Data+Attachable.swift | 2 +- .../Attachments/EncodingFormat.swift | 2 +- .../Attachments/_AttachableURLContainer.swift | 2 +- .../Events/Clock+Date.swift | 2 +- .../Testing/ABI/ABI.Record+Streaming.swift | 8 +- Sources/Testing/ABI/ABI.Record.swift | 33 ++-- Sources/Testing/ABI/ABI.swift | 6 +- .../ABI/Encoded/ABI.EncodedAttachment.swift | 16 +- .../ABI/Encoded/ABI.EncodedBacktrace.swift | 32 +++- .../ABI/Encoded/ABI.EncodedError.swift | 16 +- .../ABI/Encoded/ABI.EncodedEvent.swift | 34 +++- .../ABI/Encoded/ABI.EncodedInstant.swift | 17 +- .../ABI/Encoded/ABI.EncodedIssue.swift | 36 +++- .../ABI/Encoded/ABI.EncodedMessage.swift | 20 +- .../Encoded/ABI.EncodedSourceLocation.swift | 48 +++++ .../Testing/ABI/Encoded/ABI.EncodedTest.swift | 72 ++++++-- .../ABI/EntryPoints/ABIEntryPoint.swift | 2 +- .../Testing/ABI/EntryPoints/EntryPoint.swift | 6 +- Sources/Testing/CMakeLists.txt | 1 + Sources/Testing/ExitTests/ExitTest.swift | 8 +- .../CustomTestArgumentEncodable.swift | 4 +- .../Testing/Support/JSON.Serializable.swift | 172 ++++++++++++++++++ Sources/Testing/Support/JSON.swift | 33 +++- Tests/TestingTests/ABIEntryPointTests.swift | 13 +- Tests/TestingTests/AttachmentTests.swift | 8 +- Tests/TestingTests/BacktraceTests.swift | 6 +- Tests/TestingTests/ClockTests.swift | 2 +- Tests/TestingTests/DiscoveryTests.swift | 4 +- Tests/TestingTests/EventRecorderTests.swift | 6 +- Tests/TestingTests/EventTests.swift | 2 +- Tests/TestingTests/IssueTests.swift | 2 +- .../Runner.Plan.SnapshotTests.swift | 2 +- Tests/TestingTests/SwiftPMTests.swift | 2 +- .../Test.Case.Argument.IDTests.swift | 2 +- Tests/TestingTests/Test.SnapshotTests.swift | 2 - Tests/TestingTests/Traits/BugTests.swift | 2 +- Tests/TestingTests/Traits/TagListTests.swift | 2 +- .../_Testing_Foundation/FoundationTests.swift | 2 +- 42 files changed, 526 insertions(+), 111 deletions(-) create mode 100644 Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift create mode 100644 Sources/Testing/Support/JSON.Serializable.swift 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..76044aa90 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. /// @@ -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..81664e9e4 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,19 @@ extension ABI.Record: Codable { } } } + +extension ABI.Record: JSON.Serializable { + func makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + try dict.updateValue(V.versionNumber, forKey: "version") + switch kind { + case let .test(test): + try dict.updateValue("test", forKey: "kind") + try dict.updateValue(test, forKey: "payload") + case let .event(event): + try dict.updateValue("event", forKey: "kind") + try dict.updateValue(event, forKey: "payload") + } + return try dict.makeJSON() + } +} 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..ac2a9e3a5 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 makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + if let path { + try dict.updateValue(path, forKey: "path") + } + return try dict.makeJSON() + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift index fcfa5cc37..1771e6d3f 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift @@ -31,14 +31,34 @@ 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 makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + try dict.updateValue(symbolicatedAddresses, forKey: "symbolicatedAddresses") + return try dict.makeJSON() + } +} + +extension Backtrace.SymbolicatedAddress: JSON.Serializable { + func makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + try dict.updateValue(address, forKey: "address") + if let offset { + try dict.updateValue(offset, forKey: "offset") + } + if let symbolName { + try dict.updateValue(symbolName, forKey: "symbolName") + } + return try dict.makeJSON() + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift index 3a299cd3f..afcfd73d8 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift @@ -54,9 +54,21 @@ 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 makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + try dict.updateValue(description, forKey: "description") + try dict.updateValue(domain, forKey: "domain") + try dict.updateValue(code, forKey: "code") + return try dict.makeJSON() + } +} // MARK: - CustomTestStringConvertible diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index b8bafdde1..cedf92a68 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -107,7 +107,35 @@ 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 makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + + try dict.updateValue(kind, forKey: "kind") + try dict.updateValue(instant, forKey: "instant") + if let issue { + try dict.updateValue(issue, forKey: "issue") + } + if let _attachment { + try dict.updateValue(_attachment, forKey: "_attachment") + } + try dict.updateValue(messages, forKey: "messages") + if let testID { + try dict.updateValue(testID, forKey: "testID") + } + if let _testCase { + try dict.updateValue(_testCase, forKey: "_testCase") + } + + return try dict.makeJSON() + } +} + +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..d16c1aece 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift @@ -35,6 +35,19 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable -extension ABI.EncodedInstant: Codable {} +extension ABI.EncodedInstant: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedInstant: JSON.Serializable { + func makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + + try dict.updateValue(absolute, forKey: "absolute") + try dict.updateValue(since1970, forKey: "since1970") + + return try dict.makeJSON() + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 0ea218cc8..324b22750 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,31 @@ extension ABI { } } -// MARK: - Codable +// MARK: - Decodable + +extension ABI.EncodedIssue: Decodable {} +extension ABI.EncodedIssue.Severity: Decodable {} + +// MARK: - JSON.Serializable + +extension ABI.EncodedIssue: JSON.Serializable { + func makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + + try dict.updateValue(_severity, forKey: "_severity") + try dict.updateValue(isKnown, forKey: "isKnown") + if let sourceLocation { + try dict.updateValue(sourceLocation, forKey: "sourceLocation") + } + if let _backtrace { + try dict.updateValue(_backtrace, forKey: "_backtrace") + } + if let _error { + try dict.updateValue(_error, forKey: "_error") + } + + return try dict.makeJSON() + } +} -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..95f0016df 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift @@ -74,7 +74,21 @@ 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 makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + + try dict.updateValue(symbol, forKey: "symbol") + try dict.updateValue(text, forKey: "text") + + return try dict.makeJSON() + } +} +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..43945e3a0 --- /dev/null +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift @@ -0,0 +1,48 @@ +// +// 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 makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + + try dict.updateValue(sourceLocation._filePath, forKey: "_filePath") + try dict.updateValue(sourceLocation.fileID, forKey: "fileID") + try dict.updateValue(sourceLocation.line, forKey: "line") + try dict.updateValue(sourceLocation.column, forKey: "column") + + return try dict.makeJSON() + } +} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift index 51d01781d..20f333c73 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 makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() + + try dict.updateValue(kind, forKey: "kind") + try dict.updateValue(name, forKey: "name") + if let displayName { + try dict.updateValue(displayName, forKey: "displayName") + } + try dict.updateValue(sourceLocation, forKey: "sourceLocation") + try dict.updateValue(id, forKey: "id") + if let _testCases { + try dict.updateValue(_testCases, forKey: "_testCases") + } + if let isParameterized { + try dict.updateValue(isParameterized, forKey: "isParameterized") + } + if let _tags { + try dict.updateValue(_tags, forKey: "_tags") + } + + return try dict.makeJSON() + } +} +extension ABI.EncodedTest.Kind: JSON.Serializable {} + +extension ABI.EncodedTest.ID: JSON.Serializable { + func makeJSON() throws -> some Collection { + try stringValue.makeJSON() + } +} + +extension ABI.EncodedTestCase: JSON.Serializable { + func makeJSON() throws -> some Collection { + var dict = JSON.HeterogenousDictionary() -extension ABI.EncodedTest: Codable {} -extension ABI.EncodedTest.Kind: Codable {} -extension ABI.EncodedTestCase: Codable {} + try dict.updateValue(id, forKey: "id") + try dict.updateValue(displayName, forKey: "displayName") + + return try dict.makeJSON() + } +} 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..10a57513c 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 diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 69346b74e..136e085ab 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,10 @@ public struct ExitTest: Sendable, ~Copyable { self._lo = uuid.0 self._hi = uuid.1 } + + func makeJSON() throws -> some Collection { + try [_lo.makeJSON(), _hi.makeJSON()].joined() + } } /// A value that uniquely identifies this instance. @@ -778,7 +782,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..618ab7dd3 --- /dev/null +++ b/Sources/Testing/Support/JSON.Serializable.swift @@ -0,0 +1,172 @@ +// +// 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 { + protocol Serializable { + /// A type representing the result of by ``makeJSON()``. + associatedtype JSONBytes: Collection where JSONBytes.Element == UInt8 + + /// Serialize this value as JSON. + /// + /// - Returns: The sequence of bytes representing this value as JSON. + /// + /// - Throws: Any error that prevented serializing this value. + func makeJSON() throws -> JSONBytes + } +} + +extension JSON.Serializable { + /// Write the JSON representation of this value to the given file handle. + /// + /// - Parameters: + /// - file: The file to write to. A trailing newline is not written. + /// - flushAfterward: Whether or not to flush the file (with `fflush()`) + /// after writing. If `true`, `fflush()` is called even if an error + /// occurred while writing. + /// + /// - Throws: Any error that occurred while writing `bytes`. If an error + /// occurs while flushing the file, it is not thrown. + func writeJSON(to file: borrowing FileHandle, flushAfterward: Bool = true) throws { + try file.write(makeJSON(), flushAfterward: flushAfterward) + } +} + +// MARK: - Arbitrary bytes + +extension JSON { + struct Verbatim: JSON.Serializable where S: Collection, S.Element == UInt8 { + private var _bytes: S + + init(_ bytes: S) { + _bytes = bytes + } + + func makeJSON() throws -> S { + _bytes + } + } +} + +// MARK: - Scalars + +extension Bool: JSON.Serializable { + func makeJSON() throws -> UnsafeBufferPointer { + let stringValue: StaticString = self ? "true" : "false" + return UnsafeBufferPointer(start: stringValue.utf8Start, count: stringValue.utf8CodeUnitCount) + } +} + +extension Numeric where Self: CustomStringConvertible & JSON.Serializable { + func makeJSON() throws -> String.UTF8View { + String(describing: self).utf8 + } +} + +extension Int: JSON.Serializable {} +extension UInt64: JSON.Serializable {} +extension Double: JSON.Serializable {} + +extension String: JSON.Serializable { + func makeJSON() throws -> [UInt8] { + var result = [UInt8]() + + let scalars = self.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 += leadingZeroes + result += hexValue.utf8 + case #"""#, #"\"#: + result += #"\\#(scalar)"#.utf8 + default: + result += scalar.utf8 + } + } + } + + return result + } +} + +// MARK: - Arrays + +extension Array: JSON.Serializable where Element: JSON.Serializable { + func makeJSON() throws -> [UInt8] { + var result = [UInt8]() + + do { + result.append(UInt8(ascii: "[")) + defer { + result.append(UInt8(ascii: "]")) + } + + result += try self.lazy.map { element in + try element.makeJSON() + }.joined(separator: CollectionOfOne(UInt8(ascii: ","))) + } + + return result + } +} + +// MARK: - Dictionaries + +extension Dictionary: JSON.Serializable where Key == String, Value: JSON.Serializable { + func makeJSON() throws -> [UInt8] { + var result = [UInt8]() + + do { + result.append(UInt8(ascii: #"{"#)) + defer { + result.append(UInt8(ascii: #"}"#)) + } + + result += try self.sorted { lhs, rhs in + lhs.key < rhs.key + }.map { key, value in + let serializedKey = try key.makeJSON() + let serializedValue = try value.makeJSON() + return serializedKey + CollectionOfOne(UInt8(ascii: ":")) + serializedValue + }.joined(separator: CollectionOfOne(UInt8(ascii: #","#))) + } + + return result + } +} + +extension JSON { + typealias HeterogenousDictionary = Dictionary> +} + +extension JSON.HeterogenousDictionary { + @discardableResult + mutating func updateValue(_ value: some JSON.Serializable, forKey key: String) throws -> Value? { + let serializedValue = try JSON.Verbatim(Array(value.makeJSON())) + return updateValue(serializedValue as Value, forKey: key) + } +} + +// MARK: - RawRepresentable + +extension RawRepresentable where Self: JSON.Serializable, RawValue: JSON.Serializable { + func makeJSON() throws -> RawValue.JSONBytes { + try rawValue.makeJSON() + } +} diff --git a/Sources/Testing/Support/JSON.swift b/Sources/Testing/Support/JSON.swift index 76c7b7f07..d08cdb03b 100644 --- a/Sources/Testing/Support/JSON.swift +++ b/Sources/Testing/Support/JSON.swift @@ -8,15 +8,41 @@ // 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 -> R) throws -> R where J: JSON.Serializable { + let json = try value.makeJSON() + return try json.withContiguousStorageIfAvailable { json in + try body(.init(json)) + } ?? Array(json).withUnsafeBytes { json 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 +55,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 +87,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/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/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 From 4a3fc923f474247765aaf89a7566ac813cf07a79 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 13 Mar 2025 23:40:29 -0400 Subject: [PATCH 2/3] Make it an enum, obviously --- Sources/Testing/ABI/ABI.Record.swift | 19 +- .../ABI/Encoded/ABI.EncodedAttachment.swift | 8 +- .../ABI/Encoded/ABI.EncodedBacktrace.swift | 21 +- .../ABI/Encoded/ABI.EncodedError.swift | 13 +- .../ABI/Encoded/ABI.EncodedEvent.swift | 21 +- .../ABI/Encoded/ABI.EncodedInstant.swift | 13 +- .../ABI/Encoded/ABI.EncodedIssue.swift | 17 +- .../ABI/Encoded/ABI.EncodedMessage.swift | 13 +- .../Encoded/ABI.EncodedSourceLocation.swift | 17 +- .../Testing/ABI/Encoded/ABI.EncodedTest.swift | 40 ++-- Sources/Testing/CMakeLists.txt | 2 + Sources/Testing/ExitTests/ExitTest.swift | 12 +- .../Testing/Support/JSON.Serializable.swift | 172 +++++--------- Sources/Testing/Support/JSON.Value.swift | 213 ++++++++++++++++++ Sources/Testing/Support/JSON.swift | 5 +- 15 files changed, 373 insertions(+), 213 deletions(-) create mode 100644 Sources/Testing/Support/JSON.Value.swift diff --git a/Sources/Testing/ABI/ABI.Record.swift b/Sources/Testing/ABI/ABI.Record.swift index 81664e9e4..a8d54b2ae 100644 --- a/Sources/Testing/ABI/ABI.Record.swift +++ b/Sources/Testing/ABI/ABI.Record.swift @@ -82,17 +82,20 @@ extension ABI.Record: Decodable { } extension ABI.Record: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() - try dict.updateValue(V.versionNumber, forKey: "version") + func makeJSONValue() -> JSON.Value { + var dict = [ + "version": V.versionNumber.makeJSONValue() + ] + switch kind { case let .test(test): - try dict.updateValue("test", forKey: "kind") - try dict.updateValue(test, forKey: "payload") + dict["kind"] = "test".makeJSONValue() + dict["payload"] = test.makeJSONValue() case let .event(event): - try dict.updateValue("event", forKey: "kind") - try dict.updateValue(event, forKey: "payload") + dict["kind"] = "event".makeJSONValue() + dict["payload"] = event.makeJSONValue() } - return try dict.makeJSON() + + return .object(dict) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index ac2a9e3a5..652f2bb01 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -34,11 +34,11 @@ extension ABI.EncodedAttachment: Decodable {} // MARK: - JSON.Serializable extension ABI.EncodedAttachment: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() + func makeJSONValue() -> JSON.Value { + var dict = [String: JSON.Value]() if let path { - try dict.updateValue(path, forKey: "path") + dict["path"] = path.makeJSONValue() } - return try dict.makeJSON() + return .object(dict) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift index 1771e6d3f..35c9fbc2c 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift @@ -42,23 +42,24 @@ extension ABI.EncodedBacktrace: Decodable { // MARK: - JSON.Serializable extension ABI.EncodedBacktrace: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() - try dict.updateValue(symbolicatedAddresses, forKey: "symbolicatedAddresses") - return try dict.makeJSON() + func makeJSONValue() -> JSON.Value { + symbolicatedAddresses.makeJSONValue() } } extension Backtrace.SymbolicatedAddress: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() - try dict.updateValue(address, forKey: "address") + func makeJSONValue() -> JSON.Value { + var dict = [ + "address": address.makeJSONValue() + ] + if let offset { - try dict.updateValue(offset, forKey: "offset") + dict["offset"] = offset.makeJSONValue() } if let symbolName { - try dict.updateValue(symbolName, forKey: "symbolName") + dict["symbolName"] = symbolName.makeJSONValue() } - return try dict.makeJSON() + + return .object(dict) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift index afcfd73d8..aa46069e8 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift @@ -61,12 +61,13 @@ extension ABI.EncodedError: Decodable {} // MARK: - JSON.Serializable extension ABI.EncodedError: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() - try dict.updateValue(description, forKey: "description") - try dict.updateValue(domain, forKey: "domain") - try dict.updateValue(code, forKey: "code") - return try dict.makeJSON() + func makeJSONValue() -> JSON.Value { + let dict = [ + "description": description.makeJSONValue(), + "domain": domain.makeJSONValue(), + "code": code.makeJSONValue() + ] + return .object(dict) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index cedf92a68..b7fe74c62 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -115,26 +115,27 @@ extension ABI.EncodedEvent.Kind: Decodable {} // MARK: - JSON.Serializable extension ABI.EncodedEvent: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() + func makeJSONValue() -> JSON.Value { + var dict = [ + "kind": kind.makeJSONValue(), + "instant": instant.makeJSONValue(), + "messages": messages.makeJSONValue(), + ] - try dict.updateValue(kind, forKey: "kind") - try dict.updateValue(instant, forKey: "instant") if let issue { - try dict.updateValue(issue, forKey: "issue") + dict["issue"] = issue.makeJSONValue() } if let _attachment { - try dict.updateValue(_attachment, forKey: "_attachment") + dict["_attachment"] = _attachment.makeJSONValue() } - try dict.updateValue(messages, forKey: "messages") if let testID { - try dict.updateValue(testID, forKey: "testID") + dict["testID"] = testID.makeJSONValue() } if let _testCase { - try dict.updateValue(_testCase, forKey: "_testCase") + dict["_testCase"] = _testCase.makeJSONValue() } - return try dict.makeJSON() + return .object(dict) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift index d16c1aece..14df49828 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift @@ -42,12 +42,11 @@ extension ABI.EncodedInstant: Decodable {} // MARK: - JSON.Serializable extension ABI.EncodedInstant: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() - - try dict.updateValue(absolute, forKey: "absolute") - try dict.updateValue(since1970, forKey: "since1970") - - return try dict.makeJSON() + func makeJSONValue() -> JSON.Value { + let dict = [ + "absolute": absolute.makeJSONValue(), + "since1970": since1970.makeJSONValue() + ] + return .object(dict) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 324b22750..853aaa585 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -72,22 +72,23 @@ extension ABI.EncodedIssue.Severity: Decodable {} // MARK: - JSON.Serializable extension ABI.EncodedIssue: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() + func makeJSONValue() -> JSON.Value { + var dict = [ + "_severity": _severity.makeJSONValue(), + "isKnown": isKnown.makeJSONValue() + ] - try dict.updateValue(_severity, forKey: "_severity") - try dict.updateValue(isKnown, forKey: "isKnown") if let sourceLocation { - try dict.updateValue(sourceLocation, forKey: "sourceLocation") + dict["sourceLocation"] = sourceLocation.makeJSONValue() } if let _backtrace { - try dict.updateValue(_backtrace, forKey: "_backtrace") + dict["_backtrace"] = _backtrace.makeJSONValue() } if let _error { - try dict.updateValue(_error, forKey: "_error") + dict["_error"] = _error.makeJSONValue() } - return try dict.makeJSON() + return .object(dict) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift index 95f0016df..1a1184a3b 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift @@ -82,13 +82,12 @@ extension ABI.EncodedMessage.Symbol: Decodable {} // MARK: - JSON.Serializable extension ABI.EncodedMessage: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() - - try dict.updateValue(symbol, forKey: "symbol") - try dict.updateValue(text, forKey: "text") - - return try dict.makeJSON() + func makeJSONValue() -> JSON.Value { + let dict = [ + "symbol": symbol.makeJSONValue(), + "text": text.makeJSONValue(), + ] + return .object(dict) } } extension ABI.EncodedMessage.Symbol: JSON.Serializable {} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift index 43945e3a0..5e866abab 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift @@ -35,14 +35,13 @@ extension ABI.EncodedSourceLocation: Decodable { // MARK: - JSON.Serializable extension ABI.EncodedSourceLocation: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() - - try dict.updateValue(sourceLocation._filePath, forKey: "_filePath") - try dict.updateValue(sourceLocation.fileID, forKey: "fileID") - try dict.updateValue(sourceLocation.line, forKey: "line") - try dict.updateValue(sourceLocation.column, forKey: "column") - - return try dict.makeJSON() + func makeJSONValue() -> JSON.Value { + let dict = [ + "_filePath": sourceLocation._filePath.makeJSONValue(), + "fileID": sourceLocation.fileID.makeJSONValue(), + "line": sourceLocation.line.makeJSONValue(), + "column": sourceLocation.column.makeJSONValue(), + ] + return .object(dict) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift index 20f333c73..c40a6c2fa 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift @@ -140,44 +140,44 @@ extension ABI.EncodedTestCase: Decodable {} // MARK: - JSON.Serializable extension ABI.EncodedTest: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() + func makeJSONValue() -> JSON.Value { + var dict = [ + "kind": kind.makeJSONValue(), + "name": name.makeJSONValue(), + "sourceLocation": sourceLocation.makeJSONValue(), + "id": id.makeJSONValue(), + ] - try dict.updateValue(kind, forKey: "kind") - try dict.updateValue(name, forKey: "name") if let displayName { - try dict.updateValue(displayName, forKey: "displayName") + dict["displayName"] = displayName.makeJSONValue() } - try dict.updateValue(sourceLocation, forKey: "sourceLocation") - try dict.updateValue(id, forKey: "id") if let _testCases { - try dict.updateValue(_testCases, forKey: "_testCases") + dict["_testCases"] = _testCases.makeJSONValue() } if let isParameterized { - try dict.updateValue(isParameterized, forKey: "isParameterized") + dict["isParameterized"] = isParameterized.makeJSONValue() } if let _tags { - try dict.updateValue(_tags, forKey: "_tags") + dict["_tags"] = _tags.makeJSONValue() } - return try dict.makeJSON() + return .object(dict) } } extension ABI.EncodedTest.Kind: JSON.Serializable {} extension ABI.EncodedTest.ID: JSON.Serializable { - func makeJSON() throws -> some Collection { - try stringValue.makeJSON() + func makeJSONValue() -> JSON.Value { + stringValue.makeJSONValue() } } extension ABI.EncodedTestCase: JSON.Serializable { - func makeJSON() throws -> some Collection { - var dict = JSON.HeterogenousDictionary() - - try dict.updateValue(id, forKey: "id") - try dict.updateValue(displayName, forKey: "displayName") - - return try dict.makeJSON() + func makeJSONValue() -> JSON.Value { + let dict = [ + "id": id.makeJSONValue(), + "displayName": displayName.makeJSONValue(), + ] + return .object(dict) } } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 10a57513c..6b207d85a 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -81,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 136e085ab..639e9acf2 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -47,8 +47,12 @@ public struct ExitTest: Sendable, ~Copyable { self._hi = uuid.1 } - func makeJSON() throws -> some Collection { - try [_lo.makeJSON(), _hi.makeJSON()].joined() + func makeJSONValue() -> JSON.Value { + let dict = [ + "_lo": _lo.makeJSONValue(), + "_hi": _hi.makeJSONValue() + ] + return .object(dict) } } @@ -751,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() } } diff --git a/Sources/Testing/Support/JSON.Serializable.swift b/Sources/Testing/Support/JSON.Serializable.swift index 618ab7dd3..b30ae83c5 100644 --- a/Sources/Testing/Support/JSON.Serializable.swift +++ b/Sources/Testing/Support/JSON.Serializable.swift @@ -9,164 +9,100 @@ // extension JSON { + /// A protocol describing a value that can be serialized as JSON. protocol Serializable { - /// A type representing the result of by ``makeJSON()``. - associatedtype JSONBytes: Collection where JSONBytes.Element == UInt8 - - /// Serialize this value as JSON. - /// - /// - Returns: The sequence of bytes representing this value as JSON. + /// Serialize this instance as a JSON value. /// - /// - Throws: Any error that prevented serializing this value. - func makeJSON() throws -> JSONBytes + /// - Returns: A JSON value representing this instance. + func makeJSONValue() -> Value } } -extension JSON.Serializable { - /// Write the JSON representation of this value to the given file handle. - /// - /// - Parameters: - /// - file: The file to write to. A trailing newline is not written. - /// - flushAfterward: Whether or not to flush the file (with `fflush()`) - /// after writing. If `true`, `fflush()` is called even if an error - /// occurred while writing. - /// - /// - Throws: Any error that occurred while writing `bytes`. If an error - /// occurs while flushing the file, it is not thrown. - func writeJSON(to file: borrowing FileHandle, flushAfterward: Bool = true) throws { - try file.write(makeJSON(), flushAfterward: flushAfterward) - } -} - -// MARK: - Arbitrary bytes - -extension JSON { - struct Verbatim: JSON.Serializable where S: Collection, S.Element == UInt8 { - private var _bytes: S - - init(_ bytes: S) { - _bytes = bytes - } - - func makeJSON() throws -> S { - _bytes - } +extension JSON.Value: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + self } } // MARK: - Scalars extension Bool: JSON.Serializable { - func makeJSON() throws -> UnsafeBufferPointer { - let stringValue: StaticString = self ? "true" : "false" - return UnsafeBufferPointer(start: stringValue.utf8Start, count: stringValue.utf8CodeUnitCount) + func makeJSONValue() -> JSON.Value { + .bool(self) } } -extension Numeric where Self: CustomStringConvertible & JSON.Serializable { - func makeJSON() throws -> String.UTF8View { - String(describing: self).utf8 +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 {} -extension Double: JSON.Serializable {} +@available(*, unavailable) +extension UInt128: JSON.Serializable {} +extension UInt: JSON.Serializable {} -extension String: JSON.Serializable { - func makeJSON() throws -> [UInt8] { - var result = [UInt8]() - - let scalars = self.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 += leadingZeroes - result += hexValue.utf8 - case #"""#, #"\"#: - result += #"\\#(scalar)"#.utf8 - default: - result += scalar.utf8 - } - } - } +extension Double: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + .double(self) + } +} - return result +extension String: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + .string(self) } } // MARK: - Arrays extension Array: JSON.Serializable where Element: JSON.Serializable { - func makeJSON() throws -> [UInt8] { - var result = [UInt8]() - - do { - result.append(UInt8(ascii: "[")) - defer { - result.append(UInt8(ascii: "]")) - } - - result += try self.lazy.map { element in - try element.makeJSON() - }.joined(separator: CollectionOfOne(UInt8(ascii: ","))) - } - - return result + func makeJSONValue() -> JSON.Value { + let arrayCopy = self.map { $0.makeJSONValue() } + return .array(arrayCopy) } } // MARK: - Dictionaries extension Dictionary: JSON.Serializable where Key == String, Value: JSON.Serializable { - func makeJSON() throws -> [UInt8] { - var result = [UInt8]() - - do { - result.append(UInt8(ascii: #"{"#)) - defer { - result.append(UInt8(ascii: #"}"#)) - } - - result += try self.sorted { lhs, rhs in - lhs.key < rhs.key - }.map { key, value in - let serializedKey = try key.makeJSON() - let serializedValue = try value.makeJSON() - return serializedKey + CollectionOfOne(UInt8(ascii: ":")) + serializedValue - }.joined(separator: CollectionOfOne(UInt8(ascii: #","#))) - } - - return result + func makeJSONValue() -> JSON.Value { + let dictCopy = self.mapValues { $0.makeJSONValue() } + return .object(dictCopy) } } -extension JSON { - typealias HeterogenousDictionary = Dictionary> -} +// MARK: - Optional and RawRepresentable -extension JSON.HeterogenousDictionary { - @discardableResult - mutating func updateValue(_ value: some JSON.Serializable, forKey key: String) throws -> Value? { - let serializedValue = try JSON.Verbatim(Array(value.makeJSON())) - return updateValue(serializedValue as Value, forKey: key) +extension Optional: JSON.Serializable where Wrapped: JSON.Serializable { + func makeJSONValue() -> JSON.Value { + guard let value = self else { + return .null + } + return value.makeJSONValue() } } -// MARK: - RawRepresentable - extension RawRepresentable where Self: JSON.Serializable, RawValue: JSON.Serializable { - func makeJSON() throws -> RawValue.JSONBytes { - try rawValue.makeJSON() + 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..fa160ecb3 --- /dev/null +++ b/Sources/Testing/Support/JSON.Value.swift @@ -0,0 +1,213 @@ +// +// 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 { + /// The `null` constant (`nil` in Swift.) + case null + + /// 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 -> R) rethrows -> R { + // NOTE: StaticString.withUTF8Buffer does not rethrow. + try withExtendedLifetime(value) { + 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 -> R) rethrows -> R where V: Numeric { + var string = String(describing: value) + return try string.withUTF8 { utf8 in + try body(.init(utf8)) + } + } + + /// 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 -> R) rethrows -> R { + var result = [UInt8]() + + do { + result.append(UInt8(ascii: #"""#)) + defer { + result.append(UInt8(ascii: #"""#)) + } + + let scalars = value.unicodeScalars + result.reserveCapacity(scalars.underestimatedCount + 2) + + 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 += leadingZeroes + result += hexValue.utf8 + case #"""#, #"\"#: + result += #"\\#(scalar)"#.utf8 + default: + result += scalar.utf8 + } + } + } + + return try result.withUnsafeBytes(body) + } + + /// 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 -> R) rethrows -> 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.withUnsafeBytes(body) + } + + /// 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 -> R) rethrows -> 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.withUnsafeBytes(body) + } + + /// 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 -> R) rethrows -> R { + switch self { + case .null: + return try Self._withUnsafeBytesForKeyword("null", body) + 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 d08cdb03b..2cf6cc25b 100644 --- a/Sources/Testing/Support/JSON.swift +++ b/Sources/Testing/Support/JSON.swift @@ -24,10 +24,7 @@ enum JSON { /// /// - Throws: Whatever is thrown by `body` or by the encoding process. static func withEncoding(of value: J, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where J: JSON.Serializable { - let json = try value.makeJSON() - return try json.withContiguousStorageIfAvailable { json in - try body(.init(json)) - } ?? Array(json).withUnsafeBytes { json in + try value.makeJSONValue().withUnsafeBytes { json in try body(json) } } From 1e9106ed40fb7ee5a31a65f29e9b23b2e8a7846d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 14 Mar 2025 00:01:40 -0400 Subject: [PATCH 3/3] Adopt typed throws and add a bit of code coverage --- .../Testing/ABI/ABI.Record+Streaming.swift | 4 +- Sources/Testing/ABI/ABI.Record.swift | 2 +- .../ABI/Encoded/ABI.EncodedAttachment.swift | 2 +- .../ABI/Encoded/ABI.EncodedBacktrace.swift | 2 +- .../ABI/Encoded/ABI.EncodedError.swift | 2 +- .../ABI/Encoded/ABI.EncodedEvent.swift | 2 +- .../ABI/Encoded/ABI.EncodedInstant.swift | 2 +- .../ABI/Encoded/ABI.EncodedIssue.swift | 2 +- .../ABI/Encoded/ABI.EncodedMessage.swift | 2 +- .../Encoded/ABI.EncodedSourceLocation.swift | 2 +- .../Testing/ABI/Encoded/ABI.EncodedTest.swift | 4 +- Sources/Testing/ExitTests/ExitTest.swift | 4 +- .../Testing/Support/JSON.Serializable.swift | 10 ++-- Sources/Testing/Support/JSON.Value.swift | 53 +++++++++++++------ Sources/Testing/Support/JSON.swift | 4 +- Tests/TestingTests/MiscellaneousTests.swift | 9 ++++ .../TestSupport/TestingAdditions.swift | 19 ++++++- 17 files changed, 88 insertions(+), 37 deletions(-) diff --git a/Sources/Testing/ABI/ABI.Record+Streaming.swift b/Sources/Testing/ABI/ABI.Record+Streaming.swift index 76044aa90..4df0799da 100644 --- a/Sources/Testing/ABI/ABI.Record+Streaming.swift +++ b/Sources/Testing/ABI/ABI.Record+Streaming.swift @@ -55,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) } } } diff --git a/Sources/Testing/ABI/ABI.Record.swift b/Sources/Testing/ABI/ABI.Record.swift index a8d54b2ae..73379d3d5 100644 --- a/Sources/Testing/ABI/ABI.Record.swift +++ b/Sources/Testing/ABI/ABI.Record.swift @@ -96,6 +96,6 @@ extension ABI.Record: JSON.Serializable { dict["payload"] = event.makeJSONValue() } - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index 652f2bb01..c6e3c9692 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -39,6 +39,6 @@ extension ABI.EncodedAttachment: JSON.Serializable { if let path { dict["path"] = path.makeJSONValue() } - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift index 35c9fbc2c..a6798d9d7 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedBacktrace.swift @@ -60,6 +60,6 @@ extension Backtrace.SymbolicatedAddress: JSON.Serializable { dict["symbolName"] = symbolName.makeJSONValue() } - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift index aa46069e8..3c6d5e365 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedError.swift @@ -67,7 +67,7 @@ extension ABI.EncodedError: JSON.Serializable { "domain": domain.makeJSONValue(), "code": code.makeJSONValue() ] - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index b7fe74c62..0b6bb821e 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -135,7 +135,7 @@ extension ABI.EncodedEvent: JSON.Serializable { dict["_testCase"] = _testCase.makeJSONValue() } - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift index 14df49828..33e7c3e4a 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedInstant.swift @@ -47,6 +47,6 @@ extension ABI.EncodedInstant: JSON.Serializable { "absolute": absolute.makeJSONValue(), "since1970": since1970.makeJSONValue() ] - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 853aaa585..82bbd0b5a 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -88,7 +88,7 @@ extension ABI.EncodedIssue: JSON.Serializable { dict["_error"] = _error.makeJSONValue() } - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift index 1a1184a3b..9e4511ae3 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift @@ -87,7 +87,7 @@ extension ABI.EncodedMessage: JSON.Serializable { "symbol": symbol.makeJSONValue(), "text": text.makeJSONValue(), ] - return .object(dict) + 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 index 5e866abab..24b66e209 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedSourceLocation.swift @@ -42,6 +42,6 @@ extension ABI.EncodedSourceLocation: JSON.Serializable { "line": sourceLocation.line.makeJSONValue(), "column": sourceLocation.column.makeJSONValue(), ] - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift index c40a6c2fa..b86f4cf51 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift @@ -161,7 +161,7 @@ extension ABI.EncodedTest: JSON.Serializable { dict["_tags"] = _tags.makeJSONValue() } - return .object(dict) + return dict.makeJSONValue() } } extension ABI.EncodedTest.Kind: JSON.Serializable {} @@ -178,6 +178,6 @@ extension ABI.EncodedTestCase: JSON.Serializable { "id": id.makeJSONValue(), "displayName": displayName.makeJSONValue(), ] - return .object(dict) + return dict.makeJSONValue() } } diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 639e9acf2..bffc5d2f2 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -52,7 +52,7 @@ public struct ExitTest: Sendable, ~Copyable { "_lo": _lo.makeJSONValue(), "_hi": _hi.makeJSONValue() ] - return .object(dict) + return dict.makeJSONValue() } } @@ -626,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) } diff --git a/Sources/Testing/Support/JSON.Serializable.swift b/Sources/Testing/Support/JSON.Serializable.swift index b30ae83c5..71e42c168 100644 --- a/Sources/Testing/Support/JSON.Serializable.swift +++ b/Sources/Testing/Support/JSON.Serializable.swift @@ -76,8 +76,8 @@ extension String: JSON.Serializable { extension Array: JSON.Serializable where Element: JSON.Serializable { func makeJSONValue() -> JSON.Value { - let arrayCopy = self.map { $0.makeJSONValue() } - return .array(arrayCopy) + let selfCopy = self.map { $0.makeJSONValue() } + return .array(selfCopy) } } @@ -85,13 +85,14 @@ extension Array: JSON.Serializable where Element: JSON.Serializable { extension Dictionary: JSON.Serializable where Key == String, Value: JSON.Serializable { func makeJSONValue() -> JSON.Value { - let dictCopy = self.mapValues { $0.makeJSONValue() } - return .object(dictCopy) + 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 { @@ -100,6 +101,7 @@ extension Optional: JSON.Serializable where Wrapped: JSON.Serializable { return value.makeJSONValue() } } +#endif extension RawRepresentable where Self: JSON.Serializable, RawValue: JSON.Serializable { func makeJSONValue() -> JSON.Value { diff --git a/Sources/Testing/Support/JSON.Value.swift b/Sources/Testing/Support/JSON.Value.swift index fa160ecb3..9009684c4 100644 --- a/Sources/Testing/Support/JSON.Value.swift +++ b/Sources/Testing/Support/JSON.Value.swift @@ -12,8 +12,10 @@ 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) @@ -50,9 +52,9 @@ extension JSON.Value { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. - private static func _withUnsafeBytesForKeyword(_ value: StaticString, _ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + private static func _withUnsafeBytesForKeyword(_ value: StaticString, _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { // NOTE: StaticString.withUTF8Buffer does not rethrow. - try withExtendedLifetime(value) { + try withExtendedLifetime(value) { () throws(E) in let buffer = UnsafeBufferPointer(start: value.utf8Start, count: value.utf8CodeUnitCount) return try body(.init(buffer)) } @@ -68,11 +70,23 @@ extension JSON.Value { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. - private static func _withUnsafeBytesForNumericValue(_ value: V, _ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R where V: Numeric { + private static func _withUnsafeBytesForNumericValue(_ value: V, _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R where V: Numeric { var string = String(describing: value) - return try string.withUTF8 { utf8 in - try body(.init(utf8)) +#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. @@ -85,23 +99,24 @@ extension JSON.Value { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. - private static func _withUnsafeBytesForString(_ value: String, _ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + 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: #"""#)) } - let scalars = value.unicodeScalars - result.reserveCapacity(scalars.underestimatedCount + 2) - 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 #"""#, #"\"#: @@ -112,7 +127,9 @@ extension JSON.Value { } } - return try result.withUnsafeBytes(body) + return try result.withUnsafeBufferPointer { buffer throws(E) in + try body(.init(buffer)) + } } /// Call a function and pass it the JSON representation of an array. @@ -125,7 +142,7 @@ extension JSON.Value { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. - private static func _withUnsafeBytesForArray(_ value: [JSON.Value], _ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + private static func _withUnsafeBytesForArray(_ value: [JSON.Value], _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { var result = [UInt8]() do { @@ -141,7 +158,9 @@ extension JSON.Value { }.joined(separator: CollectionOfOne(UInt8(ascii: ","))) } - return try result.withUnsafeBytes(body) + 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 @@ -155,7 +174,7 @@ extension JSON.Value { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. - private static func _withUnsafeBytesForObject(_ value: [String: JSON.Value], _ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + private static func _withUnsafeBytesForObject(_ value: [String: JSON.Value], _ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { var result = [UInt8]() do { @@ -178,7 +197,9 @@ extension JSON.Value { }.joined(separator: CollectionOfOne(UInt8(ascii: ","))) } - return try result.withUnsafeBytes(body) + 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. @@ -190,10 +211,12 @@ extension JSON.Value { /// - Returns: Whatever is returned by `body`. /// /// - Throws: Whatever is thrown by `body`. - func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + 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): diff --git a/Sources/Testing/Support/JSON.swift b/Sources/Testing/Support/JSON.swift index 2cf6cc25b..0e1ce909a 100644 --- a/Sources/Testing/Support/JSON.swift +++ b/Sources/Testing/Support/JSON.swift @@ -23,8 +23,8 @@ enum JSON { /// - 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 -> R) throws -> R where J: JSON.Serializable { - try value.makeJSONValue().withUnsafeBytes { json in + 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) } } 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/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) } } }