Skip to content

Commit fd29a86

Browse files
committed
Ensure tests representing containing types of suite types are synthesized if necessary
Resolves rdar://127770218
1 parent 3a537b8 commit fd29a86

File tree

5 files changed

+56
-51
lines changed

5 files changed

+56
-51
lines changed

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,15 @@ extension TypeInfo {
276276
}
277277
return nil
278278
}
279+
}
279280

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()
281+
extension Test {
282+
/// A sequence of ``TypeInfo`` instances representing the types that
283+
/// recursively contain this test, if any.
284+
var allContainingTypeInfo: some Sequence<TypeInfo> {
285+
sequence(first: containingTypeInfo) { element in
286+
element?.containingTypeInfo
287+
}.compactMap { $0 }
284288
}
285289
}
286290

Sources/Testing/Test+Discovery.swift

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,32 +80,23 @@ extension Test {
8080
///
8181
/// - Parameters:
8282
/// - 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-
}
83+
private static func _synthesizeSuiteTypes(into tests: inout [ID: Self]) {
84+
// We won't have an accurate source location for synthesized suites, so use
85+
// a "runtime inferred" sentinel value instead.
86+
//
87+
// FIXME: Consider either changing `Test.sourceLocation` to Optional (an
88+
// API-breaking change) or modifying Runner.Plan so that it doesn't require
89+
// an actual Test instance to represent synthesized parent suites.
90+
lazy var inferredSourceLocation = SourceLocation(fileID: "<runtime-inferred>", filePath: "<runtime-inferred>", line: 0, column: 0)
91+
92+
for test in tests.values {
93+
for ancestorTypeInfo in test.allContainingTypeInfo {
94+
let ancestorSuiteID = ID(typeInfo: ancestorTypeInfo)
95+
if tests[ancestorSuiteID] == nil {
96+
tests[ancestorSuiteID] = Test(traits: [], sourceLocation: inferredSourceLocation, containingTypeInfo: ancestorTypeInfo)
10497
}
10598
}
10699
}
107-
108-
return tests.count - originalCount
109100
}
110101
}
111102

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/PlanTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,15 @@ struct PlanTests {
421421
#expect(plan.stepGraph.subgraph(at: typeInfo.fullyQualifiedNameComponents + CollectionOfOne("reserved1(reserved2:)")) != nil)
422422
}
423423

424+
@Test("Ancestor suite types without @Suite are synthesized")
425+
func ancestorSuiteSynthesis() async {
426+
let plan = await Runner.Plan(selecting: OutermostSuite.self)
427+
let testNames = plan.steps.map(\.test.name)
428+
#expect(testNames.contains("OutermostSuite"))
429+
#expect(testNames.contains("ChildSuite"))
430+
#expect(testNames.contains("example()"))
431+
}
432+
424433
#if !SWT_NO_SNAPSHOT_TYPES
425434
@Test("Test cases of a disabled test are not evaluated")
426435
func disabledTestCases() async throws {
@@ -478,3 +487,15 @@ private struct BasicRecursiveTrait: SuiteTrait, TestTrait, CustomStringConvertib
478487
self.description = description
479488
}
480489
}
490+
491+
extension PlanTests {
492+
// This fixture must not have an explicit `@Suite` attribute to validate suite
493+
// synthesis, so it's nested within `PlanTests` to avoid polluting the
494+
// top-level namespace. Its children can be `.hidden`, though.
495+
private struct OutermostSuite {
496+
@Suite(.hidden)
497+
struct ChildSuite {
498+
@Test func example() {}
499+
}
500+
}
501+
}

Tests/TestingTests/RunnerTests.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ final class RunnerTests: XCTestCase {
395395
let runner = await Runner(configuration: configuration)
396396
let plan = runner.plan
397397

398-
XCTAssertEqual(plan.steps.count, 0)
398+
XCTAssertEqual(plan.steps.count, 1)
399399
}
400400
}
401401

