Skip to content

Commit 9574d03

Browse files
committed
StdlibUnittest: ask the child process to terminate, and check its termination status
If a child process crashes outside of a test context, the parent process should signal test failure. This behavior is a sign of something bad happening in the child (for example, memory corruption), and should not go unnoticed.
1 parent 62a65b8 commit 9574d03

File tree

4 files changed

+140
-0
lines changed

4 files changed

+140
-0
lines changed

stdlib/private/StdlibUnittest/StdlibUnittest.swift.gyb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,12 @@ func _childProcess() {
645645

646646
while let line = _stdlib_getline() {
647647
let parts = line._split(separator: ";")
648+
649+
if parts[0] == _stdlibUnittestStreamPrefix {
650+
precondition(parts[1] == "shutdown")
651+
return
652+
}
653+
648654
let testSuiteName = parts[0]
649655
let testName = parts[1]
650656
var testParameter: Int?
@@ -865,6 +871,42 @@ struct _ParentProcess {
865871
capturedCrashStdout, capturedCrashStderr)
866872
}
867873

874+
internal mutating func _shutdownChild() -> (failed: Bool, Void) {
875+
if _pid == nil {
876+
// The child process is not running. Report that it didn't fail during
877+
// shutdown.
878+
return (failed: false, ())
879+
}
880+
print("\(_stdlibUnittestStreamPrefix);shutdown", to: &_childStdin)
881+
882+
var childCrashed = false
883+
884+
func processLine(_ line: String, isStdout: Bool) -> (done: Bool, Void) {
885+
if isStdout {
886+
print("stdout>>> \(line)")
887+
} else {
888+
if findSubstring(line, _crashedPrefix) != nil {
889+
childCrashed = true
890+
}
891+
print("stderr>>> \(line)")
892+
}
893+
return (done: false, ())
894+
}
895+
896+
_readFromChild(
897+
onStdoutLine: { processLine($0, isStdout: true) },
898+
onStderrLine: { processLine($0, isStdout: false) })
899+
900+
let status = _waitForChild()
901+
switch status {
902+
case .exit(0):
903+
return (failed: childCrashed, ())
904+
default:
905+
print("Abnormal child process termination: \(status).")
906+
return (failed: true, ())
907+
}
908+
}
909+
868910
internal enum _TestStatus {
869911
case skip([TestRunPredicate])
870912
case pass
@@ -1024,6 +1066,11 @@ struct _ParentProcess {
10241066
print("\(testSuite.name): All tests passed")
10251067
}
10261068
}
1069+
let (failed: failedOnShutdown, ()) = _shutdownChild()
1070+
if failedOnShutdown {
1071+
print("The child process failed during shutdown, aborting.")
1072+
_testSuiteFailedCallback()
1073+
}
10271074
}
10281075
}
10291076

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// RUN: %target-run-simple-swift 2>&1 | FileCheck %s
2+
// REQUIRES: executable_test
3+
4+
import StdlibUnittest
5+
#if os(Linux)
6+
import Glibc
7+
#else
8+
import Darwin
9+
#endif
10+
11+
_setTestSuiteFailedCallback() { print("abort()") }
12+
13+
//
14+
// Test that harness aborts when a test crashes if a child process crashes
15+
// after all tests have finished running.
16+
//
17+
18+
var TestSuiteChildCrashes = TestSuite("TestSuiteChildCrashes")
19+
20+
TestSuiteChildCrashes.test("passes") {
21+
atexit {
22+
fatalError("crash at exit")
23+
}
24+
}
25+
26+
// CHECK: [ RUN ] TestSuiteChildCrashes.passes
27+
// CHECK: [ OK ] TestSuiteChildCrashes.passes
28+
// CHECK: TestSuiteChildCrashes: All tests passed
29+
// CHECK: stderr>>> fatal error: crash at exit:
30+
// CHECK: stderr>>> CRASHED: SIG
31+
// CHECK: The child process failed during shutdown, aborting.
32+
// CHECK: abort()
33+
34+
runAllTests()
35+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// RUN: %target-run-simple-swift 2>&1 | FileCheck %s
2+
// REQUIRES: executable_test
3+
4+
import StdlibUnittest
5+
#if os(Linux)
6+
import Glibc
7+
#else
8+
import Darwin
9+
#endif
10+
11+
_setTestSuiteFailedCallback() { print("abort()") }
12+
13+
//
14+
// Test that harness aborts when a test crashes if a child process exits with a
15+
// non-zero code after all tests have finished running.
16+
//
17+
18+
var TestSuiteChildExits = TestSuite("TestSuiteChildExits")
19+
20+
TestSuiteChildExits.test("passes") {
21+
atexit {
22+
_exit(1)
23+
}
24+
}
25+
26+
// CHECK: [ RUN ] TestSuiteChildExits.passes
27+
// CHECK: [ OK ] TestSuiteChildExits.passes
28+
// CHECK: TestSuiteChildExits: All tests passed
29+
// CHECK: Abnormal child process termination: Exit(1).
30+
// CHECK: The child process failed during shutdown, aborting.
31+
// CHECK: abort()
32+
33+
runAllTests()
34+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// RUN: %target-run-simple-swift
2+
// REQUIRES: executable_test
3+
4+
import StdlibUnittest
5+
#if os(Linux)
6+
import Glibc
7+
#else
8+
import Darwin
9+
#endif
10+
11+
//
12+
// Test that harness correctly handles the case when there is no child process
13+
// to terminate during shutdown because it crashed during test execution.
14+
//
15+
16+
var TestSuiteChildCrashes = TestSuite("TestSuiteChildCrashes")
17+
18+
TestSuiteChildCrashes.test("passes") {
19+
expectCrashLater()
20+
fatalError("crash")
21+
}
22+
23+
runAllTests()
24+

0 commit comments

Comments
 (0)