Skip to content

Commit f53ddaf

Browse files
committed
Ensure tests representing containing types of suite types are synthesized if necessary
Resolves rdar://127770218
1 parent 4f602e0 commit f53ddaf

File tree

7 files changed

+217
-140
lines changed

7 files changed

+217
-140
lines changed

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,33 @@ public struct TypeInfo: Sendable {
4545
return nil
4646
}
4747

48-
init(fullyQualifiedName: String, unqualifiedName: String, mangledName: String?) {
48+
/// Initialize an instance of this type with the specified names.
49+
///
50+
/// - Parameters:
51+
/// - fullyQualifiedComponents: The fully-qualified name components of the
52+
/// type.
53+
/// - unqualified: The unqualified name of the type.
54+
/// - mangled: The mangled name of the type, if available.
55+
init(fullyQualifiedNameComponents: [String], unqualifiedName: String, mangledName: String? = nil) {
4956
_kind = .nameOnly(
50-
fullyQualifiedComponents: fullyQualifiedName.split(separator: ".").map(String.init),
57+
fullyQualifiedComponents: fullyQualifiedNameComponents,
5158
unqualified: unqualifiedName,
52-
mangled: mangledName
59+
mangled: mangledName,
60+
)
61+
}
62+
63+
/// Initialize an instance of this type with the specified names.
64+
///
65+
/// - Parameters:
66+
/// - fullyQualifiedName: The fully-qualified name of the type, with its
67+
/// components separated by a period character (`"."`).
68+
/// - unqualified: The unqualified name of the type.
69+
/// - mangled: The mangled name of the type, if available.
70+
init(fullyQualifiedName: String, unqualifiedName: String, mangledName: String?) {
71+
self.init(
72+
fullyQualifiedNameComponents: fullyQualifiedName.split(separator: ".").map(String.init),
73+
unqualifiedName: unqualifiedName,
74+
mangledName: mangledName
5375
)
5476
}
5577

@@ -246,44 +268,6 @@ func isClass(_ subclass: AnyClass, subclassOf superclass: AnyClass) -> Bool {
246268
}
247269
}
248270

249-
// MARK: - Containing types
250-
251-
extension TypeInfo {
252-
/// An instance of this type representing the type immediately containing the
253-
/// described type.
254-
///
255-
/// For instance, given the following declaration in the `Example` module:
256-
///
257-
/// ```swift
258-
/// struct A {
259-
/// struct B {}
260-
/// }
261-
/// ```
262-
///
263-
/// The value of this property for the type `A.B` would describe `A`, while
264-
/// the value for `A` would be `nil` because it has no enclosing type.
265-
var containingTypeInfo: Self? {
266-
let fqnComponents = fullyQualifiedNameComponents
267-
if fqnComponents.count > 2 { // the module is not a type
268-
let fqn = fqnComponents.dropLast().joined(separator: ".")
269-
#if false // currently non-functional
270-
if let type = _typeByName(fqn) {
271-
return Self(describing: type)
272-
}
273-
#endif
274-
let name = fqnComponents[fqnComponents.count - 2]
275-
return Self(fullyQualifiedName: fqn, unqualifiedName: name, mangledName: nil)
276-
}
277-
return nil
278-
}
279-
280-
/// A sequence of instances of this type representing the types that
281-
/// recursively contain it, starting with the immediate parent (if any.)
282-
var allContainingTypeInfo: some Sequence<Self> {
283-
sequence(first: self, next: \.containingTypeInfo).dropFirst()
284-
}
285-
}
286-
287271
// MARK: - CustomStringConvertible, CustomDebugStringConvertible, CustomTestStringConvertible
288272

289273
extension TypeInfo: CustomStringConvertible, CustomDebugStringConvertible {

Sources/Testing/Running/Runner.Plan.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,52 @@ extension Runner.Plan {
154154
}
155155
}
156156