@@ -501,7 +501,7 @@ final class RunnerTests: XCTestCase {
501501

502502
func testPoundIfTrueTestFunctionRuns() async throws {
503503
let testStarted = expectation(description: "Test started")
504-
testStarted.expectedFulfillmentCount = 4
504+
testStarted.expectedFulfillmentCount = 5
505505
var configuration = Configuration()
506506
configuration.eventHandler = { event, _ in
507507
if case .testStarted = event.kind {
@@ -522,7 +522,7 @@ final class RunnerTests: XCTestCase {
522522

523523
func testPoundIfFalseTestFunctionDoesNotRun() async throws {
524524
let testStarted = expectation(description: "Test started")
525-
testStarted.expectedFulfillmentCount = 2
525+
testStarted.expectedFulfillmentCount = 3
526526
var configuration = Configuration()
527527
configuration.eventHandler = { event, _ in
528528
if case .testStarted = event.kind {
@@ -545,7 +545,7 @@ final class RunnerTests: XCTestCase {
545545

546546
func testPoundIfFalseElseTestFunctionRuns() async throws {
547547
let testStarted = expectation(description: "Test started")
548-
testStarted.expectedFulfillmentCount = 4
548+
testStarted.expectedFulfillmentCount = 5
549549
var configuration = Configuration()
550550
configuration.eventHandler = { event, _ in
551551
if case .testStarted = event.kind {
@@ -568,7 +568,7 @@ final class RunnerTests: XCTestCase {
568568

569569
func testPoundIfFalseElseIfTestFunctionRuns() async throws {
570570
let testStarted = expectation(description: "Test started")
571-
testStarted.expectedFulfillmentCount = 4
571+
testStarted.expectedFulfillmentCount = 5
572572
var configuration = Configuration()
573573
configuration.eventHandler = { event, _ in
574574
if case .testStarted = event.kind {
@@ -606,9 +606,9 @@ final class RunnerTests: XCTestCase {
606606
func testNoasyncTestsAreCallable() async throws {
607607
let testStarted = expectation(description: "Test started")
608608
#if !SWT_NO_GLOBAL_ACTORS
609-
testStarted.expectedFulfillmentCount = 6
609+
testStarted.expectedFulfillmentCount = 7
610610
#else
611-
testStarted.expectedFulfillmentCount = 5
611+
testStarted.expectedFulfillmentCount = 6
612612
#endif
613613
var configuration = Configuration()
614614
configuration.eventHandler = { event, _ in
@@ -681,10 +681,10 @@ final class RunnerTests: XCTestCase {
681681
let testStarted = expectation(description: "Test started")
682682
let testSkipped = expectation(description: "Test skipped")
683683
#if SWT_TARGET_OS_APPLE
684-
testStarted.expectedFulfillmentCount = 4
684+
testStarted.expectedFulfillmentCount = 5
685685
testSkipped.expectedFulfillmentCount = 8
686686
#else
687-
testStarted.expectedFulfillmentCount = 2
687+
testStarted.expectedFulfillmentCount = 3
688688
testSkipped.expectedFulfillmentCount = 2
689689
#endif
690690
var configuration = Configuration()
@@ -782,7 +782,7 @@ final class RunnerTests: XCTestCase {
782782
func testAvailableWithSwiftVersion() async throws {
783783
let testStarted = expectation(description: "Test started")
784784
let testSkipped = expectation(description: "Test skipped")
785-
testStarted.expectedFulfillmentCount = 3
785+
testStarted.expectedFulfillmentCount = 4
786786
testSkipped.expectedFulfillmentCount = 2
787787
var configuration = Configuration()
788788
configuration.eventHandler = { event, _ in
@@ -808,7 +808,7 @@ final class RunnerTests: XCTestCase {
808808
}
809809

810810
let testStarted = expectation(description: "Test started")
811-
testStarted.expectedFulfillmentCount = 2
811+
testStarted.expectedFulfillmentCount = 3
812812
var configuration = Configuration()
813813
configuration.eventHandler = { event, _ in
814814
if case .testStarted = event.kind {
@@ -919,9 +919,9 @@ final class RunnerTests: XCTestCase {
919919
let testStarted = expectation(description: "Test started")
920920
let testSkipped = expectation(description: "Test skipped")
921921
#if SWT_TARGET_OS_APPLE
922-
testStarted.expectedFulfillmentCount = 4
922+
testStarted.expectedFulfillmentCount = 5
923923
#else
924-
testStarted.expectedFulfillmentCount = 3
924+
testStarted.expectedFulfillmentCount = 4
925925
#endif
926926
testSkipped.isInverted = true
927927
var configuration = Configuration()

0 commit comments

Comments
 (0)