Files
sourcekit-lsp/Tests/SourceKitLSPTests/CodeActionTests.swift
Alex Hoppen df594e332c Set a default timeout duration
Instead of having ad-hoc timeout durations in all the test cases, specify a default timeout duration that can be used by tests.

This allows us increase the timeout duration for all tests if we discover that e.g. sourcekitd is slower in CI setups.

rdar://91615376
2022-06-14 17:26:06 +02:00

336 lines
14 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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 LanguageServerProtocol
import LSPTestSupport
import SKTestSupport
import SourceKitLSP
import XCTest
final class CodeActionTests: XCTestCase {
typealias CodeActionCapabilities = TextDocumentClientCapabilities.CodeAction
typealias CodeActionLiteralSupport = CodeActionCapabilities.CodeActionLiteralSupport
typealias CodeActionKindCapabilities = CodeActionLiteralSupport.CodeActionKind
private func clientCapabilitiesWithCodeActionSupport() -> ClientCapabilities {
var documentCapabilities = TextDocumentClientCapabilities()
var codeActionCapabilities = CodeActionCapabilities()
let codeActionKinds = CodeActionKindCapabilities(valueSet: [.refactor, .quickFix])
let codeActionLiteralSupport = CodeActionLiteralSupport(codeActionKind: codeActionKinds)
codeActionCapabilities.codeActionLiteralSupport = codeActionLiteralSupport
documentCapabilities.codeAction = codeActionCapabilities
documentCapabilities.completion = .init(completionItem: .init(snippetSupport: true))
return ClientCapabilities(workspace: nil, textDocument: documentCapabilities)
}
private func refactorTibsWorkspace() throws -> SKTibsTestWorkspace? {
let capabilities = clientCapabilitiesWithCodeActionSupport()
return try staticSourceKitTibsWorkspace(name: "SemanticRefactor", clientCapabilities: capabilities)
}
func testCodeActionResponseLegacySupport() {
let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil])
let codeAction = CodeAction(title: "1")
let codeAction2 = CodeAction(title: "2", command: command)
var capabilities: TextDocumentClientCapabilities.CodeAction
var capabilityJson: String
var data: Data
var response: CodeActionRequestResponse
capabilityJson =
"""
{
"dynamicRegistration": true,
"codeActionLiteralSupport" : {
"codeActionKind": {
"valueSet": []
}
}
}
"""
data = capabilityJson.data(using: .utf8)!
capabilities = try! JSONDecoder().decode(TextDocumentClientCapabilities.CodeAction.self,
from: data)
response = .init(codeActions: [codeAction, codeAction2], clientCapabilities: capabilities)
let actions = try! JSONDecoder().decode([CodeAction].self, from: JSONEncoder().encode(response))
XCTAssertEqual(actions, [codeAction, codeAction2])
capabilityJson =
"""
{
"dynamicRegistration": true
}
"""
data = capabilityJson.data(using: .utf8)!
capabilities = try! JSONDecoder().decode(TextDocumentClientCapabilities.CodeAction.self,
from: data)
response = .init(codeActions: [codeAction, codeAction2], clientCapabilities: capabilities)
let commands = try! JSONDecoder().decode([Command].self, from: JSONEncoder().encode(response))
XCTAssertEqual(commands, [command])
}
func testCodeActionResponseIgnoresSupportedKinds() {
// The client guarantees that unsupported kinds will be handled, and in
// practice some clients use `"codeActionKind":{"valueSet":[]}`, since
// they support all kinds anyway. So to avoid filtering all actions, we
// ignore the supported kinds.
let unspecifiedAction = CodeAction(title: "Unspecified")
let refactorAction = CodeAction(title: "Refactor", kind: .refactor)
let quickfixAction = CodeAction(title: "Quickfix", kind: .quickFix)
let actions = [unspecifiedAction, refactorAction, quickfixAction]
var capabilities: TextDocumentClientCapabilities.CodeAction
var capabilityJson: String
var data: Data
var response: CodeActionRequestResponse
capabilityJson =
"""
{
"dynamicRegistration": true,
"codeActionLiteralSupport" : {
"codeActionKind": {
"valueSet": ["refactor"]
}
}
}
"""
data = capabilityJson.data(using: .utf8)!
capabilities = try! JSONDecoder().decode(TextDocumentClientCapabilities.CodeAction.self,
from: data)
response = .init(codeActions: actions, clientCapabilities: capabilities)
XCTAssertEqual(response, .codeActions([unspecifiedAction, refactorAction, quickfixAction]))
capabilityJson =
"""
{
"dynamicRegistration": true,
"codeActionLiteralSupport" : {
"codeActionKind": {
"valueSet": []
}
}
}
"""
data = capabilityJson.data(using: .utf8)!
capabilities = try! JSONDecoder().decode(TextDocumentClientCapabilities.CodeAction.self,
from: data)
response = .init(codeActions: actions, clientCapabilities: capabilities)
XCTAssertEqual(response, .codeActions([unspecifiedAction, refactorAction, quickfixAction]))
}
func testCodeActionResponseCommandMetadataInjection() {
let url = URL(fileURLWithPath: "/a.swift")
let textDocument = TextDocumentIdentifier(url)
let expectedMetadata: LSPAny = {
let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument)
let data = try! JSONEncoder().encode(metadata)
return try! JSONDecoder().decode(LSPAny.self, from: data)
}()
XCTAssertEqual(expectedMetadata, .dictionary(["sourcekitlsp_textDocument": ["uri": "file:///a.swift"]]))
let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil])
let codeAction = CodeAction(title: "1")
let codeAction2 = CodeAction(title: "2", command: command)
let request = CodeActionRequest(range: Position(line: 0, utf16index: 0)..<Position(line: 1, utf16index: 1),
context: .init(diagnostics: [], only: nil),
textDocument: textDocument)
var response = request.injectMetadata(toResponse: .commands([command]))
XCTAssertEqual(response,
.commands([
Command(title: command.title,
command: command.command,
arguments: command.arguments! + [expectedMetadata])
])
)
response = request.injectMetadata(toResponse: .codeActions([codeAction, codeAction2]))
XCTAssertEqual(response,
.codeActions([codeAction,
CodeAction(title: codeAction2.title,
command: Command(title: command.title,
command: command.command,
arguments: command.arguments! + [expectedMetadata]))
])
)
response = request.injectMetadata(toResponse: nil)
XCTAssertNil(response)
}
func testCommandEncoding() {
let dictionary: LSPAny = ["1": [nil, 2], "2": "text", "3": ["4": [1, 2]]]
let array: LSPAny = [1, [2,"string"], dictionary]
let arguments: LSPAny = [1, 2.2, "text", nil, array, dictionary]
let command = Command(title: "Command", command: "command.id", arguments: [arguments, arguments])
let decoded = try! JSONDecoder().decode(Command.self, from: JSONEncoder().encode(command))
XCTAssertEqual(decoded, command)
}
func testEmptyCodeActionResult() throws {
guard let ws = try refactorTibsWorkspace() else { return }
let loc = ws.testLoc("sr:foo")
try ws.openDocument(loc.url, language: .swift)
let textDocument = TextDocumentIdentifier(loc.url)
let start = Position(line: 2, utf16index: 0)
let request = CodeActionRequest(range: start..<start, context: .init(), textDocument: textDocument)
try withExtendedLifetime(ws) {
let result = try ws.sk.sendSync(request)
XCTAssertEqual(result, .codeActions([]))
}
}
func testSemanticRefactorLocalRenameResult() throws {
guard let ws = try refactorTibsWorkspace() else { return }
let loc = ws.testLoc("sr:local")
try ws.openDocument(loc.url, language: .swift)
let textDocument = TextDocumentIdentifier(loc.url)
let request = CodeActionRequest(range: loc.position..<loc.position, context: .init(), textDocument: textDocument)
try withExtendedLifetime(ws) {
let result = try ws.sk.sendSync(request)
XCTAssertEqual(result, .codeActions([]))
}
}
func testSemanticRefactorLocationCodeActionResult() throws {
guard let ws = try refactorTibsWorkspace() else { return }
let loc = ws.testLoc("sr:string")
try ws.openDocument(loc.url, language: .swift)
let textDocument = TextDocumentIdentifier(loc.url)
let request = CodeActionRequest(range: loc.position..<loc.position, context: .init(), textDocument: textDocument)
let result = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) }
let expectedCommandArgs: LSPAny = ["actionString": "source.refactoring.kind.localize.string", "positionRange": ["start": ["character": 43, "line": 1], "end": ["character": 43, "line": 1]], "title": "Localize String", "textDocument": ["uri": .string(loc.url.absoluteString)]]
let metadataArguments: LSPAny = ["sourcekitlsp_textDocument": ["uri": .string(loc.url.absoluteString)]]
let expectedCommand = Command(title: "Localize String",
command: "semantic.refactor.command",
arguments: [expectedCommandArgs] + [metadataArguments])
let expectedCodeAction = CodeAction(title: "Localize String",
kind: .refactor,
command: expectedCommand)
XCTAssertEqual(result, .codeActions([expectedCodeAction]))
}
func testSemanticRefactorRangeCodeActionResult() throws {
guard let ws = try refactorTibsWorkspace() else { return }
let rangeStartLoc = ws.testLoc("sr:extractStart")
let rangeEndLoc = ws.testLoc("sr:extractEnd")
try ws.openDocument(rangeStartLoc.url, language: .swift)
XCTAssertEqual(rangeStartLoc.url, rangeEndLoc.url)
let textDocument = TextDocumentIdentifier(rangeStartLoc.url)
let request = CodeActionRequest(range: rangeStartLoc.position..<rangeEndLoc.position, context: .init(), textDocument: textDocument)
let result = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) }
let expectedCommandArgs: LSPAny = ["actionString": "source.refactoring.kind.extract.function", "positionRange": ["start": ["character": 21, "line": 1], "end": ["character": 27, "line": 2]], "title": "Extract Method", "textDocument": ["uri": .string(rangeStartLoc.url.absoluteString)]]
let metadataArguments: LSPAny = ["sourcekitlsp_textDocument": ["uri": .string(rangeStartLoc.url.absoluteString)]]
let expectedCommand = Command(title: "Extract Method",
command: "semantic.refactor.command",
arguments: [expectedCommandArgs] + [metadataArguments])
let expectedCodeAction = CodeAction(title: "Extract Method",
kind: .refactor,
command: expectedCommand)
XCTAssertEqual(result, .codeActions([expectedCodeAction]))
}
func testCodeActionsRemovePlaceholders() throws {
let capabilities = clientCapabilitiesWithCodeActionSupport()
let ws = try staticSourceKitTibsWorkspace(name: "Fixit", clientCapabilities: capabilities)!
let def = ws.testLoc("MyStruct:def")
try ws.openDocument(def.url, language: .swift)
let syntacticDiagnosticsReceived = self.expectation(description: "Syntactic diagnotistics received")
let semanticDiagnosticsReceived = self.expectation(description: "Semantic diagnotistics received")
ws.sk.appendOneShotNotificationHandler { (note: Notification<PublishDiagnosticsNotification>) in
// syntactic diagnostics
XCTAssertEqual(note.params.uri, def.docUri)
XCTAssertEqual(note.params.diagnostics, [])
syntacticDiagnosticsReceived.fulfill()
}
var diags: [Diagnostic]! = nil
ws.sk.appendOneShotNotificationHandler { (note: Notification<PublishDiagnosticsNotification>) in
// semantic diagnostics
XCTAssertEqual(note.params.uri, def.docUri)
XCTAssertEqual(note.params.diagnostics.count, 1)
diags = note.params.diagnostics
semanticDiagnosticsReceived.fulfill()
}
self.wait(for: [syntacticDiagnosticsReceived, semanticDiagnosticsReceived], timeout: defaultTimeout)
let textDocument = TextDocumentIdentifier(def.url)
let actionsRequest = CodeActionRequest(range: def.position..<def.position, context: .init(diagnostics: diags), textDocument: textDocument)
let actionResult = try ws.sk.sendSync(actionsRequest)
guard case .codeActions(let codeActions) = actionResult else {
return XCTFail("Expected code actions, not commands as a response")
}
// Check that the Fix-It action contains snippets
guard let quickFixAction = codeActions.filter({ $0.kind == .quickFix }).spm_only else {
return XCTFail("Expected exactly one quick fix action")
}
guard let change = quickFixAction.edit?.changes?[def.docUri]?.spm_only else {
return XCTFail("Expected exactly one change")
}
XCTAssertEqual(change.newText.trimmingTrailingWhitespace(), """
func foo() {
}
""")
// Check that the refactor action contains snippets
guard let refactorAction = codeActions.filter({ $0.kind == .refactor }).spm_only else {
return XCTFail("Expected exactly one refactor action")
}
guard let command = refactorAction.command else {
return XCTFail("Expected the refactor action to have a command")
}
let editReceived = self.expectation(description: "Received ApplyEdit request")
ws.sk.appendOneShotRequestHandler { (request: Request<ApplyEditRequest>) in
defer {
editReceived.fulfill()
}
guard let change = request.params.edit.changes?[def.docUri]?.spm_only else {
return XCTFail("Expected exactly one edit")
}
XCTAssertEqual(change.newText.trimmingTrailingWhitespace(), """
func foo() {
}
""")
request.reply(ApplyEditResponse(applied: true, failureReason: nil))
}
_ = try ws.sk.sendSync(ExecuteCommandRequest(command: command.command, arguments: command.arguments))
self.wait(for: [editReceived], timeout: defaultTimeout)
}
}