Skip to content

Commit bdf5ab4

Browse files
authored
Merge pull request #1793 from plemarquand/document-tests-index-extension-fix
XCTests in extensions produced by symbols have no parent ID
2 parents 8616f7e + 857cb61 commit bdf5ab4

File tree

4 files changed

+130
-27
lines changed

4 files changed

+130
-27
lines changed

Sources/SemanticIndex/CheckedIndex.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,28 @@ package final class CheckedIndex {
161161
package func fileHasInMemoryModifications(_ uri: DocumentURI) -> Bool {
162162
return checker.fileHasInMemoryModifications(uri)
163163
}
164+
165+
/// If there are any definition occurrences of the given USR, return these.
166+
/// Otherwise return declaration occurrences.
167+
package func definitionOrDeclarationOccurrences(ofUSR usr: String) -> [SymbolOccurrence] {
168+
let definitions = occurrences(ofUSR: usr, roles: [.definition])
169+
if !definitions.isEmpty {
170+
return definitions
171+
}
172+
return occurrences(ofUSR: usr, roles: [.declaration])
173+
}
174+
175+
/// Find a `SymbolOccurrence` that is considered the primary definition of the symbol with the given USR.
176+
///
177+
/// If the USR has an ambiguous definition, the most important role of this function is to deterministically return
178+
/// the same result every time.
179+
package func primaryDefinitionOrDeclarationOccurrence(ofUSR usr: String) -> SymbolOccurrence? {
180+
let result = definitionOrDeclarationOccurrences(ofUSR: usr).sorted().first
181+
if result == nil {
182+
logger.error("Failed to find definition of \(usr) in index")
183+
}
184+
return result
185+
}
164186
}
165187

166188
/// A wrapper around `IndexStoreDB` that allows the retrieval of a `CheckedIndex` with a specified check level or the

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2402,28 +2402,6 @@ private let maxWorkspaceSymbolResults = 4096
24022402
package typealias Diagnostic = LanguageServerProtocol.Diagnostic
24032403

