@@ -40,16 +40,25 @@ extension Configuration {
40
40
/// - Parameters:
41
41
/// - testIDs: The set of test IDs to predicate tests against.
42
42
/// - membership: How to interpret the result when predicating tests.
43
- case precomputed ( _ testIDs: Test . ID . Selection , membership: Membership )
43
+ case testIDs ( _ testIDs: Set < Test . ID > , membership: Membership )
44
44
45
- /// The test filter is an arbitrary predicate function .
45
+ /// The test filter contains a set of tags to predicate tests against .
46
46
///
47
47
/// - Parameters:
48
- /// - predicate: The function to predicate tests against.
48
+ /// - tags: The set of test tags to predicate tests against.
49
+ /// - anyOf: Whether to require that tests have any (`true`) or all
50
+ /// (`false`) of the specified tags.
49
51
/// - membership: How to interpret the result when predicating tests.
50
- case function ( _ predicate : @ Sendable ( borrowing Test ) -> Bool , membership: Membership )
52
+ case tags ( _ tags : Set < Tag > , anyOf : Bool , membership: Membership )
51
53
52
- /// The test filter is a combination of other test filters.
54
+ /// The test filter contains a pattern to predicate test IDs against.
55
+ ///
56
+ /// - Parameters:
57
+ /// - pattern: The pattern to predicate test IDs against.
58
+ /// - membership: How to interpret the result when predicating tests.
59
+ case pattern( _ pattern: String , membership: Membership )
60
+
61
+ /// The test filter is a combination of other test filter kinds.
53
62
///
54
63
/// - Parameters:
55
64
/// - lhs: The first test filter's kind.
@@ -59,7 +68,7 @@ extension Configuration {
59
68
///
60
69
/// The result of a test filter with this kind is the combination of the
61
70
/// results of its subfilters using `op`.
62
- indirect case combined ( _ lhs: Kind , _ rhs: Kind , _ op: CombinationOperator )
71
+ indirect case combination ( _ lhs: Self , _ rhs: Self , _ op: CombinationOperator )
63
72
}
64
73
65
74
/// The kind of test filter.
@@ -100,8 +109,7 @@ extension Configuration.TestFilter {
100
109
/// - Parameters:
101
110
/// - testIDs: A set of test IDs to be filtered.
102
111
public init ( including testIDs: some Collection < Test . ID > ) {
103
- let selection = Test . ID. Selection ( testIDs: testIDs)
104
- self . init ( _kind: . precomputed( selection, membership: . including) )
112
+ self . init ( _kind: . testIDs( Set ( testIDs) , membership: . including) )
105
113
}
106
114
107
115
/// Initialize this instance to filter tests to those _not_ specified by a set
@@ -110,35 +118,29 @@ extension Configuration.TestFilter {
110
118
/// - Parameters:
111
119
/// - selection: A set of test IDs to be excluded.
112
120
public init ( excluding testIDs: some Collection < Test . ID > ) {
113
- let selection = Test . ID. Selection ( testIDs: testIDs)
114
- self . init ( _kind: . precomputed( selection, membership: . excluding) )
121
+ self . init ( _kind: . testIDs( Set ( testIDs) , membership: . excluding) )
115
122
}
116
123
117
- /// Initialize this instance from an arbitrary function.
124
+ /// Initialize this instance to represent a pattern expression matched against
125
+ /// a test's ID.
118
126
///
119
127
/// - Parameters:
120
128
/// - membership: How to interpret the result when predicating tests.
121
- /// - predicate: The function to predicate tests against.
122
- init ( membership: Membership , matching predicate: @escaping @Sendable ( borrowing Test ) -> Bool ) {
123
- self . init ( _kind: . function( predicate, membership: membership) )
124
- }
129
+ /// - pattern: The pattern, expressed as a `Regex`-compatible regular
130
+ /// expression, to match test IDs against.
131
+ @available ( _regexAPI, * )
132
+ init ( membership: Membership , matching pattern: String ) throws {
133
+ // Validate the regular expression by attempting to initialize a `Regex`
134
+ // representing it, but do not preserve it. This type only represents
135
+ // the pattern in the abstract, and is not responsible for actually
136
+ // applying it to a test graph — that happens later during planning.
137
+ //
138
+ // Performing this validation here currently makes such errors easier to
139
+ // surface when using the SwiftPM entry point. But longer-term, we should
140
+ // make the planning phase throwing and propagate errors from there instead.
141
+ _ = try Regex ( pattern)
125
142
126
- /// Initialize this instance to operate based on a set of tags.
127
- ///
128
- /// - Parameters:
129
- /// - tags: The set of tags to either include or exclude.
130
- /// - anyOf: Whether tests must have _any_ of the tags in `tags` (as opposed
131
- /// to all of them.)
132
- /// - membership: How to interpret the result when predicating tests.
133
- init ( tags: some Collection < Tag > , anyOf: Bool , membership: Membership ) {
134
- let tags = Set ( tags)
135
- self . init ( membership: membership) { test in
136
- if anyOf {
137
- !test. tags. isDisjoint ( with: tags) // .intersects()
138
- } else {
139
- test. tags. isSuperset ( of: tags)
140
- }
141
- }
143
+ self . init ( _kind: . pattern( pattern, membership: membership) )
142
144
}
143
145
144
146
/// Initialize this instance to include tests with a given set of tags.
@@ -148,7 +150,7 @@ extension Configuration.TestFilter {
148
150
///
149
151
/// Matching tests have had _any_ of the tags in `tags` added to them.
150
152
public init ( includingAnyOf tags: some Collection < Tag > ) {
151
- self . init ( tags : tags, anyOf: true , membership: . including)
153
+ self . init ( _kind : . tags( Set ( tags ) , anyOf: true , membership: . including) )
152
154
}
153
155
154
156
/// Initialize this instance to exclude tests with a given set of tags.
@@ -158,7 +160,7 @@ extension Configuration.TestFilter {
158
160
///
159
161
/// Matching tests have had _any_ of the tags in `tags` added to them.
160
162
public init ( excludingAnyOf tags: some Collection < Tag > ) {
161
- self . init ( tags : tags, anyOf: true , membership: . excluding)
163
+ self . init ( _kind : . tags( Set ( tags ) , anyOf: true , membership: . excluding) )
162
164
}
163
165
164
166
/// Initialize this instance to include tests with a given set of tags.
@@ -168,7 +170,7 @@ extension Configuration.TestFilter {
168
170
///
169
171
/// Matching tests have had _all_ of the tags in `tags` added to them.
170
172
public init ( includingAllOf tags: some Collection < Tag > ) {
171
- self . init ( tags : tags, anyOf: false , membership: . including)
173
+ self . init ( _kind : . tags( Set ( tags ) , anyOf: false , membership: . including) )
172
174
}
173
175
174
176
/// Initialize this instance to exclude tests with a given set of tags.
@@ -178,32 +180,92 @@ extension Configuration.TestFilter {
178
180
///
179
181
/// Matching tests have had _all_ of the tags in `tags` added to them.
180
182
public init ( excludingAllOf tags: some Collection < Tag > ) {
181
- self . init ( tags : tags, anyOf: false , membership: . excluding)
183
+ self . init ( _kind : . tags( Set ( tags ) , anyOf: false , membership: . excluding) )
182
184
}
185
+ }
183
186
184
- /// Initialize this instance to represent a regular expression matched against
185
- /// a test's ID.
186
- ///
187
- /// - Parameters:
188
- /// - membership: How to interpret the result when predicating tests.
189
- /// - regex: The regular expression to predicate test IDs against.
187
+ // MARK: - Operations
188
+
189
+ extension Configuration . TestFilter {
190
+ /// An enumeration which represents filtering logic to be applied to a test
191
+ /// graph.
192
+ fileprivate enum Operation : Sendable {
193
+ /// A filter operation which has no effect.
194
+ ///
195
+ /// All tests are allowed when this operation is applied.
196
+ case unfiltered
197
+
198
+ /// A filter operation which accepts tests included in a precomputed
199
+ /// selection of test IDs.
200
+ ///
201
+ /// - Parameters:
202
+ /// - testIDs: The set of test IDs to predicate tests against.
203
+ /// - membership: How to interpret the result when predicating tests.
204
+ case precomputed( _ testIDs: Test . ID . Selection , membership: Membership )
205
+
206
+ /// A filter operation which accepts tests which satisfy an arbitrary
207
+ /// predicate function.
208
+ ///
209
+ /// - Parameters:
210
+ /// - predicate: The function to predicate tests against.
211
+ /// - membership: How to interpret the result when predicating tests.
212
+ case function( _ predicate: @Sendable ( borrowing Test ) -> Bool , membership: Membership )
213
+
214
+ /// A filter operation which is a combination of other operations.
215
+ ///
216
+ /// - Parameters:
217
+ /// - lhs: The first test filter operation.
218
+ /// - rhs: The second test filter operation.
219
+ /// - op: The operator to apply when combining the results of the two
220
+ /// filter operations.
221
+ ///
222
+ /// The result of applying this filter operation is the combination of
223
+ /// applying the results of its sub-operations using `op`.
224
+ indirect case combination( _ lhs: Self , _ rhs: Self , _ op: CombinationOperator )
225
+ }
226
+ }
227
+
228
+ extension Configuration . TestFilter . Kind {
229
+ /// An operation which implements the filtering logic for this test filter
230
+ /// kind.
190
231
///
191
- /// The caller is responsible for ensuring that `regex` is safe to send across
192
- /// isolation boundaries. Regular expressions parsed from strings are
193
- /// generally sendable.
194
- @available ( _regexAPI, * )
195
- init ( membership: Membership , matching regex: Regex < AnyRegexOutput > ) {
196
- let regex = UncheckedSendable ( rawValue: regex)
197
- self . init ( membership: membership) { test in
198
- let id = String ( describing: test. id)
199
- return id. contains ( regex. rawValue)
232
+ /// - Throws: Any error encountered while generating an operation for this
233
+ /// test filter kind. One example is the creation of a `Regex` from a
234
+ /// `.pattern` kind: if the pattern is not a valid regular expression, an
235
+ /// error will be thrown.
236
+ var operation : Configuration . TestFilter . Operation {
237
+ get throws {
238
+ switch self {
239
+ case . unfiltered:
240
+ return . unfiltered
241
+ case let . testIDs( testIDs, membership) :
242
+ return . precomputed( Test . ID. Selection ( testIDs: testIDs) , membership: membership)
243
+ case let . tags( tags, anyOf, membership) :
244
+ return . function( { test in
245
+ if anyOf {
246
+ !test. tags. isDisjoint ( with: tags) // .intersects()
247
+ } else {
248
+ test. tags. isSuperset ( of: tags)
249
+ }
250
+ } , membership: membership)
251
+ case let . pattern( pattern, membership) :
252
+ guard #available( _regexAPI, * ) else {
253
+ throw SystemError ( description: " Filtering by regular expression matching is unavailable " )
254
+ }
255
+
256
+ let regex = UncheckedSendable ( rawValue: try Regex ( pattern) )
257
+ return . function( { test in
258
+ let id = String ( describing: test. id)
259
+ return id. contains ( regex. rawValue)
260
+ } , membership: membership)
261
+ case let . combination( lhs, rhs, op) :
262
+ return try . combination( lhs. operation, rhs. operation, op)
263
+ }
200
264
}
201
265
}
202
266
}
203
267
204
- // MARK: - Operations
205
-
206
- extension Configuration . TestFilter . Kind {
268
+ extension Configuration . TestFilter . Operation {
207
269
/// Apply this test filter to a test graph and remove tests that should not be
208
270
/// included.
209
271
///
@@ -246,7 +308,7 @@ extension Configuration.TestFilter.Kind {
246
308
. map ( \. id)
247
309
let selection = Test . ID. Selection ( testIDs: testIDs)
248
310
return Self . precomputed ( selection, membership: membership) . apply ( to: testGraph)
249
- case let . combined ( lhs, rhs, op) :
311
+ case let . combination ( lhs, rhs, op) :
250
312
return zip (
251
313
lhs. apply ( to: testGraph) ,
252
314
rhs. apply ( to: testGraph)
@@ -265,13 +327,13 @@ extension Configuration.TestFilter {
265
327
/// - testGraph: The test graph to filter.
266
328
///
267
329
/// - Returns: A copy of `testGraph` with filtered tests replaced with `nil`.
268
- func apply( to testGraph: Graph < String , Test ? > ) -> Graph < String , Test ? > {
269
- var result = _kind. apply ( to: testGraph)
330
+ func apply( to testGraph: Graph < String , Test ? > ) throws -> Graph < String , Test ? > {
331
+ var result = try _kind. operation . apply ( to: testGraph)
270
332
271
333
// After performing the test function, run through one more time and remove
272
334
// hidden tests. (Note that this property's value is not recursively set on
273
335
// combined test filters. It is only consulted on the outermost call to
274
- // apply(to:), not in _apply(to:).
336
+ // apply(to:), not in _apply(to:).)
275
337
if !includeHiddenTests {
276
338
result = result. mapValues { _, test in
277
339
( test? . isHidden == true ) ? nil : test
@@ -318,6 +380,7 @@ extension Configuration.TestFilter {
318
380
}
319
381
}
320
382
}
383
+
321
384
/// Combine this test filter with another one.
322
385
///
323
386
/// - Parameters:
@@ -336,7 +399,7 @@ extension Configuration.TestFilter {
336
399
case ( _, . unfiltered) :
337
400
self
338
401
default :
339
- Self ( _kind: . combined ( _kind, other. _kind, op) )
402
+ Self ( _kind: . combination ( _kind, other. _kind, op) )
340
403
}
341
404
result. includeHiddenTests = includeHiddenTests
342
405
0 commit comments