Skip to content

Commit c624d10

Browse files
[API Proposal] Promote TimeLimitTrait.Duration and associated decls to public API
1 parent c90e17e commit c624d10

File tree

1 file changed

+196
-0
lines changed

1 file changed

+196
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Constrain the granularity of test time limit durations
2+
3+
* Proposal:
4+
[SWT-NNNN](NNNN-constrain-the-granularity-of-test-time-limit-durations.md)
5+
* Authors: [Dennis Weissmann](https://github.com/dennisweissmann)
6+
* Status: **Awaiting review**
7+
* Implementation:
8+
[apple/swift-testing#NNNNN](https://github.com/apple/swift-testing/pull/NNNNN)
9+
* Review:
10+
([pitch](https://forums.swift.org/t/pitch-constrain-the-granularity-of-test-time
11+
-limit-durations/73146))
12+
13+
## Introduction
14+
15+
Sometimes tests might get into a state (either due the test code itself or due
16+
to the code they're testing) where they don't make forward progress and hang.
17+
Swift Testing provides a way to handle these issues using the TimeLimit trait:
18+
19+
```
20+
@Test(.timeLimit(.minutes(60))
21+
func testFunction() { ... }
22+
```
23+
24+
Currently there exist multiple overloads for the `.timeLimit` trait: one that
25+
takes a `Swift.Duration` which allows for arbitrary `Duration` values to be
26+
passed, and one that takes a `TimeLimitTrait.Duration` which constrains the
27+
minimum time limit as well as the increment to 1 minute.
28+
29+
## Motivation
30+
31+
Small time limit values in particular cause more harm than good due to tests
32+
running in environments with drastically differing performance characteristics.
33+
Particularly when running in CI systems or on virtualized hardware tests can
34+
run much slower than at desk.
35+
Swift Testing should help developers use a reasonable time limit value in its
36+
API without developers having to refer to the documentation.
37+
38+
It is crucial to emphasize that unit tests failing due to exceeding their
39+
timeout should be exceptionally rare. At the same time, a spurious unit test
40+
failure caused by a short timeout can be surprisingly costly, potentially
41+
leading to an entire CI pipeline being rerun. Determining an appropriate
42+
timeout for a specific test can be a challenging task.
43+
44+
Additionally, when the system intentionally runs multiple tests simultaneously
45+
to optimize resource utilization, the scheduler becomes the arbiter of test
46+
execution. Consequently, the test may take significantly longer than
47+
anticipated, potentially due to external factors beyond the control of the code
48+
under test.
49+
50+
A unit test should be capable of failing due to hanging, but it should not fail
51+
due to being slow, unless the developer has explicitly indicated that it
52+
should, effectively transforming it into a performance test.
53+
54+
The time limit feature is *not* intended to be used to apply small timeouts to
55+
tests to ensure test runtime doesn't regress by small amounts. This feature is
56+
intended to be used to guard against hangs and pathologically long running
57+
tests.
58+
59+
## Proposed Solution
60+
61+
We propose changing the `.timeLimit` API to accept values of a new `Duration`
62+
type defined in `TimeLimitTrait` which only allows for `.minute` values to be
63+
passed.
64+
These types already exist as SPI and this proposal is seeking to making these
65+
API.
66+
67+
## Detailed Design
68+
69+
The `TimeLimitTrait.Duration` struct only has one factory method: `public
70+
static func minutes(_ minutes: some BinaryInteger) -> Self`.
71+
72+
That ensures 2 things:
73+
1. It's impossible to create short time limits (under a minute).
74+
2. It's impossible to create high-precision increments of time.
75+
76+
Both of these features are important to ensure the API is self documenting and
77+
conveying the intended purpose.
78+
79+
For parameterized tests these time limits apply to each individual test case.
80+
81+
The `TimeLimitTrait.Duration` struct is declared as follows:
82+
83+
```swift
84+
/// A type that defines a time limit to apply to a test.
85+
///
86+
/// To add this trait to a test, use one of the following functions:
87+
///
88+
/// - ``Trait/timeLimit(_:)``
89+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
90+
public struct TimeLimitTrait: TestTrait, SuiteTrait {
91+
/// A type representing the duration of a time limit applied to a test.
92+
///
93+
/// This type is intended for use specifically for specifying test timeouts
94+
/// with ``TimeLimitTrait``. It is used instead of Swift's built-in `Duration`
95+
/// type because test timeouts do not support high-precision, arbitrarily
96+
/// short durations. The smallest allowed unit of time is minutes.
97+
public struct Duration: Sendable {
98+
99+
/// Construct a time limit duration given a number of minutes.
100+
///
101+
/// - Parameters:
102+
/// - minutes: The number of minutes the resulting duration should
103+
/// represent.
104+
///
105+
/// - Returns: A duration representing the specified number of minutes.
106+
public static func minutes(_ minutes: some BinaryInteger) -> Self
107+
}
108+
109+
/// The maximum amount of time a test may run for before timing out.
110+
public var timeLimit: Swift.Duration
111+
}
112+
```
113+
114+
The extension on `Trait` that allows for `.timeLimit(...)` to work is defined
115+
like this:
116+
117+
```
118+
/// Construct a time limit trait that causes a test to time out if it runs for
119+
/// too long.
120+
///
121+
/// - Parameters:
122+
/// - timeLimit: The maximum amount of time the test may run for.
123+
///
124+
/// - Returns: An instance of ``TimeLimitTrait``.
125+
///
126+
/// Test timeouts do not support high-precision, arbitrarily short durations
127+
/// due to variability in testing environments. The time limit must be at
128+
/// least one minute, and can only be expressed in increments of one minute.
129+
///
130+
/// When this trait is associated with a test, that test must complete within
131+
/// a time limit of, at most, `timeLimit`. If the test runs longer, an issue
132+
/// of kind ``Issue/Kind/timeLimitExceeded(timeLimitComponents:)`` is
133+
/// recorded. This timeout is treated as a test failure.
134+
///
135+
/// The time limit amount specified by `timeLimit` may be reduced if the
136+
/// testing library is configured to enforce a maximum per-test limit. When
137+
/// such a maximum is set, the effective time limit of the test this trait is
138+
/// applied to will be the lesser of `timeLimit` and that maximum. This is a
139+
/// policy which may be configured on a global basis by the tool responsible
140+
/// for launching the test process. Refer to that tool's documentation for
141+
/// more details.
142+
///
143+
/// If a test is parameterized, this time limit is applied to each of its
144+
/// test cases individually. If a test has more than one time limit associated
145+
/// with it, the shortest one is used. A test run may also be configured with
146+
/// a maximum time limit per test case.
147+
public static func timeLimit(_ timeLimit: Self.Duration) -> Self
148+
```
149+
150+
And finally, the call site of the API looks like this:
151+
152+
```swift
153+
@Test(.timeLimit(.minutes(60))
154+
func serve100CustomersInOneHour() async {
155+
for _ in 0 ..< 100 {
156+
let customer = await Customer.next()
157+
await customer.order()
158+
...
159+
}
160+
}
161+
```
162+
163+
The `TimeLimitTrait.Duration` struct has various `unavailable` overloads that
164+
are included for diagnostic purposes only. They are all documented and
165+
annotated like this:
166+
167+
```
168+
/// Construct a time limit duration given a number of <unit>.
169+
///
170+
/// This function is unavailable and is provided for diagnostic purposes only.
171+
@available(*, unavailable, message: "Time limit must be specified in minutes")
172+
```
173+
174+
## Source Compatibility
175+
176+
This is purely additional API and does not impact existing code.
177+
178+
## Integration with Supporting Tools
179+
180+
N/A
181+
182+
## Future Directions
183+
184+
N/A.
185+
186+
## Alternatives Considered
187+
188+
We have considered using `Swift.Duration` as the currency type for this API but
189+
decided against it to avoid common pitfals and misuses of this feature such as
190+
providing very small time limits that lead to flaky tests in different
191+
environments.
192+
193+
## Acknowledgments
194+
195+
The authors acknowledge valuable contributions and feedback from the Swift
196+
Testing community during the development of this proposal.

0 commit comments

Comments
 (0)