Skip to content

Commit 0b00668

Browse files
authored
Represent each parameterized test function argument as a separate element in Test.Case.arguments (#112)
1 parent f03cc98 commit 0b00668

File tree

7 files changed

+445
-150
lines changed

7 files changed

+445
-150
lines changed

Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -448,22 +448,16 @@ extension Tag.Color {
448448
extension Test.Case {
449449
/// The arguments of this test case, formatted for presentation, prefixed by
450450
/// their corresponding parameter label when available.
451-
///
452-
/// - Parameters:
453-
/// - parameters: The parameters to pair this test case's arguments with.
454-
///
455-
/// - Returns: A string containing each argument prefixed with its
456-
/// corresponding parameter label when available.
457-
fileprivate func labeledArguments(using parameters: [Test.ParameterInfo]) -> String {
458-
arguments(pairedWith: parameters).lazy
459-
.map { parameter, argument in
460-
let argumentDescription = String(describingForTest: argument)
461-
462-
let label = parameter.secondName ?? parameter.firstName
451+
fileprivate var labeledArguments: String {
452+
arguments.lazy
453+
.map { argument in
454+
let valueDescription = String(describingForTest: argument.value)
455+
456+
let label = argument.parameter.secondName ?? argument.parameter.firstName
463457
guard label != "_" else {
464-
return argumentDescription
458+
return valueDescription
465459
}
466-
return "\(label)\(argumentDescription)"
460+
return "\(label)\(valueDescription)"
467461
}
468462
.joined(separator: ", ")
469463
}
@@ -628,8 +622,8 @@ extension Event.ConsoleOutputRecorder: EventRecorder {
628622
} else {
629623
0
630624
}
631-
let labeledArguments = if let testCase = eventContext.testCase, let parameters = test?.parameters {
632-
testCase.labeledArguments(using: parameters)
625+
let labeledArguments = if let testCase = eventContext.testCase {
626+
testCase.labeledArguments
633627
} else {
634628
""
635629
}
@@ -662,12 +656,12 @@ extension Event.ConsoleOutputRecorder: EventRecorder {
662656
}
663657

664658
case .testCaseStarted:
665-
guard let testCase = eventContext.testCase, testCase.isParameterized, let parameters = test?.parameters else {
659+
guard let testCase = eventContext.testCase, testCase.isParameterized else {
666660
break
667661
}
668662
let symbol = Symbol.default.stringValue(options: options)
669663

670-
return "\(symbol) Passing \(parameters.count.counting("argument")) \(testCase.labeledArguments(using: parameters)) to \(testName)\n"
664+
return "\(symbol) Passing \(testCase.arguments.count.counting("argument")) \(testCase.labeledArguments) to \(testName)\n"
671665

672666
case .testCaseEnded:
673667
break

Sources/Testing/Test+Macro.swift

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,9 @@ extension Test {
242242
parameters paramTuples: [__ParameterInfo],
243243
testFunction: @escaping @Sendable (C.Element) async throws -> Void
244244
) -> Self where C: Collection & Sendable, C.Element: Sendable {
245-
let caseGenerator = Case.Generator(arguments: collection, testFunction: testFunction)
246-
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: paramTuples.parameters)
245+
let parameters = paramTuples.parameters
246+
let caseGenerator = Case.Generator(arguments: collection, parameters: parameters, testFunction: testFunction)
247+
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
247248
}
248249
}
249250

@@ -369,8 +370,58 @@ extension Test {
369370
parameters paramTuples: [__ParameterInfo],
370371
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
371372
) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
372-
let caseGenerator = Case.Generator(arguments: collection1, collection2, testFunction: testFunction)
373-
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: paramTuples.parameters)
373+
let parameters = paramTuples.parameters
374+
let caseGenerator = Case.Generator(arguments: collection1, collection2, parameters: parameters, testFunction: testFunction)
375+
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
376+
}
377+
378+
/// Create an instance of ``Test`` for a parameterized function.
379+
///
380+
/// This initializer overload is specialized for collections of 2-tuples to
381+
/// efficiently de-structure their elements when appropriate.
382+
///
383+
/// - Warning: This function is used to implement the `@Test` macro. Do not
384+
/// call it directly.
385+
public static func __function<C, E1, E2>(
386+
named testFunctionName: String,
387+
in containingType: Any.Type?,
388+
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
389+
displayName: String? = nil,
390+
traits: [any TestTrait],
391+
arguments collection: C,
392+
sourceLocation: SourceLocation,
393+
parameters paramTuples: [__ParameterInfo],
394+
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
395+
) -> Self where C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable {
396+
let parameters = paramTuples.parameters
397+
let caseGenerator = Case.Generator(arguments: collection, parameters: parameters, testFunction: testFunction)
398+
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
399+
}
400+
401+
/// Create an instance of ``Test`` for a parameterized function.
402+
///
403+
/// This initializer overload is specialized for dictionary collections, to
404+
/// efficiently de-structure their elements (which are known to be 2-tuples)
405+
/// when appropriate. This overload is distinct from those for other
406+
/// collections of 2-tuples because the `Element` tuple type for
407+
/// `Dictionary` includes labels (`(key: Key, value: Value)`).
408+
///
409+
/// - Warning: This function is used to implement the `@Test` macro. Do not
410+
/// call it directly.
411+
public static func __function<Key, Value>(
412+
named testFunctionName: String,
413+
in containingType: Any.Type?,
414+
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
415+
displayName: String? = nil,
416+
traits: [any TestTrait],
417+
arguments dictionary: Dictionary<Key, Value>,
418+
sourceLocation: SourceLocation,
419+
parameters paramTuples: [__ParameterInfo],
420+
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
421+
) -> Self where Key: Sendable, Value: Sendable {
422+
let parameters = paramTuples.parameters
423+
let caseGenerator = Case.Generator(arguments: dictionary, parameters: parameters, testFunction: testFunction)
424+
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
374425
}
375426

376427
/// Create an instance of ``Test`` for a parameterized function.
@@ -388,10 +439,11 @@ extension Test {
388439
parameters paramTuples: [__ParameterInfo],
389440
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
390441
) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
391-
let caseGenerator = Case.Generator(arguments: zippedCollections) {
442+
let parameters = paramTuples.parameters
443+
let caseGenerator = Case.Generator(arguments: zippedCollections, parameters: parameters) {
392444
try await testFunction($0, $1)
393445
}
394-
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: paramTuples.parameters)
446+
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
395447
}
396448
}
397449

Sources/Testing/Test.Case.Generator.swift

Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,39 @@ extension Test.Case {
7272
/// - Parameters:
7373
/// - collection: The collection of argument values for which test cases
7474
/// should be generated.
75+
/// - parameters: The parameters of the test function for which test cases
76+
/// should be generated.
7577
/// - testFunction: The test function to which each generated test case
7678
/// passes an argument value from `collection`.
79+
///
80+
/// This initializer is disfavored since it relies on `Mirror` to
81+
/// de-structure elements of tuples. Other initializers which are
82+
/// specialized to handle collections of tuple types more efficiently should
83+
/// be preferred.
84+
@_disfavoredOverload
7785
init(
7886
arguments collection: S,
87+
parameters: [Test.ParameterInfo],
7988
testFunction: @escaping @Sendable (S.Element) async throws -> Void
8089
) where S: Collection {
81-
self.init(sequence: collection) { element in
82-
Test.Case(arguments: [element]) {
83-
try await testFunction(element)
90+
if parameters.count > 1 {
91+
self.init(sequence: collection) { element in
92+
let mirror = Mirror(reflecting: element)
93+
let values: [any Sendable] = if mirror.displayStyle == .tuple {
94+
mirror.children.map { unsafeBitCast($0.value, to: (any Sendable).self) }
95+
} else {
96+
[element]
97+
}
98+
99+
return Test.Case(values: values, parameters: parameters) {
100+
try await testFunction(element)
101+
}
102+
}
103+
} else {
104+
self.init(sequence: collection) { element in
105+
Test.Case(values: [element], parameters: parameters) {
106+
try await testFunction(element)
107+
}
84108
}
85109
}
86110
}
@@ -93,34 +117,136 @@ extension Test.Case {
93117
/// cases should be generated.
94118
/// - collection2: The second collection of argument values for which test
95119
/// cases should be generated.
120+
/// - parameters: The parameters of the test function for which test cases
121+
/// should be generated.
96122
/// - testFunction: The test function to which each generated test case
97123
/// passes an argument value from `collection`.
98124
init<C1, C2>(
99125
arguments collection1: C1, _ collection2: C2,
126+
parameters: [Test.ParameterInfo],
100127
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
101128
) where S == CartesianProduct<C1, C2> {
102129
self.init(sequence: cartesianProduct(collection1, collection2)) { element in
103-
Test.Case(arguments: [element.0, element.1]) {
130+
Test.Case(values: [element.0, element.1], parameters: parameters) {
104131
try await testFunction(element.0, element.1)
105132
}
106133
}
107134
}
108135

136+
/// Initialize an instance of this type that iterates over the specified
137+
/// sequence of 2-tuple argument values.
138+
///
139+
/// - Parameters:
140+
/// - sequence: The sequence of 2-tuple argument values for which test
141+
/// cases should be generated.
142+
/// - parameters: The parameters of the test function for which test cases
143+
/// should be generated.
144+
/// - testFunction: The test function to which each generated test case
145+
/// passes an argument value from `sequence`.
146+
///
147+
/// This initializer overload is specialized for sequences of 2-tuples to
148+
/// efficiently de-structure their elements when appropriate.
149+
///
150+
/// @Comment {
151+
/// - Bug: The testing library should support variadic generics.
152+
/// ([103416861](rdar://103416861))
153+
/// }
154+
private init<E1, E2>(
155+
sequence: S,
156+
parameters: [Test.ParameterInfo],
157+
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
158+
) where S.Element == (E1, E2), E1: Sendable, E2: Sendable {
159+
if parameters.count > 1 {
160+
self.init(sequence: sequence) { element in
161+
Test.Case(values: [element.0, element.1], parameters: parameters) {
162+
try await testFunction(element)
163+
}
164+
}
165+
} else {
166+
self.init(sequence: sequence) { element in
167+
Test.Case(values: [element], parameters: parameters) {
168+
try await testFunction(element)
169+
}
170+
}
171+
}
172+
}
173+
174+
/// Initialize an instance of this type that iterates over the specified
175+
/// collection of 2-tuple argument values.
176+
///
177+
/// - Parameters:
178+
/// - collection: The collection of 2-tuple argument values for which test
179+
/// cases should be generated.
180+
/// - parameters: The parameters of the test function for which test cases
181+
/// should be generated.
182+
/// - testFunction: The test function to which each generated test case
183+
/// passes an argument value from `collection`.
184+
///
185+
/// This initializer overload is specialized for collections of 2-tuples to
186+
/// efficiently de-structure their elements when appropriate.
187+
///
188+
/// @Comment {
189+
/// - Bug: The testing library should support variadic generics.
190+
/// ([103416861](rdar://103416861))
191+
/// }
192+
init<E1, E2>(
193+
arguments collection: S,
194+
parameters: [Test.ParameterInfo],
195+
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
196+
) where S: Collection, S.Element == (E1, E2) {
197+
self.init(sequence: collection, parameters: parameters, testFunction: testFunction)
198+
}
199+
109200
/// Initialize an instance of this type that iterates over the specified
110201
/// zipped sequence of argument values.
111202
///
112203
/// - Parameters:
113204
/// - zippedCollections: A zipped sequence of argument values for which
114205
/// test cases should be generated.
206+
/// - parameters: The parameters of the test function for which test cases
207+
/// should be generated.
115208
/// - testFunction: The test function to which each generated test case
116209
/// passes an argument value from `zippedCollections`.
117210
init<C1, C2>(
118211
arguments zippedCollections: Zip2Sequence<C1, C2>,
212+
parameters: [Test.ParameterInfo],
119213
testFunction: @escaping @Sendable ((C1.Element, C2.Element)) async throws -> Void
120-
) where S == Zip2Sequence<C1, C2> {
121-
self.init(sequence: zippedCollections) { element in
122-
Test.Case(arguments: [element]) {
123-
try await testFunction(element)
214+
) where S == Zip2Sequence<C1, C2>, C1: Collection, C2: Collection {
215+
self.init(sequence: zippedCollections, parameters: parameters, testFunction: testFunction)
216+
}
217+
218+
/// Initialize an instance of this type that iterates over the specified
219+
/// dictionary of argument values.
220+
///
221+
/// - Parameters:
222+
/// - dictionary: A dictionary of argument values for which test cases
223+
/// should be generated.
224+
/// - parameters: The parameters of the test function for which test cases
225+
/// should be generated.
226+
/// - testFunction: The test function to which each generated test case
227+
/// passes an argument value from `dictionary`.
228+
///
229+
/// This initializer overload is specialized for dictionary collections, to
230+
/// efficiently de-structure their elements (which are known to be 2-tuples)
231+
/// when appropriate. This overload is distinct from those for other
232+
/// collections of 2-tuples because the `Element` tuple type for
233+
/// `Dictionary` includes labels (`(key: Key, value: Value)`).
234+
init<Key, Value>(
235+
arguments dictionary: Dictionary<Key, Value>,
236+
parameters: [Test.ParameterInfo],
237+
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
238+
) where S == Dictionary<Key, Value> {
239+
if parameters.count > 1 {
240+
self.init(sequence: dictionary) { element in
241+
Test.Case(values: [element.key, element.value], parameters: parameters) {
242+
try await testFunction(element)
243+
}
244+
}
245+
} else {
246+
self.init(sequence: dictionary) { element in
247+
Test.Case(values: [element], parameters: parameters) {
248+
try await testFunction(element)
249+
}
124250
}
125251
}
126252
}

0 commit comments

Comments
 (0)