157+
/// Recursively synthesize test instances representing suites for all missing
158+
/// values in the specified test graph.
159+
///
160+
/// - Parameters:
161+
/// - graph: The graph in which suites should be synthesized.
162+
/// - nameComponents: The name components of the suite to synthesize, based
163+
/// on the key path from the root node of the test graph to `graph`.
164+
private static func _recursivelySynthesizeSuites(in graph: inout Graph<String, Test?>, nameComponents: [String] = []) {
165+
// The recursive function. This is a local function to simplify the initial
166+
// call which does not need to pass the `sourceLocation:` inout argument.
167+
func synthesizeSuites(in graph: inout Graph<String, Test?>, nameComponents: [String] = [], sourceLocation: inout SourceLocation?) {
168+
for (key, var childGraph) in graph.children {
169+
synthesizeSuites(in: &childGraph, nameComponents: nameComponents + [key], sourceLocation: &sourceLocation)
170+
graph.children[key] = childGraph
171+
}
172+
173+
if let test = graph.value {
174+
sourceLocation = test.sourceLocation
175+
} else if let unqualifiedName = nameComponents.last, let sourceLocation {
176+
// Don't synthesize suites representing modules.
177+
if nameComponents.count <= 1 {
178+
return
179+
}
180+
181+
// Don't synthesize suites for nodes in the graph which are the
182+
// immediate ancestor of a test function. That level of the hierarchy is
183+
// used to disambiguate test functions which have equal names but
184+
// different source locations.
185+
if let firstChildTest = graph.children.values.first?.value, !firstChildTest.isSuite {
186+
return
187+
}
188+
189+
let typeInfo = TypeInfo(fullyQualifiedNameComponents: nameComponents, unqualifiedName: unqualifiedName)
190+
191+
// Note: When a suite is synthesized, it does not have an accurate
192+
// source location, so we use the source location of a close descendant
193+
// test. We do this instead of falling back to some "unknown"
194+
// placeholder in an attempt to preserve the correct sort ordering.
195+
graph.value = Test(traits: [], sourceLocation: sourceLocation, containingTypeInfo: typeInfo)
196+
}
197+
}
198+
199+
var sourceLocation: SourceLocation?
200+
synthesizeSuites(in: &graph, sourceLocation: &sourceLocation)
201+
}
202+
157203
/// Construct a graph of runner plan steps for the specified tests.
158204
///
159205
/// - Parameters:
@@ -198,6 +244,9 @@ extension Runner.Plan {
198244
// and that is already guarded earlier in the SwiftPM entry point.
199245
}
200246

247+
// Synthesize suites for nodes in the test graph for which they are missing.
248+
_recursivelySynthesizeSuites(in: &testGraph)
249+
201250
// Recursively apply all recursive suite traits to children.
202251
//
203252
// This must be done _before_ calling `prepare(for:)` on the traits below.

