Skip to content

Adds listTests route #317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 9, 2025
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
1 change: 1 addition & 0 deletions nimlangserver.nim
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ proc registerRoutes(srv: RpcSocketServer, ls: LanguageServer) =
srv.register("extension/suggest", wrapRpc(partial(extensionSuggest, ls)))
srv.register("extension/tasks", wrapRpc(partial(tasks, ls)))
srv.register("extension/runTask", wrapRpc(partial(runTask, ls)))
srv.register("extension/listTests", wrapRpc(partial(listTests, ls)))
#Notifications
srv.register("$/cancelRequest", wrapRpc(partial(cancelRequest, ls)))
srv.register("initialized", wrapRpc(partial(initialized, ls)))
Expand Down
20 changes: 20 additions & 0 deletions protocol/types.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import options
import tables

type
OptionalSeq*[T] = Option[seq[T]]
Expand Down Expand Up @@ -1042,3 +1043,22 @@ type
RunTaskResult* = object
command*: seq[string] #command and args
output*: seq[string] #output lines

TestInfo* = object
name*: string
line*: int
file*: string

TestSuiteInfo* = object
name*: string #The suite name, empty if it's a global test
tests*: seq[TestInfo]

TestProjectInfo* = object
entryPoints*: seq[string]
suites*: Table[string, TestSuiteInfo]

ListTestsParams* = object
entryPoints*: seq[string] #can be patterns? if empty we could do the same as nimble does or just run `nimble test args`

ListTestsResult* = object
projectInfo*: TestProjectInfo
15 changes: 13 additions & 2 deletions routes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import
std/[strscans, times, json, parseutils, strutils],
ls,
stew/[byteutils],
nimexpand

nimexpand,
testrunner

import macros except error

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

proc listTests*(
ls: LanguageServer, params: ListTestsParams
): Future[ListTestsResult] {.async.} =
let config = await ls.getWorkspaceConfiguration()
let nimPath = config.getNimPath()
if nimPath.isNone:
return ListTestsResult(projectInfo: TestProjectInfo(entryPoints: params.entryPoints, suites: initTable[string, TestSuiteInfo]()))
let testProjectInfo = await ls.listTestsForEntryPoint(params.entryPoints, nimPath.get())
result.projectInfo = testProjectInfo

#Notifications
proc initialized*(ls: LanguageServer, _: JsonNode): Future[void] {.async.} =
debug "Client initialized."
Expand Down
58 changes: 58 additions & 0 deletions testrunner.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import std/[os, osproc, strscans, tables, sequtils, enumerate, strutils]
import chronos, chronos/asyncproc
import protocol/types
import ls
import chronicles
import stew/byteutils
import utils

proc extractTestInfo*(rawOutput: string): TestProjectInfo =
result.suites = initTable[string, TestSuiteInfo]()
let lines = rawOutput.split("\n")
var currentSuite = ""

for i, line in enumerate(lines):
var name, file, ignore: string
var lineNumber: int
if scanf(line, "Suite: $*", name):
currentSuite = name.strip()
result.suites[currentSuite] = TestSuiteInfo(name: currentSuite)
# echo "Found suite: ", currentSuite

elif scanf(line, "$*Test: $*", ignore, name):
let insideSuite = line.startsWith("\t")
# Use currentSuite if inside a suite, empty string if not
let suiteName = if insideSuite: currentSuite else: ""

#File is always next line of a test
if scanf(lines[i+1], "$*File:$*:$i", ignore, file, lineNumber):
var testInfo = TestInfo(name: name.strip(), file: file.strip(), line: lineNumber)
# echo "Adding test: ", testInfo.name, " to suite: ", suiteName
result.suites[suiteName].tests.add(testInfo)

