Skip to content

Commit 65c7c18

Browse files
committed
Address feedback, properly handle duplicate IDs in extensions
1 parent 7bcbae4 commit 65c7c18

File tree

2 files changed

+100
-30
lines changed

2 files changed

+100
-30
lines changed

Sources/SourceKitLSP/TestDiscovery.swift

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,6 @@ fileprivate extension Array<AnnotatedTestItem> {
433433
/// A node's parent is identified by the node's ID with the last component dropped.
434434
func mergingTestsInExtensions() -> [TestItem] {
435435
var itemDict: [String: AnnotatedTestItem] = [:]
436-
var duplicatedIds: Set<String> = []
437436
for item in self {
438437
let id = item.testItem.id
439438
if var rootItem = itemDict[id] {
@@ -442,14 +441,13 @@ fileprivate extension Array<AnnotatedTestItem> {
442441
// as the root item.
443442
if rootItem.isExtension && !item.isExtension {
444443
var newItem = item
445-
newItem.testItem.children += rootItem.testItem.children
444+
newItem.testItem.children = (newItem.testItem.children + rootItem.testItem.children).deduplicateIds()
446445
rootItem = newItem
447-
} else if (rootItem.testItem.children.isEmpty && item.testItem.children.isEmpty) {
448-
duplicatedIds.insert(item.testItem.id)
449-
itemDict[item.testItem.fullyQualifiedTestId] = item
446+
} else if rootItem.testItem.children.isEmpty && item.testItem.children.isEmpty {
447+
itemDict[item.testItem.ambiguousTestDifferentiator] = item
450448
continue
451449
} else {
452-
rootItem.testItem.children += item.testItem.children
450+
rootItem.testItem.children = (rootItem.testItem.children + item.testItem.children).deduplicateIds()
453451
}
454452

455453
itemDict[id] = rootItem
@@ -480,32 +478,16 @@ fileprivate extension Array<AnnotatedTestItem> {
480478
.sorted { ($0.isExtension != $1.isExtension) ? !$0.isExtension : ($0.testItem.location < $1.testItem.location) }
481479

482480
let result = sortedItems.map {
483-
var newItem = $0.testItem
484-
485-
// If multiple testItems share the same ID we add more context to make it unique.
486-
// Two tests can share the same ID when two swift testing tests accept
487-
// arguments of different types, i.e:
488-
// @Test(arguments: [1,2,3]) func foo(_ x: Int) {}
489-
// @Test(arguments: ["a", "b", "c"]) func foo(_ x: String) {}
490-
// or when tests are in separate files but don't conflict because they are marked
491-
// private, i.e:
492-
// File1.swift: @Test private func foo() {}
493-
// File2.swift: @Test private func foo() {}
494-
// If we encounter one of these cases, we need to deduplicate the ID
495-
// by appending /filename:filename:lineNumber.
496-
if duplicatedIds.contains(newItem.id) {
497-
newItem.id = newItem.fullyQualifiedTestId
498-
}
499-
500481
guard !$0.testItem.children.isEmpty, mergedIds.contains($0.testItem.id) else {
501-
return newItem
482+
return $0.testItem
502483
}
484+
var newItem = $0.testItem
503485
newItem.children = newItem.children
504486
.map { AnnotatedTestItem(testItem: $0, isExtension: false) }
505487
.mergingTestsInExtensions()
506488
return newItem
507489
}
508-
return result
490+
return result.deduplicateIds()
509491
}
510492

511493
func prefixTestsWithModuleName(workspace: Workspace) async -> Self {
@@ -518,19 +500,58 @@ fileprivate extension Array<AnnotatedTestItem> {
518500
}
519501
}
520502

521-
extension TestItem {
522-
fileprivate var fullyQualifiedTestId: String {
523-
return "\(self.id)/\(self.sourceLocation)"
503+
fileprivate extension Array<TestItem> {
504+
/// If multiple testItems share the same ID we add more context to make it unique.
505+
/// Two tests can share the same ID when two swift testing tests accept
506+
/// arguments of different types, i.e:
507+
/// ```
508+
/// @Test(arguments: [1,2,3]) func foo(_ x: Int) {}
509+
/// @Test(arguments: ["a", "b", "c"]) func foo(_ x: String) {}
510+
/// ```
511+
///
512+
/// or when tests are in separate files but don't conflict because they are marked
513+
/// private, i.e:
514+
/// ```
515+
/// File1.swift: @Test private func foo() {}
516+
/// File2.swift: @Test private func foo() {}
517+
/// ```
518+
///
519+
/// If we encounter one of these cases, we need to deduplicate the ID
520+
/// by appending `/filename:filename:lineNumber`.
521+
func deduplicateIds() -> [TestItem] {
522+
var idCounts: [String: Int] = [:]
523+
var result: [TestItem] = []
524+
525+
for element in self where element.children.isEmpty {
526+
idCounts[element.id, default: 0] += 1
527+
}
528+
529+
for element in self {
530+
if idCounts[element.id] ?? 0 > 1 {
531+
var newItem = element
532+
newItem.id = newItem.ambiguousTestDifferentiator
533+
result.append(newItem)
534+
} else {
535+
result.append(element)
536+
}
537+
}
538+
539+
return result
524540
}
541+
}
525542

526-
private var sourceLocation: String {
543+
extension TestItem {
544+
/// A fully qualified name to disambiguate identical TestItem IDs.
545+
/// This matches the IDs produced by `swift test list` when there are
546+
/// tests that cannot be disambiguated by their simple ID.
547+
fileprivate var ambiguousTestDifferentiator: String {
527548
let filename = self.location.uri.arbitrarySchemeURL.lastPathComponent
528549
let position = location.range.lowerBound
529550
// Lines and columns start at 1.
530551
// swift-testing tests start from _after_ the @ symbol in @Test, so we need to add an extra column.
531552
// see https://github.com/swiftlang/swift-testing/blob/cca6de2be617aded98ecdecb0b3b3a81eec013f3/Sources/TestingMacros/Support/AttributeDiscovery.swift#L153
532553
let columnOffset = self.style == TestStyle.swiftTesting ? 2 : 1
533-
return "\(filename):\(position.line + 1):\(position.utf16index + columnOffset)"
554+
return "\(self.id)/\(filename):\(position.line + 1):\(position.utf16index + columnOffset)"
534555
}
535556

536557
fileprivate func prefixIDWithModuleName(workspace: Workspace) async -> TestItem {

Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,55 @@ final class DocumentTestDiscoveryTests: XCTestCase {
455455
)
456456
}
457457

458+
func testSwiftTestingTestsWithDuplicateFunctionIdentifiersInExtension() async throws {
459+
let testClient = try await TestSourceKitLSPClient()
460+
let uri = DocumentURI(for: .swift)
461+
462+
let positions = testClient.openDocument(
463+
"""
464+
import Testing
465+
1️⃣struct MySuite {
466+
3️⃣@Test(arguments: [1, 2, 3])
467+
func foo(_ x: Int) {}4️⃣
468+
}2️⃣
469+
470+
extension MySuite {
471+
5️⃣@Test(arguments: ["a", "b", "c"])
472+
func foo(_ x: String) {}6️⃣
473+
}
474+
""",
475+
uri: uri
476+
)
477+
478+
let filename = uri.fileURL?.lastPathComponent ?? ""
479+
let tests = try await testClient.send(DocumentTestsRequest(textDocument: TextDocumentIdentifier(uri)))
480+
XCTAssertEqual(
481+
tests,
482+
[
483+
TestItem(
484+
id: "MySuite",
485+
label: "MySuite",
486+
style: TestStyle.swiftTesting,
487+
location: Location(uri: uri, range: positions["1️⃣"]..<positions["2️⃣"]),
488+
children: [
489+
TestItem(
490+
id: "MySuite/foo(_:)/\(filename):\(positions["3️⃣"].line + 1):\(positions["3️⃣"].utf16index + 2)",
491+
label: "foo(_:)",
492+
style: TestStyle.swiftTesting,
493+
location: Location(uri: uri, range: positions["3️⃣"]..<positions["4️⃣"])
494+
),
495+
TestItem(
496+
id: "MySuite/foo(_:)/\(filename):\(positions["5️⃣"].line + 1):\(positions["5️⃣"].utf16index + 2)",
497+
label: "foo(_:)",
498+
style: TestStyle.swiftTesting,
499+
location: Location(uri: uri, range: positions["5️⃣"]..<positions["6️⃣"])
500+
),
501+
]
502+
)
503+
]
504+
)
505+
}
506+
458507
func testSwiftTestingSuiteWithNoTests() async throws {
459508
let testClient = try await TestSourceKitLSPClient()
460509
let uri = DocumentURI(for: .swift)

0 commit comments

Comments
 (0)