@@ -34,6 +34,41 @@ public struct ExitTest: Sendable, ~Copyable {
34
34
/// The body closure of the exit test.
35
35
fileprivate var body : @Sendable ( ) async throws -> Void = { }
36
36
37
+ /// Storage for ``observedValues``.
38
+ ///
39
+ /// Key paths are not sendable because the properties they refer to may or may
40
+ /// not be, so this property needs to be `nonisolated(unsafe)`. It is safe to
41
+ /// use it in this fashion because `ExitTestArtifacts` is sendable.
42
+ fileprivate nonisolated ( unsafe) var _observedValues = [ PartialKeyPath < ExitTestArtifacts > ] ( )
43
+
44
+ /// Key paths representing results from within this exit test that should be
45
+ /// observed and returned to the caller.
46
+ ///
47
+ /// The testing library sets this property to match what was passed by the
48
+ /// developer to the `#expect(exitsWith:)` or `#require(exitsWith:)` macro.
49
+ /// If you are implementing an exit test handler, you can check the value of
50
+ /// this property to determine what information you need to preserve from your
51
+ /// child process.
52
+ ///
53
+ /// The value of this property always includes ``Result/exitCondition`` even
54
+ /// if the test author does not specify it.
55
+ ///
56
+ /// Within a child process running an exit test, the value of this property is
57
+ /// otherwise unspecified.
58
+ @_spi ( ForToolsIntegrationOnly)
59
+ public var observedValues : [ PartialKeyPath < ExitTestArtifacts > ] {
60
+ get {
61
+ var result = _observedValues
62
+ if !result. contains ( \. exitCondition) { // O(n), but n <= 3 (no Set needed)
63
+ result. append ( \. exitCondition)
64
+ }
65
+ return result
66
+ }
67
+ set {
68
+ _observedValues = newValue
69
+ }
70
+ }
71
+
37
72
/// The source location of the exit test.
38
73
///
39
74
/// The source location is unique to each exit test and is consistent between
@@ -184,6 +219,9 @@ extension ExitTest {
184
219
///
185
220
/// - Parameters:
186
221
/// - expectedExitCondition: The expected exit condition.
222
+ /// - observedValues: An array of key paths representing results from within
223
+ /// the exit test that should be observed and returned by this macro. The
224
+ /// ``ExitTestArtifacts/exitCondition`` property is always returned.
187
225
/// - expression: The expression, corresponding to `condition`, that is being
188
226
/// evaluated (if available at compile time.)
189
227
/// - comments: An array of comments describing the expectation. This array
@@ -199,19 +237,21 @@ extension ExitTest {
199
237
/// convention.
200
238
func callExitTest(
201
239
exitsWith expectedExitCondition: ExitCondition ,
240
+ observing observedValues: [ PartialKeyPath < ExitTestArtifacts > ] ,
202
241
expression: __Expression ,
203
242
comments: @autoclosure ( ) -> [ Comment ] ,
204
243
isRequired: Bool ,
205
244
isolation: isolated ( any Actor ) ? = #isolation,
206
245
sourceLocation: SourceLocation
207
- ) async -> Result < ExitTestArtifacts , any Error > {
246
+ ) async -> Result < ExitTestArtifacts ? , any Error > {
208
247
guard let configuration = Configuration . current ?? Configuration . all. first else {
209
248
preconditionFailure ( " A test must be running on the current task to use #expect(exitsWith:). " )
210
249
}
211
250
212
251
var result : ExitTestArtifacts
213
252
do {
214
- let exitTest = ExitTest ( expectedExitCondition: expectedExitCondition, sourceLocation: sourceLocation)
253
+ var exitTest = ExitTest ( expectedExitCondition: expectedExitCondition, sourceLocation: sourceLocation)
254
+ exitTest. observedValues = observedValues
215
255
result = try await configuration. exitTestHandler ( exitTest)
216
256
217
257
#if os(Windows)
@@ -276,11 +316,15 @@ extension ExitTest {
276
316
/// the exit test.
277
317
///
278
318
/// This handler is invoked when an exit test (i.e. a call to either
279
- /// ``expect(exitsWith:_:sourceLocation:performing:)`` or
280
- /// ``require(exitsWith:_:sourceLocation:performing:)``) is started. The
281
- /// handler is responsible for initializing a new child environment (typically
282
- /// a child process) and running the exit test identified by `sourceLocation`
283
- /// there. The exit test's body can be found using ``ExitTest/find(at:)``.
319
+ /// ``expect(exitsWith:observing:_:sourceLocation:performing:)`` or
320
+ /// ``require(exitsWith:observing:_:sourceLocation:performing:)``) is started.
321
+ /// The handler is responsible for initializing a new child environment
322
+ /// (typically a child process) and running the exit test identified by
323
+ /// `sourceLocation` there.
324
+ ///
325
+ /// In the child environment, you can find the exit test again by calling
326
+ /// ``ExitTest/find(at:)`` and can run it by calling
327
+ /// ``ExitTest/callAsFunction()``.
284
328
///
285
329
/// The parent environment should suspend until the results of the exit test
286
330
/// are available or the child environment is otherwise terminated. The parent
@@ -465,20 +509,43 @@ extension ExitTest {
465
509
childEnvironment [ " SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION " ] = String ( decoding: json, as: UTF8 . self)
466
510
}
467
511
468
- return try await withThrowingTaskGroup ( of: ExitTestArtifacts ? . self) { taskGroup in
512
+ typealias ResultUpdater = @Sendable ( inout ExitTestArtifacts ) -> Void
513
+ return try await withThrowingTaskGroup ( of: ResultUpdater ? . self) { taskGroup in
514
+ // Set up stdout and stderr streams. By POSIX convention, stdin/stdout
515
+ // are line-buffered by default and stderr is unbuffered by default.
516
+ // SEE: https://en.cppreference.com/w/cpp/io/c/std_streams
517
+ var stdoutReadEnd : FileHandle ?
518
+ var stdoutWriteEnd : FileHandle ?
519
+ if exitTest. _observedValues. contains ( \. standardOutputContent) {
520
+ try FileHandle . makePipe ( readEnd: & stdoutReadEnd, writeEnd: & stdoutWriteEnd)
521
+ stdoutWriteEnd? . withUnsafeCFILEHandle { stdout in
522
+ _ = setvbuf ( stdout, nil , _IOLBF, Int ( BUFSIZ) )
523
+ }
524
+ }
525
+ var stderrReadEnd : FileHandle ?
526
+ var stderrWriteEnd : FileHandle ?
527
+ if exitTest. _observedValues. contains ( \. standardErrorContent) {
528
+ try FileHandle . makePipe ( readEnd: & stderrReadEnd, writeEnd: & stderrWriteEnd)
529
+ stderrWriteEnd? . withUnsafeCFILEHandle { stderr in
530
+ _ = setvbuf ( stderr, nil , _IONBF, Int ( BUFSIZ) )
531
+ }
532
+ }
533
+
469
534
// Create a "back channel" pipe to handle events from the child process.
470
- let backChannel = try FileHandle . Pipe ( )
535
+ var backChannelReadEnd : FileHandle !
536
+ var backChannelWriteEnd : FileHandle !
537
+ try FileHandle . makePipe ( readEnd: & backChannelReadEnd, writeEnd: & backChannelWriteEnd)
471
538
472
539
// Let the child process know how to find the back channel by setting a
473
540
// known environment variable to the corresponding file descriptor
474
541
// (HANDLE on Windows.)
475
542
var backChannelEnvironmentVariable : String ?
476
543
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
477
- backChannelEnvironmentVariable = backChannel . writeEnd . withUnsafePOSIXFileDescriptor { fd in
544
+ backChannelEnvironmentVariable = backChannelWriteEnd . withUnsafePOSIXFileDescriptor { fd in
478
545
fd. map ( String . init ( describing: ) )
479
546
}
480
547
#elseif os(Windows)
481
- backChannelEnvironmentVariable = backChannel . writeEnd . withUnsafeWindowsHANDLE { handle in
548
+ backChannelEnvironmentVariable = backChannelWriteEnd . withUnsafeWindowsHANDLE { handle in
482
549
handle. flatMap { String ( describing: UInt ( bitPattern: $0) ) }
483
550
}
484
551
#else
@@ -489,32 +556,55 @@ extension ExitTest {
489
556
}
490
557
491
558
// Spawn the child process.
492
- let processID = try withUnsafePointer ( to: backChannel . writeEnd ) { writeEnd in
559
+ let processID = try withUnsafePointer ( to: backChannelWriteEnd ) { backChannelWriteEnd in
493
560
try spawnExecutable (
494
561
atPath: childProcessExecutablePath,
495
562
arguments: childArguments,
496
563
environment: childEnvironment,
497
- additionalFileHandles: . init( start: writeEnd, count: 1 )
564
+ standardOutput: stdoutWriteEnd,
565
+ standardError: stderrWriteEnd,
566
+ additionalFileHandles: [ backChannelWriteEnd]
498
567
)
499
568
}
500
569
501
570
// Await termination of the child process.
502
571
taskGroup. addTask {
503
572
let exitCondition = try await wait ( for: processID)
504
- return ExitTestArtifacts ( exitCondition: exitCondition)
573
+ return { $0. exitCondition = exitCondition }
574
+ }
575
+
576
+ // Read back the stdout and stderr streams.
577
+ if let stdoutReadEnd {
578
+ stdoutWriteEnd? . close ( )
579
+ taskGroup. addTask {
580
+ let standardOutputContent = try stdoutReadEnd. readToEnd ( )
581
+ return { $0. standardOutputContent = standardOutputContent }
582
+ }
583
+ }
584
+ if let stderrReadEnd {
585
+ stderrWriteEnd? . close ( )
586
+ taskGroup. addTask {
587
+ let standardErrorContent = try stderrReadEnd. readToEnd ( )
588
+ return { $0. standardErrorContent = standardErrorContent }
589
+ }
505
590
}
506
591
507
592
// Read back all data written to the back channel by the child process
508
593
// and process it as a (minimal) event stream.
509
- let readEnd = backChannel . closeWriteEnd ( )
594
+ backChannelWriteEnd . close ( )
510
595
taskGroup. addTask {
511
- Self . _processRecords ( fromBackChannel: readEnd )
596
+ Self . _processRecords ( fromBackChannel: backChannelReadEnd )
512
597
return nil
513
598
}
514
599
515
- // This is a roundabout way of saying "and return the exit condition
516
- // yielded by wait(for:)".
517
- return try await taskGroup. compactMap { $0 } . first { _ in true } !
600
+ // Collate the various bits of the result. The exit condition .failure
601
+ // here is just a placeholder and will be replaced by the result of one
602
+ // of the tasks above.
603
+ var result = ExitTestArtifacts ( exitCondition: . failure)
604
+ for try await update in taskGroup {
605
+ update ? ( & result)
606
+ }
607
+ return result
518
608
}
519
609
}
520
610
}
0 commit comments