Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 12 additions & 18 deletions Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -448,22 +448,16 @@ extension Tag.Color {
extension Test.Case {
/// The arguments of this test case, formatted for presentation, prefixed by
/// their corresponding parameter label when available.
///
/// - Parameters:
/// - parameters: The parameters to pair this test case's arguments with.
///
/// - Returns: A string containing each argument prefixed with its
/// corresponding parameter label when available.
fileprivate func labeledArguments(using parameters: [Test.ParameterInfo]) -> String {
arguments(pairedWith: parameters).lazy
.map { parameter, argument in
let argumentDescription = String(describingForTest: argument)

let label = parameter.secondName ?? parameter.firstName
fileprivate var labeledArguments: String {
arguments.lazy
.map { argument in
let valueDescription = String(describingForTest: argument.value)

let label = argument.parameter.secondName ?? argument.parameter.firstName
guard label != "_" else {
return argumentDescription
return valueDescription
}
return "\(label) → \(argumentDescription)"
return "\(label) → \(valueDescription)"
}
.joined(separator: ", ")
}
Expand Down Expand Up @@ -628,8 +622,8 @@ extension Event.ConsoleOutputRecorder: EventRecorder {
} else {
0
}
let labeledArguments = if let testCase = eventContext.testCase, let parameters = test?.parameters {
testCase.labeledArguments(using: parameters)
let labeledArguments = if let testCase = eventContext.testCase {
testCase.labeledArguments
} else {
""
}
Expand Down Expand Up @@ -662,12 +656,12 @@ extension Event.ConsoleOutputRecorder: EventRecorder {
}

case .testCaseStarted:
guard let testCase = eventContext.testCase, testCase.isParameterized, let parameters = test?.parameters else {
guard let testCase = eventContext.testCase, testCase.isParameterized else {
break
}
let symbol = Symbol.default.stringValue(options: options)

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

case .testCaseEnded:
break
Expand Down
64 changes: 58 additions & 6 deletions Sources/Testing/Test+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,9 @@ extension Test {
parameters paramTuples: [__ParameterInfo],
testFunction: @escaping @Sendable (C.Element) async throws -> Void
) -> Self where C: Collection & Sendable, C.Element: Sendable {
let caseGenerator = Case.Generator(arguments: collection, testFunction: testFunction)
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: paramTuples.parameters)
let parameters = paramTuples.parameters
let caseGenerator = Case.Generator(arguments: collection, parameters: parameters, testFunction: testFunction)
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
}
}

Expand Down Expand Up @@ -369,8 +370,58 @@ extension Test {
parameters paramTuples: [__ParameterInfo],
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
let caseGenerator = Case.Generator(arguments: collection1, collection2, testFunction: testFunction)
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: paramTuples.parameters)
let parameters = paramTuples.parameters
let caseGenerator = Case.Generator(arguments: collection1, collection2, parameters: parameters, testFunction: testFunction)
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
}

/// Create an instance of ``Test`` for a parameterized function.
///
/// This initializer overload is specialized for collections of 2-tuples to
/// efficiently de-structure their elements when appropriate.
///
/// - Warning: This function is used to implement the `@Test` macro. Do not
/// call it directly.
public static func __function<C, E1, E2>(
named testFunctionName: String,
in containingType: Any.Type?,
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
displayName: String? = nil,
traits: [any TestTrait],
arguments collection: C,
sourceLocation: SourceLocation,
parameters paramTuples: [__ParameterInfo],
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
) -> Self where C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable {
let parameters = paramTuples.parameters
let caseGenerator = Case.Generator(arguments: collection, parameters: parameters, testFunction: testFunction)
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
}

/// Create an instance of ``Test`` for a parameterized function.
///
/// This initializer overload is specialized for dictionary collections, to
/// efficiently de-structure their elements (which are known to be 2-tuples)
/// when appropriate. This overload is distinct from those for other
/// collections of 2-tuples because the `Element` tuple type for
/// `Dictionary` includes labels (`(key: Key, value: Value)`).
///
/// - Warning: This function is used to implement the `@Test` macro. Do not
/// call it directly.
public static func __function<Key, Value>(
named testFunctionName: String,
in containingType: Any.Type?,
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
displayName: String? = nil,
traits: [any TestTrait],
arguments dictionary: Dictionary<Key, Value>,
sourceLocation: SourceLocation,
parameters paramTuples: [__ParameterInfo],
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
) -> Self where Key: Sendable, Value: Sendable {
let parameters = paramTuples.parameters
let caseGenerator = Case.Generator(arguments: dictionary, parameters: parameters, testFunction: testFunction)
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingType: containingType, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
}

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

Expand Down
142 changes: 134 additions & 8 deletions Sources/Testing/Test.Case.Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,39 @@ extension Test.Case {
/// - Parameters:
/// - collection: The collection of argument values for which test cases
/// should be generated.
/// - parameters: The parameters of the test function for which test cases
/// should be generated.
/// - testFunction: The test function to which each generated test case
/// passes an argument value from `collection`.
///
/// This initializer is disfavored since it relies on `Mirror` to
/// de-structure elements of tuples. Other initializers which are
/// specialized to handle collections of tuple types more efficiently should
/// be preferred.
@_disfavoredOverload
init(
arguments collection: S,
parameters: [Test.ParameterInfo],
testFunction: @escaping @Sendable (S.Element) async throws -> Void
) where S: Collection {
self.init(sequence: collection) { element in
Test.Case(arguments: [element]) {
try await testFunction(element)
if parameters.count > 1 {
self.init(sequence: collection) { element in
let mirror = Mirror(reflecting: element)
let values: [any Sendable] = if mirror.displayStyle == .tuple {
mirror.children.map { unsafeBitCast($0.value, to: (any Sendable).self) }
} else {
[element]
}

return Test.Case(values: values, parameters: parameters) {
try await testFunction(element)
}
}
} else {
self.init(sequence: collection) { element in
Test.Case(values: [element], parameters: parameters) {
try await testFunction(element)
}
}
}
}
Expand All @@ -93,34 +117,136 @@ extension Test.Case {
/// cases should be generated.
/// - collection2: The second collection of argument values for which test
/// cases should be generated.
/// - parameters: The parameters of the test function for which test cases
/// should be generated.
/// - testFunction: The test function to which each generated test case
/// passes an argument value from `collection`.
init<C1, C2>(
arguments collection1: C1, _ collection2: C2,
parameters: [Test.ParameterInfo],
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
) where S == CartesianProduct<C1, C2> {
self.init(sequence: cartesianProduct(collection1, collection2)) { element in
Test.Case(arguments: [element.0, element.1]) {
Test.Case(values: [element.0, element.1], parameters: parameters) {
try await testFunction(element.0, element.1)
}
}
}

/// Initialize an instance of this type that iterates over the specified
/// sequence of 2-tuple argument values.
///
/// - Parameters:
/// - sequence: The sequence of 2-tuple argument values for which test
/// cases should be generated.
/// - parameters: The parameters of the test function for which test cases
/// should be generated.
/// - testFunction: The test function to which each generated test case
/// passes an argument value from `sequence`.
///
/// This initializer overload is specialized for sequences of 2-tuples to
/// efficiently de-structure their elements when appropriate.
///
/// @Comment {
/// - Bug: The testing library should support variadic generics.
/// ([103416861](rdar://103416861))
/// }
private init<E1, E2>(
sequence: S,
parameters: [Test.ParameterInfo],
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
) where S.Element == (E1, E2), E1: Sendable, E2: Sendable {
if parameters.count > 1 {
self.init(sequence: sequence) { element in
Test.Case(values: [element.0, element.1], parameters: parameters) {
try await testFunction(element)
}
}
} else {
self.init(sequence: sequence) { element in
Test.Case(values: [element], parameters: parameters) {
try await testFunction(element)
}
}
}
}

/// Initialize an instance of this type that iterates over the specified
/// collection of 2-tuple argument values.
///
/// - Parameters:
/// - collection: The collection of 2-tuple argument values for which test
/// cases should be generated.
/// - parameters: The parameters of the test function for which test cases
/// should be generated.
/// - testFunction: The test function to which each generated test case
/// passes an argument value from `collection`.
///
/// This initializer overload is specialized for collections of 2-tuples to
/// efficiently de-structure their elements when appropriate.
///
/// @Comment {
/// - Bug: The testing library should support variadic generics.
/// ([103416861](rdar://103416861))
/// }
init<E1, E2>(
arguments collection: S,
parameters: [Test.ParameterInfo],
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
) where S: Collection, S.Element == (E1, E2) {
self.init(sequence: collection, parameters: parameters, testFunction: testFunction)
}

/// Initialize an instance of this type that iterates over the specified
/// zipped sequence of argument values.
///
/// - Parameters:
/// - zippedCollections: A zipped sequence of argument values for which
/// test cases should be generated.
/// - parameters: The parameters of the test function for which test cases
/// should be generated.
/// - testFunction: The test function to which each generated test case
/// passes an argument value from `zippedCollections`.
init<C1, C2>(
arguments zippedCollections: Zip2Sequence<C1, C2>,
parameters: [Test.ParameterInfo],
testFunction: @escaping @Sendable ((C1.Element, C2.Element)) async throws -> Void
) where S == Zip2Sequence<C1, C2> {
self.init(sequence: zippedCollections) { element in
Test.Case(arguments: [element]) {
try await testFunction(element)
) where S == Zip2Sequence<C1, C2>, C1: Collection, C2: Collection {
self.init(sequence: zippedCollections, parameters: parameters, testFunction: testFunction)
}

/// Initialize an instance of this type that iterates over the specified
/// dictionary of argument values.
///
/// - Parameters:
/// - dictionary: A dictionary of argument values for which test cases
/// should be generated.
/// - parameters: The parameters of the test function for which test cases
/// should be generated.
/// - testFunction: The test function to which each generated test case
/// passes an argument value from `dictionary`.
///
/// This initializer overload is specialized for dictionary collections, to
/// efficiently de-structure their elements (which are known to be 2-tuples)
/// when appropriate. This overload is distinct from those for other
/// collections of 2-tuples because the `Element` tuple type for
/// `Dictionary` includes labels (`(key: Key, value: Value)`).
init<Key, Value>(
arguments dictionary: Dictionary<Key, Value>,
parameters: [Test.ParameterInfo],
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
) where S == Dictionary<Key, Value> {
if parameters.count > 1 {
self.init(sequence: dictionary) { element in
Test.Case(values: [element.key, element.value], parameters: parameters) {
try await testFunction(element)
}
}
} else {
self.init(sequence: dictionary) { element in
Test.Case(values: [element], parameters: parameters) {
try await testFunction(element)
}
}
}
}
Expand Down
Loading