Skip to content

Commit dae1398

Browse files
committed
Revise proposed API to ensure custom behaviors are only performed once per scope, by default
1 parent a120b3e commit dae1398

File tree

5 files changed

+254
-53
lines changed

5 files changed

+254
-53
lines changed

Documentation/Proposals/NNNN-custom-test-execution-traits.md

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
* **v1**: Initial pitch.
1212
* **v2**: Dropped 'Custom' prefix from the proposed API names (although kept the
1313
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.
1417

1518
## Introduction
1619

@@ -192,6 +195,42 @@ to mitigate these downsides it's important that there be some way to distinguish
192195
traits which customize test behavior. That way, the testing library can limit
193196
these scoped access calls to only the traits which require it.
194197

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+
195234
## Detailed design
196235

197236
I propose the following new APIs:
@@ -219,7 +258,7 @@ Below are the proposed interfaces:
219258
///
220259
/// Types conforming to this protocol may be used in conjunction with a
221260
/// ``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
223262
/// customize the execution of tests. Consolidating common set-up and tear-down
224263
/// logic for tests which have similar needs allows each test function to be
225264
/// more succinct with less repetitive boilerplate so it can focus on what makes
@@ -244,8 +283,8 @@ public protocol TestExecuting: Sendable {
244283
///
245284
/// When the testing library is preparing to run a test, it finds all traits
246285
/// 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
249288
/// on all non-`nil` instances, giving each an opportunity to perform
250289
/// arbitrary work before or after invoking `function`.
251290
///
@@ -265,37 +304,72 @@ public protocol Trait: Sendable {
265304

266305
/// The type of the test executor for this trait.
267306
///
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.
272311
associatedtype TestExecutor: TestExecuting = Never
273312

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`:
275327
///
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.
280338
///
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.
284344
///
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?
289356
}
290357

291358
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?
294368
}
295369

296370
extension Trait where TestExecutor == Never {
297371
// Returns `nil`.
298-
public var testExecutor: TestExecutor? {
372+
public func executor(for test: Test, testCase: Test.Case?) -> TestExecutor?
299373
}
300374

301375
extension Never: TestExecuting {}
@@ -387,6 +461,15 @@ ideal as it required a conditional `trait as? CustomExecutionTrait` downcast at
387461
runtime, in contrast to the simpler and more performant Optional property of the
388462
proposed API.
389463

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+
390473
## Acknowledgments
391474

392475
Thanks to [Dennis Weissmann](https://github.com/dennisweissmann) for originally

Sources/Testing/Running/Runner.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ extension Runner {
9090
// and ultimately the first trait is the first one to be invoked.
9191
let executeAllTraits = step.test.traits.lazy
9292
.reversed()
93-
.compactMap { $0.testExecutor }
93+
.compactMap { $0.executor(for: step.test, testCase: testCase) }
9494
.map { $0.execute(_:for:testCase:) }
9595
.reduce(body) { executeAllTraits, testExecutor in
9696
{

Sources/Testing/Testing.docc/Traits/Trait.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,5 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors
4949
### Customizing the execution of tests
5050

5151
- ``TestExecuting``
52-
- ``Trait/testExecutor-714gp``
52+
- ``Trait/executor(for:testCase:)-26qgm``
5353
- ``Trait/TestExecutor``

Sources/Testing/Traits/Trait.swift

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,55 @@ public protocol Trait: Sendable {
4444

4545
/// The type of the test executor for this trait.
4646
///
47-
/// The default type is `Never`, which cannot be instantiated. This means the
48-
/// value of the ``testExecutor-714gp`` property for all traits with
49-
/// the default custom executor type is `nil`, meaning such traits will not
50-
/// perform any custom execution for the tests they're applied to.
47+
/// The default type is `Never`, which cannot be instantiated. The
48+
/// ``executor(for:testCase:)-26qgm`` method for any trait with this default
49+
/// test executor type must return `nil`, meaning that trait will not perform
50+
/// any custom behavior for the tests it's applied to.
5151
associatedtype TestExecutor: TestExecuting = Never
5252

53-
/// The test executor for this trait, if any.
53+
/// Get this trait's executor for the specified test and/or test case, if any.
5454
///
55-
/// If this trait's type conforms to ``TestExecuting``, the default value of
56-
/// this property is `self` and this trait will be used to customize test
57-
/// execution. This is the most straightforward way to implement a trait which
58-
/// customizes the execution of tests.
59-
///
60-
/// If the value of this property is an instance of a different type
61-
/// conforming to ``TestExecuting``, that instance will be used to perform
62-
/// test execution instead.
55+
/// - Parameters:
56+
/// - test: The test for which an executor is being requested.
57+
/// - testCase: The test case for which an executor is being requested, if
58+
/// any. When `test` represents a suite, the value of this argument is
59+
/// `nil`.
6360
///
64-
/// The default value of this property is `nil` (with the default type
65-
/// `Never?`), meaning that instances of this type will not perform any custom
66-
/// test execution for tests they are applied to.
67-
var testExecutor: TestExecutor? { get }
61+
/// - Returns: An value of ``Trait/TestExecutor`` which should be used to
62+
/// customize the behavior of `test` and/or `testCase`, or `nil` if custom
63+
/// behavior should not be performed.
64+
///
65+
/// If this trait's type conforms to ``TestExecuting``, the default value
66+
/// returned by this method depends on `test` and/or `testCase`:
67+
///
68+
/// - If `test` represents a suite, this trait must conform to ``SuiteTrait``.
69+
/// If the value of this suite trait's ``SuiteTrait/isRecursive`` property
70+
/// is `true`, then this method returns `nil`; otherwise, it returns `self`.
71+
/// This means that by default, a suite trait will _either_ perform its
72+
/// custom behavior once for the entire suite, or once per-test function it
73+
/// contains.
74+
/// - Otherwise `test` represents a test function. If `testCase` is `nil`,
75+
/// this method returns `nil`; otherwise, it returns `self`. This means that
76+
/// by default, a trait which is applied to or inherited by a test function
77+
/// will perform its custom behavior once for each of that function's cases.
78+
///
79+
/// A trait may explicitly implement this method to further customize the
80+
/// default behaviors above. For example, if a trait should perform custom
81+
/// test behavior both once per-suite and once per-test function in that suite,
82+
/// it may implement the method and return a non-`nil` executor under those
83+
/// conditions.
84+
///
85+
/// A trait may also implement this method and return `nil` if it determines
86+
/// that it does not need to perform any custom behavior for a particular test
87+
/// at runtime, even if the test has the trait applied. This can improve
88+
/// performance and make diagnostics clearer by avoiding an unnecessary call
89+
/// to ``TestExecuting/execute(_:for:testCase:)``.
90+
///
91+
/// If this trait's type does not conform to ``TestExecuting`` and its
92+
/// associated ``Trait/TestExecutor`` type is the default `Never`, then this
93+
/// method returns `nil` by default. This means that instances of this type
94+
/// will not perform any custom test execution for tests they are applied to.
95+
func executor(for test: Test, testCase: Test.Case?) -> TestExecutor?
6896
}
6997

7098
/// A protocol that allows customizing the execution of a test function (and
@@ -73,7 +101,7 @@ public protocol Trait: Sendable {
73101
///
74102
/// Types conforming to this protocol may be used in conjunction with a
75103
/// ``Trait``-conforming type by implementing the
76-
/// ``Trait/testExecutor-714gp`` property, allowing custom traits to
104+
/// ``Trait/executor(for:testCase:)-26qgm`` method, allowing custom traits to
77105
/// customize the execution of tests. Consolidating common set-up and tear-down
78106
/// logic for tests which have similar needs allows each test function to be
79107
/// more succinct with less repetitive boilerplate so it can focus on what makes
@@ -98,8 +126,8 @@ public protocol TestExecuting: Sendable {
98126
///
99127
/// When the testing library is preparing to run a test, it finds all traits
100128
/// applied to that test (including those inherited from containing suites)
101-
/// and asks each for the value of its
102-
/// ``Trait/testExecutor-714gp`` property. It then calls this method
129+
/// and asks each for its test executor (if any) by calling
130+
/// ``Trait/executor(for:testCase:)-26qgm``. It then calls this method
103131
/// on all non-`nil` instances, giving each an opportunity to perform
104132
/// arbitrary work before or after invoking `function`.
105133
///
@@ -115,8 +143,18 @@ public protocol TestExecuting: Sendable {
115143
}
116144

117145
extension Trait where Self: TestExecuting {
118-
public var testExecutor: Self? {
119-
self
146+
public func executor(for test: Test, testCase: Test.Case?) -> Self? {
147+
testCase == nil ? nil : self
148+
}
149+
}
150+
151+
extension SuiteTrait where Self: TestExecuting {
152+
public func executor(for test: Test, testCase: Test.Case?) -> Self? {
153+
if test.isSuite {
154+
isRecursive ? nil : self
155+
} else {
156+
testCase == nil ? nil : self
157+
}
120158
}
121159
}
122160

@@ -154,7 +192,7 @@ extension Trait {
154192
}
155193

156194
extension Trait where TestExecutor == Never {
157-
public var testExecutor: TestExecutor? {
195+
public func executor(for test: Test, testCase: Test.Case?) -> TestExecutor? {
158196
nil
159197
}
160198
}

0 commit comments

Comments
 (0)