Skip to content

Commit 446f928

Browse files
committed
Add a request to list all the tests within the current workspace
Fixes #611 rdar://98710526
1 parent 17eca18 commit 446f928

File tree

6 files changed

+149
-33
lines changed

6 files changed

+149
-33
lines changed

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ add_library(LanguageServerProtocol STATIC
8686
Requests/WorkspaceSemanticTokensRefreshRequest.swift
8787
Requests/WorkspaceSymbolResolveRequest.swift
8888
Requests/WorkspaceSymbolsRequest.swift
89+
Requests/WorkspaceTestsRequest.swift
8990

9091
SupportTypes/CallHierarchyItem.swift
9192
SupportTypes/ClientCapabilities.swift

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
public static let method: String = "workspace/tests"
18+
public typealias Response = [WorkspaceSymbolItem]?
19+
20+
public init() {}
21+
}

Sources/SKTestSupport/SwiftPMTestWorkspace.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@ public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
4545
var filesByPath: [RelativeFileLocation: String] = [:]
4646
for (fileLocation, contents) in files {
4747
let directories =
48-
if fileLocation.directories.isEmpty {
48+
switch fileLocation.directories.first {
49+
case "Sources", "Tests":
50+
fileLocation.directories
51+
case nil:
4952
["Sources", "MyLibrary"]
50-
} else if fileLocation.directories.first != "Sources" {
53+
default:
5154
["Sources"] + fileLocation.directories
52-
} else {
53-
fileLocation.directories
5455
}
56+
5557
filesByPath[RelativeFileLocation(directories: directories, fileLocation.fileName)] = contents
5658
}
5759
filesByPath["Package.swift"] = manifest
@@ -77,6 +79,7 @@ public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
7779
swift.path,
7880
"build",
7981
"--package-path", path.path,
82+
"--build-tests",
8083
"-Xswiftc", "-index-ignore-system-modules",
8184
"-Xcc", "-index-ignore-system-symbols",
8285
]

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ final actor WorkDoneProgressState {
163163
fileprivate enum TaskMetadata: DependencyTracker {
164164
/// A task that changes the global configuration of sourcekit-lsp in any way.
165165
///
166-
/// No other tasks must execute simulateneously with this task since they
166+
/// No other tasks must execute simultaneously with this task since they
167167
/// might be relying on this task to take effect.
168168
case globalConfigurationChange
169169

@@ -336,6 +336,8 @@ fileprivate enum TaskMetadata: DependencyTracker {
336336
self = .freestanding
337337
case is WorkspaceSymbolsRequest:
338338
self = .freestanding
339+
case is WorkspaceTestsRequest:
340+
self = .freestanding
339341
default:
340342
logger.error(
341343
"""
@@ -837,6 +839,8 @@ extension SourceKitServer: MessageHandler {
837839
await request.reply { try await shutdown(request.params) }
838840
case let request as RequestAndReply<WorkspaceSymbolsRequest>:
839841
await request.reply { try await workspaceSymbols(request.params) }
842+
case let request as RequestAndReply<WorkspaceTestsRequest>:
843+
await request.reply { try await workspaceTests(request.params) }
840844
case let request as RequestAndReply<PollIndexRequest>:
841845
await request.reply { try await pollIndex(request.params) }
842846
case let request as RequestAndReply<BarrierRequest>:
@@ -1499,7 +1503,7 @@ extension SourceKitServer {
14991503
guard matching.count >= minWorkspaceSymbolPatternLength else {
15001504
return []
15011505
}
1502-
var symbolOccurenceResults: [SymbolOccurrence] = []
1506+
var symbolOccurrenceResults: [SymbolOccurrence] = []
15031507
for workspace in workspaces {
15041508
workspace.index?.forEachCanonicalSymbolOccurrence(
15051509
containing: matching,
@@ -1511,48 +1515,35 @@ extension SourceKitServer {
15111515
guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else {
15121516
return true
15131517
}
1514-
symbolOccurenceResults.append(symbol)
1518+
symbolOccurrenceResults.append(symbol)
15151519
// FIXME: Once we have cancellation support, we should fetch all results and take the top
15161520
// `maxWorkspaceSymbolResults` symbols but bail if cancelled.
15171521
//
15181522
// Until then, take the first `maxWorkspaceSymbolResults` symbols to limit the impact of
15191523
// queries which match many symbols.
1520-
return symbolOccurenceResults.count < maxWorkspaceSymbolResults
1524+
return symbolOccurrenceResults.count < maxWorkspaceSymbolResults
15211525
}
15221526
}
1523-
return symbolOccurenceResults
1527+
return symbolOccurrenceResults
15241528
}
15251529

15261530
/// Handle a workspace/symbol request, returning the SymbolInformation.
15271531
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
15281532
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
1529-
let symbols = findWorkspaceSymbols(
1530-
matching: req.query
1531-
).map({ symbolOccurrence -> WorkspaceSymbolItem in
1532-
let symbolPosition = Position(
1533-
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
1534-
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
1535-
utf16index: symbolOccurrence.location.utf8Column - 1
1536-
)
1537-
1538-
let symbolLocation = Location(
1539-
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
1540-
range: Range(symbolPosition)
1541-
)
1542-
1543-
return .symbolInformation(
1544-
SymbolInformation(
1545-
name: symbolOccurrence.symbol.name,
1546-
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
1547-
deprecated: nil,
1548-
location: symbolLocation,
1549-
containerName: symbolOccurrence.getContainerName()
1550-
)
1551-
)
1552-
})
1533+
let symbols = findWorkspaceSymbols(matching: req.query).map(WorkspaceSymbolItem.init)
15531534
return symbols
15541535
}
15551536

1537+
func workspaceTests(_ req: WorkspaceTestsRequest) async throws -> [WorkspaceSymbolItem]? {
1538+
let testSymbols = workspaces.flatMap { (workspace) -> [SymbolOccurrence] in
1539+
guard let index = workspace.index else {
1540+
return []
1541+
}
1542+
return index.unitTests()
1543+
}
1544+
return testSymbols.map(WorkspaceSymbolItem.init)
1545+
}
1546+
15561547
/// Forwards a SymbolInfoRequest to the appropriate toolchain service for this document.
15571548
func symbolInfo(
15581549
_ req: SymbolInfoRequest,
@@ -2294,3 +2285,28 @@ fileprivate func transitiveSubtypeClosure(ofUsrs usrs: [String], index: IndexSto
22942285
}
22952286
return result
22962287
}
2288+
2289+
fileprivate extension WorkspaceSymbolItem {
2290+
init(_ symbolOccurrence: SymbolOccurrence) {
2291+
let symbolPosition = Position(
2292+
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
2293+
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
2294+
utf16index: symbolOccurrence.location.utf8Column - 1
2295+
)
2296+
2297+
let symbolLocation = Location(
2298+
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
2299+
range: Range(symbolPosition)
2300+
)
2301+
2302+
self = .symbolInformation(
2303+
SymbolInformation(
2304+
name: symbolOccurrence.symbol.name,
2305+
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
2306+
deprecated: nil,
2307+
location: symbolLocation,
2308+
containerName: symbolOccurrence.getContainerName()
2309+
)
2310+
)
2311+
}
2312+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
try XCTSkipIf(longTestsDisabled)
20+
21+
let ws = try await SwiftPMTestWorkspace(
22+
files: [
23+
"Tests/MyLibraryTests/MyTests.swift": """
24+
import XCTest
25+
26+
class 1️⃣MyTests: XCTestCase {
27+
func 2️⃣testMyLibrary() {}
28+
func unrelatedFunc() {}
29+
var testVariable: Int = 0
30+
}
31+
"""
32+
],
33+
manifest: """
34+
// swift-tools-version: 5.7
35+
36+
import PackageDescription
37+
38+
let package = Package(
39+
name: "MyLibrary",
40+
targets: [.testTarget(name: "MyLibraryTests")]
41+
)
42+
""",
43+
build: true
44+
)
45+
46+
let tests = try await ws.testClient.send(WorkspaceTestsRequest())
47+
XCTAssertEqual(
48+
tests,
49+
[
50+
WorkspaceSymbolItem.symbolInformation(
51+
SymbolInformation(
52+
name: "MyTests",
53+
kind: .class,
54+
location: Location(
55+
uri: try ws.uri(for: "MyTests.swift"),
56+
range: Range(try ws.position(of: "1️⃣", in: "MyTests.swift"))
57+
)
58+
)
59+
),
60+
WorkspaceSymbolItem.symbolInformation(
61+
SymbolInformation(
62+
name: "testMyLibrary()",
63+
kind: .method,
64+
location: Location(
65+
uri: try ws.uri(for: "MyTests.swift"),
66+
range: Range(try ws.position(of: "2️⃣", in: "MyTests.swift"))
67+
),
68+
containerName: "MyTests"
69+
)
70+
),
71+
]
72+
)
73+
}
74+
}

0 commit comments

Comments
 (0)