proc listTestsForEntryPoint*(
ls: LanguageServer, entryPoints: seq[string], nimPath: string
): Future[TestProjectInfo] {.async.} =
#For now only one entry point is supported
assert entryPoints.len == 1
let entryPoint = entryPoints[0]
if not fileExists(entryPoint):
error "Entry point does not exist", entryPoint = entryPoint
return TestProjectInfo()
let process = await startProcess(
nimPath,
arguments = @["c", "-d:unittest2ListTests", "-r", "--listFullPaths", entryPoints[0]],
options = {UsePath},
stderrHandle = AsyncProcess.Pipe,
stdoutHandle = AsyncProcess.Pipe,
)
try:
let res = await process.waitForExit(15.seconds)
if res != 0:
error "Failed to list tests", nimPath = nimPath, entryPoint = entryPoints[0], res = res
error "An error occurred while listing tests", error = string.fromBytes(process.stderrStream.read().await)
else:
let rawOutput = string.fromBytes(process.stdoutStream.read().await)
result = extractTestInfo(rawOutput)
finally:
await shutdownChildProcess(process)
28 changes: 28 additions & 0 deletions tests/textensions.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import json_rpc/[rpcclient]
import chronicles
import lspsocketclient
import chronos/asyncproc
import testhelpers

suite "Nimlangserver extensions":
let cmdParams = CommandLineParams(transport: some socket, port: getNextFreePort())
Expand Down Expand Up @@ -70,3 +71,30 @@ suite "Nimlangserver extensions":
check tasks.len == 3
check tasks[0].name == "helloWorld"
check tasks[0].description == "hello world"

test "calling extension/test should return all existing tests":
#We first need to initialize the nimble project
let projectDir = getCurrentDir() / "tests" / "projects" / "testrunner"
cd projectDir:
let (output, _) = execNimble("install", "-l")
discard execNimble("setup")

let initParams =
InitializeParams %* {
"processId": %getCurrentProcessId(),
"rootUri": fixtureUri("projects/testrunner/"),
"capabilities":
{"window": {"workDoneProgress": true}, "workspace": {"configuration": true}},
}
let initializeResult = waitFor client.initialize(initParams)

let listTestsParams = ListTestsParams(entryPoints: @["tests/projects/testrunner/tests/sampletests.nim".absolutePath])
let tests = client.call("extension/listTests", jsonutils.toJson(listTestsParams)).waitFor().jsonTo(
ListTestsResult
)
let testProjectInfo = tests.projectInfo
check testProjectInfo.suites.len == 3
check testProjectInfo.suites["Sample Tests"].tests.len == 1
check testProjectInfo.suites["Sample Tests"].tests[0].name == "Sample Test"
check testProjectInfo.suites["Sample Tests"].tests[0].file == "sampletests.nim"
check testProjectInfo.suites["Sample Tests"].tests[0].line == 4
43 changes: 1 addition & 42 deletions tests/ttestrunner.nim
Original file line number Diff line number Diff line change
@@ -1,48 +1,7 @@
import unittest
import std/[os, osproc, strscans, tables, sequtils, enumerate, strutils]
import testhelpers

type
TestInfo* = object
name*: string
line*: int
file*: string

TestSuiteInfo* = object
name*: string #The suite name, empty if it's a global test
tests*: seq[TestInfo]

TestProjectInfo* = object
entryPoints*: seq[string]
suites*: Table[string, TestSuiteInfo]




proc extractTestInfo*(rawOutput: string): TestProjectInfo =
result.suites = initTable[string, TestSuiteInfo]()
let lines = rawOutput.split("\n")
var currentSuite = ""

for i, line in enumerate(lines):
var name, file, ignore: string
var lineNumber: int
if scanf(line, "Suite: $*", name):
currentSuite = name.strip()
result.suites[currentSuite] = TestSuiteInfo(name: currentSuite)
# echo "Found suite: ", currentSuite

elif scanf(line, "$*Test: $*", ignore, name):
let insideSuite = line.startsWith("\t")
# Use currentSuite if inside a suite, empty string if not
let suiteName = if insideSuite: currentSuite else: ""

#File is always next line of a test
if scanf(lines[i+1], "$*File:$*:$i", ignore, file, lineNumber):
var testInfo = TestInfo(name: name.strip(), file: file.strip(), line: lineNumber)
# echo "Adding test: ", testInfo.name, " to suite: ", suiteName
result.suites[suiteName].tests.add(testInfo)

import testrunner


suite "Test Parser":
Expand Down