Skip to content

Commit 541d91d

Browse files
committed
Merge pull request #64 from briancroom/select-test
Allow selecting a particular test or test case to run from the command line
2 parents a32ae82 + c13db67 commit 541d91d

File tree

6 files changed

+184
-2
lines changed

6 files changed

+184
-2
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ Also, this version of XCTest does not use the external test runner binary. Inste
108108
XCTMain([testCase(TestNSString.allTests), testCase(TestNSArray.allTests), testCase(TestNSDictionary.allTests)])
109109
```
110110

111-
The `XCTMain` function does not return, and will cause your test app to exit with either `0` for success or `1` for failure.
111+
The `XCTMain` function does not return, and will cause your test app to exit with either `0` for success or `1` for failure. Command line arguments given to the executable can be used to select a particular test or test case to execute. For example:
112+
113+
```sh
114+
./FooTests FooTestCase/testFoo # Run a single test method
115+
./FooTests FooTestCase # Run all the tests in FooTestCase
116+
```
112117

113118
We are currently investigating ideas on how to make these additional steps for test discovery automatic when running on the Swift runtime.

Sources/XCTest/ArgumentParser.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2016 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
//
10+
// ArgumentParser.swift
11+
// Tools for parsing test execution configuration from command line arguments
12+
//
13+
14+
internal struct ArgumentParser {
15+
private let arguments: [String]
16+
17+
init(arguments: [String] = Process.arguments) {
18+
self.arguments = arguments
19+
}
20+
21+
var selectedTestName: String? {
22+
return arguments.count > 1 ? arguments[1] : nil
23+
}
24+
}

Sources/XCTest/TestFiltering.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2016 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
//
10+
// XCTestFiltering.swift
11+
// This provides utilities for executing only a subset of the tests provided to XCTMain
12+
//
13+
14+
internal typealias TestFilter = (XCTestCase.Type, String) -> Bool
15+
16+
internal struct TestFiltering {
17+
private let selectedTestName: String?
18+
19+
init(selectedTestName: String? = ArgumentParser().selectedTestName) {
20+
self.selectedTestName = selectedTestName
21+
}
22+
23+
var selectedTestFilter: TestFilter {
24+
if let selectedTestName = selectedTestName {
25+
if let selectedTest = SelectedTest(selectedTestName: selectedTestName) {
26+
return selectedTest.matches
27+
} else {
28+
return excludeAllFilter()
29+
}
30+
} else {
31+
return includeAllFilter()
32+
}
33+
}
34+
35+
private func excludeAllFilter() -> TestFilter {
36+
return { _ in false }
37+
}
38+
39+
private func includeAllFilter() -> TestFilter {
40+
return { _ in true }
41+
}
42+
43+
static func filterTests(entries: [XCTestCaseEntry], filter: TestFilter) -> [XCTestCaseEntry] {
44+
return entries
45+
.map({ testCase, tests in
46+
return (testCase, tests.filter({ filter(testCase, $0.0) }))
47+
})
48+
.filter({ testCase, tests in
49+
return !tests.isEmpty
50+
})
51+
}
52+
}
53+
54+
/// A selected test can be an entire test case, or a single test method
55+
/// within a test case.
56+
private struct SelectedTest {
57+
let testCaseName: String
58+
let testName: String?
59+
}
60+
61+
private extension SelectedTest {
62+
init?(selectedTestName: String) {
63+
let components = selectedTestName.characters.split(separator: "/").map(String.init)
64+
switch components.count {
65+
case 1:
66+
testCaseName = components[0]
67+
testName = nil
68+
case 2:
69+
testCaseName = components[0]
70+
testName = components[1]
71+
default: return nil
72+
}
73+
}
74+
75+
func matches(testCase testCase: XCTestCase.Type, testName: String) -> Bool {
76+
return String(reflecting: testCase) == testCaseName && (self.testName == nil || testName == self.testName)
77+
}
78+
}

Sources/XCTest/XCTestMain.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,18 @@ internal struct XCTRun {
6767
///
6868
/// XCTMain([ testCase(TestFoo.allTests) ])
6969
///
70+
/// Command line arguments can be used to select a particular test or test case to execute. For example:
71+
///
72+
/// ./FooTests FooTestCase/testFoo # Run a single test method
73+
/// ./FooTests FooTestCase # Run all the tests in FooTestCase
74+
///
7075
/// - Parameter testCases: An array of test cases run, each produced by a call to the `testCase` function
7176
/// - seealso: `testCase`
7277
@noreturn public func XCTMain(testCases: [XCTestCaseEntry]) {
78+
let filter = TestFiltering()
79+
7380
let overallDuration = measureTimeExecutingBlock {
74-
for (testCase, tests) in testCases {
81+
for (testCase, tests) in TestFiltering.filterTests(testCases, filter: filter.selectedTestFilter) {
7582
testCase.invokeTests(tests)
7683
}
7784
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// RUN: %{swiftc} %s -o %{built_tests_dir}/SelectedTest
2+
// RUN: %{built_tests_dir}/SelectedTest SelectedTest.ExecutedTestCase/test_foo > %T/one_test_method || true
3+
// RUN: %{built_tests_dir}/SelectedTest SelectedTest.ExecutedTestCase > %T/one_test_case || true
4+
// RUN: %{built_tests_dir}/SelectedTest > %T/all || true
5+
// RUN: %{xctest_checker} -p "// CHECK-METHOD: " %T/one_test_method %s
6+
// RUN: %{xctest_checker} -p "// CHECK-TESTCASE: " %T/one_test_case %s
7+
// RUN: %{xctest_checker} -p "// CHECK-ALL: " %T/all %s
8+
9+
#if os(Linux) || os(FreeBSD)
10+
import XCTest
11+
#else
12+
import SwiftXCTest
13+
#endif
14+
15+
class ExecutedTestCase: XCTestCase {
16+
static var allTests: [(String, ExecutedTestCase -> () throws -> Void)] {
17+
return [
18+
("test_bar", test_bar),
19+
("test_foo", test_foo),
20+
]
21+
}
22+
23+
// CHECK-METHOD: Test Case 'ExecutedTestCase.test_foo' started.
24+
// CHECK-METHOD: Test Case 'ExecutedTestCase.test_foo' passed \(\d+\.\d+ seconds\).
25+
// CHECK-TESTCASE: Test Case 'ExecutedTestCase.test_bar' started.
26+
// CHECK-TESTCASE: Test Case 'ExecutedTestCase.test_bar' passed \(\d+\.\d+ seconds\).
27+
// CHECK-ALL: Test Case 'ExecutedTestCase.test_bar' started.
28+
// CHECK-ALL: Test Case 'ExecutedTestCase.test_bar' passed \(\d+\.\d+ seconds\).
29+
func test_bar() {}
30+
31+
// CHECK-TESTCASE: Test Case 'ExecutedTestCase.test_foo' started.
32+
// CHECK-TESTCASE: Test Case 'ExecutedTestCase.test_foo' passed \(\d+\.\d+ seconds\).
33+
// CHECK-ALL: Test Case 'ExecutedTestCase.test_foo' started.
34+
// CHECK-ALL: Test Case 'ExecutedTestCase.test_foo' passed \(\d+\.\d+ seconds\).
35+
func test_foo() {}
36+
}
37+
// CHECK-METHOD: Executed 1 test, with 0 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
38+
// CHECK-TESTCASE: Executed 2 tests, with 0 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
39+
// CHECK-ALL: Executed 2 tests, with 0 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
40+
41+
42+
class SkippedTestCase: XCTestCase {
43+
static var allTests: [(String, SkippedTestCase -> () throws -> Void)] {
44+
return [("test_baz", test_baz)]
45+
}
46+
47+
// CHECK-ALL: Test Case 'SkippedTestCase.test_baz' started.
48+
// CHECK-ALL: Test Case 'SkippedTestCase.test_baz' passed \(\d+\.\d+ seconds\).
49+
func test_baz() {}
50+
}
51+
// CHECK-ALL: Executed 1 test, with 0 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
52+
53+
XCTMain([
54+
testCase(ExecutedTestCase.allTests),
55+
testCase(SkippedTestCase.allTests),
56+
])
57+
58+
// CHECK-METHOD: Total executed 1 test, with 0 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
59+
// CHECK-TESTCASE: Total executed 2 tests, with 0 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
60+
// CHECK-ALL: Total executed 3 tests, with 0 failures \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds

XCTest.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
AE7DD6091C8E81A0006FC722 /* ArgumentParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */; };
11+
AE7DD60A1C8E81A0006FC722 /* TestFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */; };
1012
C265F66F1C3AEB6A00520CF9 /* XCTAssert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F6691C3AEB6A00520CF9 /* XCTAssert.swift */; };
1113
C265F6701C3AEB6A00520CF9 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F66A1C3AEB6A00520CF9 /* XCTestCase.swift */; };
1214
C265F6721C3AEB6A00520CF9 /* XCTestMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265F66C1C3AEB6A00520CF9 /* XCTestMain.swift */; };
@@ -29,6 +31,8 @@
2931
/* Begin PBXFileReference section */
3032
5B5D86DB1BBC74AD00234F36 /* SwiftXCTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftXCTest.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3133
AE7DD6061C8DC6C0006FC722 /* Functional */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Functional; sourceTree = "<group>"; };
34+
AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgumentParser.swift; sourceTree = "<group>"; };
35+
AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestFiltering.swift; sourceTree = "<group>"; };
3236
B1384A411C1B3E8700EDF031 /* CONTRIBUTING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
3337
B1384A421C1B3E8700EDF031 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
3438
B1384A431C1B3E8700EDF031 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@@ -97,6 +101,8 @@
97101
C265F6671C3AEB6A00520CF9 /* XCTest */ = {
98102
isa = PBXGroup;
99103
children = (
104+
AE7DD6071C8E81A0006FC722 /* ArgumentParser.swift */,
105+
AE7DD6081C8E81A0006FC722 /* TestFiltering.swift */,
100106
C265F6691C3AEB6A00520CF9 /* XCTAssert.swift */,
101107
C265F66A1C3AEB6A00520CF9 /* XCTestCase.swift */,
102108
C265F66C1C3AEB6A00520CF9 /* XCTestMain.swift */,
@@ -237,6 +243,8 @@
237243
C265F6731C3AEB6A00520CF9 /* XCTimeUtilities.swift in Sources */,
238244
C265F6701C3AEB6A00520CF9 /* XCTestCase.swift in Sources */,
239245
DADB979C1C51BDA2005E68B6 /* XCTestExpectation.swift in Sources */,
246+
AE7DD60A1C8E81A0006FC722 /* TestFiltering.swift in Sources */,
247+
AE7DD6091C8E81A0006FC722 /* ArgumentParser.swift in Sources */,
240248
C265F66F1C3AEB6A00520CF9 /* XCTAssert.swift in Sources */,
241249
C265F6721C3AEB6A00520CF9 /* XCTestMain.swift in Sources */,
242250
);

0 commit comments

Comments
 (0)