Skip to content

Commit 4d991d5

Browse files
authored
add mtp client (#7)
* add mtp client test running alternative test discovery fix running multiple tests start debug strategy start mtp runner mtp runner parse test runner results * add mtp debugger * improve client creation * improve handling of many tests cases * refactor mtp client * improve output reporting * improve debugger * fix crash on missing project file
1 parent 8483f77 commit 4d991d5

File tree

15 files changed

+1214
-483
lines changed

15 files changed

+1214
-483
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
Neotest adapter for dotnet
1313

1414
- Based on the VSTest for dotnet allowing test functionality similar to those found in IDEs like Rider and Visual Studio.
15+
- Will use the new [Microsoft.Testing.Platform](https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-intro?tabs=dotnetcli) when available for newer projects.
1516
- Supports all testing frameworks.
1617
- DAP strategy for attaching debug adapter to test execution.
1718
- Supports `C#` and `F#`.
19+
- No external dependencies, only the `dotnet sdk` required.
1820
- Can run tests on many groupings including:
1921
- All tests
2022
- Test projects

lua/neotest-vstest/client.lua

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
local nio = require("nio")
2+
local lib = require("neotest.lib")
3+
local logger = require("neotest.logging")
4+
local dotnet_utils = require("neotest-vstest.dotnet_utils")
5+
local vstest = require("neotest-vstest.vstest")
6+
local cli_wrapper = require("neotest-vstest.vstest.cli_wrapper")
7+
local files = require("neotest-vstest.files")
8+
local utilities = require("neotest-vstest.utilities")
9+
10+
local Client = {}
11+
Client.__index = Client
12+
13+
---@param project DotnetProjectInfo
14+
function Client:new(project)
15+
local client = {
16+
project = project,
17+
test_cases = {},
18+
last_discovered = 0,
19+
semaphore = nio.control.semaphore(1),
20+
}
21+
setmetatable(client, self)
22+
return client
23+
end
24+
25+
function Client:discover_tests(path)
26+
self.semaphore.with(function()
27+
local last_modified
28+
if path then
29+
last_modified = files.get_path_last_modified(path)
30+
else
31+
last_modified = dotnet_utils.get_project_last_modified(self.project)
32+
end
33+
if last_modified and last_modified > self.last_discovered then
34+
logger.debug(
35+
"neotest-vstest: Discovering tests: "
36+
.. " last modified at "
37+
.. last_modified
38+
.. " last discovered at "
39+
.. self.last_discovered
40+
)
41+
dotnet_utils.build_project(self.project)
42+
last_modified = dotnet_utils.get_project_last_modified(self.project)
43+
self.last_discovered = last_modified or 0
44+
self.test_cases = vstest.discover_tests_in_project(self.project) or {}
45+
end
46+
end)
47+
48+
return self.test_cases
49+
end
50+
51+
function Client:discover_tests_for_path(path)
52+
self:discover_tests(path)
53+
return self.test_cases[path]
54+
end
55+
56+
---@async
57+
---@param ids string[] list of test ids to run
58+
---@return neotest-vstest.Client.RunResult
59+
function Client:run_tests(ids)
60+
local result_future = nio.control.future()
61+
local process_output_file, stream_file, result_file = vstest.run_tests(self.project, ids)
62+
63+
local result_stream_data, result_stop_stream = lib.files.stream_lines(stream_file)
64+
local output_stream_data, output_stop_stream = lib.files.stream_lines(process_output_file)
65+
66+
local result_stream = utilities.stream_queue()
67+
68+
nio.run(function()
69+
local stream = result_stream_data()
70+
for _, line in ipairs(stream) do
71+
local success, result = pcall(vim.json.decode, line)
72+
assert(success, "neotest-vstest: failed to decode result stream: " .. line)
73+
result_stream.write(result)
74+
end
75+
end)
76+
77+
local stop_stream = function()
78+
output_stop_stream()
79+
result_stop_stream()
80+
end
81+
82+
nio.run(function()
83+
cli_wrapper.spin_lock_wait_file(result_file, 5 * 30 * 1000)
84+
local parsed = {}
85+
local results = lib.files.read_lines(result_file)
86+
for _, line in ipairs(results) do
87+
local success, result = pcall(vim.json.decode, line)
88+
assert(success, "neotest-vstest: failed to decode result file: " .. line)
89+
parsed[result.id] = result.result
90+
end
91+
result_future.set(parsed)
92+
end)
93+
94+
return {
95+
result_future = result_future,
96+
result_stream = result_stream.get,
97+
output_stream = output_stream_data,
98+
stop = stop_stream,
99+
}
100+
end
101+
102+
---@async
103+
---@param ids string[] list of test ids to run
104+
---@return neotest-vstest.Client.RunResult
105+
function Client:debug_tests(ids)
106+
local result_future = nio.control.future()
107+
local pid, on_attach, process_output_file, stream_file, result_file =
108+
vstest.debug_tests(self.project, ids)
109+
110+
local result_stream_data, result_stop_stream = lib.files.stream_lines(stream_file)
111+
local output_stream_data, output_stop_stream = lib.files.stream_lines(process_output_file)
112+
113+
local result_stream = utilities.stream_queue()
114+
115+
nio.run(function()
116+
local stream = result_stream_data()
117+
for _, line in ipairs(stream) do
118+
local success, result = pcall(vim.json.decode, line)
119+
assert(success, "neotest-vstest: failed to decode result stream: " .. line)
120+
result_stream.write(result)
121+
end
122+
end)
123+
124+
local stop_stream = function()
125+
output_stop_stream()
126+
result_stop_stream()
127+
end
128+
129+
nio.run(function()
130+
cli_wrapper.spin_lock_wait_file(result_file, 5 * 30 * 1000)
131+
local parsed = {}
132+
local results = lib.files.read_lines(result_file)
133+
for _, line in ipairs(results) do
134+
local success, result = pcall(vim.json.decode, line)
135+
assert(success, "neotest-vstest: failed to decode result file: " .. line)
136+
parsed[result.id] = result.result
137+
end
138+
result_future.set(parsed)
139+
end)
140+
141+
assert(pid, "neotest-vstest: failed to get pid from debug tests")
142+
143+
return {
144+
pid = pid,
145+
on_attach = on_attach,
146+
result_future = result_future,
147+
output_stream = output_stream_data,
148+
result_stream = result_stream.get,
149+
stop = stop_stream,
150+
}
151+
end
152+
153+
return Client

lua/neotest-vstest/dotnet_utils.lua

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ end
102102
---@field dll_file string
103103
---@field proj_dir string
104104
---@field is_test_project boolean
105+
---@field is_mtp_project boolean is project compiler use Microsoft.Testting.Platform
105106

106107
---@type table<string, DotnetProjectInfo>
107108
local proj_info_cache = {}
@@ -147,6 +148,10 @@ function M.get_proj_info(path)
147148

148149
local semaphore
149150

151+
if not proj_file then
152+
return nil
153+
end
154+
150155
if project_semaphore[proj_file] then
151156
semaphore = project_semaphore[proj_file]
152157
else
@@ -178,6 +183,7 @@ function M.get_proj_info(path)
178183
"-getProperty:TargetPath",
179184
"-getProperty:MSBuildProjectDirectory",
180185
"-getProperty:IsTestProject",
186+
"-getProperty:IsTestingPlatformApplication",
181187
"-property:TargetFramework=" .. target_framework,
182188
}
183189

@@ -197,6 +203,7 @@ function M.get_proj_info(path)
197203
dll_file = properties.TargetPath,
198204
proj_dir = properties.MSBuildProjectDirectory,
199205
is_test_project = properties.IsTestProject == "true",
206+
is_mtp_project = properties.IsTestingPlatformApplication == "true",
200207
}
201208

202209
if proj_data.dll_file == "" then
@@ -212,7 +219,12 @@ function M.get_proj_info(path)
212219
end
213220

214221
semaphore.release()
215-
return proj_data
222+
return (
223+
proj_data.dll_file ~= ""
224+
and proj_data.proj_file ~= ""
225+
and (proj_data.is_test_project or proj_data.is_mtp_project)
226+
and proj_data
227+
) or nil
216228
end
217229

218230
local solution_discovery_semaphore = nio.control.semaphore(1)

0 commit comments

Comments
 (0)