mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
428 lines
15 KiB
Swift
428 lines
15 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
|
||
//
|
||
//===----------------------------------------------------------------------===//
|
||
|
||
@_spi(SourceKitLSP) import LanguageServerProtocol
|
||
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
|
||
@_spi(SourceKitLSP) import SKLogging
|
||
import SKOptions
|
||
import SKTestSupport
|
||
import SourceKitD
|
||
import SourceKitLSP
|
||
import SwiftExtensions
|
||
@_spi(Testing) import SwiftLanguageService
|
||
import XCTest
|
||
|
||
fileprivate extension HoverResponse {
|
||
func contains(string: String) -> Bool {
|
||
switch self.contents {
|
||
case .markedStrings(let markedStrings):
|
||
for markedString in markedStrings {
|
||
switch markedString {
|
||
case .markdown(let value), .codeBlock(language: _, let value):
|
||
if value.contains(string) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
case .markupContent(let markdownString):
|
||
if markdownString.value.contains(string) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
|
||
final class CrashRecoveryTests: SourceKitLSPTestCase {
|
||
func testSourcekitdCrashRecovery() async throws {
|
||
try SkipUnless.platformIsDarwin("Linux and Windows use in-process sourcekitd")
|
||
try SkipUnless.longTestsEnabled()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(
|
||
capabilities: ClientCapabilities(window: WindowClientCapabilities(workDoneProgress: true)),
|
||
usePullDiagnostics: false
|
||
)
|
||
let uri = DocumentURI(for: .swift)
|
||
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
func 1️⃣foo() {
|
||
print("Hello world")
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
// Wait for diagnostics to be produced to make sure the document open got handled by sourcekitd.
|
||
_ = try await testClient.nextDiagnosticsNotification()
|
||
|
||
// Verify that we get the expected result from a hover response before crashing sourcekitd.
|
||
|
||
let hoverRequest = HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
let preCrashHoverResponse = try await testClient.send(hoverRequest)
|
||
precondition(
|
||
preCrashHoverResponse?.contains(string: "foo()") ?? false,
|
||
"Precondition test failed. The Hover response did not contain foo(), even before crashing sourcekitd. Received response: \(String(describing: preCrashHoverResponse))"
|
||
)
|
||
|
||
testClient.handleSingleRequest { (request: CreateWorkDoneProgressRequest) -> VoidResponse in
|
||
return VoidResponse()
|
||
}
|
||
|
||
// Crash sourcekitd
|
||
|
||
let swiftLanguageService =
|
||
try unwrap(
|
||
await testClient.server.primaryLanguageService(
|
||
for: uri,
|
||
.swift,
|
||
in: testClient.server.workspaceForDocument(uri: uri)!
|
||
) as? SwiftLanguageService
|
||
)
|
||
|
||
await swiftLanguageService.crash()
|
||
|
||
let crashedNotification = try await testClient.nextNotification(ofType: WorkDoneProgress.self, timeout: .seconds(5))
|
||
XCTAssertEqual(
|
||
crashedNotification.value,
|
||
.begin(
|
||
WorkDoneProgressBegin(
|
||
title: "SourceKit-LSP: Restoring functionality",
|
||
message: "Please run 'sourcekit-lsp diagnose' to file an issue"
|
||
)
|
||
)
|
||
)
|
||
|
||
// sourcekitd's semantic request timer is only started when the first semantic request comes in.
|
||
// Send a hover request (which will fail) to trigger that timer.
|
||
// Afterwards wait for semantic functionality to be restored.
|
||
_ = try? await testClient.send(hoverRequest)
|
||
let semanticFunctionalityRestoredNotification = try await testClient.nextNotification(
|
||
ofType: WorkDoneProgress.self,
|
||
timeout: .seconds(30)
|
||
)
|
||
XCTAssertEqual(semanticFunctionalityRestoredNotification.value, .end(WorkDoneProgressEnd()))
|
||
|
||
// Check that we get the same hover response from the restored in-memory state
|
||
|
||
await assertNoThrow {
|
||
let postCrashHoverResponse = try await testClient.send(hoverRequest)
|
||
XCTAssertTrue(postCrashHoverResponse?.contains(string: "foo()") ?? false)
|
||
}
|
||
}
|
||
|
||
private func crashClangd(for testClient: TestSourceKitLSPClient, document docUri: DocumentURI) async throws {
|
||
let clangdServer = try await testClient.server.primaryLanguageService(
|
||
for: docUri,
|
||
.cpp,
|
||
in: testClient.server.workspaceForDocument(uri: docUri)!
|
||
)
|
||
|
||
let clangdCrashed = self.expectation(description: "clangd crashed")
|
||
let clangdRestarted = self.expectation(description: "clangd restarted")
|
||
|
||
await clangdServer.addStateChangeHandler { (oldState, newState) in
|
||
switch newState {
|
||
case .connectionInterrupted:
|
||
clangdCrashed.fulfill()
|
||
case .connected:
|
||
clangdRestarted.fulfill()
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
await clangdServer.crash()
|
||
|
||
try await fulfillmentOfOrThrow(clangdCrashed)
|
||
try await fulfillmentOfOrThrow(clangdRestarted)
|
||
}
|
||
|
||
func testClangdCrashRecovery() async throws {
|
||
try SkipUnless.longTestsEnabled()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .cpp)
|
||
|
||
let positions = testClient.openDocument("1️⃣", uri: uri)
|
||
|
||
// Make a change to the file that's not saved to disk. This way we can check that we re-open the correct in-memory state.
|
||
|
||
let addFuncChange = TextDocumentContentChangeEvent(
|
||
range: Range(positions["1️⃣"]),
|
||
rangeLength: 0,
|
||
text: """
|
||
|
||
void main() {
|
||
}
|
||
"""
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: 2),
|
||
contentChanges: [addFuncChange]
|
||
)
|
||
)
|
||
|
||
// Verify that we get the expected result from a hover response before crashing clangd.
|
||
|
||
let expectedHoverRange = Position(line: 1, utf16index: 5)..<Position(line: 1, utf16index: 9)
|
||
|
||
let hoverRequest = HoverRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: Position(line: 1, utf16index: 6)
|
||
)
|
||
let preCrashHoverResponse = try await testClient.send(hoverRequest)
|
||
precondition(
|
||
preCrashHoverResponse?.range == expectedHoverRange,
|
||
"Precondition test failed. The Hover response was not what we expected, even before crashing sourcekitd"
|
||
)
|
||
|
||
// Crash clangd
|
||
|
||
try await crashClangd(for: testClient, document: uri)
|
||
|
||
// Check that we have re-opened the document with the correct in-memory state
|
||
|
||
await assertNoThrow {
|
||
let postCrashHoverResponse = try await testClient.send(hoverRequest)
|
||
XCTAssertEqual(postCrashHoverResponse?.range, expectedHoverRange)
|
||
}
|
||
}
|
||
|
||
func testClangdCrashRecoveryReopensWithCorrectBuildSettings() async throws {
|
||
try SkipUnless.longTestsEnabled()
|
||
|
||
let project = try await MultiFileTestProject(files: [
|
||
"main.cpp": """
|
||
#if FOO
|
||
void 1️⃣foo2️⃣() {}
|
||
#else
|
||
void foo() {}
|
||
#endif
|
||
|
||
int main() {
|
||
3️⃣foo4️⃣();
|
||
}
|
||
""",
|
||
"compile_flags.txt": """
|
||
-DFOO
|
||
""",
|
||
])
|
||
|
||
let (mainUri, positions) = try project.openDocument("main.cpp")
|
||
|
||
// Verify that we get the expected result from a hover response before crashing clangd.
|
||
|
||
let expectedHighlightResponse = [
|
||
DocumentHighlight(range: positions["1️⃣"]..<positions["2️⃣"], kind: .text),
|
||
DocumentHighlight(range: positions["3️⃣"]..<positions["4️⃣"], kind: .text),
|
||
]
|
||
|
||
let highlightRequest = DocumentHighlightRequest(
|
||
textDocument: TextDocumentIdentifier(mainUri),
|
||
position: positions["3️⃣"]
|
||
)
|
||
let preCrashHighlightResponse = try await project.testClient.send(highlightRequest)
|
||
precondition(
|
||
preCrashHighlightResponse == expectedHighlightResponse,
|
||
"Precondition test failed. The Hover response was not what we expected, even before crashing sourcekitd"
|
||
)
|
||
|
||
// Crash clangd
|
||
|
||
try await crashClangd(for: project.testClient, document: mainUri)
|
||
|
||
// Check that we have re-opened the document with the correct build settings
|
||
// If we did not recover the correct build settings, document highlight would
|
||
// pick the definition of foo() in the #else branch.
|
||
|
||
await assertNoThrow {
|
||
let postCrashHighlightResponse = try await project.testClient.send(highlightRequest)
|
||
XCTAssertEqual(postCrashHighlightResponse, expectedHighlightResponse)
|
||
}
|
||
}
|
||
|
||
func testPreventClangdCrashLoop() async throws {
|
||
try SkipUnless.longTestsEnabled()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .cpp)
|
||
|
||
let positions = testClient.openDocument("1️⃣", uri: uri)
|
||
|
||
// Send a nonsensical request to wait for clangd to start up
|
||
|
||
let hoverRequest = HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
_ = try await testClient.send(hoverRequest)
|
||
|
||
// Keep track of clangd crashes
|
||
|
||
let clangdServer = try await testClient.server.primaryLanguageService(
|
||
for: uri,
|
||
.cpp,
|
||
in: testClient.server.workspaceForDocument(uri: uri)!
|
||
)
|
||
|
||
let clangdCrashed = self.expectation(description: "clangd crashed")
|
||
clangdCrashed.assertForOverFulfill = false
|
||
|
||
let clangdRestartedFirstTime = self.expectation(description: "clangd restarted for the first time")
|
||
let clangdRestartedSecondTime = self.expectation(description: "clangd restarted for the second time")
|
||
|
||
let clangdHasRestartedFirstTime = ThreadSafeBox(initialValue: false)
|
||
|
||
await clangdServer.addStateChangeHandler { (oldState, newState) in
|
||
switch newState {
|
||
case .connectionInterrupted:
|
||
clangdCrashed.fulfill()
|
||
case .connected:
|
||
clangdHasRestartedFirstTime.withLock { clangdHasRestartedFirstTime in
|
||
if !clangdHasRestartedFirstTime {
|
||
clangdRestartedFirstTime.fulfill()
|
||
clangdHasRestartedFirstTime = true
|
||
} else {
|
||
clangdRestartedSecondTime.fulfill()
|
||
}
|
||
}
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
await clangdServer.crash()
|
||
|
||
try await fulfillmentOfOrThrow(clangdCrashed, timeout: 5)
|
||
try await fulfillmentOfOrThrow(clangdRestartedFirstTime, timeout: 30)
|
||
// Clangd has restarted. Note the date so we can check that the second restart doesn't happen too quickly.
|
||
let firstRestartDate = Date()
|
||
|
||
// Crash clangd again. This time, it should only restart after a delay.
|
||
await clangdServer.crash()
|
||
|
||
try await fulfillmentOfOrThrow(clangdRestartedSecondTime, timeout: 30)
|
||
XCTAssert(
|
||
Date().timeIntervalSince(firstRestartDate) > 5,
|
||
"Clangd restarted too quickly after crashing twice in a row. We are not preventing crash loops."
|
||
)
|
||
}
|
||
|
||
func testPullDiagnosticsAfterCrash() async throws {
|
||
try SkipUnless.platformIsDarwin("Linux and Windows use in-process sourcekitd")
|
||
try SkipUnless.longTestsEnabled()
|
||
|
||
// Ensure that we don't cache the diagnostic report from the crashed sourcekitd and don't emit any of the internal
|
||
// sourcekitd errors such as `semantic editor disabled` in the diagnostic report.
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
testClient.openDocument(
|
||
"""
|
||
func test() {
|
||
let x: String = 1
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let swiftLanguageService =
|
||
try unwrap(
|
||
await testClient.server.primaryLanguageService(
|
||
for: uri,
|
||
.swift,
|
||
in: testClient.server.workspaceForDocument(uri: uri)!
|
||
) as? SwiftLanguageService
|
||
)
|
||
|
||
await swiftLanguageService.crash()
|
||
|
||
try await repeatUntilExpectedResult(timeout: .seconds(30)) {
|
||
let diagnostics = try await testClient.send(DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri)))
|
||
if diagnostics.fullReport?.items.count == 0 {
|
||
return false
|
||
}
|
||
let item = try XCTUnwrap(diagnostics.fullReport?.items.only)
|
||
XCTAssertEqual(item.message, "Cannot convert value of type 'Int' to specified type 'String'")
|
||
return true
|
||
}
|
||
}
|
||
|
||
func testRestartSourceKitDIfItDoesntReply() async throws {
|
||
try SkipUnless.longTestsEnabled()
|
||
try SkipUnless.platformIsDarwin("Linux and Windows use in-process sourcekitd")
|
||
|
||
let sourcekitdTerminatedExpectation = self.expectation(description: "sourcekitd terminated")
|
||
|
||
let testClient = try await TestSourceKitLSPClient(options: SourceKitLSPOptions(semanticServiceRestartTimeout: 2))
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
func test() {
|
||
let 1️⃣x = 1
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
// Monitor sourcekitd to notice when it gets terminated
|
||
let swiftService = try await unwrap(
|
||
testClient.server.primaryLanguageService(
|
||
for: uri,
|
||
.swift,
|
||
in: unwrap(testClient.server.workspaceForDocument(uri: uri))
|
||
) as? SwiftLanguageService
|
||
)
|
||
await swiftService.addStateChangeHandler { oldState, newState in
|
||
logger.debug("sourcekitd changed state: \(String(describing: oldState)) -> \(String(describing: newState))")
|
||
if newState == .connectionInterrupted {
|
||
sourcekitdTerminatedExpectation.fulfill()
|
||
}
|
||
}
|
||
|
||
try await swiftService.sourcekitd.withPreRequestHandlingHook {
|
||
// The first hover request should get cancelled by `semanticServiceRestartTimeout`
|
||
await assertThrowsError(
|
||
try await testClient.send(HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"]))
|
||
) { error in
|
||
XCTAssert(
|
||
(error as? ResponseError)?.message.contains("Timed out") ?? false,
|
||
"Received unexpected error: \(error)"
|
||
)
|
||
}
|
||
} hook: { request in
|
||
// Simulate a stuck sourcekitd that only gets unstuck when sourcekitd is terminated.
|
||
if request.description.contains("cursorinfo") {
|
||
// Use a detached task here so that a cancellation of the Task that runs this doesn't cancel the await of
|
||
// sourcekitdTerminatedExpectation. We want to simulate a sourecekitd that is stuck and doesn't listen to
|
||
// cancellation.
|
||
await Task.detached {
|
||
await orLog("awaiting sourcekitdTerminatedExpectation") {
|
||
try await fulfillmentOfOrThrow(sourcekitdTerminatedExpectation)
|
||
}
|
||
}.value
|
||
}
|
||
}
|
||
|
||
// After sourcekitd is restarted, we should get a hover result once the semantic editor is enabled again.
|
||
try await repeatUntilExpectedResult {
|
||
do {
|
||
let hover = try await testClient.send(
|
||
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
return hover?.contents.markupContent?.value != nil
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
}
|