Sources/Testing/Test+Discovery.swift

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,7 @@ extension Test {
2828
/// All available ``Test`` instances in the process, according to the runtime.
2929
///
3030
/// The order of values in this sequence is unspecified.
31-
static var all: some Sequence<Test> {
32-
get async {
33-
// Convert the raw sequence of tests to a dictionary keyed by ID.
34-
var result = await testsByID(_all)
35-
36-
// Ensure test suite types that don't have the @Suite attribute are still
37-
// represented in the result.
38-
_synthesizeSuiteTypes(into: &result)
39-
40-
return result.values
41-
}
42-
}
43-
44-
/// All available ``Test`` instances in the process, according to the runtime.
45-
///
46-
/// The order of values in this sequence is unspecified. This sequence may
47-
/// contain duplicates; callers should use ``all`` instead.
48-
private static var _all: some Sequence<Self> {
31+
static var all: some Sequence<Self> {
4932
get async {
5033
await withTaskGroup(of: [Self].self) { taskGroup in
5134
enumerateTypes(withNamesContaining: _testContainerTypeNameMagic) { _, type, _ in
@@ -60,53 +43,6 @@ extension Test {
6043
}
6144
}
6245
}
63-
64-
/// Create a dictionary mapping the IDs of a sequence of tests to those tests.
65-
///
66-
/// - Parameters:
67-
/// - tests: The sequence to convert to a dictionary.
68-
///
69-
/// - Returns: A dictionary containing `tests` keyed by those tests' IDs.
70-
static func testsByID(_ tests: some Sequence<Self>) -> [ID: Self] {
71-
[ID: Self](
72-
tests.lazy.map { ($0.id, $0) },
73-
uniquingKeysWith: { existing, _ in existing }
74-
)
75-
}
76-
77-
/// Synthesize any missing test suite types (that is, types containing test
78-
/// content that do not have the `@Suite` attribute) and add them to a
79-
/// dictionary of tests.
80-
///
81-
/// - Parameters:
82-
/// - tests: A dictionary of tests to amend.
83-
///
84-
/// - Returns: The number of key-value pairs added to `tests`.
85-
@discardableResult private static func _synthesizeSuiteTypes(into tests: inout [ID: Self]) -> Int {
86-
let originalCount = tests.count
87-
88-
// Find any instances of Test in the input that are *not* suites. We'll be
89-
// checking the containing types of each one.
90-
for test in tests.values where !test.isSuite {
91-
guard let suiteTypeInfo = test.containingTypeInfo else {
92-
continue
93-
}
94-
let suiteID = ID(typeInfo: suiteTypeInfo)
95-
if tests[suiteID] == nil {
96-
tests[suiteID] = Test(traits: [], sourceLocation: test.sourceLocation, containingTypeInfo: suiteTypeInfo, isSynthesized: true)
97-
98-
// Also synthesize any ancestral suites that don't have tests.
99-
for ancestralSuiteTypeInfo in suiteTypeInfo.allContainingTypeInfo {
100-
let ancestralSuiteID = ID(typeInfo: ancestralSuiteTypeInfo)
101-
if tests[ancestralSuiteID] == nil {
102-
tests[ancestralSuiteID] = Test(traits: [], sourceLocation: test.sourceLocation, containingTypeInfo: ancestralSuiteTypeInfo, isSynthesized: true)
103-
}
104-
}
105-
}
106-
}
107-
108-
return tests.count - originalCount
109-
}
11046
}
11147

11248
// MARK: -

Sources/Testing/Test.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -192,29 +192,18 @@ public struct Test: Sendable {
192192
containingTypeInfo != nil && testCasesState == nil
193193
}
194194

195-
/// Whether or not this instance was synthesized at runtime.
196-
///
197-
/// During test planning, suites that are not explicitly marked with the
198-
/// `@Suite` attribute are synthesized from available type information before
199-
/// being added to the plan. For such suites, the value of this property is
200-
/// `true`.
201-
@_spi(ForToolsIntegrationOnly)
202-
public var isSynthesized = false
203-
204195
/// Initialize an instance of this type representing a test suite type.
205196
init(
206197
displayName: String? = nil,
207198
traits: [any Trait],
208199
sourceLocation: SourceLocation,
209200
containingTypeInfo: TypeInfo,
210-
isSynthesized: Bool = false
211201
) {
212202
self.name = containingTypeInfo.unqualifiedName
213203
self.displayName = displayName
214204
self.traits = traits
215205
self.sourceLocation = sourceLocation
216206
self.containingTypeInfo = containingTypeInfo
217-
self.isSynthesized = isSynthesized
218207
}
219208

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

Tests/TestingTests/MiscellaneousTests.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -552,17 +552,6 @@ struct MiscellaneousTests {
552552
#expect(id.keyPathRepresentation == [""])
553553
}
554554

555-
@Test("Test.all deduping")
556-
func allTestDeduping() {
557-
let tests = [Test(name: "A") {}, Test(name: "B") {}, Test(name: "C") {}, Test(name: "D") {}, Test(name: "E") {}, Test(name: "F") {}, Test(name: "G") {},]
558-
var duplicatedTests = tests
559-
duplicatedTests += tests
560-
duplicatedTests.shuffle()
561-
let mappedTests = Test.testsByID(duplicatedTests)
562-
#expect(mappedTests.count == tests.count)
563-
#expect(mappedTests.values.allSatisfy { tests.contains($0) })
564-
}
565-
566555
@Test("failureBreakpoint() call")
567556
func failureBreakpointCall() {
568557
failureBreakpointValue = 1

0 commit comments

Comments
 (0)