24042404
fileprivate extension CheckedIndex {
2405-
/// If there are any definition occurrences of the given USR, return these.
2406-
/// Otherwise return declaration occurrences.
2407-
func definitionOrDeclarationOccurrences(ofUSR usr: String) -> [SymbolOccurrence] {
2408-
let definitions = occurrences(ofUSR: usr, roles: [.definition])
2409-
if !definitions.isEmpty {
2410-
return definitions
2411-
}
2412-
return occurrences(ofUSR: usr, roles: [.declaration])
2413-
}
2414-
2415-
/// Find a `SymbolOccurrence` that is considered the primary definition of the symbol with the given USR.
2416-
///
2417-
/// If the USR has an ambiguous definition, the most important role of this function is to deterministically return
2418-
/// the same result every time.
2419-
func primaryDefinitionOrDeclarationOccurrence(ofUSR usr: String) -> SymbolOccurrence? {
2420-
let result = definitionOrDeclarationOccurrences(ofUSR: usr).sorted().first
2421-
if result == nil {
2422-
logger.error("Failed to find definition of \(usr) in index")
2423-
}
2424-
return result
2425-
}
2426-
24272405
/// Get the name of the symbol that is a parent of this symbol, if one exists
24282406
func containerName(of symbol: SymbolOccurrence) -> String? {
24292407
// The container name of accessors is the container of the surrounding variable.

Sources/SourceKitLSP/TestDiscovery.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,19 +102,34 @@ extension SourceKitLSPServer {
102102
/// provide ranges for the test cases in source code instead of only the test's location that we get from the index.
103103
private func testItems(
104104
for testSymbolOccurrences: [SymbolOccurrence],
105+
index: CheckedIndex?,
105106
resolveLocation: (DocumentURI, Position) -> Location
106107
) -> [AnnotatedTestItem] {
107108
// Arrange tests by the USR they are contained in. This allows us to emit test methods as children of test classes.
108109
// `occurrencesByParent[nil]` are the root test symbols that aren't a child of another test symbol.
109110
var occurrencesByParent: [String?: [SymbolOccurrence]] = [:]
110111

111-
let testSymbolUsrs = Set(testSymbolOccurrences.map(\.symbol.usr))
112+
var testSymbolUsrs = Set(testSymbolOccurrences.map(\.symbol.usr))
113+
114+
// Gather any extension declarations that contains tests and add them to `occurrencesByParent` so we can properly
115+
// arrange their test items as the extension's children.
116+
for testSymbolOccurrence in testSymbolOccurrences {
117+
for parentSymbol in testSymbolOccurrence.relations.filter({ $0.roles.contains(.childOf) }).map(\.symbol) {
118+
guard parentSymbol.kind == .extension else {
119+
continue
120+
}
121+
guard let definition = index?.primaryDefinitionOrDeclarationOccurrence(ofUSR: parentSymbol.usr) else {
122+
logger.fault("Unable to find primary definition of extension '\(parentSymbol.usr)' containing tests")
123+
continue
124+
}
125+
testSymbolUsrs.insert(parentSymbol.usr)
126+
occurrencesByParent[nil, default: []].append(definition)
127+
}
128+
}
112129

113130
for testSymbolOccurrence in testSymbolOccurrences {
114131
let childOfUsrs = testSymbolOccurrence.relations
115-
.filter { $0.roles.contains(.childOf) }
116-
.map(\.symbol.usr)
117-
.filter { testSymbolUsrs.contains($0) }
132+
.filter { $0.roles.contains(.childOf) }.map(\.symbol.usr).filter { testSymbolUsrs.contains($0) }
118133
if childOfUsrs.count > 1 {
119134
logger.fault(
120135
"Test symbol \(testSymbolOccurrence.symbol.usr) is child or multiple symbols: \(childOfUsrs.joined(separator: ", "))"
@@ -168,7 +183,7 @@ extension SourceKitLSPServer {
168183
children: children.map(\.testItem),
169184
tags: []
170185
),
171-
isExtension: false
186+
isExtension: testSymbolOccurrence.symbol.kind == .extension
172187
)
173188
}
174189

@@ -233,6 +248,7 @@ extension SourceKitLSPServer {
233248
let testsFromSyntacticIndex = await workspace.syntacticTestIndex.tests()
234249
let testsFromSemanticIndex = testItems(
235250
for: semanticTestSymbolOccurrences,
251+
index: index,
236252
resolveLocation: { uri, position in Location(uri: uri, range: Range(position)) }
237253
)
238254
let filesWithTestsFromSemanticIndex = Set(testsFromSemanticIndex.map(\.testItem.location.uri))
@@ -337,6 +353,7 @@ extension SourceKitLSPServer {
337353
// swift-testing tests, which aren't part of the semantic index.
338354
return testItems(
339355
for: testSymbols,
356+
index: index,
340357
resolveLocation: { uri, position in
341358
if uri == snapshot.uri, let documentSymbols,
342359
let range = findInnermostSymbolRange(containing: position, documentSymbolsResponse: documentSymbols)

Tests/SourceKitLSPTests/DocumentTestDiscoveryTests.swift

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,92 @@ final class DocumentTestDiscoveryTests: XCTestCase {
13401340
)
13411341
}
13421342

1343+
func testXCTestIndexedTestsWithExtension() async throws {
1344+
let project = try await IndexedSingleSwiftFileTestProject(
1345+
"""
1346+
import XCTest
1347+
1348+
1️⃣final class MyTests: XCTestCase {}2️⃣
1349+
1350+
extension MyTests {
1351+
3️⃣func testOneIsTwo() {}4️⃣
1352+
}
1353+
""",
1354+
allowBuildFailure: true
1355+
)
1356+
1357+
let tests = try await project.testClient.send(
1358+
DocumentTestsRequest(textDocument: TextDocumentIdentifier(project.fileURI))
1359+
)
1360+
XCTAssertEqual(
1361+
tests,
1362+
[
1363+
TestItem(
1364+
id: "MyTests",
1365+
label: "MyTests",
1366+
location: Location(uri: project.fileURI, range: project.positions["1️⃣"]..<project.positions["2️⃣"]),
1367+
children: [
1368+
TestItem(
1369+
id: "MyTests/testOneIsTwo()",
1370+
label: "testOneIsTwo()",
1371+
location: Location(uri: project.fileURI, range: project.positions["3️⃣"]..<project.positions["4️⃣"])
1372+
)
1373+
]
1374+
)
1375+
]
1376+
)
1377+
}
1378+
1379+
func testXCTestIndexedTestsWithExtensionInSeparateFile() async throws {
1380+
let project = try await SwiftPMTestProject(
1381+
files: [
1382+
"Tests/MyLibraryTests/MyTests.swift": """
1383+
import XCTest
1384+
1385+
class MyTests: XCTestCase {
1386+
}
1387+
""",
1388+
"Tests/MyLibraryTests/MoreTests.swift": """
1389+
import XCTest
1390+
1391+
1️⃣extension MyTests {
1392+
3️⃣func testMe() {}4️⃣
1393+
}2️⃣
1394+
""",
1395+
],
1396+
manifest: """
1397+
let package = Package(
1398+
name: "MyLibrary",
1399+
targets: [.testTarget(name: "MyLibraryTests")]
1400+
)
1401+
""",
1402+
enableBackgroundIndexing: true
1403+
)
1404+
1405+
let (uri, positions) = try project.openDocument("MoreTests.swift")
1406+
1407+
let tests = try await project.testClient.send(
1408+
DocumentTestsRequest(textDocument: TextDocumentIdentifier(uri))
1409+
)
1410+
XCTAssertEqual(
1411+
tests,
1412+
[
1413+
TestItem(
1414+
id: "MyLibraryTests.MyTests",
1415+
label: "MyTests",
1416+
location: Location(uri: uri, range: positions["1️⃣"]..<positions["2️⃣"]),
1417+
children: [
1418+
TestItem(
1419+
id: "MyLibraryTests.MyTests/testMe()",
1420+
label: "testMe()",
1421+
location: Location(uri: uri, range: positions["3️⃣"]..<positions["4️⃣"])
1422+
)
1423+
]
1424+
)
1425+
]
1426+
)
1427+
}
1428+
13431429
func testXCTestInvalidXCTestSuiteConstructions() async throws {
13441430
let testClient = try await TestSourceKitLSPClient()
13451431
let uri = DocumentURI(for: .swift)

0 commit comments

Comments
 (0)