Skip to content

Commit 34ee979

Browse files
committed
Merge pull request #2643 from apple/stdlib-unittest-fail-if-child-crashes-at-exit
[WIP] StdlibUnittest: fail if child crashes at exit
2 parents 312545b + 9574d03 commit 34ee979

File tree

6 files changed

+232
-77
lines changed

6 files changed

+232
-77
lines changed

stdlib/private/StdlibUnittest/StdlibUnittest.swift.gyb

Lines changed: 137 additions & 76 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?
@@ -670,7 +676,7 @@ func _childProcess() {
670676
}
671677

672678
struct _ParentProcess {
673-
internal var _pid: pid_t = -1
679+
internal var _pid: pid_t? = nil
674680
internal var _childStdin: _FDOutputStream = _FDOutputStream(fd: -1)
675681
internal var _childStdout: _FDInputStream = _FDInputStream(fd: -1)
676682
internal var _childStderr: _FDInputStream = _FDInputStream(fd: -1)
@@ -695,8 +701,8 @@ struct _ParentProcess {
695701
}
696702

697703
mutating func _waitForChild() -> ProcessTerminationStatus {
698-
let status = posixWaitpid(_pid)
699-
_pid = -1
704+
let status = posixWaitpid(_pid!)
705+
_pid = nil
700706
_childStdin.close()
701707
_childStdout.close()
702708
_childStderr.close()
@@ -706,6 +712,49 @@ struct _ParentProcess {
706712
return status
707713
}
708714

715+
internal mutating func _readFromChild(
716+
onStdoutLine: (String) -> (done: Bool, Void),
717+
onStderrLine: (String) -> (done: Bool, Void)
718+
) {
719+
var readfds = _stdlib_fd_set()
720+
var writefds = _stdlib_fd_set()
721+
var errorfds = _stdlib_fd_set()
722+
var done = false
723+
while !((_childStdout.isEOF && _childStderr.isEOF) || done) {
724+
readfds.zero()
725+
errorfds.zero()
726+
if !_childStdout.isEOF {
727+
readfds.set(_childStdout.fd)
728+
errorfds.set(_childStdout.fd)
729+
}
730+
if !_childStderr.isEOF {
731+
readfds.set(_childStderr.fd)
732+
errorfds.set(_childStderr.fd)
733+
}
734+
var ret: CInt
735+
repeat {
736+
ret = _stdlib_select(&readfds, &writefds, &errorfds, nil)
737+
} while ret == -1 && errno == EINTR
738+
if ret <= 0 {
739+
fatalError("select() returned an error")
740+
}
741+
if readfds.isset(_childStdout.fd) || errorfds.isset(_childStdout.fd) {
742+
_childStdout.read()
743+
while let line = _childStdout.getline() {
744+
(done: done, ()) = onStdoutLine(line)
745+
}
746+
continue
747+
}
748+
if readfds.isset(_childStderr.fd) || errorfds.isset(_childStderr.fd) {
749+
_childStderr.read()
750+
while let line = _childStderr.getline() {
751+
(done: done, ()) = onStderrLine(line)
752+
}
753+
continue
754+
}
755+
}
756+
}
757+
709758
/// Returns the values of the corresponding variables in the child process.
710759
internal mutating func _runTestInChild(
711760
_ testSuite: TestSuite,
@@ -714,7 +763,7 @@ struct _ParentProcess {
714763
) -> (anyExpectFailed: Bool, seenExpectCrash: Bool,
715764
status: ProcessTerminationStatus?,
716765
crashStdout: [String], crashStderr: [String]) {
717-
if _pid <= 0 {
766+
if _pid == nil {
718767
_spawnChild()
719768
}
720769

@@ -733,95 +782,66 @@ struct _ParentProcess {
733782
_childStdin.close()
734783
}
735784

736-
var readfds = _stdlib_fd_set()
737-
var writefds = _stdlib_fd_set()
738-
var errorfds = _stdlib_fd_set()
739785
var stdoutSeenCrashDelimiter = false
740786
var stderrSeenCrashDelimiter = false
741787
var stdoutEnd = false
742788
var stderrEnd = false
743789
var capturedCrashStdout: [String] = []
744790
var capturedCrashStderr: [String] = []
745791
var anyExpectFailedInChild = false
746-
while !((_childStdout.isEOF && _childStderr.isEOF) ||
747-
(stdoutEnd && stderrEnd)) {
748792

749-
readfds.zero()
750-
errorfds.zero()
751-
if !_childStdout.isEOF {
752-
readfds.set(_childStdout.fd)
753-
errorfds.set(_childStdout.fd)
754-
}
755-
if !_childStderr.isEOF {
756-
readfds.set(_childStderr.fd)
757-
errorfds.set(_childStderr.fd)
758-
}
759-
var ret: CInt
760-
repeat {
761-
ret = _stdlib_select(&readfds, &writefds, &errorfds, nil)
762-
} while ret == -1 && errno == EINTR
763-
if ret <= 0 {
764-
fatalError("select() returned an error")
765-
}
766-
if readfds.isset(_childStdout.fd) || errorfds.isset(_childStdout.fd) {
767-
_childStdout.read()
768-
while var line = _childStdout.getline() {
769-
if let index = findSubstring(line, _stdlibUnittestStreamPrefix) {
770-
let controlMessage =
771-
line[index..<line.endIndex]._split(separator: ";")
772-
switch controlMessage[1] {
773-
case "expectCrash":
774-
stdoutSeenCrashDelimiter = true
775-
anyExpectFailedInChild = controlMessage[2] == "true"
776-
case "end":
777-
stdoutEnd = true
778-
anyExpectFailedInChild = controlMessage[2] == "true"
779-
default:
780-
fatalError("unexpected message")
781-
}
782-
line = line[line.startIndex..<index]
783-
if line.isEmpty {
784-
continue
785-
}
793+
func processLine(_ line: String, isStdout: Bool) -> (done: Bool, Void) {
794+
var line = line
795+
if let index = findSubstring(line, _stdlibUnittestStreamPrefix) {
796+
let controlMessage =
797+
line[index..<line.endIndex]._split(separator: ";")
798+
switch controlMessage[1] {
799+
case "expectCrash":
800+
if isStdout {
801+
stdoutSeenCrashDelimiter = true
802+
anyExpectFailedInChild = controlMessage[2] == "true"
803+
} else {
804+
stderrSeenCrashDelimiter = true
786805
}
787-
if stdoutSeenCrashDelimiter {
788-
capturedCrashStdout.append(line)
806+
case "end":
807+
if isStdout {
808+
stdoutEnd = true
809+
anyExpectFailedInChild = controlMessage[2] == "true"
810+
} else {
811+
stderrEnd = true
789812
}
790-
print("stdout>>> \(line)")
813+
default:
814+
fatalError("unexpected message")
815+
}
816+
line = line[line.startIndex..<index]
817+
if line.isEmpty {
818+
return (done: stdoutEnd && stderrEnd, ())
791819
}
792-
continue
793820
}
794-
if readfds.isset(_childStderr.fd) || errorfds.isset(_childStderr.fd) {
795-
_childStderr.read()
796-
while var line = _childStderr.getline() {
797-
if let index = findSubstring(line, _stdlibUnittestStreamPrefix) {
798-
let controlMessage =
799-
line[index..<line.endIndex]._split(separator: ";")
800-
switch controlMessage[1] {
801-
case "expectCrash":
802-
stderrSeenCrashDelimiter = true
803-
case "end":
804-
stderrEnd = true
805-
default:
806-
fatalError("unexpected message")
807-
}
808-
line = line[line.startIndex..<index]
809-
if line.isEmpty {
810-
continue
811-
}
812-
}
813-
if stderrSeenCrashDelimiter {
814-
capturedCrashStderr.append(line)
815-
if findSubstring(line, _crashedPrefix) != nil {
816-
line = "OK: saw expected \"\(line.lowercased())\""
817-
}
821+
if isStdout {
822+
if stdoutSeenCrashDelimiter {
823+
capturedCrashStdout.append(line)
824+
}
825+
} else {
826+
if stderrSeenCrashDelimiter {
827+
capturedCrashStderr.append(line)
828+
if findSubstring(line, _crashedPrefix) != nil {
829+
line = "OK: saw expected \"\(line.lowercased())\""
818830
}
819-
print("stderr>>> \(line)")
820831
}
821-
continue
822832
}
833+
if isStdout {
834+
print("stdout>>> \(line)")
835+
} else {
836+
print("stderr>>> \(line)")
837+
}
838+
return (done: stdoutEnd && stderrEnd, ())
823839
}
824840

841+
_readFromChild(
842+
onStdoutLine: { processLine($0, isStdout: true) },
843+
onStderrLine: { processLine($0, isStdout: false) })
844+
825845
// Check if the child has sent us "end" markers for the current test.
826846
if stdoutEnd && stderrEnd {
827847
var status: ProcessTerminationStatus? = nil
@@ -851,6 +871,42 @@ struct _ParentProcess {
851871
capturedCrashStdout, capturedCrashStderr)
852872
}
853873

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+
854910
internal enum _TestStatus {
855911
case skip([TestRunPredicate])
856912
case pass
@@ -1010,6 +1066,11 @@ struct _ParentProcess {
10101066
print("\(testSuite.name): All tests passed")
10111067
}
10121068
}
1069+
let (failed: failedOnShutdown, ()) = _shutdownChild()
1070+
if failedOnShutdown {
1071+
print("The child process failed during shutdown, aborting.")
1072+
_testSuiteFailedCallback()
1073+
}
10131074
}
10141075
}
10151076

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+

validation-test/StdlibUnittest/CrashingTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ _setOverrideOSVersion(.osx(major: 10, minor: 9, bugFix: 3))
88
_setTestSuiteFailedCallback() { print("abort()") }
99

1010
//
11-
// Test that harness aborts when a test crashes
11+
// Test that harness aborts when a test crashes during a test run.
1212
//
1313

1414
var TestSuiteCrashes = TestSuite("TestSuiteCrashes")

validation-test/stdlib/Hashing.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ HashingTestSuite.test("overridePerExecutionHashSeed/overflow") {
144144
// Test that we don't use checked arithmetic on the seed.
145145
_HashingDetail.fixedSeedOverride = UInt64.max
146146
expectEqual(0x4344_dc3a_239c_3e81, _mixUInt64(0xffff_ffff_ffff_ffff))
147+
_HashingDetail.fixedSeedOverride = 0
147148
}
148149

149150
runAllTests()

0 commit comments

Comments
 (0)