Skip to content

Commit

Permalink
use custom neotest strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
Nsidorenco committed Dec 28, 2024
1 parent 82af8c0 commit 9293f09
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 59 deletions.
57 changes: 33 additions & 24 deletions lua/neotest-dotnet/init.lua
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
local nio = require("nio")
local lib = require("neotest.lib")
local utils = require("neotest.utils")
local types = require("neotest.types")
local logger = require("neotest.logging")

local vstest = require("neotest-dotnet.vstest_wrapper")
local vstest_strategy = require("neotest-dotnet.strategies.vstest")

---@package
---@type neotest.Adapter
local DotnetNeotestAdapter = { name = "neotest-dotnet" }

DotnetNeotestAdapter.root = function(path)
function DotnetNeotestAdapter.root(path)
return lib.files.match_root_pattern("*.sln")(path)
or lib.files.match_root_pattern("*.[cf]sproj")(path)
end

DotnetNeotestAdapter.is_test_file = function(file_path)
function DotnetNeotestAdapter.is_test_file(file_path)
return (vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs"))
and vstest.discover_tests(file_path)
end

DotnetNeotestAdapter.filter_dir = function(name)
function DotnetNeotestAdapter.filter_dir(name)
return name ~= "bin" and name ~= "obj"
end

Expand Down Expand Up @@ -131,7 +131,7 @@ local function build_position(source, captured_nodes, tests_in_file, path)
end
end

DotnetNeotestAdapter.discover_positions = function(path)
function DotnetNeotestAdapter.discover_positions(path)
logger.info(string.format("neotest-dotnet: scanning %s for tests...", path))

local filetype = (vim.endswith(path, ".fs") and "fsharp") or "c_sharp"
Expand Down Expand Up @@ -179,9 +179,6 @@ DotnetNeotestAdapter.discover_positions = function(path)
end
end

logger.info("neotest-dotnet: sorted test cases:")
logger.info(nodes)

local structure = assert(build_structure(nodes, {}, {
nested_tests = false,
require_namespaces = false,
Expand All @@ -205,15 +202,12 @@ DotnetNeotestAdapter.discover_positions = function(path)
end)
end

logger.info("neotest-dotnet: test case tree:")
logger.info(tree)

logger.info(string.format("neotest-dotnet: done scanning %s for tests", path))

return tree
end

DotnetNeotestAdapter.build_spec = function(args)
function DotnetNeotestAdapter.build_spec(args)
local tree = args.tree
if not tree then
return
Expand Down Expand Up @@ -243,7 +237,7 @@ DotnetNeotestAdapter.build_spec = function(args)
local attached_path = nio.fn.tempname()

local pid = vstest.debug_tests(attached_path, stream_path, results_path, ids)
--- @type Configuration
--- @type dap.Configuration
strategy = {
type = "netcoredbg",
name = "netcoredbg - attach",
Expand All @@ -252,7 +246,7 @@ DotnetNeotestAdapter.build_spec = function(args)
env = {
DOTNET_ENVIRONMENT = "Development",
},
processId = vim.trim(pid),
processId = pid and vim.trim(pid),
before = function()
local dap = require("dap")
dap.listeners.after.configurationDone["neotest-dotnet"] = function()
Expand All @@ -266,10 +260,11 @@ DotnetNeotestAdapter.build_spec = function(args)
end

return {
command = vstest.run_tests(args.strategy == "dap", stream_path, results_path, ids),
context = {
result_path = results_path,
stream_path = stream_path,
stop_stream = stop_stream,
ids = ids,
},
stream = function()
return function()
Expand All @@ -282,33 +277,47 @@ DotnetNeotestAdapter.build_spec = function(args)
return results
end
end,
strategy = strategy,
strategy = strategy or vstest_strategy,
}
end

DotnetNeotestAdapter.results = function(spec)
function DotnetNeotestAdapter.results(spec, _result, _tree)
local max_wait = 5 * 50 * 1000 -- 5 min
logger.info("neotest-dotnet: waiting for test results")
local success, data = pcall(vstest.spin_lock_wait_file, spec.context.result_path, max_wait)

spec.context.stop_stream()

logger.info("neotest-dotnet: parsing test results")

local results = {}

if not success then
for _, id in ipairs(spec.context.ids) do
results[id] = {
status = "skipped",
output = spec.context.result_path,
errors = {
message = "failed to read result file",
},
}
end
return results
end

local parse_ok, parsed = pcall(vim.json.decode, data)
assert(parse_ok, "failed to parse result file")

if not parse_ok then
local outcome = "skipped"
results[spec.context.id] = {
status = outcome,
errors = {
message = "failed to parse result file",
},
}
for _, id in ipairs(spec.context.ids) do
results[id] = {
status = "skipped",
output = spec.context.result_path,
errors = {
message = "failed to parse result file",
},
}
end

return results
end
Expand Down
50 changes: 50 additions & 0 deletions lua/neotest-dotnet/strategies/vstest.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
local nio = require("nio")
local lib = require("neotest.lib")
local vstest = require("neotest-dotnet.vstest_wrapper")

---@async
---@param spec neotest.RunSpec
---@return neotest.Process
return function(spec)
local process_output = nio.fn.tempname()
lib.files.write(process_output, "")

local wait_file = vstest.run_tests(
spec.context.stream_path,
spec.context.result_path,
process_output,
spec.context.ids
)

local result_future = nio.control.future()

nio.run(function()
vstest.spin_lock_wait_file(wait_file, 5 * 30 * 1000)
result_future:set()
end)

local stream_data, stop_stream = lib.files.stream_lines(process_output)

return {
is_complete = function()
return result_future.is_set()
end,
output = function()
return process_output
end,
stop = function()
stop_stream()
end,
output_stream = function()
return function()
local lines = stream_data()
return table.concat(lines, "\n")
end
end,
attach = function() end,
result = function()
result_future:wait()
return 1
end,
}
end
40 changes: 21 additions & 19 deletions lua/neotest-dotnet/vstest_wrapper.lua
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ function M.spin_lock_wait_file(file_path, max_wait)
if lib.files.exists(file_path) then
spin_lock.with(function()
file_exists = true
content = require("neotest.lib").files.read(file_path)
content = lib.files.read(file_path)
end)
else
tries = tries + 1
Expand Down Expand Up @@ -328,28 +328,27 @@ function M.discover_tests(path)
end

---runs tests identified by ids.
---@param dap boolean true if normal test runner should be skipped
---@param stream_path string
---@param output_path string
---@param process_output_path string
---@param ids string|string[]
---@return string command
function M.run_tests(dap, stream_path, output_path, ids)
if not dap then
lib.process.run({ "dotnet", "build" })

local command = vim
.iter({
"run-tests",
stream_path,
output_path,
ids,
})
:flatten()
:join(" ")
invoke_test_runner(command)
end
---@return string wait_file
function M.run_tests(stream_path, output_path, process_output_path, ids)
lib.process.run({ "dotnet", "build" })

return string.format("tail -n 1 -f %s", output_path, output_path)
local command = vim
.iter({
"run-tests",
stream_path,
output_path,
process_output_path,
ids,
})
:flatten()
:join(" ")
invoke_test_runner(command)

return output_path
end

--- Uses the vstest console to spawn a test process for the debugger to attach to.
Expand All @@ -361,6 +360,8 @@ end
function M.debug_tests(attached_path, stream_path, output_path, ids)
lib.process.run({ "dotnet", "build" })

local process_output = nio.fn.tempname()

local pid_path = nio.fn.tempname()

local command = vim
Expand All @@ -370,6 +371,7 @@ function M.debug_tests(attached_path, stream_path, output_path, ids)
attached_path,
stream_path,
output_path,
process_output,
ids,
})
:flatten()
Expand Down
49 changes: 33 additions & 16 deletions scripts/run_tests.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ module TestDiscovery =

{| StreamPath = args[0]
OutputPath = args[1]
Ids = args[2..] |> Array.map Guid.Parse |}
ProcessOutput = args[2]
Ids = args[3..] |> Array.map Guid.Parse |}
|> ValueOption.Some
else
ValueOption.None
Expand All @@ -56,7 +57,8 @@ module TestDiscovery =
AttachedPath = args[1]
StreamPath = args[2]
OutputPath = args[3]
Ids = args[4..] |> Array.map Guid.Parse |}
ProcessOutput = args[4]
Ids = args[5..] |> Array.map Guid.Parse |}
|> ValueOption.Some
else
ValueOption.None
Expand Down Expand Up @@ -99,8 +101,9 @@ module TestDiscovery =

