Skip to content

Emit "barriers" into the stdout/stderr streams of an exit test. #1049

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
78 changes: 76 additions & 2 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,73 @@ extension ABI {

@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
extension ExitTest {
/// A barrier value to insert into the standard output and standard error
/// streams immediately before and after the body of an exit test runs in
/// order to distinguish output produced by the host process.
///
/// The value of this property was randomly generated. It could conceivably
/// show up in actual output from an exit test, but the statistical likelihood
/// of that happening is negligible.
static var barrierValue: [UInt8] {
[
0x39, 0x74, 0x87, 0x6d, 0x96, 0xdd, 0xf6, 0x17,
0x7f, 0x05, 0x61, 0x5d, 0x46, 0xeb, 0x37, 0x0c,
0x90, 0x07, 0xca, 0xe5, 0xed, 0x0b, 0xc4, 0xc4,
0x46, 0x36, 0xc5, 0xb8, 0x9c, 0xc7, 0x86, 0x57,
]
}

/// Remove the leading and trailing barrier values from the given array of
/// bytes along.
///
/// - Parameters:
/// - buffer: The buffer to trim.
///
/// - Returns: A copy of `buffer`. If a barrier value (equal to
/// ``barrierValue``) is present in `buffer`, it and everything before it
/// are trimmed from the beginning of the copy. If there is more than one
/// barrier value present, the last one and everything after it are trimmed
/// from the end of the copy. If no barrier value is present, `buffer` is
/// returned verbatim.
private static func _trimToBarrierValues(_ buffer: [UInt8]) -> [UInt8] {
let barrierValue = barrierValue
let firstBarrierByte = barrierValue[0]

// If the buffer is too small to contain the barrier value, exit early.
guard buffer.count > barrierValue.count else {
return buffer
}

// Find all the indices where the first byte of the barrier is present.
let splits = buffer.indices.filter { buffer[$0] == firstBarrierByte }

// Trim off the leading barrier value. If we didn't find any barrier values,
// we do nothing.
let leadingIndex = splits.first { buffer[$0...].starts(with: barrierValue) }
guard let leadingIndex else {
return buffer
}
var trimmedBuffer = buffer[leadingIndex...].dropFirst(barrierValue.count)

// If there's a trailing barrier value, trim it too. If it's at the same
// index as the leading barrier value, that means only one barrier value
// was present and we should assume it's the leading one.
let trailingIndex = splits.last { buffer[$0...].starts(with: barrierValue) }
if let trailingIndex, trailingIndex > leadingIndex {
trimmedBuffer = trimmedBuffer[..<trailingIndex]
}

return Array(trimmedBuffer)
}

/// Write barrier values (equal to ``barrierValue``) to the standard output
/// and standard error streams of the current process.
private static func _writeBarrierValues() {
let barrierValue = Self.barrierValue
try? FileHandle.stdout.write(barrierValue)
try? FileHandle.stderr.write(barrierValue)
}

/// A handler that is invoked when an exit test starts.
///
/// - Parameters:
Expand Down Expand Up @@ -544,6 +611,13 @@ extension ExitTest {
}

result.body = { [configuration, body = result.body] in
Self._writeBarrierValues()
defer {
// We will generally not end up writing these values if the process
// exits abnormally.
Self._writeBarrierValues()
}

try await Configuration.withCurrent(configuration, perform: body)
}
return result
Expand Down Expand Up @@ -708,14 +782,14 @@ extension ExitTest {
if let stdoutReadEnd {
stdoutWriteEnd?.close()
taskGroup.addTask {
let standardOutputContent = try stdoutReadEnd.readToEnd()
let standardOutputContent = try Self._trimToBarrierValues(stdoutReadEnd.readToEnd())
return { $0.standardOutputContent = standardOutputContent }
}
}
if let stderrReadEnd {
stderrWriteEnd?.close()
taskGroup.addTask {
let standardErrorContent = try stderrReadEnd.readToEnd()
let standardErrorContent = try Self._trimToBarrierValues(stderrReadEnd.readToEnd())
return { $0.standardErrorContent = standardErrorContent }
}
}
Expand Down
2 changes: 2 additions & 0 deletions Tests/TestingTests/ExitTestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ private import _TestingInternals
}
#expect(result.statusAtExit == .exitCode(EXIT_SUCCESS))
#expect(result.standardOutputContent.contains("STANDARD OUTPUT".utf8))
#expect(!result.standardOutputContent.contains(ExitTest.barrierValue))
#expect(result.standardErrorContent.isEmpty)

result = try await #require(exitsWith: .success, observing: [\.standardErrorContent]) {
Expand All @@ -360,6 +361,7 @@ private import _TestingInternals
#expect(result.statusAtExit == .exitCode(EXIT_SUCCESS))
#expect(result.standardOutputContent.isEmpty)
#expect(result.standardErrorContent.contains("STANDARD ERROR".utf8.reversed()))
#expect(!result.standardErrorContent.contains(ExitTest.barrierValue))
}

@Test("Arguments to the macro are not captured during expansion (do not need to be literals/const)")
Expand Down