Skip to content

Commit 73fbf80

Browse files
authored
Adds listTests route (#317)
* Adds `listTests` route * Fixes test * adds missing import
1 parent f0b6534 commit 73fbf80

File tree

6 files changed

+121
-44
lines changed

6 files changed

+121
-44
lines changed

nimlangserver.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ proc registerRoutes(srv: RpcSocketServer, ls: LanguageServer) =
6666
srv.register("extension/suggest", wrapRpc(partial(extensionSuggest, ls)))
6767
srv.register("extension/tasks", wrapRpc(partial(tasks, ls)))
6868
srv.register("extension/runTask", wrapRpc(partial(runTask, ls)))
69+
srv.register("extension/listTests", wrapRpc(partial(listTests, ls)))
6970
#Notifications
7071
srv.register("$/cancelRequest", wrapRpc(partial(cancelRequest, ls)))
7172
srv.register("initialized", wrapRpc(partial(initialized, ls)))

protocol/types.nim

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import options
3+
import tables
34

45
type
56
OptionalSeq*[T] = Option[seq[T]]
@@ -1042,3 +1043,22 @@ type
10421043
RunTaskResult* = object
10431044
command*: seq[string] #command and args
10441045
output*: seq[string] #output lines
1046+
1047+
TestInfo* = object
1048+
name*: string
1049+
line*: int
1050+
file*: string
1051+
1052+
TestSuiteInfo* = object
1053+
name*: string #The suite name, empty if it's a global test
1054+
tests*: seq[TestInfo]
1055+
1056+
TestProjectInfo* = object
1057+
entryPoints*: seq[string]
1058+
suites*: Table[string, TestSuiteInfo]
1059+
1060+
ListTestsParams* = object
1061+
entryPoints*: seq[string] #can be patterns? if empty we could do the same as nimble does or just run `nimble test args`
1062+
1063+
ListTestsResult* = object
1064+
projectInfo*: TestProjectInfo

routes.nim

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import
1919
std/[strscans, times, json, parseutils, strutils],
2020
ls,
2121
stew/[byteutils],
22-
nimexpand
23-
22+
nimexpand,
23+
testrunner
24+
2425
import macros except error
2526

2627
proc getNphPath(): Option[string] =
@@ -847,6 +848,16 @@ proc runTask*(
847848
debug "Ran nimble cmd/task", command = $params.command, output = $result.output
848849
await process.shutdownChildProcess()
849850

851+
proc listTests*(
852+
ls: LanguageServer, params: ListTestsParams
853+
): Future[ListTestsResult] {.async.} =
854+
let config = await ls.getWorkspaceConfiguration()
855+
let nimPath = config.getNimPath()
856+
if nimPath.isNone:
857+
return ListTestsResult(projectInfo: TestProjectInfo(entryPoints: params.entryPoints, suites: initTable[string, TestSuiteInfo]()))
858+
let testProjectInfo = await ls.listTestsForEntryPoint(params.entryPoints, nimPath.get())
859+
result.projectInfo = testProjectInfo
860+
850861
#Notifications
851862
proc initialized*(ls: LanguageServer, _: JsonNode): Future[void] {.async.} =
852863
debug "Client initialized."

testrunner.nim

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import std/[os, osproc, strscans, tables, sequtils, enumerate, strutils]
2+
import chronos, chronos/asyncproc
3+
import protocol/types
4+
import ls
5+
import chronicles
6+
import stew/byteutils
7+
import utils
8+
9+
proc extractTestInfo*(rawOutput: string): TestProjectInfo =
10+
result.suites = initTable[string, TestSuiteInfo]()
11+
let lines = rawOutput.split("\n")
12+
var currentSuite = ""
13+
14+
for i, line in enumerate(lines):
15+
var name, file, ignore: string
16+
var lineNumber: int
17+
if scanf(line, "Suite: $*", name):
18+
currentSuite = name.strip()
19+
result.suites[currentSuite] = TestSuiteInfo(name: currentSuite)
20+
# echo "Found suite: ", currentSuite
21+
22+
elif scanf(line, "$*Test: $*", ignore, name):
23+
let insideSuite = line.startsWith("\t")
24+
# Use currentSuite if inside a suite, empty string if not
25+
let suiteName = if insideSuite: currentSuite else: ""
26+
27+
#File is always next line of a test
28+
if scanf(lines[i+1], "$*File:$*:$i", ignore, file, lineNumber):
29+
var testInfo = TestInfo(name: name.strip(), file: file.strip(), line: lineNumber)
30+
# echo "Adding test: ", testInfo.name, " to suite: ", suiteName
31+
result.suites[suiteName].tests.add(testInfo)
32+
33+
proc listTestsForEntryPoint*(
34+
ls: LanguageServer, entryPoints: seq[string], nimPath: string
35+
): Future[TestProjectInfo] {.async.} =
36+
#For now only one entry point is supported
37+
assert entryPoints.len == 1
38+
let entryPoint = entryPoints[0]
39+
if not fileExists(entryPoint):
40+
error "Entry point does not exist", entryPoint = entryPoint
41+
return TestProjectInfo()
42+
let process = await startProcess(
43+
nimPath,
44+
arguments = @["c", "-d:unittest2ListTests", "-r", "--listFullPaths", entryPoints[0]],
45+
options = {UsePath},
46+
stderrHandle = AsyncProcess.Pipe,
47+
stdoutHandle = AsyncProcess.Pipe,
48+
)
49+
try:
50+
let res = await process.waitForExit(15.seconds)
51+
if res != 0:
52+
error "Failed to list tests", nimPath = nimPath, entryPoint = entryPoints[0], res = res
53+
error "An error occurred while listing tests", error = string.fromBytes(process.stderrStream.read().await)
54+
else:
55+
let rawOutput = string.fromBytes(process.stdoutStream.read().await)
56+
result = extractTestInfo(rawOutput)
57+
finally:
58+
await shutdownChildProcess(process)

tests/textensions.nim

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import json_rpc/[rpcclient]
66
import chronicles
77
import lspsocketclient
88
import chronos/asyncproc
9+
import testhelpers
910

1011
suite "Nimlangserver extensions":
1112
let cmdParams = CommandLineParams(transport: some socket, port: getNextFreePort())
@@ -70,3 +71,30 @@ suite "Nimlangserver extensions":
7071
check tasks.len == 3
7172
check tasks[0].name == "helloWorld"
7273
check tasks[0].description == "hello world"
74+
75+
test "calling extension/test should return all existing tests":
76+
#We first need to initialize the nimble project
77+
let projectDir = getCurrentDir() / "tests" / "projects" / "testrunner"
78+
cd projectDir:
79+
let (output, _) = execNimble("install", "-l")
80+
discard execNimble("setup")
81+
82+
let initParams =
83+
InitializeParams %* {
84+
"processId": %getCurrentProcessId(),
85+
"rootUri": fixtureUri("projects/testrunner/"),
86+
"capabilities":
87+
{"window": {"workDoneProgress": true}, "workspace": {"configuration": true}},
88+
}
89+
let initializeResult = waitFor client.initialize(initParams)
90+
91+
let listTestsParams = ListTestsParams(entryPoints: @["tests/projects/testrunner/tests/sampletests.nim".absolutePath])
92+
let tests = client.call("extension/listTests", jsonutils.toJson(listTestsParams)).waitFor().jsonTo(
93+
ListTestsResult
94+
)
95+
let testProjectInfo = tests.projectInfo
96+
check testProjectInfo.suites.len == 3
97+
check testProjectInfo.suites["Sample Tests"].tests.len == 1
98+
check testProjectInfo.suites["Sample Tests"].tests[0].name == "Sample Test"
99+
check testProjectInfo.suites["Sample Tests"].tests[0].file == "sampletests.nim"
100+
check testProjectInfo.suites["Sample Tests"].tests[0].line == 4

tests/ttestrunner.nim

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,7 @@
11
import unittest
22
import std/[os, osproc, strscans, tables, sequtils, enumerate, strutils]
33
import testhelpers
4-
5-
type
6-
TestInfo* = object
7-
name*: string
8-
line*: int
9-
file*: string
10-
11-
TestSuiteInfo* = object
12-
name*: string #The suite name, empty if it's a global test
13-
tests*: seq[TestInfo]
14-
15-
TestProjectInfo* = object
16-
entryPoints*: seq[string]
17-
suites*: Table[string, TestSuiteInfo]
18-
19-
20-
21-
22-
proc extractTestInfo*(rawOutput: string): TestProjectInfo =
23-
result.suites = initTable[string, TestSuiteInfo]()
24-
let lines = rawOutput.split("\n")
25-
var currentSuite = ""
26-
27-
for i, line in enumerate(lines):
28-
var name, file, ignore: string
29-
var lineNumber: int
30-
if scanf(line, "Suite: $*", name):
31-
currentSuite = name.strip()
32-
result.suites[currentSuite] = TestSuiteInfo(name: currentSuite)
33-
# echo "Found suite: ", currentSuite
34-
35-
elif scanf(line, "$*Test: $*", ignore, name):
36-
let insideSuite = line.startsWith("\t")
37-
# Use currentSuite if inside a suite, empty string if not
38-
let suiteName = if insideSuite: currentSuite else: ""
39-
40-
#File is always next line of a test
41-
if scanf(lines[i+1], "$*File:$*:$i", ignore, file, lineNumber):
42-
var testInfo = TestInfo(name: name.strip(), file: file.strip(), line: lineNumber)
43-
# echo "Adding test: ", testInfo.name, " to suite: ", suiteName
44-
result.suites[suiteName].tests.add(testInfo)
45-
4+
import testrunner
465

476

487
suite "Test Parser":

0 commit comments

Comments
 (0)