Skip to content
Draft
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
29 changes: 13 additions & 16 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
91 changes: 86 additions & 5 deletions Sources/Testing/Support/FileHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(_ body: (SWT_FILEHandle) throws -> R) rethrows -> R {
borrowing func withUnsafeCFILEHandle<R>(_ body: (SWT_FILEHandle) throws -> R) rethrows -> R where R: ~Copyable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale behind the R: ~Copyable in the where clause here? Is it to prevent copying of anything returned by body as a performance optimisation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a remnant of an earlier iteration. I'll revert it.

try body(_fileHandle)
}

Expand All @@ -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<R>(_ body: (CInt?) throws -> R) rethrows -> R {
borrowing func withUnsafePOSIXFileDescriptor<R>(_ 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)
Expand Down Expand Up @@ -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<R>(_ body: (HANDLE?) throws -> R) rethrows -> R {
borrowing func withUnsafeWindowsHANDLE<R>(_ body: (HANDLE?) throws -> R) rethrows -> R where R: ~Copyable {
try withUnsafePOSIXFileDescriptor { fd in
guard let fd else {
return try body(nil)
Expand All @@ -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<R>(_ body: () throws -> R) rethrows -> R {
borrowing func withLock<R>(_ 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)
Expand Down Expand Up @@ -354,12 +354,86 @@ extension FileHandle {
let endIndex = buffer.index(buffer.startIndex, offsetBy: countRead)
result.append(contentsOf: buffer[..<endIndex])
}
} while 0 == feof(file)
} while !isAtEnd
}
}

return result
}

/// Read until a byte matching the given function is encountered.
///
/// - Parameters:
/// - isDelimiter: A function that determines if the given byte marks the
/// end of the read operation.
///
/// - Returns: A copy of the contents of the file handle starting at the
/// current offset and ending at either the first delimiter byte or the end
/// of the file (whichever comes first). The delimiter byte is not included
/// in the result.
///
/// - Throws: Any error that occurred while reading the file or that was
/// thrown by `isDelimiter`.
func read(until isDelimiter: (UInt8) throws -> Bool) throws -> [UInt8] {
var line = [UInt8]()
line.reserveCapacity(1024)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reasoning behind this capacity? Seems to me you could get much longer lines for a given delimiter, so is this just based off experience?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I wrote my question I forgot this is for lines of the JSON event stream; now the limit makes more sense to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Utterly arbitrary. Gotta pick something, no way to know what.


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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just verifying it really is a char if it's not EOF?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value we get back is an int or CInt and needs to be cast to the correct type. In practice, the if let can't fail, so this code is equivalent to let byteRead = UInt8(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
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions Tests/TestingTests/SwiftPMTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<V: ABI.Version>(fromPath filePath: String) throws -> [ABI.Record<V>] {
try FileHandle(forReadingAtPath: filePath).readToEnd()
.split(whereSeparator: \.isASCIINewline)
.map { line in
try line.withUnsafeBytes { line in
return try JSON.decode(ABI.Record<V>.self, from: line)
}
var result = [ABI.Record<V>]()
try FileHandle(forReadingAtPath: filePath).read(delimitingWhere: \.isASCIINewline) { line, _ in
try line.withUnsafeBytes { line in
try result.append(JSON.decode(ABI.Record<V>.self, from: line))
}
}
return result
}

@Suite("Swift Package Manager Integration Tests")
Expand Down