Skip to content

Commit bd3c200

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

File tree

5 files changed

+147
-33
lines changed

5 files changed

+147
-33
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: 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
@@ -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: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ final actor WorkDoneProgressState {
162162
fileprivate enum TaskMetadata: DependencyTracker {
163163
/// A task that changes the global configuration of sourcekit-lsp in any way.
164164
///
165-
/// No other tasks must execute simulateneously with this task since they
165+
/// No other tasks must execute simultaneously with this task since they
166166
/// might be relying on this task to take effect.
167167
case globalConfigurationChange
168168

@@ -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>:
@@ -1463,7 +1467,7 @@ extension SourceKitServer {
14631467
guard matching.count >= minWorkspaceSymbolPatternLength else {
14641468
return []
14651469
}
1466-
var symbolOccurenceResults: [SymbolOccurrence] = []
1470+
var symbolOccurrenceResults: [SymbolOccurrence] = []
14671471
for workspace in workspaces {
14681472
workspace.index?.forEachCanonicalSymbolOccurrence(
14691473
containing: matching,
@@ -1475,48 +1479,35 @@ extension SourceKitServer {
14751479
guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else {
14761480
return true
14771481
}
1478-
symbolOccurenceResults.append(symbol)
1482+
symbolOccurrenceResults.append(symbol)
14791483
// FIXME: Once we have cancellation support, we should fetch all results and take the top
14801484
// `maxWorkspaceSymbolResults` symbols but bail if cancelled.
14811485
//
14821486
// Until then, take the first `maxWorkspaceSymbolResults` symbols to limit the impact of
14831487
// queries which match many symbols.
1484-
return symbolOccurenceResults.count < maxWorkspaceSymbolResults
1488+
return symbolOccurrenceResults.count < maxWorkspaceSymbolResults
14851489
}
14861490
}
1487-
return symbolOccurenceResults
1491+
return symbolOccurrenceResults
14881492
}
14891493

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+
return index.unitTests()
1507+
}
1508+
return testSymbols.map(WorkspaceSymbolItem.init)
1509+
}
1510+
15201511
/// Forwards a SymbolInfoRequest to the appropriate toolchain service for this document.
15211512
func symbolInfo(
15221513
_ req: SymbolInfoRequest,
@@ -2213,3 +2204,27 @@ fileprivate struct DocumentNotificationRequestQueue {
22132204
queue = []
22142205
}
22152206
}
2207+
fileprivate extension WorkspaceSymbolItem {
2208+
init(_ symbolOccurrence: SymbolOccurrence) {
2209+
let symbolPosition = Position(
2210+
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
2211+
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
2212+
utf16index: symbolOccurrence.location.utf8Column - 1
2213+
)
2214+
2215+
let symbolLocation = Location(
2216+
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
2217+
range: Range(symbolPosition)
2218+
)
2219+
2220+
self = .symbolInformation(
2221+
SymbolInformation(
2222+
name: symbolOccurrence.symbol.name,
2223+
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
2224+
deprecated: nil,
2225+
location: symbolLocation,
2226+
containerName: symbolOccurrence.getContainerName()
2227+
)
2228+
)
2229+
}
2230+
}
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)