Skip to content
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
4 changes: 1 addition & 3 deletions nimlangserver.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ requires "nim >= 2.2.10",
"."

task test, "run tests":
--run
--silent
setCommand("c", "tests/all.nim")
exec "nim c --hints:off -r tests/all.nim"

task book, "Generate book":
exec "mdbook build book -d ../docs"
Expand Down
6 changes: 5 additions & 1 deletion tests/all.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ import
textensions,
tmisc,
ttestrunner,
tmcp
tmcp,
tlspendpoints,
tlspdiagnostics,
tlspconfig,
tutils
1 change: 1 addition & 0 deletions tests/all.nim.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
--define:"chronicles_log_level=DEBUG"
#--define:"chronicles_timestamps=UnixTime"
--define:"chronicles_timestamps=None"
--define:"nimUnittestOutputLevel:VERBOSE"
12 changes: 11 additions & 1 deletion tests/lspsocketclient.nim
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ proc processMessage(client: LspSocketClient, msg: string) {.raises: [].} =
error "Method not implemented ", meth = meth
elif "id" in serverReq: #Response here
let id = serverReq["id"].jsonTo(int)
client.responses[id].complete(serverReq["result"])
if "error" in serverReq:
client.responses[id].fail(newException(JsonRpcError, $serverReq["error"]))
else:
client.responses[id].complete(serverReq["result"])
else:
error "Unknown msg", msg = msg
except CatchableError as exc:
Expand Down Expand Up @@ -185,6 +188,13 @@ proc notificationHandle*(

result = newFuture[void]("notificationHandle")

proc registerRequest*(client: LspSocketClient, name: string, handler: Rpc) =
client.calls[name] = newSeq[JsonNode]()
client.routes[name] = proc(params: JsonNode): Future[JsonNode] {.async.} =
{.cast(gcsafe).}:
client.calls[name].add params
return await handler(params)

proc registerNotification*(client: LspSocketClient, names: varargs[string]) =
for name in names:
client.register(name, partial(notificationHandle, (client, name)))
Expand Down
22 changes: 20 additions & 2 deletions tests/testhelpers.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
import std/[os, osproc, sequtils, strutils, sugar, unittest]
import std/[os, osproc, sequtils, strutils, sugar]
import unittest2, chronos

template waitUntil*(condition: untyped, timeout = 10.seconds): bool =
block:
var satisfied = false
let deadline = Moment.now() + timeout
while true:
satisfied = condition
if satisfied:
break
if Moment.now() > deadline:
break
waitFor sleepAsync(10)
satisfied

template cd*(dir: string, body: untyped) =
## Sets the current dir to ``dir``, executes ``body`` and restores the
Expand Down Expand Up @@ -54,4 +68,8 @@ proc execNimbleYes*(args: varargs[string]): ProcessOutput =
proc createNimbleProject*(projectDir: string) =
cdNewDir projectDir:
let (output, exitCode) = execNimbleYes("init")
check exitCode == 0
check exitCode == 0

proc normalizeText*(s: string): string =
# windows/linux compat
s.replace("\r\n", "\n").strip(leading = false)
9 changes: 9 additions & 0 deletions tests/textensions.nim
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ suite "Nimlangserver extensions":
check tasks[0].name == "helloWorld"
check tasks[0].description == "hello world"

test "calling extension/runTask should run the task and return its output":
let runTaskParams = RunTaskParams(command: @["helloWorld"])
let runTaskRes = client.call(
"extension/runTask", jsonutils.toJson(runTaskParams)
).waitFor().jsonTo(RunTaskResult)

check runTaskRes.command == @["helloWorld"]
check runTaskRes.output.anyIt(it.contains("hello world"))

test "calling extension/listTests should return all existing tests":
#We first need to initialize the nimble project
let projectDir = getCurrentDir() / "tests" / "projects" / "testrunner"
Expand Down
171 changes: 171 additions & 0 deletions tests/tlspconfig.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import ../[nimlangserver, ls, utils]
import ../protocol/[enums, types]
import std/[options, json, os, sequtils, strformat, tables]
import chronos
import chronos/asyncproc
import lspsocketclient
import testhelpers
import unittest2

const CallTimeout = 30.seconds

var editorConfiguration = %*[
{
"projectMapping": [],
"buildOnSave": false,
"buildCommand": "c",
"lintOnSave": false,
"provider": "lsp",
"useNimsuggestCheck": false,
"logNimsuggest": false,
"nimsuggestRestartTimeout": 60,
"inlayHints": {
"typeHints": {"enable": true},
"parameterHints": {"enable": true},
"exceptionHints": {"enable": true, "hintStringLeft": "!", "hintStringRight": ""},
},
"notificationVerbosity": "info",
"transportMode": "stdio",
"formatOnSave": false,
"maxNimsuggestProcesses": 0,
"nimsuggestIdleTimeout": 120000,
}
]

suite "LSP configuration pulled from the client":
let cmdParams =
CommandLineParams(mode: some lsp, transport: some socket, port: getNextFreePort())
let ls = main(cmdParams)
let client = newLspSocketClient()
client.registerNotification(
"window/showMessage", "extension/statusUpdate", "textDocument/publishDiagnostics",
"$/progress",
)

proc answerConfiguration(params: JsonNode): Future[JsonNode] {.async.} =
{.cast(gcsafe).}:
return editorConfiguration

proc answerNull(params: JsonNode): Future[JsonNode] {.async.} =
newJNull()

client.registerRequest("workspace/configuration", answerConfiguration)
client.registerRequest("client/registerCapability", answerNull)
client.registerRequest("window/workDoneProgress/create", answerNull)
client.registerRequest("workspace/inlayHint/refresh", answerNull)

waitFor client.connect("localhost", cmdParams.port)

let initParams =
LspInitializeParams %* {
"processId": %getCurrentProcessId(),
"rootUri": fixtureUri("projects/hw/"),
"capabilities": {
"window": {"workDoneProgress": true},
"workspace": {
"configuration": true,
"didChangeConfiguration": {"dynamicRegistration": true},
"inlayHint": {"refreshSupport": true},
},
},
}
discard waitFor client.initialize(initParams)
client.notify("initialized", newJObject())
check waitUntil(ls.workspaceConfiguration.finished)

let
helloWorldFile = "projects/hw/hw.nim"
helloWorldUri = fixtureUri(helloWorldFile)
client.notify("textDocument/didOpen", %createDidOpenParams(helloWorldFile))
check waitFor client.waitForNotificationMessage(
fmt"Nimsuggest initialized for {uriToPath(helloWorldUri)}"
)

suiteTeardown:
waitFor ls.stopNimsuggestProcesses()

test "the server registers workspace/didChangeConfiguration dynamically":
let registrations = client.calls["client/registerCapability"]
check registrations.len > 0

var registeredDidChangeConfiguration = false
for call in registrations:
for registration in call{"registrations"}:
if registration{"method"}.getStr == "workspace/didChangeConfiguration":
registeredDidChangeConfiguration = true
check registeredDidChangeConfiguration

test "the server pulls the configuration and the answer reaches it":
check client.calls["workspace/configuration"].len > 0
var askedForNimSection = false
for item in client.calls["workspace/configuration"][0]{"items"}:
if item{"section"}.getStr == "nim":
askedForNimSection = true
check askedForNimSection
check ls.workspaceConfiguration.finished

let conf = waitFor ls.getWorkspaceConfiguration()
check conf.nimsuggestIdleTimeout == some 120000
check conf.logNimsuggest == some false

test "the pulled configuration is what nimsuggest was started with":
let status = to(
waitFor client.call("extension/status", newJObject()).wait(CallTimeout),
NimLangServerStatus,
)
check status.nimsuggestInstances.len == 1
check status.nimsuggestInstances[0].capabilities.anyIt($it == "exceptionInlayHints")

test "a project check reports progress to the client":
proc checkIdle(ls: LanguageServer, projectFile: string): bool =
if ls.checkInProgress or projectFile notin ls.projectFiles:
return false
let ns = ls.projectFiles[projectFile].ns
ns.finished and not ns.read().checkProjectInProgress

check waitUntil(
ls.checkIdle(uriToPath(helloWorldUri)), timeout = 60.seconds
)

let createdBefore = client.calls["window/workDoneProgress/create"].len

client.notify(
"textDocument/didSave",
%*{
"textDocument": {"uri": helloWorldUri},
"text": readFile("tests" / helloWorldFile),
},
)

check waitUntil(
client.calls["window/workDoneProgress/create"].len > createdBefore,
timeout = 30.seconds,
)

let token = client.calls["window/workDoneProgress/create"][^1]{"token"}.getStr
check token.len > 0
proc reportedAgainst(client: LspSocketClient, token: string): bool =
for progress in client.calls["$/progress"]:
if progress{"token"}.getStr == token:
return true
false

check waitUntil(client.reportedAgainst(token), timeout = 30.seconds)

test "toggling the exception inlay hints restarts nimsuggest and asks for a refresh":
let projectFile = uriToPath(helloWorldUri)
check projectFile in ls.projectFiles
let
pidBefore = ls.projectFiles[projectFile].process.pid
refreshesBefore = client.calls["workspace/inlayHint/refresh"].len

editorConfiguration[0]["inlayHints"]["exceptionHints"]["enable"] = %false
client.notify("workspace/didChangeConfiguration", %*{"settings": newJNull()})

check waitUntil(client.calls["workspace/inlayHint/refresh"].len > refreshesBefore)

check waitUntil(
projectFile in ls.projectFiles and
ls.projectFiles[projectFile].process.pid != pidBefore,
timeout = 30.seconds,
)
122 changes: 122 additions & 0 deletions tests/tlspdiagnostics.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import ../[nimlangserver, ls, lstransports, utils]
import ../protocol/[enums, types]
import std/[options, json, os, jsonutils, sequtils, strutils, sugar, strformat]
import json_rpc/[rpcclient]
import chronicles
import lspsocketclient
import unittest2

const CallTimeout = 30.seconds

suite "LSP diagnostics":
let cmdParams =
CommandLineParams(mode: some lsp, transport: some socket, port: getNextFreePort())
let ls = main(cmdParams)
let client = newLspSocketClient()
client.registerNotification(
"window/showMessage", "window/workDoneProgress/create", "workspace/configuration",
"extension/statusUpdate", "textDocument/publishDiagnostics", "$/progress",
)
waitFor client.connect("localhost", cmdParams.port)

let initParams =
LspInitializeParams %* {
"processId": %getCurrentProcessId(),
"rootUri": fixtureUri("projects/hw/"),
"capabilities": {"window": {"workDoneProgress": false}},
}
let initializeResult = waitFor client.initialize(initParams)
client.notify("initialized", newJObject())

let
helloWorldFile = "projects/hw/hw.nim"
helloWorldUri = fixtureUri(helloWorldFile)
client.notify("textDocument/didOpen", %createDidOpenParams(helloWorldFile))
check waitFor client.waitForNotificationMessage(
fmt"Nimsuggest initialized for {uriToPath(helloWorldUri)}"
)
client.notify(
"textDocument/didSave",
%*{
"textDocument": {"uri": helloWorldUri},
"text": readFile("tests" / helloWorldFile),
},
)

suiteTeardown:
waitFor ls.stopNimsuggestProcesses()

test "Opening a file with a type error publishes diagnostics for it":
proc hasAnyDiagnostic(
json: JsonNode
): bool {.gcsafe, raises: [CatchableError].} =
{.cast(gcsafe).}:
json{"uri"}.getStr == helloWorldUri and json{"diagnostics"}.len > 0

check waitFor client.waitForNotification(
"textDocument/publishDiagnostics", hasAnyDiagnostic
)

test "The published diagnostic carries a uri, a range and a message":
proc isWellFormed(json: JsonNode): bool {.gcsafe, raises: [CatchableError].} =
{.cast(gcsafe).}:
if json{"uri"}.getStr != helloWorldUri:
return false
for diagnostic in json{"diagnostics"}:
let
message = diagnostic{"message"}.getStr
line = diagnostic{"range"}{"start"}{"line"}
if message.len > 0 and line.kind == JInt and line.getInt >= 0:
return true
false

check waitFor client.waitForNotification(
"textDocument/publishDiagnostics", isWellFormed
)

test "A diagnostic reports its severity and source":
proc hasSeverity(json: JsonNode): bool {.gcsafe, raises: [CatchableError].} =
for diagnostic in json{"diagnostics"}:
if diagnostic{"severity"}.kind == JInt and diagnostic{"source"}.getStr.len > 0:
return true
false

check waitFor client.waitForNotification(
"textDocument/publishDiagnostics", hasSeverity
)

test "The server keeps serving after diagnostics have been published":
let status = to(
waitFor client.call("extension/status", newJObject()).wait(CallTimeout),
NimLangServerStatus,
)
check status.version == LSPVersion

test "initialize advertises the providers whose routes are registered":
let capabilities = initializeResult.capabilities
check capabilities.textDocumentSync.isSome
check not capabilities.completionProvider.isNil
check capabilities.hoverProvider.get(false)
check capabilities.definitionProvider.get(false)
check capabilities.referencesProvider.get(false)
check capabilities.documentSymbolProvider.get(false)
check capabilities.workspaceSymbolProvider.get(false)
check capabilities.documentHighlightProvider.get(false)
check capabilities.typeDefinitionProvider.get(false)
check capabilities.declarationProvider.get(false)

test "initialize advertises the extension commands it can execute":
let provider = initializeResult.capabilities.executeCommandProvider
check provider.isSome
let commands = provider.get.commands.get(@[])
check RESTART_COMMAND in commands
check CHECK_PROJECT_COMMAND in commands
check RECOMPILE_COMMAND in commands

test "The server answers extension/capabilities consistently with its routes":
let capabilities = to(
waitFor client.call("extension/capabilities", newJObject()).wait(CallTimeout),
seq[string],
)
for capability in LspExtensionCapability:
check $capability in capabilities
Loading
Loading