Skip to content

Commit 43cc5da

Browse files
authored
[Commands][SR-4589] Add --filter option to swift test
- Deprecate `--specifier` option and update CHANGELOG.md - Add diagnostics for `--specifier` - Enable chaining multiple options together like `--filter ".*JSON.*" --list-tests` `--filter ".*JSON.*" --parallel` - Add functional tests for the new `--filter` option
1 parent f9c948d commit 43cc5da

File tree

3 files changed

+135
-48
lines changed

3 files changed

+135
-48
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
Note: This is in reverse chronological order, so newer entries are added to the top.
22

3+
Swift 4.0
4+
---------
5+
* `--specifier` option for `swift test` is now deprecated.
6+
Use `--filter` instead which supports regex.
7+
38
Swift 3.0
49
---------
510

Sources/Commands/SwiftTestTool.swift

Lines changed: 120 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ import Utility
1616

1717
import func POSIX.exit
1818

19+
20+
/// Diagnostics info for deprecated `--specifier` option
21+
struct SpecifierDeprecatedDiagnostic: DiagnosticData {
22+
static let id = DiagnosticID(
23+
type: AnyDiagnostic.self,
24+
name: "org.swift.diags.specifier-deprecated",
25+
defaultBehavior: .warning,
26+
description: {
27+
$0 <<< "'--specifier' option is deprecated, use '--filter' instead."
28+
}
29+
)
30+
}
31+
1932
private enum TestError: Swift.Error {
2033
case invalidListTestJSONData
2134
case multipleTestProducts
@@ -42,15 +55,16 @@ public class TestToolOptions: ToolOptions {
4255
if shouldPrintVersion {
4356
return .version
4457
}
45-
// List the test cases.
58+
59+
if shouldRunInParallel {
60+
return .runParallel
61+
}
62+
4663
if shouldListTests {
4764
return .listTests
4865
}
49-
// Run tests in parallel.
50-
if shouldRunInParallel {
51-
return .runInParallel
52-
}
53-
return .run(specifier)
66+
67+
return .runSerial
5468
}
5569

5670
/// If the test target should be built before testing.
@@ -65,15 +79,26 @@ public class TestToolOptions: ToolOptions {
6579
/// List the tests and exit.
6680
var shouldListTests = false
6781

68-
/// Run only these specified tests.
69-
var specifier: String?
82+
var testCaseSpecifier: TestCaseSpecifier = .none
83+
}
84+
85+
/// Tests filtering specifier
86+
///
87+
/// This is used to filter tests to run
88+
/// .none => No filtering
89+
/// .specific => Specify test with fully quantified name
90+
/// .regex => RegEx pattern
91+
public enum TestCaseSpecifier {
92+
case none
93+
case specific(String)
94+
case regex(String)
7095
}
7196

7297
public enum TestMode {
7398
case version
7499
case listTests
75-
case run(String?)
76-
case runInParallel
100+
case runSerial
101+
case runParallel
77102
}
78103

79104
/// swift-test tool namespace
@@ -96,25 +121,47 @@ public class SwiftTestTool: SwiftTool<TestToolOptions> {
96121
case .listTests:
97122
let testPath = try buildTestsIfNeeded(options)
98123
let testSuites = try getTestSuites(path: testPath)
124+
let tests = testSuites.filteredTests(specifier: options.testCaseSpecifier)
125+
99126
// Print the tests.
100-
for testSuite in testSuites {
101-
for testCase in testSuite.tests {
102-
for test in testCase.tests {
103-
print(testCase.name + "/" + test)
104-
}
105-
}
127+
for test in tests {
128+
print(test.specifier)
106129
}
107130

108-
case .run(let specifier):
131+
case .runSerial:
109132
let testPath = try buildTestsIfNeeded(options)
110-
let success: Bool = TestRunner(path: testPath, xctestArg: specifier, processSet: processSet).test()
111-
exit(success ? 0 : 1)
133+
let testSuites = try getTestSuites(path: testPath)
134+
var ranSuccessfully = true
135+
136+
switch options.testCaseSpecifier {
137+
case .none:
138+
let runner = TestRunner(
139+
path: testPath,
140+
xctestArg: nil,
141+
processSet: processSet
142+
)
143+
ranSuccessfully = runner.test()
144+
case .regex, .specific:
145+
if case .specific = options.testCaseSpecifier {
146+
diagnostics.emit(data: SpecifierDeprecatedDiagnostic())
147+
}
148+
let tests = testSuites.filteredTests(specifier: options.testCaseSpecifier)
149+
for test in tests {
150+
let runner = TestRunner(path: testPath,
151+
xctestArg: test.specifier,
152+
processSet: processSet)
153+
ranSuccessfully = ranSuccessfully && runner.test()
154+
}
155+
}
112156

113-
case .runInParallel:
157+
exit(ranSuccessfully ? 0 : 1)
158+
159+
case .runParallel:
114160
let testPath = try buildTestsIfNeeded(options)
115161
let testSuites = try getTestSuites(path: testPath)
162+
let tests = testSuites.filteredTests(specifier: options.testCaseSpecifier)
116163
let runner = ParallelTestRunner(testPath: testPath, processSet: processSet)
117-
try runner.run(testSuites)
164+
try runner.run(tests)
118165
exit(runner.ranSuccesfully ? 0 : 1)
119166
}
120167
}
@@ -173,10 +220,14 @@ public class SwiftTestTool: SwiftTool<TestToolOptions> {
173220
to: { $0.shouldRunInParallel = $1 })
174221

175222
binder.bind(
176-
option: parser.add(option: "--specifier", shortName: "-s", kind: String.self,
177-
usage: "Run a specific test class or method, Format: <test-target>.<test-case> or " +
223+
option: parser.add(option: "--specifier", shortName: "-s", kind: String.self),
224+
to: { $0.testCaseSpecifier = .specific($1) })
225+
226+
binder.bind(
227+
option: parser.add(option: "--filter", kind: String.self,
228+
usage: "Run test cases matching regular expression, Format: <test-target>.<test-case> or " +
178229
"<test-target>.<test-case>/<test>"),
179-
to: { $0.specifier = $1 })
230+
to: { $0.testCaseSpecifier = .regex($1) })
180231
}
181232

