Skip to content

Commit 8e3612d

Browse files
committed
Add a request to list all the tests within the current workspace
Fixes swiftlang#611 rdar://98710526
1 parent 8af0bb5 commit 8e3612d

File tree

5 files changed

+151
-28
lines changed

5 files changed

+151
-28
lines changed

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public let builtinRequests: [_RequestType.Type] = [
8282
WorkspaceSemanticTokensRefreshRequest.self,
8383
WorkspaceSymbolResolveRequest.self,
8484
WorkspaceSymbolsRequest.self,
85+
WorkspaceTestsRequest.self,
8586
]
8687

8788
/// The set of known notifications.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// A request that returns symbols for all the test classes and test methods within the current workspace.
14+
///
15+
/// **(LSP Extension)**
16+
public struct WorkspaceTestsRequest: RequestType, Hashable {
17+
18+
public static let method: String = "workspace/tests"
19+
public typealias Response = [WorkspaceSymbolItem]?
20+
21+
public init() {}
22+
}

Sources/SKTestSupport/SwiftPMTestWorkspace.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
4444
var filesByPath: [RelativeFileLocation: String] = [:]
4545
for (fileLocation, contents) in files {
4646
let directories =
47-
if fileLocation.directories.isEmpty {
47+
switch fileLocation.directories.first {
48+
case "Sources", "Tests":
49+
fileLocation.directories
50+
case nil:
4851
["Sources", "MyLibrary"]
49-
} else if fileLocation.directories.first != "Sources" {
52+
default:
5053
["Sources"] + fileLocation.directories
51-
} else {
52-
fileLocation.directories
5354
}
55+
5456
filesByPath[RelativeFileLocation(directories: directories, fileLocation.fileName)] = contents
5557
}
5658
filesByPath["Package.swift"] = manifest
@@ -75,6 +77,7 @@ public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
7577
swift.path,
7678
"build",
7779
"--package-path", path.path,
80+
"--build-tests",
7881
"-Xswiftc", "-index-ignore-system-modules",
7982
"-Xcc", "-index-ignore-system-symbols",
8083
]

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ fileprivate enum TaskMetadata: DependencyTracker {
335335
self = .freestanding
336336
case is WorkspaceSymbolsRequest:
337337
self = .freestanding
338+
case is WorkspaceTestsRequest:
339+
self = .freestanding
338340
default:
339341
logger.error(
340342
"""
@@ -836,6 +838,8 @@ extension SourceKitServer: MessageHandler {
836838
await request.reply { try await shutdown(request.params) }
837839
case let request as RequestAndReply<WorkspaceSymbolsRequest>:
838840
await request.reply { try await workspaceSymbols(request.params) }
841+
case let request as RequestAndReply<WorkspaceTestsRequest>:
842+
await request.reply { try await workspaceTests(request.params) }
839843
case let request as RequestAndReply<PollIndexRequest>:
840844
await request.reply { try await pollIndex(request.params) }
841845
case let request as RequestAndReply<BarrierRequest>:
@@ -1490,33 +1494,30 @@ extension SourceKitServer {
14901494
/// Handle a workspace/symbol request, returning the SymbolInformation.
14911495
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
14921496
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
1493-
let symbols = findWorkspaceSymbols(
1494-
matching: req.query
1495-
).map({ symbolOccurrence -> WorkspaceSymbolItem in
1496-
let symbolPosition = Position(
1497-
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
1498-
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
1499-
utf16index: symbolOccurrence.location.utf8Column - 1
1500-
)
1501-
1502-
let symbolLocation = Location(
1503-
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
1504-
range: Range(symbolPosition)
1505-
)
1506-
1507-
return .symbolInformation(
1508-
SymbolInformation(
1509-
name: symbolOccurrence.symbol.name,
1510-
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
1511-
deprecated: nil,
1512-
location: symbolLocation,
1513-
containerName: symbolOccurrence.getContainerName()
1514-
)
1515-
)
1516-
})
1497+
let symbols = findWorkspaceSymbols(matching: req.query).map(WorkspaceSymbolItem.init)
15171498
return symbols
15181499
}
15191500

1501+
func workspaceTests(_ req: WorkspaceTestsRequest) async throws -> [WorkspaceSymbolItem]? {
1502+
let testSymbols = workspaces.flatMap { (workspace) -> [SymbolOccurrence] in
1503+
guard let index = workspace.index else {
1504+
return []
1505+
}
1506+
let xctestSubclasses = index.occurrences(ofUSR: "c:objc(cs)XCTest", roles: [.baseOf])
1507+
.flatMap(\.relations)
1508+
.filter { $0.roles.contains(.baseOf) }
1509+
.flatMap { index.occurrences(ofUSR: $0.symbol.usr, roles: [.definition]) }
1510+
let testMethods = xctestSubclasses.flatMap { xctestSubclass in
1511+
index.occurrences(relatedToUSR: xctestSubclass.symbol.usr, roles: [.childOf])
1512+
}.filter { testMethodSymbol in
1513+
return testMethodSymbol.symbol.kind == .instanceMethod && testMethodSymbol.symbol.name.starts(with: "test")
1514+
&& testMethodSymbol.roles.contains(.definition)
1515+
}
1516+
return xctestSubclasses + testMethods
1517+
}
1518+
return testSymbols.map(WorkspaceSymbolItem.init)
1519+
}
1520+
15201521
/// Forwards a SymbolInfoRequest to the appropriate toolchain service for this document.
15211522
func symbolInfo(
15221523
_ req: SymbolInfoRequest,
@@ -2213,3 +2214,27 @@ fileprivate struct DocumentNotificationRequestQueue {
22132214
queue = []
22142215
}
22152216
}
2217+
fileprivate extension WorkspaceSymbolItem {
2218+
init(_ symbolOccurrence: SymbolOccurrence) {
2219+
let symbolPosition = Position(
2220+
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
2221+
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
2222+
utf16index: symbolOccurrence.location.utf8Column - 1
2223+
)
2224+
2225+
let symbolLocation = Location(
2226+
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
2227+
range: Range(symbolPosition)
2228+
)
2229+
2230+
self = .symbolInformation(
2231+
SymbolInformation(
2232+
name: symbolOccurrence.symbol.name,
2233+
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
2234+
deprecated: nil,
2235+
location: symbolLocation,
2236+
containerName: symbolOccurrence.getContainerName()
2237+
)
2238+
)
2239+
}
2240+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import SKTestSupport
15+
import XCTest
16+
17+
final class WorkspaceTestsTests: XCTestCase {
18+
func testWorkspaceTests() async throws {
19+
let ws = try await SwiftPMTestWorkspace(
20+
files: [
21+
"Tests/MyLibraryTests/MyTests.swift": """
22+
import XCTest
23+
24+
class 1️⃣MyTests: XCTest {
25+
func 2️⃣testMyLibrary() {}
26+
func unrelatedFunc() {}
27+
var testVariable: Int = 0
28+
}
29+
"""
30+
],
31+
manifest: """
32+
// swift-tools-version: 5.7
33+
34+
import PackageDescription
35+
36+
let package = Package(
37+
name: "MyLibrary",
38+
targets: [.testTarget(name: "MyLibraryTests")]
39+
)
40+
""",
41+
build: true
42+
)
43+
44+
let workspaceTests = try await ws.testClient.send(WorkspaceTestsRequest())
45+
XCTAssertEqual(
46+
workspaceTests,
47+
[
48+
WorkspaceSymbolItem.symbolInformation(
49+
SymbolInformation(
50+
name: "MyTests",
51+
kind: .class,
52+
location: Location(
53+
uri: try ws.uri(for: "MyTests.swift"),
54+
range: Range(try ws.position(of: "1️⃣", in: "MyTests.swift"))
55+
)
56+
)
57+
),
58+
WorkspaceSymbolItem.symbolInformation(
59+
SymbolInformation(
60+
name: "testMyLibrary()",
61+
kind: .method,
62+
location: Location(
63+
uri: try ws.uri(for: "MyTests.swift"),
64+
range: Range(try ws.position(of: "2️⃣", in: "MyTests.swift"))
65+
),
66+
containerName: "MyTests"
67+
)
68+
),
69+
]
70+
)
71+
}
72+
}

0 commit comments

Comments
 (0)