diff --git a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift index 2e950bb74..cc150740e 100644 --- a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift @@ -120,7 +120,7 @@ private func entryPoint( args?.eventStreamVersion = eventStreamVersionIfNil } - let eventHandler = try eventHandlerForStreamingEvents(version: args?.eventStreamVersion, forwardingTo: recordHandler) + let eventHandler = try eventHandlerForStreamingEvents(version: args?.eventStreamVersion, encodeAsJSONLines: false, forwardingTo: recordHandler) let exitCode = await entryPoint(passing: args, eventHandler: eventHandler) // To maintain compatibility with Xcode 16 Beta 1, suppress custom exit codes. diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 904333c59..725ef91ff 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -468,8 +468,11 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr // Event stream output (experimental) if let eventStreamOutputPath = args.eventStreamOutputPath { let file = try FileHandle(forWritingAtPath: eventStreamOutputPath) - let eventHandler = try eventHandlerForStreamingEvents(version: args.eventStreamVersion) { json in - try? _writeJSONLine(json, to: file) + let eventHandler = try eventHandlerForStreamingEvents(version: args.eventStreamVersion, encodeAsJSONLines: true) { json in + _ = try? file.withLock { + try file.write(json) + try file.write("\n") + } } configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in eventHandler(event, context) @@ -536,13 +539,20 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr /// /// - Parameters: /// - version: The ABI version to use. +/// - encodeAsJSONLines: Whether or not to ensure JSON passed to +/// `eventHandler` is encoded as JSON Lines (i.e. that it does not contain +/// extra newlines.) /// - eventHandler: The event handler to forward encoded events to. The /// encoding of events depends on `version`. /// /// - Returns: An event handler. /// /// - Throws: If `version` is not a supported ABI version. -func eventHandlerForStreamingEvents(version: Int?, forwardingTo eventHandler: @escaping @Sendable (UnsafeRawBufferPointer) -> Void) throws -> Event.Handler { +func eventHandlerForStreamingEvents( + version: Int?, + encodeAsJSONLines: Bool, + forwardingTo eventHandler: @escaping @Sendable (UnsafeRawBufferPointer) -> Void +) throws -> Event.Handler { switch version { #if !SWT_NO_SNAPSHOT_TYPES case -1: @@ -551,57 +561,11 @@ func eventHandlerForStreamingEvents(version: Int?, forwardingTo eventHandler: @e eventHandlerForStreamingEventSnapshots(to: eventHandler) #endif case nil, 0: - ABIv0.Record.eventHandler(forwardingTo: eventHandler) + ABIv0.Record.eventHandler(encodeAsJSONLines: encodeAsJSONLines, forwardingTo: eventHandler) case let .some(unsupportedVersion): throw _EntryPointError.invalidArgument("--event-stream-version", value: "\(unsupportedVersion)") } } - -/// Post-process encoded JSON and write it to a file. -/// -/// - Parameters: -/// - json: The JSON to write. -/// - file: The file to write to. -/// -/// - Throws: Whatever is thrown when writing to `file`. -private func _writeJSONLine(_ json: UnsafeRawBufferPointer, to file: borrowing FileHandle) throws { - func isASCIINewline(_ byte: UInt8) -> Bool { - byte == UInt8(ascii: "\r") || byte == UInt8(ascii: "\n") - } - - func write(_ json: UnsafeRawBufferPointer) throws { - try file.withLock { - try file.write(json) - try file.write("\n") - } - } - - // 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 _slowPath(json.contains(where: isASCIINewline)) { -#if DEBUG - let message = Event.ConsoleOutputRecorder.warning( - "JSON encoder produced one or more newline characters while encoding an event to JSON. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new", - options: .for(.stderr) - ) -#if SWT_TARGET_OS_APPLE - try? FileHandle.stderr.write(message) -#else - print(message) -#endif -#endif - - // Remove the newline characters to conform to JSON lines specification. - var json = Array(json) - json.removeAll(where: isASCIINewline) - try json.withUnsafeBytes { json in - try write(json) - } - } else { - // No newlines found, no need to copy the buffer. - try write(json) - } -} #endif // MARK: - Command-line interface options diff --git a/Sources/Testing/ABI/v0/ABIv0.Record+Streaming.swift b/Sources/Testing/ABI/v0/ABIv0.Record+Streaming.swift index 41922e801..3bb334742 100644 --- a/Sources/Testing/ABI/v0/ABIv0.Record+Streaming.swift +++ b/Sources/Testing/ABI/v0/ABIv0.Record+Streaming.swift @@ -10,10 +10,46 @@ #if canImport(Foundation) && (!SWT_NO_FILE_IO || !SWT_NO_ABI_ENTRY_POINT) extension ABIv0.Record { + /// Post-process encoded JSON and write it to a file. + /// + /// - Parameters: + /// - json: The JSON to write. + /// - file: The file to write to. + /// + /// - Throws: Whatever is thrown when writing to `file`. + private static func _asJSONLine(_ json: UnsafeRawBufferPointer, _ eventHandler: (_ recordJSON: UnsafeRawBufferPointer) throws -> Void) rethrows { + // 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 _slowPath(json.contains(where: \.isASCIINewline)) { + #if DEBUG + let message = Event.ConsoleOutputRecorder.warning( + "JSON encoder produced one or more newline characters while encoding an event to JSON. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new", + options: .for(.stderr) + ) + #if SWT_TARGET_OS_APPLE + try? FileHandle.stderr.write(message) + #else + print(message) + #endif + #endif + + // Remove the newline characters to conform to JSON lines specification. + var json = Array(json) + json.removeAll(where: \.isASCIINewline) + try json.withUnsafeBytes(eventHandler) + } else { + // No newlines found, no need to copy the buffer. + try eventHandler(json) + } + } + /// Create an event handler that encodes events as JSON and forwards them to /// an ABI-friendly event handler. /// /// - Parameters: + /// - encodeAsJSONLines: Whether or not to ensure JSON passed to + /// `eventHandler` is encoded as JSON Lines (i.e. that it does not contain + /// extra newlines.) /// - eventHandler: The event handler to forward events to. See /// ``ABIv0/EntryPoint-swift.typealias`` for more information. /// @@ -27,10 +63,17 @@ extension ABIv0.Record { /// performs additional postprocessing before writing JSON data to ensure it /// does not contain any newline characters. static func eventHandler( + encodeAsJSONLines: Bool, forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void ) -> Event.Handler { + // Encode as JSON Lines if requested. + var eventHandlerCopy = eventHandler + if encodeAsJSONLines { + eventHandlerCopy = { @Sendable in _asJSONLine($0, eventHandler) } + } + let humanReadableOutputRecorder = Event.HumanReadableOutputRecorder() - return { event, context in + return { [eventHandler = eventHandlerCopy] event, context in if case .testDiscovered = event.kind, let test = context.test { try? JSON.withEncoding(of: Self(encoding: test)) { testJSON in eventHandler(testJSON) diff --git a/Sources/Testing/Support/Additions/NumericAdditions.swift b/Sources/Testing/Support/Additions/NumericAdditions.swift index 56e36c20d..39495a7a2 100644 --- a/Sources/Testing/Support/Additions/NumericAdditions.swift +++ b/Sources/Testing/Support/Additions/NumericAdditions.swift @@ -25,3 +25,12 @@ extension Numeric { return "\(self) \(noun)s" } } + +// MARK: - + +extension UInt8 { + /// Whether or not this instance is an ASCII newline character (`\n` or `\r`). + var isASCIINewline: Bool { + self == UInt8(ascii: "\r") || self == UInt8(ascii: "\n") + } +} diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 51764fa1a..6b114efe6 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -228,7 +228,7 @@ struct SwiftPMTests { func decodeABIv0RecordStream(fromFileAtPath path: String) throws -> [ABIv0.Record] { try FileHandle(forReadingAtPath: path).readToEnd() - .split(separator: 10) // "\n" + .split(whereSeparator: \.isASCIINewline) .map { line in try line.withUnsafeBytes { line in try JSON.decode(ABIv0.Record.self, from: line)