Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
//

private import TestingInternals
#if canImport(Foundation)
private import Foundation
#endif

#if !SWT_NO_EXIT_TESTS
/// A type describing an exit test.
Expand Down Expand Up @@ -223,10 +220,12 @@ extension ExitTest {
/// `__swiftPMEntryPoint()` function. The effect of using it under other
/// configurations is undefined.
static func findInEnvironmentForSwiftPM() -> Self? {
let sourceLocationString = Environment.variable(named: "SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION")
if let sourceLocationData = sourceLocationString?.data(using: .utf8),
let sourceLocation = try? JSONDecoder().decode(SourceLocation.self, from: sourceLocationData) {
return find(at: sourceLocation)
if var sourceLocationString = Environment.variable(named: "SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION") {
return try? sourceLocationString.withUTF8 { sourceLocationBuffer in
let sourceLocationBuffer = UnsafeRawBufferPointer(sourceLocationBuffer)
let sourceLocation = try JSON.decode(SourceLocation.self, from: sourceLocationBuffer)
return find(at: sourceLocation)
}
}
return nil
}
Expand Down Expand Up @@ -286,10 +285,12 @@ extension ExitTest {
#endif
// Insert a specific variable that tells the child process which exit test
// to run.
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION"] = try String(data: JSONEncoder().encode(exitTest.sourceLocation), encoding: .utf8)!
try JSON.withEncoding(of: exitTest.sourceLocation) { json in
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION"] = String(decoding: json, as: UTF8.self)
}

return try await _spawnAndWait(
forExecutableAtPath: childProcessExecutablePath,
forExecutableAtPath: childProcessExecutablePath,
arguments: childArguments,
environment: childEnvironment
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if canImport(Foundation)
private import Foundation
#endif

/// A protocol for customizing how arguments passed to parameterized tests are
/// encoded, which is used to match against when running specific arguments.
///
Expand Down Expand Up @@ -107,15 +103,7 @@ extension Test.Case.Argument.ID {
///
/// - Throws: Any error encountered during encoding.
private static func _encode(_ value: some Encodable, parameter: Test.Parameter) throws -> [UInt8] {
let encoder = JSONEncoder()

// Keys must be sorted to ensure deterministic matching of encoded data.
encoder.outputFormatting.insert(.sortedKeys)

// Set user info keys which clients may wish to use during encoding.
encoder.userInfo[._testParameterUserInfoKey] = parameter

return .init(try encoder.encode(value))
try JSON.withEncoding(of: value, userInfo: [._testParameterUserInfoKey: parameter], Array.init)
}
#endif
}
Expand Down
10 changes: 4 additions & 6 deletions Sources/Testing/Running/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
//

private import TestingInternals
#if canImport(Foundation)
private import Foundation
#endif

/// The entry point to the testing library used by Swift Package Manager.
///
Expand Down Expand Up @@ -321,17 +318,17 @@ private func _eventHandlerForStreamingEvents(toFileAtPath path: String) throws -
event: Event.Snapshot(snapshotting: event),
eventContext: Event.Context.Snapshot(snapshotting: context)
)
if var snapshotJSON = try? JSONEncoder().encode(snapshot) {
try? JSON.withEncoding(of: snapshot) { snapshotJSON in
func isASCIINewline(_ byte: UInt8) -> Bool {
byte == 10 || byte == 13
}

#if DEBUG
// We don't actually expect JSONEncoder() to produce output containing
// We don't actually expect the JSON encoder to produce output containing
// newline characters, so in debug builds we'll log a diagnostic message.
if snapshotJSON.contains(where: isASCIINewline) {
let message = Event.ConsoleOutputRecorder.warning(
"JSONEncoder() produced one or more newline characters while encoding an event snapshot with kind '\(event.kind)'. Please file a bug report at https://github.com/apple/swift-testing/issues/new",
"JSON encoder produced one or more newline characters while encoding an event snapshot with kind '\(event.kind)'. Please file a bug report at https://github.com/apple/swift-testing/issues/new",
options: .for(.stderr)
)
#if SWT_TARGET_OS_APPLE
Expand All @@ -343,6 +340,7 @@ private func _eventHandlerForStreamingEvents(toFileAtPath path: String) throws -
#endif

// Remove newline characters to conform to JSON lines specification.
var snapshotJSON = Array(snapshotJSON)
snapshotJSON.removeAll(where: isASCIINewline)
if !snapshotJSON.isEmpty {
try? file.withLock {
Expand Down
66 changes: 66 additions & 0 deletions Sources/Testing/Support/JSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// 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
//

#if 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<R>(of value: some Encodable, userInfo: [CodingUserInfoKey: Any] = [:], _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
#if canImport(Foundation)
let encoder = JSONEncoder()

// Keys must be sorted to ensure deterministic matching of encoded data.
encoder.outputFormatting.insert(.sortedKeys)

// Set user info keys that clients want to use during encoding.
encoder.userInfo.merge(userInfo, uniquingKeysWith: { _, rhs in rhs})

let data = try encoder.encode(value)
return try data.withUnsafeBytes(body)
#else
throw SystemError(description: "JSON encoding requires Foundation which is not available in this environment.")
#endif
}

/// Decode a value from JSON data.
///
/// - Parameters:
/// - type: The type of value to decode.
/// - jsonRepresentation: The JSON encoding of the value to decode.
///
/// - Returns: An instance of `T` decoded from `jsonRepresentation`.
///
/// - Throws: Whatever is thrown by the decoding process.
static func decode<T>(_ type: T.Type, from jsonRepresentation: UnsafeRawBufferPointer) throws -> T where T: Decodable {
#if canImport(Foundation)
try withExtendedLifetime(jsonRepresentation) {
let data = Data(
bytesNoCopy: .init(mutating: jsonRepresentation.baseAddress!),
count: jsonRepresentation.count,
deallocator: .none
)
return try JSONDecoder().decode(type, from: data)
}
#else
throw SystemError(description: "JSON decoding requires Foundation which is not available in this environment.")
#endif
}
}
6 changes: 4 additions & 2 deletions Sources/Testing/Traits/Tags/Tag.Color+Loading.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ func loadTagColors(fromFileInDirectoryAtPath swiftTestingDirectoryPath: String =
// nil is a valid decoded color value (representing "no color") that we can
// use for merging tag color data from multiple sources, but it is not valid
// as an actual tag color, so we have a step here that filters it.
return try JSONDecoder().decode([Tag: Tag.Color?].self, from: tagColorsData)
.compactMapValues { $0 }
return try tagColorsData.withUnsafeBytes { tagColorsData in
try JSON.decode([Tag: Tag.Color?].self, from: tagColorsData)
.compactMapValues { $0 }
}
}
#endif
8 changes: 8 additions & 0 deletions Sources/TestingInternals/include/Includes.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
#include <unistd.h>
#endif

#if __has_include(<sys/fcntl.h>)
#include <sys/fcntl.h>
#endif

#if __has_include(<sys/stat.h>)
#include <sys/stat.h>
#endif
Expand Down Expand Up @@ -75,6 +79,10 @@
#include <limits.h>
#endif

#if __has_include(<spawn.h>)
#include <spawn.h>
#endif

#if __has_include(<crt_externs.h>)
#include <crt_externs.h>
#endif
Expand Down
6 changes: 1 addition & 5 deletions Tests/TestingTests/BacktraceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
//

@testable @_spi(ForToolsIntegrationOnly) import Testing
#if canImport(Foundation)
import Foundation
#endif

struct BacktracedError: Error {}

Expand Down Expand Up @@ -51,8 +48,7 @@ struct BacktraceTests {
@Test("Encoding/decoding")
func encodingAndDecoding() throws {
let original = Backtrace.current()
let data = try JSONEncoder().encode(original)
let copy = try JSONDecoder().decode(Backtrace.self, from: data)
let copy = try JSON.encodeAndDecode(original)
#expect(original == copy)
}
#endif
Expand Down
7 changes: 1 addition & 6 deletions Tests/TestingTests/ClockTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if canImport(Foundation)
import Foundation
#endif
@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
private import TestingInternals

Expand Down Expand Up @@ -129,9 +126,7 @@ struct ClockTests {
func codable() async throws {
let now = Test.Clock.Instant()
let instant = now.advanced(by: .nanoseconds(100))
let decoded = try JSONDecoder().decode(Test.Clock.Instant.self,
from: JSONEncoder().encode(instant))

let decoded = try JSON.encodeAndDecode(instant)
#expect(instant == decoded)
#expect(instant != now)
}
Expand Down
9 changes: 2 additions & 7 deletions Tests/TestingTests/EventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if canImport(Foundation)
import Foundation
#endif
@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
private import TestingInternals

Expand Down Expand Up @@ -62,8 +59,7 @@ struct EventTests {
let testCaseID = Test.Case.ID(argumentIDs: nil)
let event = Event(kind, testID: testID, testCaseID: testCaseID, instant: .now)
let eventSnapshot = Event.Snapshot(snapshotting: event)
let encoded = try JSONEncoder().encode(eventSnapshot)
let decoded = try JSONDecoder().decode(Event.Snapshot.self, from: encoded)
let decoded = try JSON.encodeAndDecode(eventSnapshot)

#expect(String(describing: decoded) == String(describing: eventSnapshot))
}
Expand All @@ -73,8 +69,7 @@ struct EventTests {
let eventContext = Event.Context()
let snapshot = Event.Context.Snapshot(snapshotting: eventContext)

let encoded = try JSONEncoder().encode(snapshot)
let decoded = try JSONDecoder().decode(Event.Context.Snapshot.self, from: encoded)
let decoded = try JSON.encodeAndDecode(snapshot)

#expect(String(describing: decoded.test) == String(describing: eventContext.test.map(Test.Snapshot.init(snapshotting:))))
#expect(String(describing: decoded.testCase) == String(describing: eventContext.testCase.map(Test.Case.Snapshot.init(snapshotting:))))
Expand Down
4 changes: 1 addition & 3 deletions Tests/TestingTests/IssueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
private import TestingInternals
import Foundation

#if canImport(XCTest)
import XCTest
Expand Down Expand Up @@ -1408,8 +1407,7 @@ struct IssueCodingTests {
comments: ["Comment"],
sourceContext: SourceContext(backtrace: Backtrace.current(), sourceLocation: SourceLocation()))
let issueSnapshot = Issue.Snapshot(snapshotting: issue)
let encoded = try JSONEncoder().encode(issueSnapshot)
let decoded = try JSONDecoder().decode(Issue.Snapshot.self, from: encoded)
let decoded = try JSON.encodeAndDecode(issueSnapshot)

#expect(String(describing: decoded) == String(describing: issueSnapshot))
}
Expand Down
6 changes: 1 addition & 5 deletions Tests/TestingTests/Runner.Plan.SnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@

@testable @_spi(ForToolsIntegrationOnly) import Testing

#if canImport(Foundation)
import Foundation
#endif

@Suite("Runner.Plan.Snapshot tests")
struct Runner_Plan_SnapshotTests {
#if canImport(Foundation)
Expand All @@ -26,7 +22,7 @@ struct Runner_Plan_SnapshotTests {

let plan = await Runner.Plan(configuration: configuration)
let snapshot = Runner.Plan.Snapshot(snapshotting: plan)
let decoded = try JSONDecoder().decode(Runner.Plan.Snapshot.self, from: JSONEncoder().encode(snapshot))
let decoded = try JSON.encodeAndDecode(snapshot)

try #require(decoded.steps.count == snapshot.steps.count)

Expand Down
6 changes: 5 additions & 1 deletion Tests/TestingTests/SwiftPMTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ struct SwiftPMTests {
func decodeEventStream(fromFileAt url: URL) throws -> [EventAndContextSnapshot] {
try Data(contentsOf: url, options: [.mappedIfSafe])
.split(separator: 10) // "\n"
.map { try JSONDecoder().decode(EventAndContextSnapshot.self, from: $0) }
.map { line in
try line.withUnsafeBytes { line in
try JSON.decode(EventAndContextSnapshot.self, from: line)
}
}
}

@Test("--experimental-event-stream-output argument (writes to a stream and can be read back)")
Expand Down
7 changes: 3 additions & 4 deletions Tests/TestingTests/Test.Case.Argument.IDTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
//

@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
#if canImport(Foundation)
import Foundation
#endif

@Suite("Test.Case.Argument.ID Tests")
struct Test_Case_Argument_IDTests {
Expand Down Expand Up @@ -41,7 +38,9 @@ struct Test_Case_Argument_IDTests {
let argument = try #require(testCase.arguments.first)
let argumentID = try #require(argument.id)
#if canImport(Foundation)
let decodedArgument = try JSONDecoder().decode(MyCustomTestArgument.self, from: Data(argumentID.bytes))
let decodedArgument = try argumentID.bytes.withUnsafeBufferPointer { argumentID in
try JSON.decode(MyCustomTestArgument.self, from: .init(argumentID))
}
#expect(decodedArgument == MyCustomTestArgument(x: 123, y: "abc"))
#endif
}
Expand Down
8 changes: 2 additions & 6 deletions Tests/TestingTests/Test.SnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

@_spi(ForToolsIntegrationOnly) import Testing

#if canImport(Foundation)
import Foundation
#endif
@_spi(ForToolsIntegrationOnly) @testable import Testing

@Suite("Test.Snapshot tests")
struct Test_SnapshotTests {
Expand All @@ -21,7 +17,7 @@ struct Test_SnapshotTests {
func codable() throws {
let test = try #require(Test.current)
let snapshot = Test.Snapshot(snapshotting: test)
let decoded = try JSONDecoder().decode(Test.Snapshot.self, from: JSONEncoder().encode(snapshot))
let decoded = try JSON.encodeAndDecode(snapshot)

#expect(decoded.id == snapshot.id)
#expect(decoded.name == snapshot.name)
Expand Down
16 changes: 16 additions & 0 deletions Tests/TestingTests/TestSupport/TestingAdditions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,19 @@ extension Configuration {
/// whose output could make it hard to read "real" output from the testing
/// library.
let testsWithSignificantIOAreEnabled = Environment.flag(named: "SWT_ENABLE_TESTS_WITH_SIGNIFICANT_IO") == true

extension JSON {
/// 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<T>(_ value: T) throws -> T where T: Codable {
try JSON.withEncoding(of: value) { data in
try JSON.decode(T.self, from: data)
}
}
}
Loading