mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
Windows requires absolute paths to have a volume letter associated with them. Without the path being absolute SourceKit will assert. Provide custom paths on Windows.
459 lines
16 KiB
Swift
459 lines
16 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// 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 BuildServerProtocol
|
|
import LanguageServerProtocol
|
|
import LSPTestSupport
|
|
import SKCore
|
|
import SKTestSupport
|
|
import SourceKitLSP
|
|
import TSCBasic
|
|
import XCTest
|
|
|
|
// Workaround ambiguity with Foundation.
|
|
typealias LSPNotification = LanguageServerProtocol.Notification
|
|
|
|
/// Build system to be used for testing BuildSystem and BuildSystemDelegate functionality with SourceKitServer
|
|
/// and other components.
|
|
final class TestBuildSystem: BuildSystem {
|
|
var indexStorePath: AbsolutePath? = nil
|
|
var indexDatabasePath: AbsolutePath? = nil
|
|
var indexPrefixMappings: [PathPrefixMapping] = []
|
|
|
|
weak var delegate: BuildSystemDelegate?
|
|
|
|
/// Build settings by file.
|
|
var buildSettingsByFile: [DocumentURI: FileBuildSettings] = [:]
|
|
|
|
/// Files currently being watched by our delegate.
|
|
var watchedFiles: Set<DocumentURI> = []
|
|
|
|
func registerForChangeNotifications(for uri: DocumentURI, language: Language) {
|
|
watchedFiles.insert(uri)
|
|
|
|
// Inform our delegate of settings for the file if they're available.
|
|
guard let delegate = self.delegate,
|
|
let settings = self.buildSettingsByFile[uri] else {
|
|
return
|
|
}
|
|
|
|
DispatchQueue.global().async {
|
|
delegate.fileBuildSettingsChanged([uri: .modified(settings)])
|
|
}
|
|
}
|
|
|
|
func unregisterForChangeNotifications(for uri: DocumentURI) {
|
|
watchedFiles.remove(uri)
|
|
}
|
|
|
|
func buildTargets(reply: @escaping (LSPResult<[BuildTarget]>) -> Void) {
|
|
reply(.failure(buildTargetsNotSupported))
|
|
}
|
|
|
|
func buildTargetSources(targets: [BuildTargetIdentifier], reply: @escaping (LSPResult<[SourcesItem]>) -> Void) {
|
|
reply(.failure(buildTargetsNotSupported))
|
|
}
|
|
|
|
func buildTargetOutputPaths(targets: [BuildTargetIdentifier], reply: @escaping (LSPResult<[OutputsItem]>) -> Void) {
|
|
reply(.failure(buildTargetsNotSupported))
|
|
}
|
|
|
|
func filesDidChange(_ events: [FileEvent]) {}
|
|
|
|
public func fileHandlingCapability(for uri: DocumentURI) -> FileHandlingCapability {
|
|
if buildSettingsByFile[uri] != nil {
|
|
return .handled
|
|
} else {
|
|
return .unhandled
|
|
}
|
|
}
|
|
}
|
|
|
|
final class BuildSystemTests: XCTestCase {
|
|
|
|
/// Connection and lifetime management for the service.
|
|
var testServer: TestSourceKitServer! = nil
|
|
|
|
/// The primary interface to make requests to the SourceKitServer.
|
|
var sk: TestClient! = nil
|
|
|
|
/// The document manager of the server
|
|
var documentManager: DocumentManager!
|
|
|
|
/// The server's workspace data. Accessing this is unsafe if the server does so concurrently.
|
|
var workspace: Workspace! = nil
|
|
|
|
/// The build system that we use to verify SourceKitServer behavior.
|
|
var buildSystem: TestBuildSystem! = nil
|
|
|
|
/// Whether clangd exists in the toolchain.
|
|
var haveClangd: Bool = false
|
|
|
|
override func setUp() {
|
|
haveClangd = ToolchainRegistry.shared.toolchains.contains { $0.clangd != nil }
|
|
testServer = TestSourceKitServer()
|
|
buildSystem = TestBuildSystem()
|
|
|
|
let server = testServer.server!
|
|
documentManager = server._documentManager
|
|
|
|
self.workspace = Workspace(
|
|
documentManager: DocumentManager(),
|
|
rootUri: nil,
|
|
capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()),
|
|
toolchainRegistry: ToolchainRegistry.shared,
|
|
buildSetup: TestSourceKitServer.serverOptions.buildSetup,
|
|
underlyingBuildSystem: buildSystem,
|
|
index: nil,
|
|
indexDelegate: nil)
|
|
|
|
server._workspaces = [workspace]
|
|
workspace.buildSystemManager.delegate = server
|
|
|
|
sk = testServer.client
|
|
_ = try! sk.sendSync(InitializeRequest(
|
|
processId: nil,
|
|
rootPath: nil,
|
|
rootURI: nil,
|
|
initializationOptions: nil,
|
|
capabilities: ClientCapabilities(workspace: nil, textDocument: nil),
|
|
trace: .off,
|
|
workspaceFolders: nil))
|
|
}
|
|
|
|
override func tearDown() {
|
|
buildSystem = nil
|
|
workspace = nil
|
|
sk = nil
|
|
testServer = nil
|
|
}
|
|
|
|
func testClangdDocumentUpdatedBuildSettings() {
|
|
guard haveClangd else { return }
|
|
|
|
#if os(Windows)
|
|
let url = URL(fileURLWithPath: "C:/\(UUID())/file.m")
|
|
#else
|
|
let url = URL(fileURLWithPath: "/\(UUID())/file.m")
|
|
#endif
|
|
let doc = DocumentURI(url)
|
|
let args = [url.path, "-DDEBUG"]
|
|
let text = """
|
|
#ifdef FOO
|
|
static void foo() {}
|
|
#endif
|
|
|
|
int main() {
|
|
foo();
|
|
return 0;
|
|
}
|
|
"""
|
|
|
|
buildSystem.buildSettingsByFile[doc] = FileBuildSettings(compilerArguments: args)
|
|
|
|
sk.allowUnexpectedNotification = false
|
|
|
|
sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
|
|
uri: doc,
|
|
language: .objective_c,
|
|
version: 12,
|
|
text: text
|
|
)), { (note: Notification<PublishDiagnosticsNotification>) in
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text)
|
|
})
|
|
|
|
// Modify the build settings and inform the delegate.
|
|
// This should trigger a new publish diagnostics and we should no longer have errors.
|
|
let newSettings = FileBuildSettings(compilerArguments: args + ["-DFOO"])
|
|
buildSystem.buildSettingsByFile[doc] = newSettings
|
|
|
|
let expectation = XCTestExpectation(description: "refresh")
|
|
sk.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
|
|
XCTAssertEqual(note.params.diagnostics.count, 0)
|
|
XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text)
|
|
expectation.fulfill()
|
|
}
|
|
|
|
buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)])
|
|
|
|
let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout)
|
|
guard result == .completed else {
|
|
XCTFail("wait for diagnostics failed with \(result)")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testSwiftDocumentUpdatedBuildSettings() {
|
|
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
|
let doc = DocumentURI(url)
|
|
let args = FallbackBuildSystem(buildSetup: .default).settings(for: doc, .swift)!.compilerArguments
|
|
|
|
buildSystem.buildSettingsByFile[doc] = FileBuildSettings(compilerArguments: args)
|
|
|
|
let text = """
|
|
#if FOO
|
|
func foo() {}
|
|
#endif
|
|
|
|
foo()
|
|
"""
|
|
|
|
sk.allowUnexpectedNotification = false
|
|
|
|
sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
|
|
uri: doc,
|
|
language: .swift,
|
|
version: 12,
|
|
text: text
|
|
)), { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Syntactic analysis - no expected errors here.
|
|
XCTAssertEqual(note.params.diagnostics.count, 0)
|
|
XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text)
|
|
}, { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Semantic analysis - expect one error here.
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
})
|
|
|
|
// Modify the build settings and inform the delegate.
|
|
// This should trigger a new publish diagnostics and we should no longer have errors.
|
|
let newSettings = FileBuildSettings(compilerArguments: args + ["-DFOO"])
|
|
buildSystem.buildSettingsByFile[doc] = newSettings
|
|
|
|
let expectation = XCTestExpectation(description: "refresh")
|
|
expectation.expectedFulfillmentCount = 2
|
|
sk.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Semantic analysis - SourceKit currently caches diagnostics so we still see an error.
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
expectation.fulfill()
|
|
}
|
|
sk.appendOneShotNotificationHandler { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Semantic analysis - no expected errors here because we fixed the settings.
|
|
XCTAssertEqual(note.params.diagnostics.count, 0)
|
|
expectation.fulfill()
|
|
}
|
|
buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)])
|
|
|
|
let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout)
|
|
guard result == .completed else {
|
|
XCTFail("wait for diagnostics failed with \(result)")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testClangdDocumentFallbackWithholdsDiagnostics() {
|
|
guard haveClangd else { return }
|
|
|
|
#if os(Windows)
|
|
let url = URL(fileURLWithPath: "C:/\(UUID())/file.m")
|
|
#else
|
|
let url = URL(fileURLWithPath: "/\(UUID())/file.m")
|
|
#endif
|
|
let doc = DocumentURI(url)
|
|
let args = [url.path, "-DDEBUG"]
|
|
let text = """
|
|
#ifdef FOO
|
|
static void foo() {}
|
|
#endif
|
|
|
|
int main() {
|
|
foo();
|
|
return 0;
|
|
}
|
|
"""
|
|
|
|
sk.allowUnexpectedNotification = false
|
|
|
|
sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
|
|
uri: doc,
|
|
language: .objective_c,
|
|
version: 12,
|
|
text: text
|
|
)), { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Expect diagnostics to be withheld.
|
|
XCTAssertEqual(note.params.diagnostics.count, 0)
|
|
XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text)
|
|
})
|
|
|
|
// Modify the build settings and inform the delegate.
|
|
// This should trigger a new publish diagnostics and we should see a diagnostic.
|
|
let newSettings = FileBuildSettings(compilerArguments: args)
|
|
buildSystem.buildSettingsByFile[doc] = newSettings
|
|
|
|
let expectation = XCTestExpectation(description: "refresh due to fallback --> primary")
|
|
sk.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text)
|
|
expectation.fulfill()
|
|
}
|
|
|
|
buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)])
|
|
|
|
let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout)
|
|
guard result == .completed else {
|
|
XCTFail("wait for diagnostics failed with \(result)")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testSwiftDocumentFallbackWithholdsSemanticDiagnostics() {
|
|
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
|
let doc = DocumentURI(url)
|
|
|
|
// Primary settings must be different than the fallback settings.
|
|
var primarySettings = FallbackBuildSystem(buildSetup: .default).settings(for: doc, .swift)!
|
|
primarySettings.compilerArguments.append("-DPRIMARY")
|
|
|
|
let text = """
|
|
#if FOO
|
|
func foo() {}
|
|
#endif
|
|
|
|
foo()
|
|
func
|
|
"""
|
|
|
|
sk.allowUnexpectedNotification = false
|
|
|
|
sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
|
|
uri: doc,
|
|
language: .swift,
|
|
version: 12,
|
|
text: text
|
|
)), { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Syntactic analysis - one expected errors here (for `func`).
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text)
|
|
}, { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Should be the same syntactic analysis since we are using fallback arguments
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
})
|
|
|
|
// Swap from fallback settings to primary build system settings.
|
|
buildSystem.buildSettingsByFile[doc] = primarySettings
|
|
let expectation = XCTestExpectation(description: "refresh due to fallback --> primary")
|
|
expectation.expectedFulfillmentCount = 2
|
|
sk.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Syntactic analysis with new args - one expected errors here (for `func`).
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
expectation.fulfill()
|
|
}
|
|
sk.appendOneShotNotificationHandler { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Semantic analysis - two errors since `-DFOO` was not passed.
|
|
XCTAssertEqual(note.params.diagnostics.count, 2)
|
|
expectation.fulfill()
|
|
}
|
|
buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(primarySettings)])
|
|
|
|
let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout)
|
|
guard result == .completed else {
|
|
XCTFail("wait for diagnostics failed with \(result)")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testSwiftDocumentBuildSettingsChangedFalseAlarm() {
|
|
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
|
let doc = DocumentURI(url)
|
|
|
|
sk.allowUnexpectedNotification = false
|
|
|
|
sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
|
|
uri: doc,
|
|
language: .swift,
|
|
version: 12,
|
|
text: """
|
|
func
|
|
"""
|
|
)), { (note: Notification<PublishDiagnosticsNotification>) in
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
XCTAssertEqual("func", self.documentManager.latestSnapshot(doc)!.text)
|
|
}, { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Using fallback system, so we will receive the same syntactic diagnostics from before.
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
})
|
|
|
|
// Modify the build settings and inform the SourceKitServer.
|
|
// This shouldn't trigger new diagnostics since nothing actually changed (false alarm).
|
|
buildSystem.delegate?.fileBuildSettingsChanged([doc: .removedOrUnavailable])
|
|
|
|
let expectation = XCTestExpectation(description: "refresh doesn't occur")
|
|
expectation.isInverted = true
|
|
sk.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
XCTAssertEqual("func", self.documentManager.latestSnapshot(doc)!.text)
|
|
expectation.fulfill()
|
|
}
|
|
|
|
let result = XCTWaiter.wait(for: [expectation], timeout: 1)
|
|
guard result == .completed else {
|
|
XCTFail("wait for diagnostics failed with \(result)")
|
|
return
|
|
}
|
|
}
|
|
|
|
func testMainFilesChanged() throws {
|
|
let ws = try mutableSourceKitTibsTestWorkspace(name: "MainFiles")!
|
|
let unique_h = ws.testLoc("unique").docIdentifier.uri
|
|
|
|
ws.testServer.client.allowUnexpectedNotification = false
|
|
|
|
let expectation = self.expectation(description: "initial")
|
|
ws.testServer.client.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
|
|
// Should withhold diagnostics since we should be using fallback arguments.
|
|
XCTAssertEqual(note.params.diagnostics.count, 0)
|
|
expectation.fulfill()
|
|
}
|
|
|
|
try ws.openDocument(unique_h.fileURL!, language: .cpp)
|
|
wait(for: [expectation], timeout: defaultTimeout)
|
|
|
|
let use_d = self.expectation(description: "update settings to d.cpp")
|
|
ws.testServer.client.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
if let diag = note.params.diagnostics.first {
|
|
XCTAssertEqual(diag.severity, .warning)
|
|
XCTAssertEqual(diag.message, "UNIQUE_INCLUDED_FROM_D")
|
|
}
|
|
use_d.fulfill()
|
|
}
|
|
|
|
try ws.buildAndIndex()
|
|
wait(for: [use_d], timeout: defaultTimeout)
|
|
|
|
let use_c = self.expectation(description: "update settings to c.cpp")
|
|
ws.testServer.client.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
|
|
XCTAssertEqual(note.params.diagnostics.count, 1)
|
|
if let diag = note.params.diagnostics.first {
|
|
XCTAssertEqual(diag.severity, .warning)
|
|
XCTAssertEqual(diag.message, "UNIQUE_INCLUDED_FROM_C")
|
|
}
|
|
use_c.fulfill()
|
|
}
|
|
|
|
try ws.edit(rebuild: true) { (changes, _) in
|
|
changes.write("""
|
|
// empty
|
|
""", to: ws.testLoc("d_func").url)
|
|
changes.write("""
|
|
#include "unique.h"
|
|
""", to: ws.testLoc("c_func").url)
|
|
}
|
|
|
|
wait(for: [use_c], timeout: defaultTimeout)
|
|
}
|
|
|
|
private func clangBuildSettings(for uri: DocumentURI) -> FileBuildSettings {
|
|
return FileBuildSettings(compilerArguments: [uri.pseudoPath, "-DDEBUG"])
|
|
}
|
|
}
|