11
11
* ** v1** : Initial pitch.
12
12
* ** v2** : Dropped 'Custom' prefix from the proposed API names (although kept the
13
13
word in certain documentation passages where it clarified behavior).
14
+ * ** v3** : Changed the ` Trait ` requirement from a property to a method which
15
+ accepts the test and/or test case, and modify its default implementations such
16
+ that custom behavior is either performed per-suite or per-test case by default.
14
17
15
18
## Introduction
16
19
@@ -192,6 +195,42 @@ to mitigate these downsides it's important that there be some way to distinguish
192
195
traits which customize test behavior. That way, the testing library can limit
193
196
these scoped access calls to only the traits which require it.
194
197
198
+ ### Avoiding unnecessary (re-)execution
199
+
200
+ Traits can be applied to either test functions or suites, and traits applied to
201
+ suites can optionally support inheritance by implementing the ` isRecursive `
202
+ property of the ` SuiteTrait ` protocol. When a trait is directly applied to a
203
+ test function, if the trait customizes the behavior of tests it's applied to, it
204
+ should be given the opportunity to perform its custom behavior once for every
205
+ invocation of that test function. In particular, if the test function is
206
+ parameterized and runs multiple times, then the trait applied to it should
207
+ perform its custom behavior once for every invocation. This should not be
208
+ surprising to users, since it's consistent with the behavior of ` init ` and
209
+ ` deinit ` for an instance ` @Test ` method.
210
+
211
+ It may be useful for certain kinds of traits to perform custom logic once for
212
+ _ all_ the invocations of a parameterized test. Although this should be possible,
213
+ we believe it shouldn't be the default since it could lead to work being
214
+ repeated multiple times needlessly, or unintentional state sharing across tests,
215
+ unless the trait is implemented carefully to avoid those problems.
216
+
217
+ When a trait conforms to ` SuiteTrait ` and is applied to a suite, the question of
218
+ when its custom behavior (if any) should be performed is less obvious. Some
219
+ suite traits support inheritance and are recursively applied to all the test
220
+ functions they contain (including transitively, via sub-suites). Other suite
221
+ traits don't support inheritance, and only affect the specific suite they're
222
+ applied to. (It's also worth noting that a sub-suite _ can_ have the same
223
+ non-recursive suite trait one of its ancestors has, as long as it's applied
224
+ explicitly.)
225
+
226
+ As a general rule of thumb, we believe most traits will either want to perform
227
+ custom logic once for _ all_ children or once for _ each_ child, not both.
228
+ Therefore, when it comes to suite traits, the default behavior should depend on
229
+ whether it supports inheritance: a recursive suite trait should by default
230
+ perform custom logic before each test, and a non-recursive one per-suite. But
231
+ the APIs should be flexible enough to support both, for advanced traits which
232
+ need it.
233
+
195
234
## Detailed design
196
235
197
236
I propose the following new APIs:
@@ -219,7 +258,7 @@ Below are the proposed interfaces:
219
258
///
220
259
/// Types conforming to this protocol may be used in conjunction with a
221
260
/// ``Trait``-conforming type by implementing the
222
- /// ``Trait/testExecutor-714gp `` property , allowing custom traits to
261
+ /// ``Trait/executor(for:testCase:)-26qgm `` method , allowing custom traits to
223
262
/// customize the execution of tests. Consolidating common set-up and tear-down
224
263
/// logic for tests which have similar needs allows each test function to be
225
264
/// more succinct with less repetitive boilerplate so it can focus on what makes
@@ -244,8 +283,8 @@ public protocol TestExecuting: Sendable {
244
283
///
245
284
/// When the testing library is preparing to run a test, it finds all traits
246
285
/// applied to that test (including those inherited from containing suites)
247
- /// and asks each for the value of its
248
- /// ``Trait/testExecutor-714gp`` property . It then calls this method
286
+ /// and asks each for its test executor (if any) by calling
287
+ /// ``Trait/executor(for:testCase:)-26qgm`` . It then calls this method
249
288
/// on all non-`nil` instances, giving each an opportunity to perform
250
289
/// arbitrary work before or after invoking `function`.
251
290
///
@@ -265,37 +304,72 @@ public protocol Trait: Sendable {
265
304
266
305
/// The type of the test executor for this trait.
267
306
///
268
- /// The default type is `Never`, which cannot be instantiated. This means the
269
- /// value of the ``testExecutor-714gp `` property for all traits with
270
- /// the default custom executor type is `nil`, meaning such traits will not
271
- /// perform any custom execution for the tests they're applied to.
307
+ /// The default type is `Never`, which cannot be instantiated. The
308
+ /// ``executor(for:testCase:)-26qgm `` method for any trait with this default
309
+ /// test executor type must return `nil`, meaning that trait will not perform
310
+ /// any custom behavior for the tests it's applied to.
272
311
associatedtype TestExecutor : TestExecuting = Never
273
312
274
- /// The test executor for this trait, if any.
313
+ /// Get this trait's executor for the specified test and/or test case, if any.
314
+ ///
315
+ /// - Parameters:
316
+ /// - test: The test for which an executor is being requested.
317
+ /// - testCase: The test case for which an executor is being requested, if
318
+ /// any. When `test` represents a suite, the value of this argument is
319
+ /// `nil`.
320
+ ///
321
+ /// - Returns: An value of ``Trait/TestExecutor`` which should be used to
322
+ /// customize the behavior of `test` and/or `testCase`, or `nil` if custom
323
+ /// behavior should not be performed.
324
+ ///
325
+ /// If this trait's type conforms to ``TestExecuting``, the default value
326
+ /// returned by this method depends on `test` and/or `testCase`:
275
327
///
276
- /// If this trait's type conforms to ``TestExecuting``, the default value of
277
- /// this property is `self` and this trait will be used to customize test
278
- /// execution. This is the most straightforward way to implement a trait which
279
- /// customizes the execution of tests.
328
+ /// - If `test` represents a suite, this trait must conform to ``SuiteTrait``.
329
+ /// If the value of this suite trait's ``SuiteTrait/isRecursive`` property
330
+ /// is `true`, then this method returns `nil`; otherwise, it returns `self`.
331
+ /// This means that by default, a suite trait will _either_ perform its
332
+ /// custom behavior once for the entire suite, or once per-test function it
333
+ /// contains.
334
+ /// - Otherwise `test` represents a test function. If `testCase` is `nil`,
335
+ /// this method returns `nil`; otherwise, it returns `self`. This means that
336
+ /// by default, a trait which is applied to or inherited by a test function
337
+ /// will perform its custom behavior once for each of that function's cases.
280
338
///
281
- /// If the value of this property is an instance of a different type
282
- /// conforming to ``TestExecuting``, that instance will be used to perform
283
- /// test execution instead.
339
+ /// A trait may explicitly implement this method to further customize the
340
+ /// default behaviors above. For example, if a trait should perform custom
341
+ /// test behavior both once per-suite and once per-test function in that suite,
342
+ /// it may implement the method and return a non-`nil` executor under those
343
+ /// conditions.
284
344
///
285
- /// The default value of this property is `nil` (with the default type
286
- /// `Never?`), meaning that instances of this type will not perform any custom
287
- /// test execution for tests they are applied to.
288
- var testExecutor: TestExecutor? { get }
345
+ /// A trait may also implement this method and return `nil` if it determines
346
+ /// that it does not need to perform any custom behavior for a particular test
347
+ /// at runtime, even if the test has the trait applied. This can improve
348
+ /// performance and make diagnostics clearer by avoiding an unnecessary call
349
+ /// to ``TestExecuting/execute(_:for:testCase:)``.
350
+ ///
351
+ /// If this trait's type does not conform to ``TestExecuting`` and its
352
+ /// associated ``Trait/TestExecutor`` type is the default `Never`, then this
353
+ /// method returns `nil` by default. This means that instances of this type
354
+ /// will not perform any custom test execution for tests they are applied to.
355
+ func executor (for test : Test, testCase : Test.Case? ) -> TestExecutor?
289
356
}
290
357
291
358
extension Trait where Self : TestExecuting {
292
- // Returns `self`.
293
- public var testExecutor: Self ? {
359
+ // Returns `nil` if `testCase` is `nil`, else `self`.
360
+ public func executor (for test : Test, testCase : Test.Case? ) -> Self ?
361
+ }
362
+
363
+ extension SuiteTrait where Self : TestExecuting {
364
+ // If `test` is a suite, returns `nil` if `isRecursive` is `true`, else `self`.
365
+ // Otherwise, `test` is a function and this returns `nil` if `testCase` is
366
+ // `nil`, else `self`.
367
+ public func executor (for test : Test, testCase : Test.Case? ) -> Self ?
294
368
}
295
369
296
370
extension Trait where TestExecutor == Never {
297
371
// Returns `nil`.
298
- public var testExecutor: TestExecutor ? {
372
+ public func executor ( for test : Test, testCase : Test.Case ? ) -> TestExecutor ?
299
373
}
300
374
301
375
extension Never : TestExecuting {}
@@ -387,6 +461,15 @@ ideal as it required a conditional `trait as? CustomExecutionTrait` downcast at
387
461
runtime, in contrast to the simpler and more performant Optional property of the
388
462
proposed API.
389
463
464
+ ### API names
465
+
466
+ We considered "run" as the base verb for the proposed new concept instead of
467
+ "execute", which would imply the names ` TestRunning ` , ` TestRunner ` ,
468
+ ` runner(for:testCase) ` , and ` run(_:for:testCase:) ` . The word "run" is used in
469
+ many other contexts related to testing though, such as the ` Runner ` SPI type and
470
+ more casually to refer to a run which occurred of a test, in the past tense, so
471
+ overloading this term again may cause confusion.
472
+
390
473
## Acknowledgments
391
474
392
475
Thanks to [ Dennis Weissmann] ( https://github.com/dennisweissmann ) for originally
0 commit comments