Skip to content

Commit 0112a46

Browse files
authored
Synthesize Test instances for all suite types. (#311)
This PR extends test discovery to synthesize `Test` instances for all suite types (that is, types containing other suites or test functions but which do not have the `@Suite` attribute explicitly applied to them.) This ensures that deep type hierarchies like the following are fully represented in test plans: ```swift struct AllTests { struct FoodTruckTests { @suite struct MechanicalTests { struct BatteryTests { @test func isCharged() { ... } } } } } ``` Resolves rdar://125241067. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent ff1cf91 commit 0112a46

File tree

4 files changed

+64
-9
lines changed

4 files changed

+64
-9
lines changed

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,44 @@ public struct TypeInfo: Sendable {
134134
}
135135
}
136136

137+
// MARK: - Containing types
138+
139+
extension TypeInfo {
140+
/// An instance of this type representing the type immediately containing the
141+
/// described type.
142+
///
143+
/// For instance, given the following declaration in the `Example` module:
144+
///
145+
/// ```swift
146+
/// struct A {
147+
/// struct B {}
148+
/// }
149+
/// ```
150+
///
151+
/// The value of this property for the type `A.B` would describe `A`, while
152+
/// the value for `A` would be `nil` because it has no enclosing type.
153+
var containingTypeInfo: Self? {
154+
let fqnComponents = fullyQualifiedNameComponents
155+
if fqnComponents.count > 2 { // the module is not a type
156+
let fqn = fqnComponents.dropLast().joined(separator: ".")
157+
#if false // currently non-functional
158+
if let type = _typeByName(fqn) {
159+
return Self(describing: type)
160+
}
161+
#endif
162+
let name = fqnComponents[fqnComponents.count - 2]
163+
return Self(fullyQualifiedName: fqn, unqualifiedName: name)
164+
}
165+
return nil
166+
}
167+
168+
/// A sequence of instances of this type representing the types that
169+
/// recursively contain it, starting with the immediate parent (if any.)
170+
var allContainingTypeInfo: some Sequence<Self> {
171+
sequence(first: self, next: \.containingTypeInfo).dropFirst()
172+
}
173+
}
174+
137175
// MARK: - CustomStringConvertible, CustomDebugStringConvertible
138176

139177
extension TypeInfo: CustomStringConvertible, CustomDebugStringConvertible {

Sources/Testing/Test+Discovery.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,15 @@ extension Test {
103103
}
104104
let suiteID = ID(typeInfo: suiteTypeInfo)
105105
if tests[suiteID] == nil {
106-
// If the real test is hidden, so shall the synthesized test be hidden.
107-
// Copy the exact traits from the real test in case they someday carry
108-
// any interesting metadata.
109-
let traits = test.traits.compactMap { $0 as? HiddenTrait }
110-
tests[suiteID] = Test(traits: traits, sourceLocation: test.sourceLocation, containingTypeInfo: suiteTypeInfo)
106+
tests[suiteID] = Test(traits: [], sourceLocation: test.sourceLocation, containingTypeInfo: suiteTypeInfo, isSynthesized: true)
107+
108+
// Also synthesize any ancestral suites that don't have tests.
109+
for ancestralSuiteTypeInfo in suiteTypeInfo.allContainingTypeInfo {
110+
let ancestralSuiteID = ID(typeInfo: ancestralSuiteTypeInfo)
111+
if tests[ancestralSuiteID] == nil {
112+
tests[ancestralSuiteID] = Test(traits: [], sourceLocation: test.sourceLocation, containingTypeInfo: ancestralSuiteTypeInfo, isSynthesized: true)
113+
}
114+
}
111115
}
112116
}
113117

Sources/Testing/Test.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,29 @@ public struct Test: Sendable {
112112
containingTypeInfo != nil && testCases == nil
113113
}
114114

115+
/// Whether or not this instance was synthesized at runtime.
116+
///
117+
/// During test planning, suites that are not explicitly marked with the
118+
/// `@Suite` attribute are synthesized from available type information before
119+
/// being added to the plan. For such suites, the value of this property is
120+
/// `true`.
121+
@_spi(ForToolsIntegrationOnly)
122+
public var isSynthesized = false
123+
115124
/// Initialize an instance of this type representing a test suite type.
116125
init(
117126
displayName: String? = nil,
118127
traits: [any Trait],
119128
sourceLocation: SourceLocation,
120-
containingTypeInfo: TypeInfo
129+
containingTypeInfo: TypeInfo,
130+
isSynthesized: Bool = false
121131
) {
122132
self.name = containingTypeInfo.unqualifiedName
123133
self.displayName = displayName
124134
self.traits = traits
125135
self.sourceLocation = sourceLocation
126136
self.containingTypeInfo = containingTypeInfo
137+
self.isSynthesized = isSynthesized
127138
}
128139

129140
/// Initialize an instance of this type representing a test function.

Tests/TestingTests/PlanTests.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,12 @@ struct PlanTests {
420420
#expect(plan.stepGraph.subgraph(at: nameComponents(of: SendableTests.self) + CollectionOfOne("reserved1(reserved2:)")) != nil)
421421
}
422422

423-
@Test("Runner.Plan.independentlyRunnableSteps property")
423+
@Test("Runner.Plan.independentlyRunnableSteps property (all tests top-level)")
424424
func independentlyRunnableTests() async throws {
425-
let plan = await Runner.Plan(selecting: IndependentlyRunnableTests.self)
426-
#expect(plan.independentlyRunnableSteps.count == 2)
425+
let plan = await Runner.Plan(configuration: .init())
426+
for step in plan.independentlyRunnableSteps {
427+
#expect(step.test.id.nameComponents.count == 1, "Test is not top-level: \(step.test)")
428+
}
427429
}
428430
}
429431

0 commit comments

Comments
 (0)