member __.HandleRawMessage(_) = ()

type PlaygroundTestRunHandler(streamOutputPath, outputFilePath) =
type PlaygroundTestRunHandler(streamOutputPath, outputFilePath, processOutputPath) =
let resultsDictionary = ConcurrentDictionary()
let processOutputWriter = new StreamWriter(processOutputPath, append = true)

interface ITestRunEventsHandler with
member _.HandleTestRunComplete
Expand All @@ -109,7 +112,9 @@ module TestDiscovery =
use outputWriter = new StreamWriter(outputFilePath, append = false)
outputWriter.WriteLine(JsonConvert.SerializeObject(resultsDictionary))

member __.HandleLogMessage(level, message) = logHandler level message
member __.HandleLogMessage(_level, message) =
if not <| String.IsNullOrWhiteSpace message then
processOutputWriter.WriteLine(message)

member __.HandleRawMessage(_rawMessage) = ()

Expand Down Expand Up @@ -157,6 +162,9 @@ module TestDiscovery =

member __.LaunchProcessWithDebuggerAttached(_testProcessStartInfo) = 1

interface IDisposable with
member _.Dispose() = processOutputWriter.Dispose()

type DebugLauncher(pidFile: string, attachedFile: string) =
interface ITestHostLauncher2 with
member this.LaunchTestHost(defaultTestHostStartInfo: TestProcessStartInfo) =
Expand Down Expand Up @@ -264,26 +272,35 @@ module TestDiscovery =
}
|> ignore
| RunTests args ->
let testCases = getTestCases args.Ids
task {
let testCases = getTestCases args.Ids

let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath)
// spawn as task to allow running concurrent tests
r.RunTestsAsync(testCases, sourceSettings, testHandler) |> ignore
()
| DebugTests args ->
let testCases = getTestCases args.Ids
use testHandler =
new PlaygroundTestRunHandler(args.StreamPath, args.OutputPath, args.ProcessOutput)
// spawn as task to allow running concurrent tests
do! r.RunTestsAsync(testCases, sourceSettings, testHandler)
Console.WriteLine($"Done running tests for ids: ")

let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath)
let debugLauncher = DebugLauncher(args.PidPath, args.AttachedPath)
Console.WriteLine($"Starting {testCases.Length} tests in debug-mode")
for id in args.Ids do
Console.Write($"{id} ")

return ()
}
|> ignore
| DebugTests args ->
task {
let testCases = getTestCases args.Ids

use testHandler =
new PlaygroundTestRunHandler(args.StreamPath, args.OutputPath, args.ProcessOutput)

let debugLauncher = DebugLauncher(args.PidPath, args.AttachedPath)
Console.WriteLine($"Starting {testCases.Length} tests in debug-mode")

do! Task.Yield()
r.RunTestsWithCustomTestHost(testCases, sourceSettings, testHandler, debugLauncher)
}
|> ignore

()
| _ -> loop <- false

r.EndSession()
Expand Down

0 comments on commit 9293f09

Please sign in to comment.