-
Notifications
You must be signed in to change notification settings - Fork 102
/
Copy pathTimeLimitTrait.swift
336 lines (309 loc) · 12.9 KB
/
TimeLimitTrait.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//
/// A type that defines a time limit to apply to a test.
///
/// To add this trait to a test, use one of the following functions:
///
/// - ``Trait/timeLimit(_:)``
@available(_clockAPI, *)
public struct TimeLimitTrait: TestTrait, SuiteTrait {
/// A type representing the duration of a time limit applied to a test.
///
/// This type is intended for use specifically for specifying test timeouts
/// with ``TimeLimitTrait``. It is used instead of Swift's built-in `Duration`
/// type because test timeouts do not support high-precision, arbitrarily
/// short durations. The smallest allowed unit of time is minutes.
public struct Duration: Sendable {
/// The underlying Swift `Duration` which this time limit duration
/// represents.
var underlyingDuration: Swift.Duration
/// Construct a time limit duration given a number of minutes.
///
/// - Parameters:
/// - minutes: The number of minutes the resulting duration should
/// represent.
///
/// - Returns: A duration representing the specified number of minutes.
public static func minutes(_ minutes: some BinaryInteger) -> Self {
Self(underlyingDuration: .seconds(60) * minutes)
}
}
/// The maximum amount of time a test may run for before timing out.
public var timeLimit: Swift.Duration
public var isRecursive: Bool {
// Since test functions cannot be nested inside other test functions,
// inheriting time limits from a parent test implies inheriting them from
// parent test suite types. Types do not take any time to execute on their
// own (other than some testing library overhead.)
true
}
}
// MARK: -
@available(_clockAPI, *)
extension Trait where Self == TimeLimitTrait {
/// Construct a time limit trait that causes a test to time out if it runs for
/// too long.
///
/// - Parameters:
/// - timeLimit: The maximum amount of time the test may run for.
///
/// - Returns: An instance of ``TimeLimitTrait``.
///
/// Test timeouts do not support high-precision, arbitrarily short durations
/// due to variability in testing environments. The time limit must be at
/// least one minute, and can only be expressed in increments of one minute.
///
/// When this trait is associated with a test, that test must complete within
/// a time limit of, at most, `timeLimit`. If the test runs longer, an issue
/// of kind ``Issue/Kind/timeLimitExceeded(timeLimitComponents:)`` is
/// recorded. This timeout is treated as a test failure.
///
/// The time limit amount specified by `timeLimit` may be reduced if the
/// testing library is configured to enforce a maximum per-test limit. When
/// such a maximum is set, the effective time limit of the test this trait is
/// applied to will be the lesser of `timeLimit` and that maximum. This is a
/// policy which may be configured on a global basis by the tool responsible
/// for launching the test process. Refer to that tool's documentation for
/// more details.
///
/// If a test is parameterized, this time limit is applied to each of its
/// test cases individually. If a test has more than one time limit associated
/// with it, the shortest one is used. A test run may also be configured with
/// a maximum time limit per test case.
@_spi(Experimental)
public static func timeLimit(_ timeLimit: Duration) -> Self {
return Self(timeLimit: timeLimit)
}
/// Construct a time limit trait that causes a test to time out if it runs for
/// too long.
///
/// - Parameters:
/// - timeLimit: The maximum amount of time the test may run for.
///
/// - Returns: An instance of ``TimeLimitTrait``.
///
/// Test timeouts do not support high-precision, arbitrarily short durations
/// due to variability in testing environments. The time limit must be at
/// least one minute, and can only be expressed in increments of one minute.
///
/// When this trait is associated with a test, that test must complete within
/// a time limit of, at most, `timeLimit`. If the test runs longer, an issue
/// of kind ``Issue/Kind/timeLimitExceeded(timeLimitComponents:)`` is
/// recorded. This timeout is treated as a test failure.
///
/// The time limit amount specified by `timeLimit` may be reduced if the
/// testing library is configured to enforce a maximum per-test limit. When
/// such a maximum is set, the effective time limit of the test this trait is
/// applied to will be the lesser of `timeLimit` and that maximum. This is a
/// policy which may be configured on a global basis by the tool responsible
/// for launching the test process. Refer to that tool's documentation for
/// more details.
///
/// If a test is parameterized, this time limit is applied to each of its
/// test cases individually. If a test has more than one time limit associated
/// with it, the shortest one is used. A test run may also be configured with
/// a maximum time limit per test case.
public static func timeLimit(_ timeLimit: Self.Duration) -> Self {
return Self(timeLimit: timeLimit.underlyingDuration)
}
}
@available(_clockAPI, *)
extension TimeLimitTrait.Duration {
/// Construct a time limit duration given a number of seconds.
///
/// This function is unavailable and is provided for diagnostic purposes only.
@available(*, unavailable, message: "Time limit must be specified in minutes")
public static func seconds(_ seconds: some BinaryInteger) -> Self {
fatalError("Unsupported")
}
/// Construct a time limit duration given a number of seconds.
///
/// This function is unavailable and is provided for diagnostic purposes only.
@available(*, unavailable, message: "Time limit must be specified in minutes")
public static func seconds(_ seconds: Double) -> Self {
fatalError("Unsupported")
}
/// Construct a time limit duration given a number of milliseconds.
///
/// This function is unavailable and is provided for diagnostic purposes only.
@available(*, unavailable, message: "Time limit must be specified in minutes")
public static func milliseconds(_ milliseconds: some BinaryInteger) -> Self {
fatalError("Unsupported")
}
/// Construct a time limit duration given a number of milliseconds.
///
/// This function is unavailable and is provided for diagnostic purposes only.
@available(*, unavailable, message: "Time limit must be specified in minutes")
public static func milliseconds(_ milliseconds: Double) -> Self {
fatalError("Unsupported")
}
/// Construct a time limit duration given a number of microseconds.
///
/// This function is unavailable and is provided for diagnostic purposes only.
@available(*, unavailable, message: "Time limit must be specified in minutes")
public static func microseconds(_ microseconds: some BinaryInteger) -> Self {
fatalError("Unsupported")
}
/// Construct a time limit duration given a number of microseconds.
///
/// This function is unavailable and is provided for diagnostic purposes only.
@available(*, unavailable, message: "Time limit must be specified in minutes")
public static func microseconds(_ microseconds: Double) -> Self {
fatalError("Unsupported")
}
/// Construct a time limit duration given a number of nanoseconds.
///
/// This function is unavailable and is provided for diagnostic purposes only.
@available(*, unavailable, message: "Time limit must be specified in minutes")
public static func nanoseconds(_ nanoseconds: some BinaryInteger) -> Self {
fatalError("Unsupported")
}
}
// MARK: -
@available(_clockAPI, *)
extension Test {
/// The maximum amount of time the cases of this test may run for.
///
/// Time limits are associated with tests using this trait:
///
/// - ``Trait/timeLimit(_:)``
///
/// If a test has more than one time limit associated with it, the value of
/// this property is the shortest one. If a test has no time limits associated
/// with it, the value of this property is `nil`.
public var timeLimit: Duration? {
traits.lazy
.compactMap { $0 as? TimeLimitTrait }
.map(\.timeLimit)
.min()
}
/// Get the maximum amount of time the cases of this test may run for, taking
/// the current configuration and any library-imposed rules into account.
///
/// - Parameters:
/// - configuration: The current configuration.
///
/// - Returns: The maximum amount of time the cases of this test may run for,
/// or `nil` if the test may run indefinitely.
@_spi(ForToolsIntegrationOnly)
public func adjustedTimeLimit(configuration: Configuration) -> Duration? {
// If this instance doesn't have a time limit configured, use the default
// specified by the configuration.
var timeLimit = timeLimit ?? configuration.defaultTestTimeLimit
// Round the time limit.
timeLimit = timeLimit.map { timeLimit in
let granularity = configuration.testTimeLimitGranularity
return granularity * (timeLimit / granularity).rounded(.awayFromZero)
}
// Do not exceed the maximum time limit specified by the configuration.
// Perform this step after rounding to avoid exceeding the maximum (which
// could occur if it is not a multiple of the granularity value.)
if let maximumTestTimeLimit = configuration.maximumTestTimeLimit {
timeLimit = timeLimit.map { timeLimit in
min(timeLimit, maximumTestTimeLimit)
} ?? maximumTestTimeLimit
}
return timeLimit
}
}
// MARK: -
/// An error that is reported when a test times out.
///
/// This type is not part of the public interface of the testing library.
struct TimeoutError: Error, CustomStringConvertible {
/// The time limit exceeded by the test that timed out.
var timeLimit: TimeValue
var description: String {
"Timed out after \(timeLimit) seconds."
}
}
#if !SWT_NO_UNSTRUCTURED_TASKS
/// Invoke a function with a timeout.
///
/// - Parameters:
/// - timeLimit: The amount of time until the closure times out.
/// - body: The function to invoke.
/// - timeoutHandler: A function to invoke if `body` times out.
///
/// - Throws: Any error thrown by `body`.
///
/// If `body` does not return or throw before `timeLimit` is reached,
/// `timeoutHandler` is called and given the opportunity to handle the timeout
/// and `body` is cancelled.
///
/// This function is not part of the public interface of the testing library.
@available(_clockAPI, *)
func withTimeLimit(
_ timeLimit: Duration,
_ body: @escaping @Sendable () async throws -> Void,
timeoutHandler: @escaping @Sendable () -> Void
) async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
// If sleep() returns instead of throwing a CancellationError, that means
// the timeout was reached before this task could be cancelled, so call
// the timeout handler.
try await Test.Clock.sleep(for: timeLimit)
timeoutHandler()
}
group.addTask(operation: body)
defer {
group.cancelAll()
}
try await group.next()!
}
}
#endif
/// Invoke a closure with a time limit derived from an instance of ``Test``.
///
/// - Parameters:
/// - test: The test that may time out.
/// - configuration: The current configuration.
/// - body: The function to invoke.
/// - timeoutHandler: A function to invoke if `body` times out. The time limit
/// applied to `body` is passed to this function.
///
/// - Throws: Any error thrown by `body`.
///
/// This function is provided as a cross-platform convenience wrapper around
/// ``withTimeLimit(_:_:timeoutHandler:)``. If the current platform does not
/// support time limits or the Swift clock API, this function invokes `body`
/// with no time limit.
///
/// This function is not part of the public interface of the testing library.
func withTimeLimit(
for test: Test,
configuration: Configuration,
_ body: @escaping @Sendable () async throws -> Void,
timeoutHandler: @escaping @Sendable (_ timeLimit: (seconds: Int64, attoseconds: Int64)) -> Void
) async throws {
if #available(_clockAPI, *),
let timeLimit = test.adjustedTimeLimit(configuration: configuration) {
#if SWT_NO_UNSTRUCTURED_TASKS
// This environment may not support full concurrency, so check if the body
// closure timed out after it returns. This won't help us catch hangs, but
// it will at least report tests that run longer than expected.
let start = Test.Clock.Instant.now
defer {
if start.duration(to: .now) > timeLimit {
timeoutHandler(timeLimit.components)
}
}
try await body()
#else
return try await withTimeLimit(timeLimit) {
try await body()
} timeoutHandler: {
timeoutHandler(timeLimit.components)
}
#endif
}
try await body()
}