Skip to content

Commit d2a238d

Browse files
authored
Emit "barriers" into the stdout/stderr streams of an exit test. (#1049)
This PR causes Swift Testing to write "barriers" (known sequences of bytes) to `stdout` and `stderr` in the child process created by an exit test. Then, in the parent, these values are used to splice off any leading or trailing output that wasn't generated by the exit test's body (such as content generated by the host process, XCTest/Xcode, etc.) This reduces the amount of extraneous data reported back to the exit test's parent process. Thanks to @briancroom for the suggestion. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 0c20a72 commit d2a238d

File tree

2 files changed

+78
-2
lines changed

2 files changed

+78
-2
lines changed

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,73 @@ extension ABI {
545545

546546
@_spi(ForToolsIntegrationOnly)
547547
extension ExitTest {
548+
/// A barrier value to insert into the standard output and standard error
549+
/// streams immediately before and after the body of an exit test runs in
550+
/// order to distinguish output produced by the host process.
551+
///
552+
/// The value of this property was randomly generated. It could conceivably
553+
/// show up in actual output from an exit test, but the statistical likelihood
554+
/// of that happening is negligible.
555+
static var barrierValue: [UInt8] {
556+
[
557+
0x39, 0x74, 0x87, 0x6d, 0x96, 0xdd, 0xf6, 0x17,
558+
0x7f, 0x05, 0x61, 0x5d, 0x46, 0xeb, 0x37, 0x0c,
559+
0x90, 0x07, 0xca, 0xe5, 0xed, 0x0b, 0xc4, 0xc4,
560+
0x46, 0x36, 0xc5, 0xb8, 0x9c, 0xc7, 0x86, 0x57,
561+
]
562+
}
563+
564+
/// Remove the leading and trailing barrier values from the given array of
565+
/// bytes along.
566+
///
567+
/// - Parameters:
568+
/// - buffer: The buffer to trim.
569+
///
570+
/// - Returns: A copy of `buffer`. If a barrier value (equal to
571+
/// ``barrierValue``) is present in `buffer`, it and everything before it
572+
/// are trimmed from the beginning of the copy. If there is more than one
573+
/// barrier value present, the last one and everything after it are trimmed
574+
/// from the end of the copy. If no barrier value is present, `buffer` is
575+
/// returned verbatim.
576+
private static func _trimToBarrierValues(_ buffer: [UInt8]) -> [UInt8] {
577+
let barrierValue = barrierValue
578+
let firstBarrierByte = barrierValue[0]
579+
580+
// If the buffer is too small to contain the barrier value, exit early.
581+
guard buffer.count > barrierValue.count else {
582+
return buffer
583+
}
584+
585+
// Find all the indices where the first byte of the barrier is present.
586+
let splits = buffer.indices.filter { buffer[$0] == firstBarrierByte }
587+
588+
// Trim off the leading barrier value. If we didn't find any barrier values,
589+
// we do nothing.
590+
let leadingIndex = splits.first { buffer[$0...].starts(with: barrierValue) }
591+
guard let leadingIndex else {
592+
return buffer
593+
}
594+
var trimmedBuffer = buffer[leadingIndex...].dropFirst(barrierValue.count)
595+
596+
// If there's a trailing barrier value, trim it too. If it's at the same
597+
// index as the leading barrier value, that means only one barrier value
598+
// was present and we should assume it's the leading one.
599+
let trailingIndex = splits.last { buffer[$0...].starts(with: barrierValue) }
600+
if let trailingIndex, trailingIndex > leadingIndex {
601+
trimmedBuffer = trimmedBuffer[..<trailingIndex]
602+
}
603+
604+
return Array(trimmedBuffer)
605+
}
606+
607+
/// Write barrier values (equal to ``barrierValue``) to the standard output
608+
/// and standard error streams of the current process.
609+
private static func _writeBarrierValues() {
610+
let barrierValue = Self.barrierValue
611+
try? FileHandle.stdout.write(barrierValue)
612+
try? FileHandle.stderr.write(barrierValue)
613+
}
614+
548615
/// A handler that is invoked when an exit test starts.
549616
///
550617
/// - Parameters:
@@ -697,6 +764,13 @@ extension ExitTest {
697764
}
698765

699766
result.body = { [configuration, body = result.body] exitTest in
767+
Self._writeBarrierValues()
768+
defer {
769+
// We will generally not end up writing these values if the process
770+
// exits abnormally.
771+
Self._writeBarrierValues()
772+
}
773+
700774
try await Configuration.withCurrent(configuration) {
701775
try exitTest._decodeCapturedValuesForEntryPoint()
702776
try await body(&exitTest)
@@ -877,14 +951,14 @@ extension ExitTest {
877951
if let stdoutReadEnd {
878952
stdoutWriteEnd?.close()
879953
taskGroup.addTask {
880-
let standardOutputContent = try stdoutReadEnd.readToEnd()
954+
let standardOutputContent = try Self._trimToBarrierValues(stdoutReadEnd.readToEnd())
881955
return { $0.standardOutputContent = standardOutputContent }
882956
}
883957
}
884958
if let stderrReadEnd {
885959
stderrWriteEnd?.close()
886960
taskGroup.addTask {
887-
let standardErrorContent = try stderrReadEnd.readToEnd()
961+
let standardErrorContent = try Self._trimToBarrierValues(stderrReadEnd.readToEnd())
888962
return { $0.standardErrorContent = standardErrorContent }
889963
}
890964
}

Tests/TestingTests/ExitTestTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ private import _TestingInternals
350350
}
351351
#expect(result.exitStatus == .exitCode(EXIT_SUCCESS))
352352
#expect(result.standardOutputContent.contains("STANDARD OUTPUT".utf8))
353+
#expect(!result.standardOutputContent.contains(ExitTest.barrierValue))
353354
#expect(result.standardErrorContent.isEmpty)
354355

355356
result = try await #require(processExitsWith: .success, observing: [\.standardErrorContent]) {
@@ -360,6 +361,7 @@ private import _TestingInternals
360361
#expect(result.exitStatus == .exitCode(EXIT_SUCCESS))
361362
#expect(result.standardOutputContent.isEmpty)
362363
#expect(result.standardErrorContent.contains("STANDARD ERROR".utf8.reversed()))
364+
#expect(!result.standardErrorContent.contains(ExitTest.barrierValue))
363365
}
364366

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

0 commit comments

Comments
 (0)