Skip to content

Commit 6a56cfd

Browse files
committed
Merge pull request #1979 from apple/StdlibUnittest-parameterized-tests
StdlibUnittest: new feature: data-parameterized tests
2 parents 88d056a + 27e2f7f commit 6a56cfd

File tree

2 files changed

+322
-135
lines changed

2 files changed

+322
-135
lines changed

stdlib/private/StdlibUnittest/StdlibUnittest.swift.gyb

Lines changed: 187 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -476,10 +476,16 @@ func _childProcess() {
476476
let parts = line._split(separator: ";")
477477
let testSuiteName = parts[0]
478478
let testName = parts[1]
479+
var testParameter: Int?
480+
if parts.count > 2 {
481+
testParameter = Int(parts[2])!
482+
} else {
483+
testParameter = nil
484+
}
479485

480486
let testSuite = _allTestSuites[_testSuiteNameToIndex[testSuiteName]!]
481487
_anyExpectFailed = false
482-
testSuite._runTest(testName)
488+
testSuite._runTest(name: testName, parameter: testParameter)
483489

484490
print("\(_stdlibUnittestStreamPrefix);end;\(_anyExpectFailed)")
485491

@@ -530,15 +536,23 @@ struct _ParentProcess {
530536
}
531537

532538
/// Returns the values of the corresponding variables in the child process.
533-
mutating func _runTestInChild(testSuite: TestSuite, _ testName: String)
534-
-> (anyExpectFailed: Bool, seenExpectCrash: Bool,
539+
internal mutating func _runTestInChild(
540+
testSuite: TestSuite,
541+
_ testName: String,
542+
parameter: Int?
543+
) -> (anyExpectFailed: Bool, seenExpectCrash: Bool,
535544
status: ProcessTerminationStatus?,
536545
crashStdout: [String], crashStderr: [String]) {
537546
if _pid <= 0 {
538547
_spawnChild()
539548
}
540549

541-
print("\(testSuite.name);\(testName)", to: &_childStdin)
550+
print("\(testSuite.name);\(testName)", terminator: "", to: &_childStdin)
551+
if let parameter = parameter {
552+
print(";", terminator: "", to: &_childStdin)
553+
print(parameter, terminator: "", to: &_childStdin)
554+
}
555+
print("", to: &_childStdin)
542556

543557
let currentTest = testSuite._testByName(testName)
544558
if let stdinText = currentTest.stdinText {
@@ -667,6 +681,100 @@ struct _ParentProcess {
667681
capturedCrashStdout, capturedCrashStderr)
668682
}
669683

684+
internal enum _TestStatus {
685+
case skip([TestRunPredicate])
686+
case pass
687+
case fail
688+
case uxPass
689+
case xFail
690+
}
691+
692+
internal mutating func runOneTest(
693+
fullTestName fullTestName: String,
694+
testSuite: TestSuite,
695+
test t: TestSuite._Test,
696+
testParameter: Int?
697+
) -> _TestStatus {
698+
let activeSkips = t.getActiveSkipPredicates()
699+
if !activeSkips.isEmpty {
700+
return .skip(activeSkips)
701+
}
702+
703+
let activeXFails = t.getActiveXFailPredicates()
704+
let expectXFail = !activeXFails.isEmpty
705+
let activeXFailsText = expectXFail ? " (XFAIL: \(activeXFails))" : ""
706+
print("[ RUN ] \(fullTestName)\(activeXFailsText)")
707+
708+
var expectCrash = false
709+
var childTerminationStatus: ProcessTerminationStatus? = nil
710+
var crashStdout: [String] = []
711+
var crashStderr: [String] = []
712+
if _runTestsInProcess {
713+
if t.stdinText != nil {
714+
print("The test \(fullTestName) requires stdin input and can't be run in-process, marking as failed")
715+
_anyExpectFailed = true
716+
} else {
717+
_anyExpectFailed = false
718+
testSuite._runTest(name: t.name, parameter: testParameter)
719+
}
720+
} else {
721+
(_anyExpectFailed, expectCrash, childTerminationStatus, crashStdout,
722+
crashStderr) =
723+
_runTestInChild(testSuite, t.name, parameter: testParameter)
724+
}
725+
726+
// Determine if the test passed, not taking XFAILs into account.
727+
var testPassed = false
728+
switch (childTerminationStatus, expectCrash) {
729+
case (.none, false):
730+
testPassed = !_anyExpectFailed
731+
732+
case (.none, true):
733+
testPassed = false
734+
print("expecting a crash, but the test did not crash")
735+
736+
case (.some(_), false):
737+
testPassed = false
738+
print("the test crashed unexpectedly")
739+
740+
case (.some(_), true):
741+
testPassed = !_anyExpectFailed
742+
}
743+
if testPassed && t.crashOutputMatches.count > 0 {
744+
// If we still think that the test passed, check if the crash
745+
// output matches our expectations.
746+
let crashOutput = crashStdout + crashStderr
747+
for expectedSubstring in t.crashOutputMatches {
748+
var found = false
749+
for s in crashOutput {
750+
if findSubstring(s, expectedSubstring) != nil {
751+
found = true
752+
break
753+
}
754+
}
755+
if !found {
756+
print("did not find expected string after crash: \(expectedSubstring.debugDescription)")
757+
testPassed = false
758+
}
759+
}
760+
}
761+
762+
// Apply XFAILs.
763+
switch (testPassed, expectXFail) {
764+
case (true, false):
765+
return .pass
766+
767+
case (true, true):
768+
return .uxPass
769+
770+
case (false, false):
771+
return .fail
772+
773+
case (false, true):
774+
return .xFail
775+
}
776+
}
777+
670778
mutating func run() {
671779
if let filter = _filter {
672780
print("StdlibUnittest: using filter: \(filter)")
@@ -676,92 +784,43 @@ struct _ParentProcess {
676784
var failedTests: [String] = []
677785
var skippedTests: [String] = []
678786
for t in testSuite._tests {
679-
let fullTestName = "\(testSuite.name).\(t.name)"
680-
if let filter = _filter where findSubstring(fullTestName, filter) == nil {
681-
continue
682-
}
683-
684-
let activeSkips = t.getActiveSkipPredicates()
685-
if !activeSkips.isEmpty {
686-
skippedTests.append(t.name)
687-
print("[ SKIP ] \(fullTestName) (skip: \(activeSkips))")
688-
continue
689-
}
690-
691-
let activeXFails = t.getActiveXFailPredicates()
692-
let expectXFail = !activeXFails.isEmpty
693-
let activeXFailsText = expectXFail ? " (XFAIL: \(activeXFails))" : ""
694-
print("[ RUN ] \(fullTestName)\(activeXFailsText)")
695-
696-
var expectCrash = false
697-
var childTerminationStatus: ProcessTerminationStatus? = nil
698-
var crashStdout: [String] = []
699-
var crashStderr: [String] = []
700-
if _runTestsInProcess {
701-
if t.stdinText != nil {
702-
print("The test \(fullTestName) requires stdin input and can't be run in-process, marking as failed")
703-
_anyExpectFailed = true
704-
} else {
705-
_anyExpectFailed = false
706-
testSuite._runTest(t.name)
787+
for testParameter in t.parameterValues {
788+
var testName = t.name
789+
if let testParameter = testParameter {
790+
testName += "/"
791+
testName += String(testParameter)
707792
}
708-
} else {
709-
(_anyExpectFailed, expectCrash, childTerminationStatus, crashStdout,
710-
crashStderr) =
711-
_runTestInChild(testSuite, t.name)
712-
}
793+
var fullTestName = "\(testSuite.name).\(testName)"
794+
if let filter = _filter
795+
where findSubstring(fullTestName, filter) == nil {
713796

714-
// Determine if the test passed, not taking XFAILs into account.
715-
var testPassed = false
716-
switch (childTerminationStatus, expectCrash) {
717-
case (.none, false):
718-
testPassed = !_anyExpectFailed
719-
720-
case (.none, true):
721-
testPassed = false
722-
print("expecting a crash, but the test did not crash")
723-
724-
case (.some(_), false):
725-
testPassed = false
726-
print("the test crashed unexpectedly")
727-
728-
case (.some(_), true):
729-
testPassed = !_anyExpectFailed
730-
}
731-
if testPassed && t.crashOutputMatches.count > 0 {
732-
// If we still think that the test passed, check if the crash
733-
// output matches our expectations.
734-
let crashOutput = crashStdout + crashStderr
735-
for expectedCrashOutput in t.crashOutputMatches {
736-
var found = false
737-
for s in crashOutput {
738-
if findSubstring(s, expectedCrashOutput) != nil {
739-
found = true
740-
break
741-
}
742-
}
743-
if !found {
744-
print("did not find expected string after crash: \(expectedCrashOutput.debugDescription)")
745-
testPassed = false
746-
}
797+
continue
747798
}
748-
}
749-
750-
// Apply XFAILs.
751-
switch (testPassed, expectXFail) {
752-
case (true, false):
753-
print("[ OK ] \(fullTestName)")
754-
755-
case (true, true):
756-
uxpassedTests.append(t.name)
757-
print("[ UXPASS ] \(fullTestName)")
758799

759-
case (false, false):
760-
failedTests.append(t.name)
761-
print("[ FAIL ] \(fullTestName)")
762-
763-
case (false, true):
764-
print("[ XFAIL ] \(fullTestName)")
800+
switch runOneTest(
801+
fullTestName: fullTestName,
802+
testSuite: testSuite,
803+
test: t,
804+
testParameter: testParameter
805+
) {
806+
case .skip(let activeSkips):
807+
skippedTests.append(testName)
808+
print("[ SKIP ] \(fullTestName) (skip: \(activeSkips))")
809+
810+
case .pass:
811+
print("[ OK ] \(fullTestName)")
812+
813+
case .uxPass:
814+
uxpassedTests.append(testName)
815+
print("[ UXPASS ] \(fullTestName)")
816+
817+
case .fail:
818+
failedTests.append(testName)
819+
print("[ FAIL ] \(fullTestName)")
820+
821+
case .xFail:
822+
print("[ XFAIL ] \(fullTestName)")
823+
}
765824
}
766825
}
767826

@@ -927,7 +986,7 @@ public class TestSuite {
927986
_testTearDownCode = code
928987
}
929988

930-
func _runTest(testName: String) {
989+
func _runTest(name testName: String, parameter: Int?) {
931990
PersistentState.ranSomething = true
932991
for r in _allResettables {
933992
r.reset()
@@ -937,7 +996,15 @@ public class TestSuite {
937996
f()
938997
}
939998
let test = _testByName(testName)
940-
test.code()
999+
switch test.code {
1000+
case .single(let code):
1001+
precondition(
1002+
parameter == nil,
1003+
"can't pass parameters to parameterized tests")
1004+
code()
1005+
case .parameterized(code: let code, _):
1006+
code(parameter!)
1007+
}
9411008
if let f = _testTearDownCode {
9421009
f()
9431010
}
@@ -950,15 +1017,20 @@ public class TestSuite {
9501017
return _tests[_testNameToIndex[testName]!]
9511018
}
9521019

953-
struct _Test {
1020+
internal enum _TestCode {
1021+
case single(code: () -> Void)
1022+
case parameterized(code: (Int) -> Void, count: Int)
1023+
}
1024+
1025+
internal struct _Test {
9541026
let name: String
9551027
let testLoc: SourceLoc
9561028
let xfail: [TestRunPredicate]
9571029
let skip: [TestRunPredicate]
9581030
let stdinText: String?
9591031
let stdinEndsWithEOF: Bool
9601032
let crashOutputMatches: [String]
961-
let code: () -> Void
1033+
let code: _TestCode
9621034

9631035
/// Whether the test harness should stop reusing the child process after
9641036
/// running this test.
@@ -973,6 +1045,15 @@ public class TestSuite {
9731045
func getActiveSkipPredicates() -> [TestRunPredicate] {
9741046
return skip.filter { $0.evaluate() }
9751047
}
1048+
1049+
var parameterValues: [Int?] {
1050+
switch code {
1051+
case .single:
1052+
return [nil]
1053+
case .parameterized(code: _, count: let count):
1054+
return (0..<count).map { $0 }
1055+
}
1056+
}
9761057
}
9771058

9781059
public struct _TestBuilder {
@@ -1016,16 +1097,30 @@ public class TestSuite {
10161097
return self
10171098
}
10181099

1019-
public func code(testFunction: () -> Void) {
1100+
internal func _build(testCode: _TestCode) {
10201101
_testSuite._tests.append(
10211102
_Test(
10221103
name: _name, testLoc: _data._testLoc!, xfail: _data._xfail,
10231104
skip: _data._skip,
10241105
stdinText: _data._stdinText,
10251106
stdinEndsWithEOF: _data._stdinEndsWithEOF,
1026-
crashOutputMatches: _data._crashOutputMatches, code: testFunction))
1107+
crashOutputMatches: _data._crashOutputMatches,
1108+
code: testCode))
10271109
_testSuite._testNameToIndex[_name] = _testSuite._tests.count - 1
10281110
}
1111+
1112+
public func code(testFunction: () -> Void) {
1113+
_build(.single(code: testFunction))
1114+
}
1115+
1116+
public func forEach<Data>(
1117+
in parameterSets: [Data],
1118+
testFunction: (Data) -> Void
1119+
) {
1120+
_build(.parameterized(
1121+
code: { (i: Int) in testFunction(parameterSets[i]) },
1122+
count: parameterSets.count))
1123+
}
10291124
}
10301125

10311126
var name: String

0 commit comments

Comments
 (0)