diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index da4da4c91..50a59b48c 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -1001,27 +1001,21 @@ extension ExitTest { /// - backChannel: The file handle to read from. Reading continues until an /// error is encountered or the end of the file is reached. private static func _processRecords(fromBackChannel backChannel: borrowing FileHandle) { - let bytes: [UInt8] do { - bytes = try backChannel.readToEnd() + try backChannel.read(delimitingWhere: \.isASCIINewline) { recordJSON, _ in + if recordJSON.isEmpty { + return // skip empty lines + } + try recordJSON.withUnsafeBufferPointer { recordJSON in + try Self._processRecord(.init(recordJSON), fromBackChannel: backChannel) + } + } } catch { // NOTE: an error caught here indicates an I/O problem. // TODO: should we record these issues as systemic instead? Issue(for: error).record() return } - - for recordJSON in bytes.split(whereSeparator: \.isASCIINewline) where !recordJSON.isEmpty { - do { - try recordJSON.withUnsafeBufferPointer { recordJSON in - try Self._processRecord(.init(recordJSON), fromBackChannel: backChannel) - } - } catch { - // NOTE: an error caught here indicates a decoding problem. - // TODO: should we record these issues as systemic instead? - Issue(for: error).record() - } - } } /// Decode a line of JSON read from a back channel file handle and handle it @@ -1089,8 +1083,11 @@ extension ExitTest { guard let fileHandle = Self._makeFileHandle(forEnvironmentVariableNamed: "SWT_CAPTURED_VALUES", mode: "rb") else { return } - let capturedValuesJSON = try fileHandle.readToEnd() - let capturedValuesJSONLines = capturedValuesJSON.split(whereSeparator: \.isASCIINewline) + var capturedValuesJSONLines = [[UInt8]]() + capturedValuesJSONLines.reserveCapacity(capturedValues.count) + try fileHandle.read(delimitingWhere: \.isASCIINewline) { line, _ in + capturedValuesJSONLines.append(line) + } assert(capturedValues.count == capturedValuesJSONLines.count, "Expected to decode \(capturedValues.count) captured value(s) for the current exit test, but received \(capturedValuesJSONLines.count). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") // Walk the list of captured values' types, map them to their JSON blobs, diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index d038db101..bb5d0165e 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -211,7 +211,7 @@ struct FileHandle: ~Copyable, Sendable { /// /// Use this function when calling C I/O interfaces such as `fputs()` on the /// underlying C file handle. - borrowing func withUnsafeCFILEHandle(_ body: (SWT_FILEHandle) throws -> R) rethrows -> R { + borrowing func withUnsafeCFILEHandle(_ body: (SWT_FILEHandle) throws -> R) rethrows -> R where R: ~Copyable { try body(_fileHandle) } @@ -228,7 +228,7 @@ struct FileHandle: ~Copyable, Sendable { /// that require a file descriptor instead of the standard `FILE *` /// representation. If the file handle cannot be converted to a file /// descriptor, `nil` is passed to `body`. - borrowing func withUnsafePOSIXFileDescriptor(_ body: (CInt?) throws -> R) rethrows -> R { + borrowing func withUnsafePOSIXFileDescriptor(_ body: (CInt?) throws -> R) rethrows -> R where R: ~Copyable { try withUnsafeCFILEHandle { handle in #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) let fd = fileno(handle) @@ -260,7 +260,7 @@ struct FileHandle: ~Copyable, Sendable { /// that require the file's `HANDLE` representation instead of the standard /// `FILE *` representation. If the file handle cannot be converted to a /// Windows handle, `nil` is passed to `body`. - borrowing func withUnsafeWindowsHANDLE(_ body: (HANDLE?) throws -> R) rethrows -> R { + borrowing func withUnsafeWindowsHANDLE(_ body: (HANDLE?) throws -> R) rethrows -> R where R: ~Copyable { try withUnsafePOSIXFileDescriptor { fd in guard let fd else { return try body(nil) @@ -287,7 +287,7 @@ struct FileHandle: ~Copyable, Sendable { /// to the underlying file. It can be used when, for example, write operations /// are split across multiple calls but must not be interleaved with writes on /// other threads. - borrowing func withLock(_ body: () throws -> R) rethrows -> R { + borrowing func withLock(_ body: () throws -> R) rethrows -> R where R: ~Copyable { try withUnsafeCFILEHandle { handle in #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) flockfile(handle) @@ -354,12 +354,86 @@ extension FileHandle { let endIndex = buffer.index(buffer.startIndex, offsetBy: countRead) result.append(contentsOf: buffer[.. Bool) throws -> [UInt8] { + var line = [UInt8]() + line.reserveCapacity(1024) + + try withUnsafeCFILEHandle { file in + try withLock { + repeat { +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) + let byteRead = getc_unlocked(file) +#elseif os(Windows) + let byteRead = _fgetc_nolock(file) +#else + let byteRead = fgetc(file) +#endif + if byteRead == EOF { + if 0 != ferror(file) { + throw CError(rawValue: swt_errno()) + } + } else if let byteRead = UInt8(exactly: byteRead) { + if try isDelimiter(byteRead) { + break + } else { + line.append(byteRead) + } + } + } while !isAtEnd + } + } + + return line + } + + /// Read until the end of the file, yielding sequences of bytes read delimited + /// by bytes that match the given function. + /// + /// - Parameters: + /// - isDelimiter: A function that determines if the given byte marks the + /// end of the read operation. + /// - body: A function to call for each subsequence of bytes read. Set the + /// `stop` argument to `true` to exit the loop early. + /// + /// - Throws: Any error that occurred while reading the file or that was + /// thrown by `isDelimiter` or `body`. + /// + /// Use this function to, for example, read lines delimited by `"\n"` from the + /// file. + /// + /// This function does not produce a sequence because it would require + /// consuming the file handle and also because it would limit the ability of + /// the caller to handle I/O errors that occur while reading. + borrowing func read(delimitingWhere isDelimiter: (UInt8) throws -> Bool, _ body: ([UInt8], _ stop: inout Bool) throws -> Void) throws { + var stop = false + while !stop { + let bytesRead = try read(until: isDelimiter) + if bytesRead.isEmpty && isAtEnd { + break + } + try body(bytesRead, &stop) + } + } } // MARK: - Writing @@ -559,6 +633,13 @@ extension FileHandle { // MARK: - Attributes extension FileHandle { + /// Is the current cursor offset at the end of the file? + var isAtEnd: Bool { + withUnsafeCFILEHandle { file in + 0 != feof(file) + } + } + /// Is this file handle a TTY or PTY? var isTTY: Bool { #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 4668fbb25..6ad7b1d1f 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -23,13 +23,13 @@ private func configurationForEntryPoint(withArguments args: [String]) throws -> /// Reads event stream output from the provided file matching event stream /// version `V`. private func decodedEventStreamRecords(fromPath filePath: String) throws -> [ABI.Record] { - try FileHandle(forReadingAtPath: filePath).readToEnd() - .split(whereSeparator: \.isASCIINewline) - .map { line in - try line.withUnsafeBytes { line in - return try JSON.decode(ABI.Record.self, from: line) - } + var result = [ABI.Record]() + try FileHandle(forReadingAtPath: filePath).read(delimitingWhere: \.isASCIINewline) { line, _ in + try line.withUnsafeBytes { line in + try result.append(JSON.decode(ABI.Record.self, from: line)) } + } + return result } @Suite("Swift Package Manager Integration Tests")