From ccf5f3d2036acfc543c07fe373b63147904ac142 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 17 Sep 2024 09:56:37 -0400 Subject: [PATCH] Hoist JSON Lines logic from EntryPoint.swift. This PR moves the logic that implements JSON Lines support (i.e. the code that strips newlines from JSON generated by Foundation) out of EntryPoint.swift so that it can be used by other callers. This change was originally part of #697. I'm splitting it out into its own PR to make that one (both really) easier to read and understand. --- .../ABI/EntryPoints/ABIEntryPoint.swift | 2 +- .../Testing/ABI/EntryPoints/EntryPoint.swift | 64 ++++--------------- .../ABI/v0/ABIv0.Record+Streaming.swift | 45 ++++++++++++- .../Support/Additions/NumericAdditions.swift | 9 +++ Tests/TestingTests/SwiftPMTests.swift | 2 +- 5 files changed, 69 insertions(+), 53 deletions(-) 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)