182233
/// Locates XCTestHelper tool inside the libexec directory and bin directory.
@@ -234,6 +285,20 @@ public class SwiftTestTool: SwiftTool<TestToolOptions> {
234285
}
235286
}
236287

288+
/// A structure representing an individual unit test.
289+
struct UnitTest {
290+
/// The name of the unit test.
291+
let name: String
292+
293+
/// The name of the test case.
294+
let testCase: String
295+
296+
/// The specifier argument which can be passed to XCTest.
297+
var specifier: String {
298+
return testCase + "/" + name
299+
}
300+
}
301+
237302
/// A class to run tests on a XCTest binary.
238303
///
239304
/// Note: Executes the XCTest with inherited environment as it is convenient to pass senstive
@@ -311,20 +376,6 @@ final class TestRunner {
311376

312377
/// A class to run tests in parallel.
313378
final class ParallelTestRunner {
314-
/// A structure representing an individual unit test.
315-
struct UnitTest {
316-
/// The name of the unit test.
317-
let name: String
318-
319-
/// The name of the test case.
320-
let testCase: String
321-
322-
/// The specifier argument which can be passed to XCTest.
323-
var specifier: String {
324-
return testCase + "/" + name
325-
}
326-
}
327-
328379
/// An enum representing result of a unit test execution.
329380
enum TestResult {
330381
case success(UnitTest)
@@ -372,17 +423,13 @@ final class ParallelTestRunner {
372423
progressBar.update(percent: 100*numCurrentTest/numTests, text: test.specifier)
373424
}
374425

375-
func enqueueTests(_ testSuites: [TestSuite]) throws {
426+
func enqueueTests(_ tests: [UnitTest]) throws {
376427
// FIXME: Add a count property in SynchronizedQueue.
377428
var numTests = 0
378429
// Enqueue all the tests.
379-
for testSuite in testSuites {
380-
for testCase in testSuite.tests {
381-
for test in testCase.tests {
382-
numTests += 1
383-
pendingTests.enqueue(UnitTest(name: test, testCase: testCase.name))
384-
}
385-
}
430+
for test in tests {
431+
numTests += 1
432+
pendingTests.enqueue(test)
386433
}
387434
self.numTests = numTests
388435
self.numCurrentTest = 0
@@ -393,9 +440,9 @@ final class ParallelTestRunner {
393440
}
394441

395442
/// Executes the tests spawning parallel workers. Blocks calling thread until all workers are finished.
396-
func run(_ testSuites: [TestSuite]) throws {
443+
func run(_ tests: [UnitTest]) throws {
397444
// Enqueue all the tests.
398-
try enqueueTests(testSuites)
445+
try enqueueTests(tests)
399446

400447
// Create the worker threads.
401448
let workers: [Thread] = (0..<numJobs).map({ _ in
@@ -521,3 +568,28 @@ struct TestSuite {
521568
})
522569
}
523570
}
571+
572+
573+
fileprivate extension Sequence where Iterator.Element == TestSuite {
574+
/// Returns all the unit tests of the test suites.
575+
var allTests: [UnitTest] {
576+
return flatMap { $0.tests }.flatMap({ testCase in
577+
testCase.tests.map{ UnitTest(name: $0, testCase: testCase.name) }
578+
})
579+
}
580+
581+
/// Return tests matching the provided specifier
582+
func filteredTests(specifier: TestCaseSpecifier) -> [UnitTest] {
583+
switch specifier {
584+
case .none:
585+
return allTests
586+
case .regex(let pattern):
587+
return allTests.filter({ test in
588+
test.specifier.range(of: pattern,
589+
options: .regularExpression) != nil
590+
})
591+
case .specific(let name):
592+
return allTests.filter{ $0.specifier == name }
593+
}
594+
}
595+
}

Tests/FunctionalTests/MiscellaneousTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,15 @@ class MiscellaneousTestCase: XCTestCase {
383383
#endif
384384
}
385385

386+
func testSwiftTestFilter() throws {
387+
#if os(macOS)
388+
fixture(name: "Miscellaneous/ParallelTestsPkg") { prefix in
389+
let output = try SwiftPMProduct.SwiftTest.execute(["--filter", ".*1"], chdir: prefix, printIfError: true)
390+
XCTAssert(output.contains("testExample1"))
391+
}
392+
#endif
393+
}
394+
386395
func testExecutableAsBuildOrderDependency() throws {
387396
// Test that we can build packages which have modules depending on executable modules.
388397
fixture(name: "Miscellaneous/ExecDependency") { prefix in
@@ -530,6 +539,7 @@ class MiscellaneousTestCase: XCTestCase {
530539
("testSpaces", testSpaces),
531540
("testSecondBuildIsNullInModulemapGen", testSecondBuildIsNullInModulemapGen),
532541
("testSwiftTestParallel", testSwiftTestParallel),
542+
("testSwiftTestFilter", testSwiftTestFilter),
533543
("testOverridingSwiftcArguments", testOverridingSwiftcArguments),
534544
("testPkgConfigClangModules", testPkgConfigClangModules),
535545
("testCanKillSubprocessOnSigInt", testCanKillSubprocessOnSigInt),

0 commit comments

Comments
 (0)