Skip to content

Commit 92c1165

Browse files
authored
Avoid constructing a Test for an unavailable parameterized test function. (swiftlang#146)
* Revert "Do not evaluate unavailable test function parameters. (swiftlang#133)" This reverts commit 9e105df. * Avoid constructing a `Test` for an unavailable parameterized test function. This PR rethinks the work in swiftlang#133. Instead of lazily evaluating individual arguments, we can instead lazily evaluate the entire test. In the event the test is unavailable, we substitute a zero-argument no-op function so that there is no opportunity for us to touch unavailable type metadata. 🤞🏻 Resolves rdar://118996437.
1 parent 7e18964 commit 92c1165

13 files changed

+290
-222
lines changed

Sources/Testing/Parameterization/Test.Case.Generator.swift

+28-39
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,14 @@ extension Test.Case {
1919
/// ([103416861](rdar://103416861))
2020
/// }
2121
struct Generator<S>: Sendable where S: Sequence & Sendable, S.Element: Sendable {
22-
/// A closure that produces the underlying sequence of argument values.
22+
/// The underlying sequence of argument values.
2323
///
24-
/// The resulting sequence _must_ be iterable multiple times. Hence,
25-
/// initializers accept only _collections_, not sequences. The constraint
26-
/// here is only to `Sequence` to allow the storage of computed sequences
27-
/// over collections (such as `CartesianProduct` or `Zip2Sequence`) that are
28-
/// safe to iterate multiple times.
29-
///
30-
/// This property is a closure rather than an instance of `S` so that it can
31-
/// be lazily evaluated rather than requiring evaluation as soon as the
32-
/// owning instance of ``Test`` is initialized.
33-
private var _sequence: @Sendable () async -> S
24+
/// The sequence _must_ be iterable multiple times. Hence, initializers
25+
/// accept only _collections_, not sequences. The constraint here is only to
26+
/// `Sequence` to allow the storage of computed sequences over collections
27+
/// (such as `CartesianProduct` or `Zip2Sequence`) that are safe to iterate
28+
/// multiple times.
29+
private var _sequence: S
3430

3531
/// A closure that maps an element from `_sequence` to a test case instance.
3632
///
@@ -43,12 +39,12 @@ extension Test.Case {
4339
/// Initialize an instance of this type.
4440
///
4541
/// - Parameters:
46-
/// - sequence: A closure that produces the sequence of argument values
47-
/// for which test cases should be generated.
48-
/// - mapElement: A function that maps each element in the result of
49-
/// `sequence` to a corresponding instance of ``Test/Case``.
42+
/// - sequence: The sequence of argument values for which test cases
43+
/// should be generated.
44+
/// - mapElement: A function that maps each element in `sequence` to a
45+
/// corresponding instance of ``Test/Case``.
5046
private init(
51-
sequence: @escaping @Sendable () async -> S,
47+
sequence: S,
5248
mapElement: @escaping @Sendable (_ element: S.Element) -> Test.Case
5349
) {
5450
_sequence = sequence
@@ -65,9 +61,7 @@ extension Test.Case {
6561
) where S == CollectionOfOne<Void> {
6662
// A beautiful hack to give us the right number of cases: iterate over a
6763
// collection containing a single Void value.
68-
self.init {
69-
CollectionOfOne(())
70-
} mapElement: { _ in
64+
self.init(sequence: CollectionOfOne(())) { _ in
7165
Test.Case(arguments: [], body: testFunction)
7266
}
7367
}
@@ -89,7 +83,7 @@ extension Test.Case {
8983
/// be preferred.
9084
@_disfavoredOverload
9185
init(
92-
arguments collection: @escaping @Sendable () async -> S,
86+
arguments collection: S,
9387
parameters: [Test.ParameterInfo],
9488
testFunction: @escaping @Sendable (S.Element) async throws -> Void
9589
) where S: Collection {
@@ -128,13 +122,11 @@ extension Test.Case {
128122
/// - testFunction: The test function to which each generated test case
129123
/// passes an argument value from `collection`.
130124
init<C1, C2>(
131-
arguments collection1: @escaping @Sendable () async -> C1, _ collection2: @escaping @Sendable () async -> C2,
125+
arguments collection1: C1, _ collection2: C2,
132126
parameters: [Test.ParameterInfo],
133127
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
134128
) where S == CartesianProduct<C1, C2> {
135-
self.init {
136-
await cartesianProduct(collection1(), collection2())
137-
} mapElement: { element in
129+
self.init(sequence: cartesianProduct(collection1, collection2)) { element in
138130
Test.Case(values: [element.0, element.1], parameters: parameters) {
139131
try await testFunction(element.0, element.1)
140132
}
@@ -160,7 +152,7 @@ extension Test.Case {
160152
/// ([103416861](rdar://103416861))
161153
/// }
162154
private init<E1, E2>(
163-
sequence: @escaping @Sendable () async -> S,
155+
sequence: S,
164156
parameters: [Test.ParameterInfo],
165157
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
166158
) where S.Element == (E1, E2), E1: Sendable, E2: Sendable {
@@ -198,7 +190,7 @@ extension Test.Case {
198190
/// ([103416861](rdar://103416861))
199191
/// }
200192
init<E1, E2>(
201-
arguments collection: @escaping @Sendable () async -> S,
193+
arguments collection: S,
202194
parameters: [Test.ParameterInfo],
203195
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
204196
) where S: Collection, S.Element == (E1, E2) {
@@ -216,7 +208,7 @@ extension Test.Case {
216208
/// - testFunction: The test function to which each generated test case
217209
/// passes an argument value from `zippedCollections`.
218210
init<C1, C2>(
219-
arguments zippedCollections: @escaping @Sendable () async -> Zip2Sequence<C1, C2>,
211+
arguments zippedCollections: Zip2Sequence<C1, C2>,
220212
parameters: [Test.ParameterInfo],
221213
testFunction: @escaping @Sendable ((C1.Element, C2.Element)) async throws -> Void
222214
) where S == Zip2Sequence<C1, C2>, C1: Collection, C2: Collection {
@@ -240,7 +232,7 @@ extension Test.Case {
240232
/// collections of 2-tuples because the `Element` tuple type for
241233
/// `Dictionary` includes labels (`(key: Key, value: Value)`).
242234
init<Key, Value>(
243-
arguments dictionary: @escaping @Sendable () async -> Dictionary<Key, Value>,
235+
arguments dictionary: Dictionary<Key, Value>,
244236
parameters: [Test.ParameterInfo],
245237
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
246238
) where S == Dictionary<Key, Value> {
@@ -261,17 +253,14 @@ extension Test.Case {
261253
}
262254
}
263255

264-
// MARK: - Sequence generation
256+
// MARK: - Sequence
265257

266-
extension Test.Case.Generator {
267-
/// Generate a sequence of test cases corresponding to the sequence of
268-
/// elements passed to this instance during instantiation.
269-
///
270-
/// - Returns:
271-
/// A sequence of ``Test/Case`` instances.
272-
///
273-
/// Each call to this function generates a new sequence.
274-
func generate() async -> some Sequence<Test.Case> {
275-
await _sequence().lazy.map(_mapElement)
258+
extension Test.Case.Generator: Sequence {
259+
func makeIterator() -> some IteratorProtocol<Test.Case> {
260+
_sequence.lazy.map(_mapElement).makeIterator()
261+
}
262+
263+
var underestimatedCount: Int {
264+
_sequence.underestimatedCount
276265
}
277266
}

Sources/Testing/Running/Runner.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ extension Runner {
171171
}
172172
}
173173

174-
if let step = stepGraph.value, case .run = step.action, let testCases = await step.test.testCases {
174+
if let step = stepGraph.value, case .run = step.action, let testCases = step.test.testCases {
175175
try await Test.withCurrent(step.test) {
176176
try await _withErrorHandling(for: step, sourceLocation: step.test.sourceLocation) {
177177
try await _runTestCases(testCases, within: step)

Sources/Testing/Test+Macro.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ extension Test {
235235
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
236236
displayName: String? = nil,
237237
traits: [any TestTrait],
238-
arguments collection: @escaping @Sendable () async -> C,
238+
arguments collection: C,
239239
sourceLocation: SourceLocation,
240240
parameters paramTuples: [__ParameterInfo],
241241
testFunction: @escaping @Sendable (C.Element) async throws -> Void
@@ -363,7 +363,7 @@ extension Test {
363363
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
364364
displayName: String? = nil,
365365
traits: [any TestTrait],
366-
arguments collection1: @escaping @Sendable () async -> C1, _ collection2: @escaping @Sendable () async -> C2,
366+
arguments collection1: C1, _ collection2: C2,
367367
sourceLocation: SourceLocation,
368368
parameters paramTuples: [__ParameterInfo],
369369
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
@@ -386,7 +386,7 @@ extension Test {
386386
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
387387
displayName: String? = nil,
388388
traits: [any TestTrait],
389-
arguments collection: @escaping @Sendable () async -> C,
389+
arguments collection: C,
390390
sourceLocation: SourceLocation,
391391
parameters paramTuples: [__ParameterInfo],
392392
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
@@ -412,7 +412,7 @@ extension Test {
412412
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
413413
displayName: String? = nil,
414414
traits: [any TestTrait],
415-
arguments dictionary: @escaping @Sendable () async -> Dictionary<Key, Value>,
415+
arguments dictionary: Dictionary<Key, Value>,
416416
sourceLocation: SourceLocation,
417417
parameters paramTuples: [__ParameterInfo],
418418
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
@@ -432,7 +432,7 @@ extension Test {
432432
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
433433
displayName: String? = nil,
434434
traits: [any TestTrait],
435-
arguments zippedCollections: @escaping @Sendable () async -> Zip2Sequence<C1, C2>,
435+
arguments zippedCollections: Zip2Sequence<C1, C2>,
436436
sourceLocation: SourceLocation,
437437
parameters paramTuples: [__ParameterInfo],
438438
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void

Sources/Testing/Test.swift

+35-16
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,32 @@ public struct Test: Sendable {
5353
/// The source location of this test.
5454
public var sourceLocation: SourceLocation
5555

56+
/// The (underestimated) number of iterations that will need to occur during
57+
/// testing.
58+
///
59+
/// The value of this property is inherently capped at `Int.max`. In practice,
60+
/// the number of iterations that can run in a reasonable timespan will be
61+
/// significantly lower.
62+
///
63+
/// For instances of ``Test`` that represent non-parameterized test functions
64+
/// (that is, test functions that do not iterate over a sequence of inputs),
65+
/// the value of this property is always `1`. For instances of ``Test`` that
66+
/// represent test suite types, the value of this property is always `nil`.
67+
///
68+
/// For more information about underestimated counts, see the documentation
69+
/// for [`Sequence`](https://developer.apple.com/documentation/swift/array/underestimatedcount-4ggqp).
70+
@_spi(ExperimentalParameterizedTesting)
71+
public var underestimatedCaseCount: Int? {
72+
// NOTE: it is important that we only expose an _underestimated_ count for
73+
// two reasons:
74+
// 1. If the total number of cases exceeds `.max` due to combinatoric
75+
// complexity, `count` would be too low; and
76+
// 2. We reserve the right to support async sequences as input in the
77+
// future, and async sequences do not have `count` properties (but an
78+
// underestimated count of `0` is still technically correct.)
79+
testCases?.underestimatedCount
80+
}
81+
5682
/// The type containing this test, if any.
5783
///
5884
/// If a test is associated with a free function or static function, the value
@@ -70,29 +96,22 @@ public struct Test: Sendable {
7096

7197
/// Storage for the ``testCases`` property.
7298
///
73-
/// This use of `AnySequence` is necessary because it is not currently
74-
/// possible to express `Sequence<Test.Case> & Sendable` as an existential
75-
/// (`any`) ([96960993](rdar://96960993)). It is also not possible to have a
76-
/// value of an underlying generic sequence type without specifying its
77-
/// generic parameters.
78-
private var _testCases: (@Sendable () async -> AnySequence<Test.Case>)?
99+
/// This use of `UncheckedSendable` and of `AnySequence` is necessary because
100+
/// it is not currently possible to express `Sequence<Test.Case> & Sendable`
101+
/// as an existential (`any`) ([96960993](rdar://96960993)). It is also not
102+
/// possible to have a value of an underlying generic sequence type without
103+
/// specifying its generic parameters.
104+
private var _testCases: UncheckedSendable<AnySequence<Test.Case>>?
79105

80106
/// The set of test cases associated with this test, if any.
81107
///
82108
/// For parameterized tests, each test case is associated with a single
83109
/// combination of parameterized inputs. For non-parameterized tests, a single
84110
/// test case is synthesized. For test suite types (as opposed to test
85111
/// functions), the value of this property is `nil`.
86-
///
87-
/// - Warning: The parameterized inputs to a test may have limited
88-
/// availability if the test has the `@available` attribute applied to it.
89-
/// This property does not evaluate availability, and the effect of reading
90-
/// it on a platform where the inputs are unavailable is undefined.
91112
@_spi(ExperimentalParameterizedTesting)
92113
public var testCases: (some Sequence<Test.Case> & Sendable)? {
93-
get async {
94-
await _testCases?()
95-
}
114+
_testCases?.rawValue
96115
}
97116

98117
/// Whether or not this test is parameterized.
@@ -121,7 +140,7 @@ public struct Test: Sendable {
121140
///
122141
/// A test suite can be declared using the ``Suite(_:_:)`` macro.
123142
public var isSuite: Bool {
124-
containingType != nil && _testCases == nil
143+
containingType != nil && testCases == nil
125144
}
126145

127146
/// Initialize an instance of this type representing a test suite type.
@@ -156,7 +175,7 @@ public struct Test: Sendable {
156175
self.sourceLocation = sourceLocation
157176
self.containingType = containingType
158177
self.xcTestCompatibleSelector = xcTestCompatibleSelector
159-
self._testCases = { await .init(testCases.generate()) }
178+
self._testCases = .init(rawValue: .init(testCases))
160179
self.parameters = parameters
161180
}
162181
}

Sources/TestingMacros/Support/AttributeDiscovery.swift

+1-4
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,7 @@ struct AttributeInfo {
218218
ArrayElementSyntax(expression: traitExpr)
219219
}
220220
}))
221-
// TODO: extract arguments: ... here, rather than assuming all "other" arguments are parameterized inputs
222-
arguments += otherArguments.map { argument in
223-
Argument(label: argument.label, expression: "{ \(argument.expression.trimmed) }")
224-
}
221+
arguments += otherArguments
225222
arguments.append(Argument(label: "sourceLocation", expression: sourceLocation))
226223

227224
return LabeledExprListSyntax(arguments)

0 commit comments

Comments
 (0)