Skip to content

Add a request to list all the tests within a workspace or document #978

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/LanguageServerProtocol/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ add_library(LanguageServerProtocol STATIC
Requests/DocumentSemanticTokensRangeRequest.swift
Requests/DocumentSemanticTokensRequest.swift
Requests/DocumentSymbolRequest.swift
Requests/DocumentTestsRequest.swift
Requests/ExecuteCommandRequest.swift
Requests/FoldingRangeRequest.swift
Requests/FormattingRequests.swift
Expand Down Expand Up @@ -86,6 +87,7 @@ add_library(LanguageServerProtocol STATIC
Requests/WorkspaceSemanticTokensRefreshRequest.swift
Requests/WorkspaceSymbolResolveRequest.swift
Requests/WorkspaceSymbolsRequest.swift
Requests/WorkspaceTestsRequest.swift

SupportTypes/CallHierarchyItem.swift
SupportTypes/ClientCapabilities.swift
Expand Down
2 changes: 2 additions & 0 deletions Sources/LanguageServerProtocol/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public let builtinRequests: [_RequestType.Type] = [
DocumentSemanticTokensRangeRequest.self,
DocumentSemanticTokensRequest.self,
DocumentSymbolRequest.self,
DocumentTestsRequest.self,
ExecuteCommandRequest.self,
FoldingRangeRequest.self,
HoverRequest.self,
Expand Down Expand Up @@ -82,6 +83,7 @@ public let builtinRequests: [_RequestType.Type] = [
WorkspaceSemanticTokensRefreshRequest.self,
WorkspaceSymbolResolveRequest.self,
WorkspaceSymbolsRequest.self,
WorkspaceTestsRequest.self,
]

/// The set of known notifications.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

/// A request that returns symbols for all the test classes and test methods within a file.
///
/// **(LSP Extension)**
public struct DocumentTestsRequest: TextDocumentRequest, Hashable {
public static let method: String = "document/tests"
public typealias Response = [WorkspaceSymbolItem]?

public var textDocument: TextDocumentIdentifier

public init(textDocument: TextDocumentIdentifier) {
self.textDocument = textDocument
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

/// A request that returns symbols for all the test classes and test methods within the current workspace.
///
/// **(LSP Extension)**
public struct WorkspaceTestsRequest: RequestType, Hashable {
public static let method: String = "workspace/tests"
public typealias Response = [WorkspaceSymbolItem]?

public init() {}
}
2 changes: 1 addition & 1 deletion Sources/SKCore/BuildSystemManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ extension BuildSystemManager: MainFilesDelegate {
/// For Swift or normal C files, this will be the file itself. For header
/// files, we pick a main file that includes the header since header files
/// don't have build settings by themselves.
private func mainFile(for uri: DocumentURI, language: Language, useCache: Bool = true) async -> DocumentURI {
public func mainFile(for uri: DocumentURI, language: Language, useCache: Bool = true) async -> DocumentURI {
if language == .swift {
// Swift doesn't have main files. Skip the main file provider query.
return uri
Expand Down
11 changes: 7 additions & 4 deletions Sources/SKTestSupport/SwiftPMTestWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
var filesByPath: [RelativeFileLocation: String] = [:]
for (fileLocation, contents) in files {
let directories =
if fileLocation.directories.isEmpty {
switch fileLocation.directories.first {
case "Sources", "Tests":
fileLocation.directories
case nil:
["Sources", "MyLibrary"]
} else if fileLocation.directories.first != "Sources" {
default:
["Sources"] + fileLocation.directories
} else {
fileLocation.directories
}

filesByPath[RelativeFileLocation(directories: directories, fileLocation.fileName)] = contents
}
filesByPath["Package.swift"] = manifest
Expand All @@ -77,6 +79,7 @@ public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
swift.path,
"build",
"--package-path", path.path,
"--build-tests",
"-Xswiftc", "-index-ignore-system-modules",
"-Xcc", "-index-ignore-system-symbols",
]
Expand Down
3 changes: 2 additions & 1 deletion Sources/SourceKitLSP/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ add_library(SourceKitLSP STATIC
Sequence+AsyncMap.swift
SourceKitIndexDelegate.swift
SourceKitLSPCommandMetadata.swift
SourceKitServer+Options.swift
SourceKitServer.swift
SourceKitServer+Options.swift
TestDiscovery.swift
ToolchainLanguageServer.swift
Workspace.swift
)
Expand Down
70 changes: 39 additions & 31 deletions Sources/SourceKitLSP/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ final actor WorkDoneProgressState {
fileprivate enum TaskMetadata: DependencyTracker {
/// A task that changes the global configuration of sourcekit-lsp in any way.
///
/// No other tasks must execute simulateneously with this task since they
/// No other tasks must execute simultaneously with this task since they
/// might be relying on this task to take effect.
case globalConfigurationChange

Expand Down Expand Up @@ -336,6 +336,8 @@ fileprivate enum TaskMetadata: DependencyTracker {
self = .freestanding
case is WorkspaceSymbolsRequest:
self = .freestanding
case is WorkspaceTestsRequest:
self = .freestanding
default:
logger.error(
"""
Expand Down Expand Up @@ -382,7 +384,7 @@ public actor SourceKitServer {

var languageServices: [LanguageServerType: [ToolchainLanguageServer]] = [:]

private let documentManager = DocumentManager()
let documentManager = DocumentManager()

private var packageLoadingWorkDoneProgress = WorkDoneProgressState(
"SourceKitLSP.SourceKitServer.reloadPackage",
Expand All @@ -398,7 +400,7 @@ public actor SourceKitServer {
/// Must only be accessed from `queue`.
private var uriToWorkspaceCache: [DocumentURI: WeakWorkspace] = [:]

private var workspaces: [Workspace] = [] {
private(set) var workspaces: [Workspace] = [] {
didSet {
uriToWorkspaceCache = [:]
}
Expand Down Expand Up @@ -837,6 +839,10 @@ extension SourceKitServer: MessageHandler {
await request.reply { try await shutdown(request.params) }
case let request as RequestAndReply<WorkspaceSymbolsRequest>:
await request.reply { try await workspaceSymbols(request.params) }
case let request as RequestAndReply<WorkspaceTestsRequest>:
await request.reply { try await workspaceTests(request.params) }
case let request as RequestAndReply<DocumentTestsRequest>:
await self.handleRequest(for: request, requestHandler: self.documentTests)
case let request as RequestAndReply<PollIndexRequest>:
await request.reply { try await pollIndex(request.params) }
case let request as RequestAndReply<BarrierRequest>:
Expand Down Expand Up @@ -1499,7 +1505,7 @@ extension SourceKitServer {
guard matching.count >= minWorkspaceSymbolPatternLength else {
return []
}
var symbolOccurenceResults: [SymbolOccurrence] = []
var symbolOccurrenceResults: [SymbolOccurrence] = []
for workspace in workspaces {
workspace.index?.forEachCanonicalSymbolOccurrence(
containing: matching,
Expand All @@ -1511,45 +1517,22 @@ extension SourceKitServer {
guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else {
return true
}
symbolOccurenceResults.append(symbol)
symbolOccurrenceResults.append(symbol)
// FIXME: Once we have cancellation support, we should fetch all results and take the top
// `maxWorkspaceSymbolResults` symbols but bail if cancelled.
//
// Until then, take the first `maxWorkspaceSymbolResults` symbols to limit the impact of
// queries which match many symbols.
return symbolOccurenceResults.count < maxWorkspaceSymbolResults
return symbolOccurrenceResults.count < maxWorkspaceSymbolResults
}
}
return symbolOccurenceResults
return symbolOccurrenceResults
}

/// Handle a workspace/symbol request, returning the SymbolInformation.
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
let symbols = findWorkspaceSymbols(
matching: req.query
).map({ symbolOccurrence -> WorkspaceSymbolItem in
let symbolPosition = Position(
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
utf16index: symbolOccurrence.location.utf8Column - 1
)

let symbolLocation = Location(
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
range: Range(symbolPosition)
)

return .symbolInformation(
SymbolInformation(
name: symbolOccurrence.symbol.name,
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
deprecated: nil,
location: symbolLocation,
containerName: symbolOccurrence.getContainerName()
)
)
})
let symbols = findWorkspaceSymbols(matching: req.query).map(WorkspaceSymbolItem.init)
return symbols
}

Expand Down Expand Up @@ -2294,3 +2277,28 @@ fileprivate func transitiveSubtypeClosure(ofUsrs usrs: [String], index: IndexSto
}
return result
}

extension WorkspaceSymbolItem {
init(_ symbolOccurrence: SymbolOccurrence) {
let symbolPosition = Position(
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
utf16index: symbolOccurrence.location.utf8Column - 1
)

let symbolLocation = Location(
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
range: Range(symbolPosition)
)

self = .symbolInformation(
SymbolInformation(
name: symbolOccurrence.symbol.name,
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
deprecated: nil,
location: symbolLocation,
containerName: symbolOccurrence.getContainerName()
)
)
}
}
61 changes: 61 additions & 0 deletions Sources/SourceKitLSP/TestDiscovery.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import IndexStoreDB
import LanguageServerProtocol

fileprivate extension SymbolOccurrence {
/// Assuming that this is a symbol occurrence returned by the index, return whether it can constitute the definition
/// of a test case.
///
/// The primary intention for this is to filter out references to test cases and extension declarations of test cases.
/// The latter is important to filter so we don't include extension declarations for the derived `DiscoveredTests`
/// files on non-Darwin platforms.
var canBeTestDefinition: Bool {
guard roles.contains(.definition) else {
return false
}
guard symbol.kind == .class || symbol.kind == .instanceMethod else {
return false
}
return true
}
}

extension SourceKitServer {
func workspaceTests(_ req: WorkspaceTestsRequest) async throws -> [WorkspaceSymbolItem]? {
let testSymbols = workspaces.flatMap { (workspace) -> [SymbolOccurrence] in
return workspace.index?.unitTests() ?? []
}
return
testSymbols
.filter { $0.canBeTestDefinition }
.map(WorkspaceSymbolItem.init)
}

func documentTests(
_ req: DocumentTestsRequest,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async throws -> [WorkspaceSymbolItem]? {
let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri)
let mainFileUri = await workspace.buildSystemManager.mainFile(
for: req.textDocument.uri,
language: snapshot.language
)
let testSymbols = workspace.index?.unitTests(referencedByMainFiles: [mainFileUri.pseudoPath]) ?? []
return
testSymbols
.filter { $0.canBeTestDefinition }
.map(WorkspaceSymbolItem.init)
}
}
Loading