Skip to content

Hoist JSON Lines logic from EntryPoint.swift. #701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 17, 2024
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
2 changes: 1 addition & 1 deletion Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
64 changes: 14 additions & 50 deletions Sources/Testing/ABI/EntryPoints/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
45 changes: 44 additions & 1 deletion Sources/Testing/ABI/v0/ABIv0.Record+Streaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions Sources/Testing/Support/Additions/NumericAdditions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
2 changes: 1 addition & 1 deletion Tests/TestingTests/SwiftPMTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down