Skip to content

Commit 5202136

Browse files
Generalize the selectedTests facility to be closure-based, to allow filtering tests via other criteria than ID (#48)
* Generalize the selectedTests facility to be closure-based, to allow filtering tests via other criteria than ID --------- Co-authored-by: Suzy Ratcliff <suzyr@apple.com>
1 parent 83208c4 commit 5202136

File tree

5 files changed

+69
-48
lines changed

5 files changed

+69
-48
lines changed

Sources/Testing/Running/Configuration.swift

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -137,38 +137,38 @@ public struct Configuration: Sendable {
137137

138138
// MARK: - Test selection
139139

140-
/// The selected tests to run, if any.
140+
/// A function that handles filtering tests.
141141
///
142-
/// This property should be used for testing membership (whether a test ID has
143-
/// been selected) since it is more optimized for that use case. It also
144-
/// provides the backing storage for ``selectedTestIDs``.
145-
///
146-
/// This property is optional and defaults to `nil` because it is possible to
147-
/// select specific tests to run but not provide any tests in that list. That
148-
/// is a supported use case: it results in zero tests being run and no issues
149-
/// recorded.
150-
///
151-
/// A practical example of when this situation can happen is when testing is
152-
/// configured via an Xcode Test Plan, the "Automatically Include New Tests"
153-
/// option is disabled, and zero tests are enabled.
154-
var selectedTests: Test.ID.Selection?
155-
156-
/// The IDs of the selected tests to run, if any.
157-
///
158-
/// This property is optional and defaults to `nil` because it is possible to
159-
/// select specific tests to run but not provide any tests in that list. That
160-
/// is a supported use case: it results in zero tests being run and no issues
161-
/// recorded.
142+
/// - Parameters:
143+
/// - test: An test that needs to be filtered.
144+
///
145+
/// - Returns: A Boolean value representing if the test satisfied the filter.
146+
public typealias TestFilter = @Sendable (Test) -> Bool
147+
148+
/// The test filter to which tests should be filtered when run.
149+
public var testFilter: TestFilter?
150+
151+
/// The granularity to enforce test filtering.
152+
///
153+
/// By default, all tests are run and no filter is set.
154+
/// - Parameters:
155+
/// - selection: An set of test ids to be filtered.
156+
public mutating func setTestFilter(toMatch selection: Set<Test.ID>?) {
157+
self.setTestFilter(toMatch: selection.map(Test.ID.Selection.init))
158+
}
159+
160+
/// The granularity to enforce test filtering.
162161
///
163-
/// A practical example of when this situation can happen is when testing is
164-
/// configured via an Xcode Test Plan, the "Automatically Include New Tests"
165-
/// option is disabled, and zero tests are enabled.
166-
public var selectedTestIDs: Set<Test.ID>? {
167-
get {
168-
selectedTests?.testIDs
162+
/// By default, all tests are run and no filter is set.
163+
/// - Parameters:
164+
/// - selection: An selection of test ids to be filtered.
165+
mutating func setTestFilter(toMatch selection: Test.ID.Selection?) {
166+
guard let selectedTests = selection else {
167+
self.testFilter = nil
168+
return
169169
}
170-
set {
171-
selectedTests = newValue.map { .init(testIDs: $0) }
170+
self.testFilter = { test in
171+
selectedTests.contains(test)
172172
}
173173
}
174174
}

Sources/Testing/Running/Runner.Plan.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,16 @@ extension Runner.Plan {
127127
///
128128
/// - Parameters:
129129
/// - test: The test to query.
130-
/// - selectedTests: The selected test IDs to use in determining whether
131-
/// `test` is selected, if one is configured.
130+
/// - filter: The filter to decide if the test is included.
132131
///
133132
/// - Returns: Whether or not the specified test is selected. If
134133
/// `selectedTests` is `nil`, `test` is considered selected if it is not
135134
/// hidden.
136-
private static func _isTestIncluded(_ test: Test, in selectedTests: Test.ID.Selection?) -> Bool {
137-
guard let selectedTests else {
135+
private static func _isTestIncluded(_ test: Test, using filter: Configuration.TestFilter?) -> Bool {
136+
guard let filter else {
138137
return !test.isHidden
139138
}
140-
return selectedTests.contains(test)
139+
return filter(test)
141140
}
142141

143142
/// Construct a graph of runner plan steps for the specified tests.
@@ -160,8 +159,7 @@ extension Runner.Plan {
160159
// them, in which case it will be .recordIssue().
161160
var testGraph = Graph<String, Test?>()
162161
var actionGraph = Graph<String, Action>(value: .run)
163-
let selectedTests = configuration.selectedTests
164-
for test in tests where _isTestIncluded(test, in: selectedTests) {
162+
for test in tests where _isTestIncluded(test, using: configuration.testFilter) {
165163
let idComponents = test.id.keyPathRepresentation
166164
testGraph.insertValue(test, at: idComponents)
167165
actionGraph.insertValue(.run, at: idComponents, intermediateValue: .run)

Tests/TestingTests/PlanTests.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// See https://swift.org/LICENSE.txt for license information
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
10-
10+
1111
@testable @_spi(ExperimentalTestRunning) import Testing
1212

1313
@Suite("Runner.Plan Tests")
@@ -26,8 +26,9 @@ struct PlanTests {
2626
testB,
2727
]
2828

29+
let selection = Test.ID.Selection(testIDs: [innerTestType.id])
2930
var configuration = Configuration()
30-
configuration.selectedTestIDs = [innerTestType.id]
31+
configuration.setTestFilter(toMatch: selection)
3132

3233
let plan = await Runner.Plan(tests: tests, configuration: configuration)
3334
#expect(plan.steps.contains(where: { $0.test == outerTestType }))
@@ -51,7 +52,8 @@ struct PlanTests {
5152
]
5253

5354
var configuration = Configuration()
54-
configuration.selectedTestIDs = [innerTestType.id, outerTestType.id]
55+
let selection = Test.ID.Selection(testIDs: [innerTestType.id, outerTestType.id])
56+
configuration.setTestFilter(toMatch: selection)
5557

5658
let plan = await Runner.Plan(tests: tests, configuration: configuration)
5759
let planTests = plan.steps.map(\.test)
@@ -70,7 +72,10 @@ struct PlanTests {
7072
let tests = [outerTestType, deeplyNestedTest]
7173

7274
var configuration = Configuration()
73-
configuration.selectedTestIDs = [outerTestType.id, deeplyNestedTest.id]
75+
let selection = Test.ID.Selection(testIDs: [outerTestType.id, deeplyNestedTest.id])
76+
configuration.testFilter = { test in
77+
selection.contains(test)
78+
}
7479

7580
let plan = await Runner.Plan(tests: tests, configuration: configuration)
7681

@@ -88,7 +93,10 @@ struct PlanTests {
8893
let tests = [testSuiteA, testSuiteB, testSuiteC, testFuncX]
8994

9095
var configuration = Configuration()
91-
configuration.selectedTestIDs = [testSuiteA.id]
96+
let selection = Test.ID.Selection(testIDs: [testSuiteA.id])
97+
configuration.testFilter = { test in
98+
selection.contains(test)
99+
}
92100

93101
let plan = await Runner.Plan(tests: tests, configuration: configuration)
94102
let testFuncXWithTraits = try #require(plan.steps.map(\.test).first { $0.name == "x()" })

Tests/TestingTests/RunnerTests.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,10 @@ final class RunnerTests: XCTestCase {
250250
let testFunc = try #require(await testFunction(named: "duelingConditions()", in: NeverRunTests.self))
251251

252252
var configuration = Configuration()
253-
configuration.selectedTests = .init(testIDs: [testSuite.id])
253+
let selection = Test.ID.Selection(testIDs: [testSuite.id])
254+
configuration.testFilter = { test in
255+
selection.contains(test)
256+
}
254257

255258
let runner = await Runner(testing: [
256259
testSuite,
@@ -294,11 +297,16 @@ final class RunnerTests: XCTestCase {
294297
(SendableTests.self, "disabled()"),
295298
]
296299

297-
var configuration = Configuration()
298-
configuration.selectedTestIDs = Set(tests.map {
300+
let selectedTestIDs = Set(tests.map {
299301
Test.ID(type: $0).child(named: $1)
300302
})
301-
XCTAssertEqual(false, configuration.selectedTestIDs?.isEmpty)
303+
XCTAssertFalse(selectedTestIDs.isEmpty)
304+
305+
var configuration = Configuration()
306+
let selection = Test.ID.Selection(testIDs: selectedTestIDs)
307+
configuration.testFilter = { test in
308+
selection.contains(test)
309+
}
302310

303311
let runner = await Runner(configuration: configuration)
304312
let plan = runner.plan

Tests/TestingTests/TestSupport/TestingAdditions.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ func runTest(for containingType: Any.Type, configuration: Configuration = .init(
6767
/// If no test is found representing `containingType`, nothing is run.
6868
func runTestFunction(named name: String, in containingType: Any.Type, configuration: Configuration = .init()) async {
6969
var configuration = configuration
70-
configuration.selectedTestIDs = [Test.ID(type: containingType).child(named: name)]
70+
let testID = Test.ID.Selection(testIDs: [Test.ID(type: containingType).child(named: name)])
71+
configuration.testFilter = { test in
72+
testID.contains(test)
73+
}
7174

7275
let runner = await Runner(configuration: configuration)
7376
await runner.run()
@@ -90,7 +93,8 @@ extension Runner {
9093
let moduleName = String(fileID[..<fileID.lastIndex(of: "/")!])
9194

9295
var configuration = configuration
93-
configuration.selectedTestIDs = [Test.ID(moduleName: moduleName, nameComponents: [testName], sourceLocation: nil)]
96+
let selection = Test.ID.Selection(testIDs: [Test.ID(moduleName: moduleName, nameComponents: [testName], sourceLocation: nil)])
97+
configuration.setTestFilter(toMatch: selection)
9498

9599
await self.init(configuration: configuration)
96100
}
@@ -104,7 +108,10 @@ extension Runner.Plan {
104108
/// - configuration: The configuration to use for planning.
105109
init(selecting containingType: Any.Type, configuration: Configuration = .init()) async {
106110
var configuration = configuration
107-
configuration.selectedTestIDs = [Test.ID(type: containingType)]
111+
let selection = Test.ID.Selection(testIDs: [Test.ID(type: containingType)])
112+
configuration.testFilter = { test in
113+
selection.contains(test)
114+
}
108115

109116
await self.init(configuration: configuration)
110117
}

0 commit comments

Comments
 (0)