Skip to content

Commit 0a50c17

Browse files
committed
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 93fd967 commit 0a50c17

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
@@ -530,6 +530,73 @@ extension ABI {
530530

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

684751
result.body = { [configuration, body = result.body] exitTest in
752+
Self._writeBarrierValues()
753+
defer {
754+
// We will generally not end up writing these values if the process
755+
// exits abnormally.
756+
Self._writeBarrierValues()
757+
}
758+
685759
try await Configuration.withCurrent(configuration) {
686760
try exitTest._decodeCapturedValuesForEntryPoint()
687761
try await body(&exitTest)
@@ -862,14 +936,14 @@ extension ExitTest {
862936
if let stdoutReadEnd {
863937
stdoutWriteEnd?.close()
864938
taskGroup.addTask {
865-
let standardOutputContent = try stdoutReadEnd.readToEnd()
939+
let standardOutputContent = try Self._trimToBarrierValues(stdoutReadEnd.readToEnd())
866940
return { $0.standardOutputContent = standardOutputContent }
867941
}
868942
}
869943
if let stderrReadEnd {
870944
stderrWriteEnd?.close()
871945
taskGroup.addTask {
872-
let standardErrorContent = try stderrReadEnd.readToEnd()
946+
let standardErrorContent = try Self._trimToBarrierValues(stderrReadEnd.readToEnd())
873947
return { $0.standardErrorContent = standardErrorContent }
874948
}
875949
}

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)