Files
sourcekit-lsp/Tests/SourceKitLSPTests/BuildSystemTests.swift
Alex Hoppen 5c60d1d39c Add a new test project type that uses a custom build server
This allows us to more easily test behavior for build servers that have different behavior than SwiftPM and compile commands without having to implement the build server in Python.
2025-03-05 23:54:32 -08:00

261 lines
9.1 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
@_spi(Testing) import BuildSystemIntegration
import LanguageServerProtocol
import LanguageServerProtocolExtensions
import SKOptions
import SKTestSupport
@_spi(Testing) import SemanticIndex
@_spi(Testing) import SourceKitLSP
import SwiftExtensions
import TSCBasic
import ToolchainRegistry
import XCTest
fileprivate actor TestBuildSystem: CustomBuildServer {
private let connectionToSourceKitLSP: any Connection
private var buildSettingsByFile: [DocumentURI: TextDocumentSourceKitOptionsResponse] = [:]
func setBuildSettings(for uri: DocumentURI, to buildSettings: TextDocumentSourceKitOptionsResponse?) {
buildSettingsByFile[uri] = buildSettings
connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil))
}
init(projectRoot: URL, connectionToSourceKitLSP: any Connection) {
self.connectionToSourceKitLSP = connectionToSourceKitLSP
}
func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) -> BuildTargetSourcesResponse {
return dummyTargetSourcesResponse(buildSettingsByFile.keys)
}
func textDocumentSourceKitOptionsRequest(
_ request: TextDocumentSourceKitOptionsRequest
) async throws -> TextDocumentSourceKitOptionsResponse? {
return buildSettingsByFile[request.textDocument.uri]
}
}
final class BuildSystemTests: XCTestCase {
/// The mock client used to communicate with the SourceKit-LSP server.p
///
/// - Note: Set before each test run in `setUp`.
private var testClient: TestSourceKitLSPClient! = nil
/// The server's workspace data. Accessing this is unsafe if the server does so concurrently.
///
/// - Note: Set before each test run in `setUp`.
private var workspace: Workspace! = nil
/// The build system that we use to verify SourceKitLSPServer behavior.
///
/// - Note: Set before each test run in `setUp`.
private var buildSystem: TestBuildSystem! = nil
/// Whether clangd exists in the toolchain.
///
/// - Note: Set before each test run in `setUp`.
private var haveClangd: Bool = false
override func setUp() async throws {
testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
let server = testClient.server
let testBuildSystem = ThreadSafeBox<TestBuildSystem?>(initialValue: nil)
let buildSystemManager = await BuildSystemManager(
buildSystemSpec: BuildSystemSpec(
kind: .injected({ projectRoot, connectionToSourceKitLSP in
assert(testBuildSystem.value == nil, "Build system injector hook can only create a single TestBuildSystem")
let buildSystem = TestBuildSystem(
projectRoot: projectRoot,
connectionToSourceKitLSP: connectionToSourceKitLSP
)
testBuildSystem.value = buildSystem
return LocalConnection(receiverName: "TestBuildSystem", handler: buildSystem)
}),
projectRoot: URL(fileURLWithPath: "/"),
configPath: URL(fileURLWithPath: "/")
),
toolchainRegistry: .forTesting,
options: try .testDefault(),
connectionToClient: DummyBuildSystemManagerConnectionToClient(),
buildSystemHooks: BuildSystemHooks()
)
buildSystem = try unwrap(testBuildSystem.value)
self.workspace = await Workspace.forTesting(
options: try .testDefault(),
testHooks: Hooks(),
buildSystemManager: buildSystemManager,
indexTaskScheduler: .forTesting
)
await server.setWorkspaces([(workspace: workspace, isImplicit: false)])
await workspace.buildSystemManager.setDelegate(workspace)
}
override func tearDown() {
buildSystem = nil
workspace = nil
testClient = nil
}
// MARK: - Tests
func testClangdDocumentUpdatedBuildSettings() async throws {
guard haveClangd else { return }
let doc = DocumentURI(for: .objective_c)
let args = [doc.pseudoPath, "-DDEBUG"]
let text = """
#ifdef FOO
static void foo() {}
#endif
int main() {
foo();
return 0;
}
"""
await buildSystem.setBuildSettings(for: doc, to: TextDocumentSourceKitOptionsResponse(compilerArguments: args))
let documentManager = await self.testClient.server.documentManager
testClient.openDocument(text, uri: doc)
let diags = try await testClient.nextDiagnosticsNotification()
XCTAssertEqual(diags.diagnostics.count, 1)
XCTAssertEqual(text, try 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 = TextDocumentSourceKitOptionsResponse(compilerArguments: args + ["-DFOO"])
await buildSystem.setBuildSettings(for: doc, to: newSettings)
try await repeatUntilExpectedResult {
guard let refreshedDiags = try? await testClient.nextDiagnosticsNotification(timeout: .seconds(1)) else {
return false
}
return try text == documentManager.latestSnapshot(doc).text && refreshedDiags.diagnostics.count == 0
}
}
func testSwiftDocumentUpdatedBuildSettings() async throws {
let doc = DocumentURI(for: .swift)
let args = fallbackBuildSettings(
for: doc,
language: .swift,
options: SourceKitLSPOptions.FallbackBuildSystemOptions()
)!.compilerArguments
await buildSystem.setBuildSettings(for: doc, to: TextDocumentSourceKitOptionsResponse(compilerArguments: args))
let text = """
#if FOO
func foo() {}
#endif
foo()
"""
let documentManager = await self.testClient.server.documentManager
testClient.openDocument(text, uri: doc)
let diags1 = try await testClient.nextDiagnosticsNotification()
XCTAssertEqual(diags1.diagnostics.count, 1)
XCTAssertEqual(text, try 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 = TextDocumentSourceKitOptionsResponse(compilerArguments: args + ["-DFOO"])
await buildSystem.setBuildSettings(for: doc, to: newSettings)
// No expected errors here because we fixed the settings.
let diags2 = try await testClient.nextDiagnosticsNotification()
XCTAssertEqual(diags2.diagnostics.count, 0)
}
func testClangdDocumentFallbackWithholdsDiagnostics() async throws {
let doc = DocumentURI(for: .objective_c)
let args = [doc.pseudoPath, "-DDEBUG"]
let text = """
#ifdef FOO
static void foo() {}
#endif
int main() {
foo();
return 0;
}
"""
let documentManager = await self.testClient.server.documentManager
testClient.openDocument(text, uri: doc)
let openDiags = try await testClient.nextDiagnosticsNotification()
// Expect diagnostics to be withheld.
XCTAssertEqual(openDiags.diagnostics.count, 0)
XCTAssertEqual(text, try 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 = TextDocumentSourceKitOptionsResponse(compilerArguments: args)
await buildSystem.setBuildSettings(for: doc, to: newSettings)
let refreshedDiags = try await testClient.nextDiagnosticsNotification()
XCTAssertEqual(refreshedDiags.diagnostics.count, 1)
XCTAssertEqual(text, try documentManager.latestSnapshot(doc).text)
}
func testSwiftDocumentFallbackWithholdsSemanticDiagnostics() async throws {
let doc = DocumentURI(for: .swift)
// Primary settings must be different than the fallback settings.
let fallbackSettings = fallbackBuildSettings(
for: doc,
language: .swift,
options: SourceKitLSPOptions.FallbackBuildSystemOptions()
)!
let primarySettings = TextDocumentSourceKitOptionsResponse(
compilerArguments: fallbackSettings.compilerArguments + ["-DPRIMARY"],
workingDirectory: fallbackSettings.workingDirectory
)
let text = """
#if FOO
func foo() {}
#endif
foo()
func
"""
let documentManager = await self.testClient.server.documentManager
testClient.openDocument(text, uri: doc)
let openDiags = try await testClient.nextDiagnosticsNotification()
XCTAssertEqual(openDiags.diagnostics.count, 1)
XCTAssertEqual(text, try documentManager.latestSnapshot(doc).text)
// Swap from fallback settings to primary build system settings.
await buildSystem.setBuildSettings(for: doc, to: primarySettings)
// Two errors since `-DFOO` was not passed.
let refreshedDiags = try await testClient.nextDiagnosticsNotification()
XCTAssertEqual(refreshedDiags.diagnostics.count, 2)
}
}