From 1045b1b52c6a7693bc9567b51173b3dc389abd10 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Sat, 3 Feb 2024 17:49:43 +0000 Subject: [PATCH 01/17] feat(83): First pass at creating async background for dotnet test -t - Should generate output file of test names that are easily parsable and compared to the test names from tree-sitter discovery --- lua/neotest-dotnet/utils/dotnet-utils.lua | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lua/neotest-dotnet/utils/dotnet-utils.lua diff --git a/lua/neotest-dotnet/utils/dotnet-utils.lua b/lua/neotest-dotnet/utils/dotnet-utils.lua new file mode 100644 index 0000000..c418b1e --- /dev/null +++ b/lua/neotest-dotnet/utils/dotnet-utils.lua @@ -0,0 +1,78 @@ +local nio = require("nio") +local async = require("neotest.async") +local FanoutAccum = require("neotest.types").FanoutAccum +local logger = require("neotest.logging") + +local DotNetUtils = {} + +-- Write function that use nio to run dotnet test -t in --no-build in the background and saves output to a temp file +function DotNetUtils.get_test_full_names(project_path, output_file) + local data_accum = FanoutAccum(function(prev, new) + if not prev then + return new + end + return prev .. new + end, nil) + + local stream_path = vim.fn.tempname() + local open_err, stream_fd = async.uv.fs_open(stream_path, "w", 438) + assert(not open_err, open_err) + + data_accum:subscribe(function(data) + local write_err, _ = async.uv.fs_write(stream_fd, data) + assert(not write_err, write_err) + end) + + local test_names_started = false + local finish_future = async.control.future() + local result_code + + local test_command = "dotnet test -t " .. project_path .. "-- NUnit.DisplayName=FullName" + local success, job = pcall(nio.fn.jobstart, test_command, { + cwd = project_path, + pty = true, + on_stdout = function(_, data) + for _, line in ipairs(data) do + if test_names_started then + nio.run(function() + data_accum:push(table.concat(data, "\n")) + end) + end + if line:find("The following Tests are available") then + test_names_started = false + end + end + end, + on_exit = function(_, code) + result_code = code + finish_future.set() + end, + }) + + if not success then + local write_err, _ = nio.uv.fs_write(stream_fd, job) + assert(not write_err, write_err) + result_code = 1 + finish_future.set() + end + + return { + is_complete = function() + return result_code ~= nil + end, + result = function() + if result_code == nil then + finish_future:wait() + end + local close_err = nio.uv.fs_close(stream_fd) + assert(not close_err, close_err) + pcall(nio.fn.chanclose, job) + return { + result_code = result_code, + output = nio.fn.readfile(stream_path), + } + end, + } +end + +return DotNetUtils From eaacf13104aece0147a590d44b01d6e469130a78 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Sat, 3 Feb 2024 19:54:34 +0000 Subject: [PATCH 02/17] feat(83): Fixes to routine to write the test list output to temp file --- lua/neotest-dotnet/init.lua | 6 +++++ lua/neotest-dotnet/utils/dotnet-utils.lua | 29 ++++++++++++----------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 688e785..589a0cc 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -1,4 +1,6 @@ local lib = require("neotest.lib") +local dotnet_utils = require("neotest-dotnet.utils.dotnet-utils") +local nio = require("nio") local logger = require("neotest.logging") local result_utils = require("neotest-dotnet.utils.result-utils") local trx_utils = require("neotest-dotnet.utils.trx-utils") @@ -104,6 +106,10 @@ DotnetNeotestAdapter.discover_positions = function(path) position_id = "require('neotest-dotnet')._position_id", }) + local proj_root = lib.files.match_root_pattern("*.csproj")(path) + local test_list_job = dotnet_utils.get_test_full_names(proj_root) + local test_list = test_list_job.result() + return tree end diff --git a/lua/neotest-dotnet/utils/dotnet-utils.lua b/lua/neotest-dotnet/utils/dotnet-utils.lua index c418b1e..3a8e4cd 100644 --- a/lua/neotest-dotnet/utils/dotnet-utils.lua +++ b/lua/neotest-dotnet/utils/dotnet-utils.lua @@ -5,8 +5,7 @@ local logger = require("neotest.logging") local DotNetUtils = {} --- Write function that use nio to run dotnet test -t in --no-build in the background and saves output to a temp file -function DotNetUtils.get_test_full_names(project_path, output_file) +function DotNetUtils.get_test_full_names(project_path) local data_accum = FanoutAccum(function(prev, new) if not prev then return new @@ -19,27 +18,27 @@ function DotNetUtils.get_test_full_names(project_path, output_file) assert(not open_err, open_err) data_accum:subscribe(function(data) - local write_err, _ = async.uv.fs_write(stream_fd, data) - assert(not write_err, write_err) + vim.loop.fs_write(stream_fd, data, nil, function(write_err) + assert(not write_err, write_err) + end) end) local test_names_started = false local finish_future = async.control.future() local result_code - local test_command = "dotnet test -t " .. project_path .. "-- NUnit.DisplayName=FullName" + local test_command = "dotnet test -t " .. project_path .. " -- NUnit.DisplayName=FullName" local success, job = pcall(nio.fn.jobstart, test_command, { - cwd = project_path, pty = true, on_stdout = function(_, data) for _, line in ipairs(data) do if test_names_started then - nio.run(function() - data_accum:push(table.concat(data, "\n")) - end) + -- Trim leading and trailing whitespace before writing + line = line:gsub("^%s*(.-)%s*$", "%1") + data_accum:push(line .. "\n") end if line:find("The following Tests are available") then - test_names_started = false + test_names_started = true end end end, @@ -61,15 +60,17 @@ function DotNetUtils.get_test_full_names(project_path, output_file) return result_code ~= nil end, result = function() - if result_code == nil then - finish_future:wait() - end + finish_future:wait() local close_err = nio.uv.fs_close(stream_fd) assert(not close_err, close_err) pcall(nio.fn.chanclose, job) + local output = nio.fn.readfile(stream_path) + + logger.debug("DotNetUtils.get_test_full_names output: ") + logger.debug(output) return { result_code = result_code, - output = nio.fn.readfile(stream_path), + output = output, } end, } From e633aa233c3833331c2924fcd99d1a7b13e68c77 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Tue, 6 Feb 2024 20:48:49 +0000 Subject: [PATCH 03/17] feat(83): Fixes up new post processing method --- lua/neotest-dotnet/frameworks/mstest.lua | 7 + lua/neotest-dotnet/frameworks/nunit.lua | 7 + lua/neotest-dotnet/frameworks/xunit.lua | 191 +++++++++++++------ lua/neotest-dotnet/init.lua | 17 +- lua/neotest-dotnet/neotest-types.lua | 8 + lua/neotest-dotnet/types/framework-utils.lua | 3 - 6 files changed, 168 insertions(+), 65 deletions(-) create mode 100644 lua/neotest-dotnet/neotest-types.lua delete mode 100644 lua/neotest-dotnet/types/framework-utils.lua diff --git a/lua/neotest-dotnet/frameworks/mstest.lua b/lua/neotest-dotnet/frameworks/mstest.lua index 89e87a9..c22c1d7 100644 --- a/lua/neotest-dotnet/frameworks/mstest.lua +++ b/lua/neotest-dotnet/frameworks/mstest.lua @@ -59,4 +59,11 @@ M.build_parameterized_test_positions = function(base_node, source, captured_node return nodes end +---Modifies the tree using supplementary information from dotnet test -t or other methods +---@param tree neotest.Tree The tree to modify +---@param path string The path to the file the tree was built from +M.post_process_tree_list = function(tree, path) + return tree +end + return M diff --git a/lua/neotest-dotnet/frameworks/nunit.lua b/lua/neotest-dotnet/frameworks/nunit.lua index 0c8d54f..a06457b 100644 --- a/lua/neotest-dotnet/frameworks/nunit.lua +++ b/lua/neotest-dotnet/frameworks/nunit.lua @@ -60,4 +60,11 @@ M.build_parameterized_test_positions = function(base_node, source, captured_node return nodes end +---Modifies the tree using supplementary information from dotnet test -t or other methods +---@param tree neotest.Tree The tree to modify +---@param path string The path to the file the tree was built from +M.post_process_tree_list = function(tree, path) + return tree +end + return M diff --git a/lua/neotest-dotnet/frameworks/xunit.lua b/lua/neotest-dotnet/frameworks/xunit.lua index de6383d..f7fc1be 100644 --- a/lua/neotest-dotnet/frameworks/xunit.lua +++ b/lua/neotest-dotnet/frameworks/xunit.lua @@ -1,6 +1,13 @@ local logger = require("neotest.logging") - ----@type FrameworkUtils +local lib = require("neotest.lib") +local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") +local types = require("neotest.types") +local Tree = types.Tree + +---@class FrameworkUtils +---@field get_treesitter_queries function the TS queries for the framework +---@field build_parameterized_test_positions function Builds a tree of parameterized test nodes +---@field post_process_tree_list function Modifies the tree using supplementary information from dotnet test -t or other methods local M = {} local function parameter_string_to_table(parameter_string) @@ -29,66 +36,142 @@ end ---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. ---@param base_node table The initial root node to build the positions from ----@param source any ----@param captured_nodes any +---@param source any The source code to build the positions from +---@param captured_nodes any The nodes captured by the TS query ---@param match_type string The type of node that was matched by the TS query ---@return table M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) - logger.debug("neotest-dotnet(X-Unit Utils): Building parameterized test positions from source") - logger.debug("neotest-dotnet(X-Unit Utils): Base node: ") - logger.debug(base_node) - - logger.debug("neotest-dotnet(X-Unit Utils): Match Type: " .. match_type) - - local query = [[ - ;;query - (attribute_list - (attribute - name: (identifier) @attribute_name (#any-of? @attribute_name "InlineData") - ((attribute_argument_list) @arguments) - ) - ) - ]] - - local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse("c_sharp", query) - or vim.treesitter.parse_query("c_sharp", query) - - -- Set type to test (otherwise it will be test.parameterized) - local parameterized_test_node = vim.tbl_extend("force", base_node, { type = "test" }) - local nodes = { parameterized_test_node } - - -- Test method has parameters, so we need to create a sub-position for each test case - local capture_indices = {} - for i, capture in ipairs(param_query.captures) do - capture_indices[capture] = i - end - local arguments_index = capture_indices["arguments"] - - for _, match in param_query:iter_matches(captured_nodes[match_type .. ".definition"], source) do - local params_text = vim.treesitter.get_node_text(captured_nodes["parameter_list"], source) - local args_node = match[arguments_index] - local args_text = vim.treesitter.get_node_text(args_node, source) - local params_table = parameter_string_to_table(params_text) - local args_table = argument_string_to_table(args_text) - - local named_params = "" - for i, param in ipairs(params_table) do - named_params = named_params .. param .. ": " .. args_table[i] - if i ~= #params_table then - named_params = named_params .. ", " + -- logger.debug("neotest-dotnet(X-Unit Utils): Building parameterized test positions from source") + -- logger.debug("neotest-dotnet(X-Unit Utils): Base node: ") + -- logger.debug(base_node) + -- + -- logger.debug("neotest-dotnet(X-Unit Utils): Match Type: " .. match_type) + -- + -- local query = [[ + -- ;;query + -- (attribute_list + -- (attribute + -- name: (identifier) @attribute_name (#any-of? @attribute_name "InlineData") + -- ((attribute_argument_list) @arguments) + -- ) + -- ) + -- ]] + -- + -- local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse("c_sharp", query) + -- or vim.treesitter.parse_query("c_sharp", query) + -- + -- -- Set type to test (otherwise it will be test.parameterized) + -- local parameterized_test_node = vim.tbl_extend("force", base_node, { type = "test" }) + -- local nodes = { parameterized_test_node } + -- + -- -- Test method has parameters, so we need to create a sub-position for each test case + -- local capture_indices = {} + -- for i, capture in ipairs(param_query.captures) do + -- capture_indices[capture] = i + -- end + -- local arguments_index = capture_indices["arguments"] + -- + -- for _, match in param_query:iter_matches(captured_nodes[match_type .. ".definition"], source) do + -- local params_text = vim.treesitter.get_node_text(captured_nodes["parameter_list"], source) + -- local args_node = match[arguments_index] + -- local args_text = vim.treesitter.get_node_text(args_node, source) + -- local params_table = parameter_string_to_table(params_text) + -- local args_table = argument_string_to_table(args_text) + -- + -- local named_params = "" + -- for i, param in ipairs(params_table) do + -- named_params = named_params .. param .. ": " .. args_table[i] + -- if i ~= #params_table then + -- named_params = named_params .. ", " + -- end + -- end + -- + -- nodes[#nodes + 1] = vim.tbl_extend("force", parameterized_test_node, { + -- name = parameterized_test_node.name .. "(" .. named_params .. ")", + -- range = { args_node:range() }, + -- }) + -- end + -- + -- logger.debug("neotest-dotnet(X-Unit Utils): Built parameterized test positions: ") + -- logger.debug(nodes) + + return base_node +end + +---Modifies the tree using supplementary information from dotnet test -t or other methods +---@param tree neotest.Tree The tree to modify +---@param path string The path to the file the tree was built from +M.post_process_tree_list = function(tree, path) + local proj_root = lib.files.match_root_pattern("*.csproj")(path) + local test_list_job = DotnetUtils.get_test_full_names(proj_root) + local dotnet_tests = test_list_job.result().output + local tree_as_list = tree:to_list() + local processed_tests = {} + + local function process_test_names(node_tree) + for i, node in ipairs(node_tree) do + if + node.type == "test" + and processed_tests[node.id] == nil + and processed_tests[node.name] == nil + then + local matched_tests = {} + + logger.debug("neotest-dotnet: Processing test name: " .. node.id) + + -- Find positions first match of '::' in value.id + local _, start = string.find(node.id, "::") + local node_test_name = string.sub(node.id, (start or 0) + 1) + + -- Replace all :: with . in node_test_name + node_test_name = string.gsub(node_test_name, "::", ".") + + for _, dotnet_name in ipairs(dotnet_tests) do + if string.find(dotnet_name, node_test_name, 0, true) then + table.insert(matched_tests, dotnet_name) + end + end + + if #matched_tests > 1 then + -- This is a parameterized test (multiple matches for the same test) + local parameterized_tests = {} + local parent_node_ranges = node.range + for j, matched_name in ipairs(matched_tests) do + local sub_id = path .. "::" .. string.gsub(matched_name, "%.", "::") + local sub_node = { + id = sub_id, + is_class = false, + name = matched_name, + path = path, + range = { + parent_node_ranges[1] + j, + parent_node_ranges[2], + parent_node_ranges[1] + j, + parent_node_ranges[4], + }, + type = "test", + } + table.insert(parameterized_tests, sub_node) + table.insert(processed_tests, matched_name) + end + + table.insert(processed_tests, node.id) + table.insert(node_tree, i + 1, parameterized_tests) + elseif #matched_tests == 1 then + -- Replace the name with the fully qualified test name + node_tree[i] = vim.tbl_extend("force", node, { name = matched_tests[1] }) + end end - end - nodes[#nodes + 1] = vim.tbl_extend("force", parameterized_test_node, { - name = parameterized_test_node.name .. "(" .. named_params .. ")", - range = { args_node:range() }, - }) + process_test_names(node) + end end - logger.debug("neotest-dotnet(X-Unit Utils): Built parameterized test positions: ") - logger.debug(nodes) + process_test_names(tree_as_list) - return nodes + return Tree.from_list(tree_as_list, function(pos) + return pos.id + end) end return M diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 589a0cc..e7e6bdb 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -1,6 +1,4 @@ local lib = require("neotest.lib") -local dotnet_utils = require("neotest-dotnet.utils.dotnet-utils") -local nio = require("nio") local logger = require("neotest.logging") local result_utils = require("neotest-dotnet.utils.result-utils") local trx_utils = require("neotest-dotnet.utils.trx-utils") @@ -69,8 +67,7 @@ DotnetNeotestAdapter._position_id = function(...) return framework_base.position_id(...) end ---- Implementation of core neotest function. ----@param path any +---@param path any The path to the file to discover positions in ---@return neotest.Tree DotnetNeotestAdapter.discover_positions = function(path) local content = lib.files.read(path) @@ -106,11 +103,15 @@ DotnetNeotestAdapter.discover_positions = function(path) position_id = "require('neotest-dotnet')._position_id", }) - local proj_root = lib.files.match_root_pattern("*.csproj")(path) - local test_list_job = dotnet_utils.get_test_full_names(proj_root) - local test_list = test_list_job.result() + logger.debug("neotest-dotnet: Original Position Tree: ") + logger.debug(tree:to_list()) + + local modified_tree = test_framework.post_process_tree_list(tree, path) + + logger.debug("neotest-dotnet: Post-processed Position Tree: ") + logger.debug(modified_tree:to_list()) - return tree + return modified_tree end ---@summary Neotest core interface method: Build specs for running tests diff --git a/lua/neotest-dotnet/neotest-types.lua b/lua/neotest-dotnet/neotest-types.lua new file mode 100644 index 0000000..f04dcaa --- /dev/null +++ b/lua/neotest-dotnet/neotest-types.lua @@ -0,0 +1,8 @@ +--- Nested tree structure with nodes containing data and having any +--- number of children +---@class neotest.Tree +---@field private _data any +---@field private _children neotest.Tree[] +---@field private _nodes table +---@field private _key fun(data: any): string +---@field private _parent? neotest.Tree diff --git a/lua/neotest-dotnet/types/framework-utils.lua b/lua/neotest-dotnet/types/framework-utils.lua deleted file mode 100644 index 57daea4..0000000 --- a/lua/neotest-dotnet/types/framework-utils.lua +++ /dev/null @@ -1,3 +0,0 @@ ----@class FrameworkUtils ----@field get_treesitter_queries function the TS queries for the framework ----@field build_parameterized_test_positions function Builds a tree of parameterized test nodes From 56d3e56ba584bfe61fa21fdde165e1f5a887b05c Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Wed, 7 Feb 2024 19:17:57 +0000 Subject: [PATCH 04/17] feat(dotnet-test): Fixes nesting issue with parameterized tests --- lua/neotest-dotnet/frameworks/xunit.lua | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lua/neotest-dotnet/frameworks/xunit.lua b/lua/neotest-dotnet/frameworks/xunit.lua index f7fc1be..da953c2 100644 --- a/lua/neotest-dotnet/frameworks/xunit.lua +++ b/lua/neotest-dotnet/frameworks/xunit.lua @@ -106,14 +106,14 @@ M.post_process_tree_list = function(tree, path) local test_list_job = DotnetUtils.get_test_full_names(proj_root) local dotnet_tests = test_list_job.result().output local tree_as_list = tree:to_list() - local processed_tests = {} + -- local processed_tests = {} local function process_test_names(node_tree) for i, node in ipairs(node_tree) do if node.type == "test" - and processed_tests[node.id] == nil - and processed_tests[node.name] == nil + -- and processed_tests[node.id] == nil + -- and processed_tests[node.name] == nil then local matched_tests = {} @@ -134,10 +134,10 @@ M.post_process_tree_list = function(tree, path) if #matched_tests > 1 then -- This is a parameterized test (multiple matches for the same test) - local parameterized_tests = {} local parent_node_ranges = node.range for j, matched_name in ipairs(matched_tests) do local sub_id = path .. "::" .. string.gsub(matched_name, "%.", "::") + local sub_test = {} local sub_node = { id = sub_id, is_class = false, @@ -151,12 +151,15 @@ M.post_process_tree_list = function(tree, path) }, type = "test", } - table.insert(parameterized_tests, sub_node) - table.insert(processed_tests, matched_name) + table.insert(sub_test, sub_node) + table.insert(node_tree, sub_test) + -- table.insert(processed_tests, matched_name) end - table.insert(processed_tests, node.id) - table.insert(node_tree, i + 1, parameterized_tests) + logger.debug("testing: node_tree after parameterized tests: ") + logger.debug(node_tree) + + -- table.insert(node_tree, i + 1, parameterized_tests) elseif #matched_tests == 1 then -- Replace the name with the fully qualified test name node_tree[i] = vim.tbl_extend("force", node, { name = matched_tests[1] }) @@ -169,6 +172,8 @@ M.post_process_tree_list = function(tree, path) process_test_names(tree_as_list) + logger.debug("neotest-dotnet: Processed tree before leaving method: ") + logger.debug(tree_as_list) return Tree.from_list(tree_as_list, function(pos) return pos.id end) From ca5640a5f82e4ab2d7195ae588412e68c0eb3522 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Wed, 7 Feb 2024 20:13:13 +0000 Subject: [PATCH 05/17] feat(scope): Fixes xunit query --- lua/neotest-dotnet/frameworks/xunit.lua | 1 - lua/neotest-dotnet/tree-sitter/xunit-queries.lua | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lua/neotest-dotnet/frameworks/xunit.lua b/lua/neotest-dotnet/frameworks/xunit.lua index da953c2..4d434fa 100644 --- a/lua/neotest-dotnet/frameworks/xunit.lua +++ b/lua/neotest-dotnet/frameworks/xunit.lua @@ -153,7 +153,6 @@ M.post_process_tree_list = function(tree, path) } table.insert(sub_test, sub_node) table.insert(node_tree, sub_test) - -- table.insert(processed_tests, matched_name) end logger.debug("testing: node_tree after parameterized tests: ") diff --git a/lua/neotest-dotnet/tree-sitter/xunit-queries.lua b/lua/neotest-dotnet/tree-sitter/xunit-queries.lua index 6e0689b..065875b 100644 --- a/lua/neotest-dotnet/tree-sitter/xunit-queries.lua +++ b/lua/neotest-dotnet/tree-sitter/xunit-queries.lua @@ -46,13 +46,13 @@ function M.get_queries(custom_attributes) name: (identifier) @extra_attributes (#not-any-of? @extra_attributes "ClassData") ) )* - name: (identifier) @test.parameterized.name + name: (identifier) @test.name parameters: (parameter_list (parameter name: (identifier) )* ) @parameter_list - ) @test.parameterized.definition + ) @test.definition ]] end From 8af2c7889ade9c54b7e96552cd6ac05f0589fe9a Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Fri, 9 Feb 2024 20:14:28 +0000 Subject: [PATCH 06/17] feat(dotnet-test): Supports discovery when using custom display name --- lua/neotest-dotnet/frameworks/nunit.lua | 4 +- .../frameworks/test-framework-base.lua | 7 ++ lua/neotest-dotnet/frameworks/xunit.lua | 100 ++---------------- .../tree-sitter/xunit-queries.lua | 14 +++ 4 files changed, 34 insertions(+), 91 deletions(-) diff --git a/lua/neotest-dotnet/frameworks/nunit.lua b/lua/neotest-dotnet/frameworks/nunit.lua index a06457b..aed77e8 100644 --- a/lua/neotest-dotnet/frameworks/nunit.lua +++ b/lua/neotest-dotnet/frameworks/nunit.lua @@ -9,8 +9,8 @@ end ---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. ---@param base_node table The initial root node to build the positions from ----@param source any ----@param captured_nodes any +---@param source any The source code to build the positions from +---@param captured_nodes any The nodes captured by the TS query ---@param match_type string The type of node that was matched by the TS query ---@return table M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) diff --git a/lua/neotest-dotnet/frameworks/test-framework-base.lua b/lua/neotest-dotnet/frameworks/test-framework-base.lua index d6a0d7a..c478c1e 100644 --- a/lua/neotest-dotnet/frameworks/test-framework-base.lua +++ b/lua/neotest-dotnet/frameworks/test-framework-base.lua @@ -132,6 +132,12 @@ function TestFrameworkBase.build_position(file_path, source, captured_nodes) local match_type = TestFrameworkBase.get_match_type(captured_nodes) local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) + local display_name = nil + + if captured_nodes["display_name"] then + display_name = vim.treesitter.get_node_text(captured_nodes["display_name"], source) + end + local definition = captured_nodes[match_type .. ".definition"] -- Introduce the C# concept of a "class" to the node, so we can distinguish between a class and a namespace. @@ -146,6 +152,7 @@ function TestFrameworkBase.build_position(file_path, source, captured_nodes) local node = { type = match_type, is_class = is_class, + display_name = display_name, path = file_path, name = name, range = { definition:range() }, diff --git a/lua/neotest-dotnet/frameworks/xunit.lua b/lua/neotest-dotnet/frameworks/xunit.lua index 4d434fa..b8c82a2 100644 --- a/lua/neotest-dotnet/frameworks/xunit.lua +++ b/lua/neotest-dotnet/frameworks/xunit.lua @@ -2,6 +2,7 @@ local logger = require("neotest.logging") local lib = require("neotest.lib") local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") local types = require("neotest.types") +local node_tree_utils = require("neotest-dotnet.utils.neotest-node-tree-utils") local Tree = types.Tree ---@class FrameworkUtils @@ -10,26 +11,6 @@ local Tree = types.Tree ---@field post_process_tree_list function Modifies the tree using supplementary information from dotnet test -t or other methods local M = {} -local function parameter_string_to_table(parameter_string) - local params = {} - for param in string.gmatch(parameter_string:gsub("[()]", ""), "([^,]+)") do - -- Split string on whitespace separator and take last element (the param name) - local type_identifier_split = vim.split(param, "%s") - table.insert(params, type_identifier_split[#type_identifier_split]) - end - - return params -end - -local function argument_string_to_table(arg_string) - local args = {} - for arg in string.gmatch(arg_string:gsub("[()]", ""), "([^, ]+)") do - table.insert(args, arg) - end - - return args -end - function M.get_treesitter_queries(custom_attribute_args) return require("neotest-dotnet.tree-sitter.xunit-queries").get_queries(custom_attribute_args) end @@ -41,60 +22,6 @@ end ---@param match_type string The type of node that was matched by the TS query ---@return table M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) - -- logger.debug("neotest-dotnet(X-Unit Utils): Building parameterized test positions from source") - -- logger.debug("neotest-dotnet(X-Unit Utils): Base node: ") - -- logger.debug(base_node) - -- - -- logger.debug("neotest-dotnet(X-Unit Utils): Match Type: " .. match_type) - -- - -- local query = [[ - -- ;;query - -- (attribute_list - -- (attribute - -- name: (identifier) @attribute_name (#any-of? @attribute_name "InlineData") - -- ((attribute_argument_list) @arguments) - -- ) - -- ) - -- ]] - -- - -- local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse("c_sharp", query) - -- or vim.treesitter.parse_query("c_sharp", query) - -- - -- -- Set type to test (otherwise it will be test.parameterized) - -- local parameterized_test_node = vim.tbl_extend("force", base_node, { type = "test" }) - -- local nodes = { parameterized_test_node } - -- - -- -- Test method has parameters, so we need to create a sub-position for each test case - -- local capture_indices = {} - -- for i, capture in ipairs(param_query.captures) do - -- capture_indices[capture] = i - -- end - -- local arguments_index = capture_indices["arguments"] - -- - -- for _, match in param_query:iter_matches(captured_nodes[match_type .. ".definition"], source) do - -- local params_text = vim.treesitter.get_node_text(captured_nodes["parameter_list"], source) - -- local args_node = match[arguments_index] - -- local args_text = vim.treesitter.get_node_text(args_node, source) - -- local params_table = parameter_string_to_table(params_text) - -- local args_table = argument_string_to_table(args_text) - -- - -- local named_params = "" - -- for i, param in ipairs(params_table) do - -- named_params = named_params .. param .. ": " .. args_table[i] - -- if i ~= #params_table then - -- named_params = named_params .. ", " - -- end - -- end - -- - -- nodes[#nodes + 1] = vim.tbl_extend("force", parameterized_test_node, { - -- name = parameterized_test_node.name .. "(" .. named_params .. ")", - -- range = { args_node:range() }, - -- }) - -- end - -- - -- logger.debug("neotest-dotnet(X-Unit Utils): Built parameterized test positions: ") - -- logger.debug(nodes) - return base_node end @@ -106,25 +33,21 @@ M.post_process_tree_list = function(tree, path) local test_list_job = DotnetUtils.get_test_full_names(proj_root) local dotnet_tests = test_list_job.result().output local tree_as_list = tree:to_list() - -- local processed_tests = {} local function process_test_names(node_tree) for i, node in ipairs(node_tree) do - if - node.type == "test" - -- and processed_tests[node.id] == nil - -- and processed_tests[node.name] == nil - then + if node.type == "test" then local matched_tests = {} + local node_test_name = node.name - logger.debug("neotest-dotnet: Processing test name: " .. node.id) - - -- Find positions first match of '::' in value.id - local _, start = string.find(node.id, "::") - local node_test_name = string.sub(node.id, (start or 0) + 1) + -- If node.display_name is not nil, use it to match the test name + if node.display_name ~= nil then + node_test_name = node.display_name + else + node_test_name = node_tree_utils.get_qualified_test_name_from_id(node.id) + end - -- Replace all :: with . in node_test_name - node_test_name = string.gsub(node_test_name, "::", ".") + logger.debug("neotest-dotnet: Processing test name: " .. node_test_name) for _, dotnet_name in ipairs(dotnet_tests) do if string.find(dotnet_name, node_test_name, 0, true) then @@ -157,8 +80,6 @@ M.post_process_tree_list = function(tree, path) logger.debug("testing: node_tree after parameterized tests: ") logger.debug(node_tree) - - -- table.insert(node_tree, i + 1, parameterized_tests) elseif #matched_tests == 1 then -- Replace the name with the fully qualified test name node_tree[i] = vim.tbl_extend("force", node, { name = matched_tests[1] }) @@ -173,6 +94,7 @@ M.post_process_tree_list = function(tree, path) logger.debug("neotest-dotnet: Processed tree before leaving method: ") logger.debug(tree_as_list) + return Tree.from_list(tree_as_list, function(pos) return pos.id end) diff --git a/lua/neotest-dotnet/tree-sitter/xunit-queries.lua b/lua/neotest-dotnet/tree-sitter/xunit-queries.lua index 065875b..33a4ac5 100644 --- a/lua/neotest-dotnet/tree-sitter/xunit-queries.lua +++ b/lua/neotest-dotnet/tree-sitter/xunit-queries.lua @@ -19,6 +19,13 @@ function M.get_queries(custom_attributes) (attribute_list (attribute name: (identifier) @attribute_name (#any-of? @attribute_name "Fact" "ClassData" ]] .. custom_fact_attributes .. [[) + (attribute_argument_list + (attribute_argument + (string_literal + (string_literal_fragment) @display_name + ) + ) + )* ) )+ name: (identifier) @test.name @@ -39,6 +46,13 @@ function M.get_queries(custom_attributes) (attribute_list (attribute name: (identifier) @attribute_name (#any-of? @attribute_name "Theory") + (attribute_argument_list + (attribute_argument + (string_literal + (string_literal_fragment) @display_name + ) + ) + )* ) ) (attribute_list From 4ebc336c19646791b75c2ae1a30f2b8e403b9d63 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Sun, 11 Feb 2024 09:18:45 +0000 Subject: [PATCH 07/17] feat(scope): Fixes parent name of parameterized tests --- .../frameworks/test-framework-base.lua | 28 +++++++++++++++++++ lua/neotest-dotnet/frameworks/xunit.lua | 15 ++++++++-- lua/neotest-dotnet/init.lua | 14 +--------- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/lua/neotest-dotnet/frameworks/test-framework-base.lua b/lua/neotest-dotnet/frameworks/test-framework-base.lua index c478c1e..f65caa4 100644 --- a/lua/neotest-dotnet/frameworks/test-framework-base.lua +++ b/lua/neotest-dotnet/frameworks/test-framework-base.lua @@ -166,4 +166,32 @@ function TestFrameworkBase.build_position(file_path, source, captured_nodes) .build_parameterized_test_positions(node, source, captured_nodes, match_type) end +--- Assuming a position_id of the form "C:\path\to\file.cs::namespace::class::method", +--- with the rule that the first :: is the separator between the file path and the rest of the position_id, +--- returns the '.' separated fully qualified name of the test, with each segment corresponding to the namespace, class, and method. +---@param position_id string The position_id of the neotest test node +---@return string The fully qualified name of the test +function TestFrameworkBase.get_qualified_test_name_from_id(position_id) + local _, first_colon_end = string.find(position_id, ".cs::") + local full_name = string.sub(position_id, first_colon_end + 1) + full_name = string.gsub(full_name, "::", ".") + return full_name +end + +function TestFrameworkBase.get_test_nodes_data(tree) + local test_nodes = {} + for _, node in tree:iter_nodes() do + if node:data().type == "test" then + table.insert(test_nodes, node) + end + end + + -- Add an additional full_name property to the test nodes + for _, node in ipairs(test_nodes) do + local full_name = M.get_qualified_test_name_from_id(node:data().id) + node:data().full_name = full_name + end + + return test_nodes +end return TestFrameworkBase diff --git a/lua/neotest-dotnet/frameworks/xunit.lua b/lua/neotest-dotnet/frameworks/xunit.lua index b8c82a2..60ed421 100644 --- a/lua/neotest-dotnet/frameworks/xunit.lua +++ b/lua/neotest-dotnet/frameworks/xunit.lua @@ -50,7 +50,8 @@ M.post_process_tree_list = function(tree, path) logger.debug("neotest-dotnet: Processing test name: " .. node_test_name) for _, dotnet_name in ipairs(dotnet_tests) do - if string.find(dotnet_name, node_test_name, 0, true) then + -- First remove parameters from test name so we just match the "base" test name + if string.find(dotnet_name:gsub("%b()", ""), node_test_name, 0, true) then table.insert(matched_tests, dotnet_name) end end @@ -73,16 +74,24 @@ M.post_process_tree_list = function(tree, path) parent_node_ranges[4], }, type = "test", + framework = "xunit", } table.insert(sub_test, sub_node) table.insert(node_tree, sub_test) end + node_tree[1] = vim.tbl_extend( + "force", + node, + { name = matched_tests[1]:gsub("%b()", ""), framework = "xunit" } + ) + logger.debug("testing: node_tree after parameterized tests: ") logger.debug(node_tree) elseif #matched_tests == 1 then - -- Replace the name with the fully qualified test name - node_tree[i] = vim.tbl_extend("force", node, { name = matched_tests[1] }) + logger.debug("testing: matched one test with name: " .. matched_tests[1]) + node_tree[1] = + vim.tbl_extend("force", node, { name = matched_tests[1], framework = "xunit" }) end end diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index e7e6bdb..f0c5882 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -128,7 +128,7 @@ DotnetNeotestAdapter.build_spec = function(args) logger.debug("neotest-dotnet: Created " .. #specs .. " specs, with contents: ") logger.debug(specs) - if args.is_custom_dotnet_debug or args.strategy == "dap" then + if args.strategy == "dap" then if #specs > 1 then logger.warn( "neotest-dotnet: DAP strategy does not support multiple test projects. Please debug test projects or individual tests. Falling back to using default strategy." @@ -136,18 +136,6 @@ DotnetNeotestAdapter.build_spec = function(args) args.strategy = "integrated" return specs else - -- Change the strategy to custom netcoredbg strategy and pass in the additional dap args from the user - if args.is_custom_dotnet_debug then - vim.notify( - [[The custom command -'run({strategy = require('neotest-dotnet.strategies.netcoredbg'), is_custom_dotnet_debug true})' -is no longer required. -You can now simply use run({strategy = 'dap'}) to debug your tests. -This custom command will be deprecated in future versions of this adapter.]], - vim.log.levels.WARN, - { title = "neotest-dotnet" } - ) - end specs[1].dap_args = dap_args specs[1].strategy = require("neotest-dotnet.strategies.netcoredbg") end From 2e4ab032e9d9f81bc0003d5a09a06f97c87b7e42 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Sun, 11 Feb 2024 13:19:20 +0000 Subject: [PATCH 08/17] BREAKING CHANGE: Beginning of overhaul of adapter structure - Not yet functional, but moves a lot of functionality down into specific test framework services. --- lua/neotest-dotnet/framework-discovery.lua | 147 +++++++++ lua/neotest-dotnet/frameworks/nunit.lua | 70 ---- .../frameworks/test-framework-base.lua | 153 +++++---- lua/neotest-dotnet/init.lua | 61 +--- .../mstest.lua => mstest/init.lua} | 0 .../ts-queries.lua} | 0 lua/neotest-dotnet/neotest-types.lua | 8 - .../{frameworks/xunit.lua => nunit/init.lua} | 78 ++++- .../ts-queries.lua} | 0 .../types/neotest-dotnet-types.lua | 12 + lua/neotest-dotnet/types/neotest-types.lua | 78 +++++ lua/neotest-dotnet/utils/build-spec-utils.lua | 2 +- .../utils/neotest-node-tree-utils.lua | 8 +- lua/neotest-dotnet/utils/result-utils.lua | 142 -------- lua/neotest-dotnet/xunit/init.lua | 311 ++++++++++++++++++ .../ts-queries.lua} | 0 16 files changed, 706 insertions(+), 364 deletions(-) create mode 100644 lua/neotest-dotnet/framework-discovery.lua delete mode 100644 lua/neotest-dotnet/frameworks/nunit.lua rename lua/neotest-dotnet/{frameworks/mstest.lua => mstest/init.lua} (100%) rename lua/neotest-dotnet/{tree-sitter/mstest-queries.lua => mstest/ts-queries.lua} (100%) delete mode 100644 lua/neotest-dotnet/neotest-types.lua rename lua/neotest-dotnet/{frameworks/xunit.lua => nunit/init.lua} (63%) rename lua/neotest-dotnet/{tree-sitter/nunit-queries.lua => nunit/ts-queries.lua} (100%) create mode 100644 lua/neotest-dotnet/types/neotest-dotnet-types.lua create mode 100644 lua/neotest-dotnet/types/neotest-types.lua delete mode 100644 lua/neotest-dotnet/utils/result-utils.lua create mode 100644 lua/neotest-dotnet/xunit/init.lua rename lua/neotest-dotnet/{tree-sitter/xunit-queries.lua => xunit/ts-queries.lua} (100%) diff --git a/lua/neotest-dotnet/framework-discovery.lua b/lua/neotest-dotnet/framework-discovery.lua new file mode 100644 index 0000000..1ecf8a8 --- /dev/null +++ b/lua/neotest-dotnet/framework-discovery.lua @@ -0,0 +1,147 @@ +local xunit = require("neotest-dotnet.xunit") +local nunit = require("neotest-dotnet.nunit") +local mstest = require("neotest-dotnet.mstest") + +local async = require("neotest.async") + +local M = {} + +M.xunit_test_attributes = { + "Fact", + "Theory", +} + +M.nunit_test_attributes = { + "Test", + "TestCase", + "TestCaseSource", +} + +M.mstest_test_attributes = { + "TestMethod", + "DataTestMethod", +} + +M.specflow_test_attributes = { + "SkippableFactAttribute", + "TestMethodAttribute", + "TestAttribute", + "NUnit.Framework.TestAttribute", +} + +M.all_test_attributes = vim.tbl_flatten({ + M.xunit_test_attributes, + M.nunit_test_attributes, + M.mstest_test_attributes, + M.specflow_test_attributes, +}) + +--- Gets a list of the standard and customized test attributes for xUnit, for use in a tree-sitter predicates +---@param custom_attribute_args table The user configured mapping of the custom test attributes +---@param framework string The name of the test framework +---@return +function M.attribute_match_list(custom_attribute_args, framework) + local attribute_match_list = {} + if framework == "xunit" then + attribute_match_list = M.xunit_test_attributes + end + if framework == "mstest" then + attribute_match_list = M.mstest_test_attributes + end + if framework == "nunit" then + attribute_match_list = M.nunit_test_attributes + end + + if custom_attribute_args and custom_attribute_args[framework] then + attribute_match_list = + vim.tbl_flatten({ attribute_match_list, custom_attribute_args[framework] }) + end + + return M.join_test_attributes(attribute_match_list) +end + +function M.join_test_attributes(attributes) + local joined_attributes = attributes + and table.concat( + vim.tbl_map(function(attribute) + return '"' .. attribute .. '"' + end, attributes), + " " + ) + or "" + return joined_attributes +end + +function M.get_test_framework_utils_from_source(source, custom_attribute_args) + local xunit_attributes = M.attribute_match_list(custom_attribute_args, "xunit") + local mstest_attributes = M.attribute_match_list(custom_attribute_args, "mstest") + local nunit_attributes = M.attribute_match_list(custom_attribute_args, "nunit") + + local framework_query = [[ + (attribute + name: (identifier) @attribute_name (#any-of? @attribute_name ]] .. xunit_attributes .. " " .. nunit_attributes .. " " .. mstest_attributes .. [[) + ) + + (attribute + name: (qualified_name) @attribute_name (#match? @attribute_name "SkippableFactAttribute$") + ) + + (attribute + name: (qualified_name) @attribute_name (#match? @attribute_name "TestMethodAttribute$") + ) + + (attribute + name: (qualified_name) @attribute_name (#match? @attribute_name "TestAttribute$") + ) + ]] + + async.scheduler() + local root = vim.treesitter.get_string_parser(source, "c_sharp"):parse()[1]:root() + local parsed_query = vim.fn.has("nvim-0.9.0") == 1 + and vim.treesitter.query.parse("c_sharp", framework_query) + or vim.treesitter.parse_query("c_sharp", framework_query) + for _, captures in parsed_query:iter_matches(root, source) do + local test_attribute = vim.fn.has("nvim-0.9.0") == 1 + and vim.treesitter.get_node_text(captures[1], source) + or vim.treesitter.query.get_node_text(captures[1], source) + if test_attribute then + if + string.find(xunit_attributes, test_attribute) + or string.find(test_attribute, "SkippableFactAttribute") + then + return xunit + elseif + string.find(nunit_attributes, test_attribute) + or string.find(test_attribute, "TestAttribute") + then + return nunit + elseif + string.find(mstest_attributes, test_attribute) + or string.find(test_attribute, "TestMethodAttribute") + then + return mstest + else + -- Default fallback + return xunit + end + end + end +end + +function M.get_test_framework_utils_from_tree(tree) + for _, node in tree:iter_nodes() do + local framework = node:data().framework + if framework == "xunit" then + return xunit + elseif framework == "nunit" then + return nunit + elseif framework == "mstest" then + return mstest + end + end + + -- Default fallback (no test nodes anyway) + return xunit +end + +return M diff --git a/lua/neotest-dotnet/frameworks/nunit.lua b/lua/neotest-dotnet/frameworks/nunit.lua deleted file mode 100644 index aed77e8..0000000 --- a/lua/neotest-dotnet/frameworks/nunit.lua +++ /dev/null @@ -1,70 +0,0 @@ -local logger = require("neotest.logging") - ----@type FrameworkUtils -local M = {} - -function M.get_treesitter_queries(custom_attribute_args) - return require("neotest-dotnet.tree-sitter.nunit-queries").get_queries(custom_attribute_args) -end - ----Builds a position from captured nodes, optionally parsing parameters to create sub-positions. ----@param base_node table The initial root node to build the positions from ----@param source any The source code to build the positions from ----@param captured_nodes any The nodes captured by the TS query ----@param match_type string The type of node that was matched by the TS query ----@return table -M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) - logger.debug("neotest-dotnet(NUnit Utils): Building parameterized test positions from source") - logger.debug("neotest-dotnet(NUnit Utils): Base node: ") - logger.debug(base_node) - - logger.debug("neotest-dotnet(NUnit Utils): Match Type: " .. match_type) - - local query = [[ - ;;query - (attribute_list - (attribute - name: (identifier) @attribute_name (#any-of? @attribute_name "TestCase") - ((attribute_argument_list) @arguments) - ) - ) - ]] - - local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse("c_sharp", query) - or vim.treesitter.parse_query("c_sharp", query) - - -- Set type to test (otherwise it will be test.parameterized) - local parameterized_test_node = vim.tbl_extend("force", base_node, { type = "test" }) - local nodes = { parameterized_test_node } - - -- Test method has parameters, so we need to create a sub-position for each test case - local capture_indices = {} - for i, capture in ipairs(param_query.captures) do - capture_indices[capture] = i - end - local arguments_index = capture_indices["arguments"] - - for _, match in param_query:iter_matches(captured_nodes[match_type .. ".definition"], source) do - local args_node = match[arguments_index] - local args_text = vim.treesitter.get_node_text(args_node, source):gsub("[()]", "") - - nodes[#nodes + 1] = vim.tbl_extend("force", parameterized_test_node, { - name = parameterized_test_node.name .. "(" .. args_text .. ")", - range = { args_node:range() }, - }) - end - - logger.debug("neotest-dotnet(NUnit Utils): Built parameterized test positions: ") - logger.debug(nodes) - - return nodes -end - ----Modifies the tree using supplementary information from dotnet test -t or other methods ----@param tree neotest.Tree The tree to modify ----@param path string The path to the file the tree was built from -M.post_process_tree_list = function(tree, path) - return tree -end - -return M diff --git a/lua/neotest-dotnet/frameworks/test-framework-base.lua b/lua/neotest-dotnet/frameworks/test-framework-base.lua index f65caa4..292f896 100644 --- a/lua/neotest-dotnet/frameworks/test-framework-base.lua +++ b/lua/neotest-dotnet/frameworks/test-framework-base.lua @@ -6,64 +6,6 @@ local async = require("neotest.async") local TestFrameworkBase = {} ---- Returns the utils module for the test framework being used, given the current file ----@return FrameworkUtils -function TestFrameworkBase.get_test_framework_utils(source, custom_attribute_args) - local xunit_attributes = attributes.attribute_match_list(custom_attribute_args, "xunit") - local mstest_attributes = attributes.attribute_match_list(custom_attribute_args, "mstest") - local nunit_attributes = attributes.attribute_match_list(custom_attribute_args, "nunit") - - local framework_query = [[ - (attribute - name: (identifier) @attribute_name (#any-of? @attribute_name ]] .. xunit_attributes .. " " .. nunit_attributes .. " " .. mstest_attributes .. [[) - ) - - (attribute - name: (qualified_name) @attribute_name (#match? @attribute_name "SkippableFactAttribute$") - ) - - (attribute - name: (qualified_name) @attribute_name (#match? @attribute_name "TestMethodAttribute$") - ) - - (attribute - name: (qualified_name) @attribute_name (#match? @attribute_name "TestAttribute$") - ) - ]] - - async.scheduler() - local root = vim.treesitter.get_string_parser(source, "c_sharp"):parse()[1]:root() - local parsed_query = vim.fn.has("nvim-0.9.0") == 1 - and vim.treesitter.query.parse("c_sharp", framework_query) - or vim.treesitter.parse_query("c_sharp", framework_query) - for _, captures in parsed_query:iter_matches(root, source) do - local test_attribute = vim.fn.has("nvim-0.9.0") == 1 - and vim.treesitter.get_node_text(captures[1], source) - or vim.treesitter.query.get_node_text(captures[1], source) - if test_attribute then - if - string.find(xunit_attributes, test_attribute) - or string.find(test_attribute, "SkippableFactAttribute") - then - return xunit - elseif - string.find(nunit_attributes, test_attribute) - or string.find(test_attribute, "TestAttribute") - then - return nunit - elseif - string.find(mstest_attributes, test_attribute) - or string.find(test_attribute, "TestMethodAttribute") - then - return mstest - else - -- Default fallback - return xunit - end - end - end -end - function TestFrameworkBase.get_match_type(captured_nodes) if captured_nodes["test.name"] then return "test" @@ -166,32 +108,85 @@ function TestFrameworkBase.build_position(file_path, source, captured_nodes) .build_parameterized_test_positions(node, source, captured_nodes, match_type) end ---- Assuming a position_id of the form "C:\path\to\file.cs::namespace::class::method", ---- with the rule that the first :: is the separator between the file path and the rest of the position_id, ---- returns the '.' separated fully qualified name of the test, with each segment corresponding to the namespace, class, and method. ----@param position_id string The position_id of the neotest test node ----@return string The fully qualified name of the test -function TestFrameworkBase.get_qualified_test_name_from_id(position_id) - local _, first_colon_end = string.find(position_id, ".cs::") - local full_name = string.sub(position_id, first_colon_end + 1) - full_name = string.gsub(full_name, "::", ".") - return full_name -end +---Creates a table of intermediate results from the parsed xml result data +---@param test_results table +---@return DotnetResult[] +function TestFrameworkBase.create_intermediate_results(test_results) + ---@type DotnetResult[] + local intermediate_results = {} + + local outcome_mapper = { + Passed = "passed", + Failed = "failed", + Skipped = "skipped", + NotExecuted = "skipped", + } -function TestFrameworkBase.get_test_nodes_data(tree) - local test_nodes = {} - for _, node in tree:iter_nodes() do - if node:data().type == "test" then - table.insert(test_nodes, node) + for _, value in pairs(test_results) do + if value._attr.testName ~= nil then + local error_info + local outcome = outcome_mapper[value._attr.outcome] + local has_errors = value.Output and value.Output.ErrorInfo or nil + + if has_errors and outcome == "failed" then + local stackTrace = value.Output.ErrorInfo.StackTrace or "" + error_info = value.Output.ErrorInfo.Message .. "\n" .. stackTrace + end + local intermediate_result = { + status = string.lower(outcome), + raw_output = value.Output and value.Output.StdOut or outcome, + test_name = value._attr.testName, + error_info = error_info, + } + table.insert(intermediate_results, intermediate_result) end end - -- Add an additional full_name property to the test nodes - for _, node in ipairs(test_nodes) do - local full_name = M.get_qualified_test_name_from_id(node:data().id) - node:data().full_name = full_name + return intermediate_results +end + +---Converts and adds the results of the test_results list to the neotest_results table. +---@param intermediate_results DotnetResult[] The marshalled dotnet console outputs +---@param test_nodes neotest.Tree +---@return neotest.Result[] +function TestFrameworkBase.convert_intermediate_results(intermediate_results, test_nodes) + local neotest_results = {} + + for _, intermediate_result in ipairs(intermediate_results) do + for _, node in ipairs(test_nodes) do + local node_data = node:data() + + if intermediate_result.test_name == node_data.full_name then + -- For non-inlined parameterized tests, check if we already have an entry for the test. + -- If so, we need to check for a failure, and ensure the entire group of tests is marked as failed. + neotest_results[node_data.id] = neotest_results[node_data.id] + or { + status = intermediate_result.status, + short = node_data.full_name .. ":" .. intermediate_result.status, + errors = {}, + } + + if intermediate_result.status == "failed" then + -- Mark as failed for the whole thing + neotest_results[node_data.id].status = "failed" + neotest_results[node_data.id].short = node_data.full_name .. ":failed" + end + + if intermediate_result.error_info then + table.insert(neotest_results[node_data.id].errors, { + message = intermediate_result.test_name .. ": " .. intermediate_result.error_info, + }) + + -- Mark as failed + neotest_results[node_data.id].status = "failed" + end + + break + end + end end - return test_nodes + return neotest_results end + return TestFrameworkBase diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index f0c5882..d63eb0a 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -1,11 +1,8 @@ local lib = require("neotest.lib") local logger = require("neotest.logging") -local result_utils = require("neotest-dotnet.utils.result-utils") local trx_utils = require("neotest-dotnet.utils.trx-utils") -local framework_base = require("neotest-dotnet.frameworks.test-framework-base") -local attribute = require("neotest-dotnet.frameworks.test-attributes") +local FrameworkDiscovery = require("neotest-dotnet.framework-discovery") local build_spec_utils = require("neotest-dotnet.utils.build-spec-utils") -local neotest_node_tree_utils = require("neotest-dotnet.utils.neotest-node-tree-utils") local DotnetNeotestAdapter = { name = "neotest-dotnet" } local dap_args @@ -29,7 +26,7 @@ DotnetNeotestAdapter.is_test_file = function(file_path) local found_standard_test_attribute -- Combine all attribute list arrays into one - local all_attributes = attribute.all_test_attributes + local all_attributes = FrameworkDiscovery.all_test_attributes for _, test_attribute in ipairs(all_attributes) do if string.find(content, "%[" .. test_attribute) then @@ -60,18 +57,24 @@ DotnetNeotestAdapter.filter_dir = function(name) end DotnetNeotestAdapter._build_position = function(...) - return framework_base.build_position(...) + local args = { ... } + local framework = + FrameworkDiscovery.get_test_framework_utils_from_source(args[2], custom_attribute_args) -- args[2] is the content of the file + return framework.build_position(...) end DotnetNeotestAdapter._position_id = function(...) - return framework_base.position_id(...) + local args = { ... } + local framework = args[1].framework and require("neotest-dotnet." .. args[1].framework) + or require("neotest-dotnet.xunit") + return framework.position_id(...) end ---@param path any The path to the file to discover positions in ---@return neotest.Tree DotnetNeotestAdapter.discover_positions = function(path) local content = lib.files.read(path) - local test_framework = framework_base.get_test_framework_utils(content, custom_attribute_args) + local test_framework = FrameworkDiscovery.get_test_framework_utils(content, custom_attribute_args) local framework_queries = test_framework.get_treesitter_queries(custom_attribute_args) local query = [[ @@ -155,38 +158,10 @@ DotnetNeotestAdapter.results = function(spec, _, tree) logger.debug("neotest-dotnet: Fetching results from neotest tree (as list): ") logger.debug(tree:to_list()) - local test_nodes = neotest_node_tree_utils.get_test_nodes_data(tree) - - logger.debug("neotest-dotnet: Test Nodes: ") - logger.debug(test_nodes) + local test_framework = FrameworkDiscovery.get_test_framework_utils_from_tree(tree) local parsed_data = trx_utils.parse_trx(output_file) local test_results = parsed_data.TestRun and parsed_data.TestRun.Results - local test_definitions = parsed_data.TestRun and parsed_data.TestRun.TestDefinitions - - logger.debug("neotest-dotnet: TRX Results Output: ") - logger.debug(test_results) - - logger.debug("neotest-dotnet: TRX Test Definitions Output: ") - logger.debug(test_definitions) - - local intermediate_results - - if test_results and test_definitions then - if #test_results.UnitTestResult > 1 then - test_results = test_results.UnitTestResult - end - if #test_definitions.UnitTest > 1 then - test_definitions = test_definitions.UnitTest - end - - intermediate_results = result_utils.create_intermediate_results(test_results, test_definitions) - end - - -- No test results. Something went wrong. Check for runtime error - if not intermediate_results then - return result_utils.get_runtime_error(spec.context.id) - end logger.info( "neotest-dotnet: Found " @@ -195,16 +170,12 @@ DotnetNeotestAdapter.results = function(spec, _, tree) .. output_file ) - logger.debug("neotest-dotnet: Intermediate Results: ") - logger.debug(intermediate_results) - - local neotest_results = - result_utils.convert_intermediate_results(intermediate_results, test_nodes) + logger.debug("neotest-dotnet: TRX Results Output for" .. output_file .. ": ") + logger.debug(test_results) - logger.debug("neotest-dotnet: Neotest Results after conversion of Intermediate Results: ") - logger.debug(neotest_results) + local results = test_framework.generate_test_results(test_results, tree, spec.context.id) - return neotest_results + return results end setmetatable(DotnetNeotestAdapter, { diff --git a/lua/neotest-dotnet/frameworks/mstest.lua b/lua/neotest-dotnet/mstest/init.lua similarity index 100% rename from lua/neotest-dotnet/frameworks/mstest.lua rename to lua/neotest-dotnet/mstest/init.lua diff --git a/lua/neotest-dotnet/tree-sitter/mstest-queries.lua b/lua/neotest-dotnet/mstest/ts-queries.lua similarity index 100% rename from lua/neotest-dotnet/tree-sitter/mstest-queries.lua rename to lua/neotest-dotnet/mstest/ts-queries.lua diff --git a/lua/neotest-dotnet/neotest-types.lua b/lua/neotest-dotnet/neotest-types.lua deleted file mode 100644 index f04dcaa..0000000 --- a/lua/neotest-dotnet/neotest-types.lua +++ /dev/null @@ -1,8 +0,0 @@ ---- Nested tree structure with nodes containing data and having any ---- number of children ----@class neotest.Tree ----@field private _data any ----@field private _children neotest.Tree[] ----@field private _nodes table ----@field private _key fun(data: any): string ----@field private _parent? neotest.Tree diff --git a/lua/neotest-dotnet/frameworks/xunit.lua b/lua/neotest-dotnet/nunit/init.lua similarity index 63% rename from lua/neotest-dotnet/frameworks/xunit.lua rename to lua/neotest-dotnet/nunit/init.lua index 60ed421..fc23e66 100644 --- a/lua/neotest-dotnet/frameworks/xunit.lua +++ b/lua/neotest-dotnet/nunit/init.lua @@ -1,18 +1,14 @@ local logger = require("neotest.logging") -local lib = require("neotest.lib") -local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") local types = require("neotest.types") local node_tree_utils = require("neotest-dotnet.utils.neotest-node-tree-utils") local Tree = types.Tree +local lib = require("neotest.lib") +local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") ----@class FrameworkUtils ----@field get_treesitter_queries function the TS queries for the framework ----@field build_parameterized_test_positions function Builds a tree of parameterized test nodes ----@field post_process_tree_list function Modifies the tree using supplementary information from dotnet test -t or other methods local M = {} function M.get_treesitter_queries(custom_attribute_args) - return require("neotest-dotnet.tree-sitter.xunit-queries").get_queries(custom_attribute_args) + return require("neotest-dotnet.tree-sitter.nunit-queries").get_queries(custom_attribute_args) end ---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. @@ -22,7 +18,50 @@ end ---@param match_type string The type of node that was matched by the TS query ---@return table M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) - return base_node + logger.debug("neotest-dotnet(NUnit Utils): Building parameterized test positions from source") + logger.debug("neotest-dotnet(NUnit Utils): Base node: ") + logger.debug(base_node) + + logger.debug("neotest-dotnet(NUnit Utils): Match Type: " .. match_type) + + local query = [[ + ;;query + (attribute_list + (attribute + name: (identifier) @attribute_name (#any-of? @attribute_name "TestCase") + ((attribute_argument_list) @arguments) + ) + ) + ]] + + local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse("c_sharp", query) + or vim.treesitter.parse_query("c_sharp", query) + + -- Set type to test (otherwise it will be test.parameterized) + local parameterized_test_node = vim.tbl_extend("force", base_node, { type = "test" }) + local nodes = { parameterized_test_node } + + -- Test method has parameters, so we need to create a sub-position for each test case + local capture_indices = {} + for i, capture in ipairs(param_query.captures) do + capture_indices[capture] = i + end + local arguments_index = capture_indices["arguments"] + + for _, match in param_query:iter_matches(captured_nodes[match_type .. ".definition"], source) do + local args_node = match[arguments_index] + local args_text = vim.treesitter.get_node_text(args_node, source):gsub("[()]", "") + + nodes[#nodes + 1] = vim.tbl_extend("force", parameterized_test_node, { + name = parameterized_test_node.name .. "(" .. args_text .. ")", + range = { args_node:range() }, + }) + end + + logger.debug("neotest-dotnet(NUnit Utils): Built parameterized test positions: ") + logger.debug(nodes) + + return nodes end ---Modifies the tree using supplementary information from dotnet test -t or other methods @@ -35,10 +74,11 @@ M.post_process_tree_list = function(tree, path) local tree_as_list = tree:to_list() local function process_test_names(node_tree) - for i, node in ipairs(node_tree) do + for _, node in ipairs(node_tree) do if node.type == "test" then local matched_tests = {} local node_test_name = node.name + local running_id = node.id -- If node.display_name is not nil, use it to match the test name if node.display_name ~= nil then @@ -74,24 +114,28 @@ M.post_process_tree_list = function(tree, path) parent_node_ranges[4], }, type = "test", - framework = "xunit", + framework = "nunit", + running_id = running_id, } table.insert(sub_test, sub_node) table.insert(node_tree, sub_test) end - node_tree[1] = vim.tbl_extend( - "force", - node, - { name = matched_tests[1]:gsub("%b()", ""), framework = "xunit" } - ) + node_tree[1] = vim.tbl_extend("force", node, { + name = matched_tests[1]:gsub("%b()", ""), + framework = "unit", + running_id = running_id, + }) logger.debug("testing: node_tree after parameterized tests: ") logger.debug(node_tree) elseif #matched_tests == 1 then logger.debug("testing: matched one test with name: " .. matched_tests[1]) - node_tree[1] = - vim.tbl_extend("force", node, { name = matched_tests[1], framework = "xunit" }) + node_tree[1] = vim.tbl_extend( + "force", + node, + { name = matched_tests[1], framework = "nunit", running_id = running_id } + ) end end diff --git a/lua/neotest-dotnet/tree-sitter/nunit-queries.lua b/lua/neotest-dotnet/nunit/ts-queries.lua similarity index 100% rename from lua/neotest-dotnet/tree-sitter/nunit-queries.lua rename to lua/neotest-dotnet/nunit/ts-queries.lua diff --git a/lua/neotest-dotnet/types/neotest-dotnet-types.lua b/lua/neotest-dotnet/types/neotest-dotnet-types.lua new file mode 100644 index 0000000..98ac72e --- /dev/null +++ b/lua/neotest-dotnet/types/neotest-dotnet-types.lua @@ -0,0 +1,12 @@ +---@meta + +---@class DotnetResult[] +---@field status string +---@field raw_output string +---@field test_name string +---@field error_info string + +---@class FrameworkUtils +---@field get_treesitter_queries fun(custom_attribute_args: any): string Gets the TS queries for the framework +---@field build_position fun(file_path: string, source: any, captured_nodes: any): any Builds a position from captured nodes +---@field position_id fun(position: any, parents: any): string Creates the id for a position based on the position node and parents diff --git a/lua/neotest-dotnet/types/neotest-types.lua b/lua/neotest-dotnet/types/neotest-types.lua new file mode 100644 index 0000000..1382860 --- /dev/null +++ b/lua/neotest-dotnet/types/neotest-types.lua @@ -0,0 +1,78 @@ +local M = {} + +---@enum neotest.PositionType +M.PositionType = { + dir = "dir", + file = "file", + namespace = "namespace", + test = "test", +} + +---@class neotest.Position +---@field id string +---@field type neotest.PositionType +---@field name string +---@field path string +---@field range integer[] + +---@enum neotest.ResultStatus +M.ResultStatus = { + passed = "passed", + failed = "failed", + skipped = "skipped", +} + +---@class neotest.Result +---@field status neotest.ResultStatus +---@field output? string Path to file containing full output data +---@field short? string Shortened output string +---@field errors? neotest.Error[] + +---@class neotest.Error +---@field message string +---@field line? integer + +---@class neotest.Process +---@field output async fun(): string Path to file containing output data +---@field is_complete fun() boolean Is process complete +---@field result async fun() integer Get result code of process (async) +---@field attach async fun() Attach to the running process for user input +---@field stop async fun() Stop the running process +---@field output_stream async fun(): async fun(): string Async iterator of process output + +---@class neotest.StrategyContext +---@field position neotest.Position +---@field adapter neotest.Adapter + +---@alias neotest.Strategy async fun(spec: neotest.RunSpec, context: neotest.StrategyContext): neotest.Process + +---@class neotest.StrategyResult +---@field code integer +---@field output string + +---@class neotest.RunArgs +---@field tree neotest.Tree +---@field extra_args? string[] +---@field strategy string + +---@class neotest.RunSpec +---@field command string[] +---@field env? table +---@field cwd? string +---@field context? table Arbitrary data to preserve state between running and result collection +---@field strategy? table|neotest.Strategy Arguments for strategy or override for chosen strategy +---@field stream fun(output_stream: fun(): string[]): fun(): table + +---@class neotest.Tree +---@field private _data any +---@field private _children neotest.Tree[] +---@field private _nodes table +---@field private _key fun(data: any): string +---@field private _parent? neotest.Tree +---@field from_list fun(data: any[], key: fun(data: any): string): neotest.Tree +---@field to_list fun(): any[] + +---@class neotest.Adapter +---@field name string + +return M diff --git a/lua/neotest-dotnet/utils/build-spec-utils.lua b/lua/neotest-dotnet/utils/build-spec-utils.lua index 6c7949a..e71ce34 100644 --- a/lua/neotest-dotnet/utils/build-spec-utils.lua +++ b/lua/neotest-dotnet/utils/build-spec-utils.lua @@ -93,7 +93,7 @@ function BuildSpecUtils.create_specs(tree, specs, dotnet_additional_args) -- Allow a more lenient 'contains' match for the filter, accepting tradeoff that it may -- also run tests with similar names. This allows us to run parameterized tests individually -- or as a group. - local fqn = BuildSpecUtils.build_test_fqn(position.id) + local fqn = BuildSpecUtils.build_test_fqn(position.running_id or position.id) local filter = '--filter FullyQualifiedName~"' .. fqn .. '"' local proj_root = lib.files.match_root_pattern("*.csproj")(position.path) diff --git a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua index b38985e..d8a1e3e 100644 --- a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua +++ b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua @@ -22,8 +22,12 @@ function M.get_test_nodes_data(tree) -- Add an additional full_name property to the test nodes for _, node in ipairs(test_nodes) do - local full_name = M.get_qualified_test_name_from_id(node:data().id) - node:data().full_name = full_name + if node:data().framework == "xunit" or node:data().framework == "nunit" then + node:data().full_name = node:data().name + else + local full_name = M.get_qualified_test_name_from_id(node:data().id) + node:data().full_name = full_name + end end return test_nodes diff --git a/lua/neotest-dotnet/utils/result-utils.lua b/lua/neotest-dotnet/utils/result-utils.lua deleted file mode 100644 index 336ce08..0000000 --- a/lua/neotest-dotnet/utils/result-utils.lua +++ /dev/null @@ -1,142 +0,0 @@ -local M = {} - ----@class DotnetResult[] ----@field status string ----@field raw_output string ----@field test_name string ----@field error_info string - -local outcome_mapper = { - Passed = "passed", - Failed = "failed", - Skipped = "skipped", - NotExecuted = "skipped", -} - -function M.get_runtime_error(position_id) - local run_outcome = {} - run_outcome[position_id] = { - status = "failed", - } - return run_outcome -end - ----Creates a table of intermediate results from the parsed xml result data ----@param test_results table ----@param test_definitions table ----@return DotnetResult[] -function M.create_intermediate_results(test_results, test_definitions) - ---@type DotnetResult[] - local intermediate_results = {} - - for _, value in pairs(test_results) do - local qualified_test_name - - if value._attr.testId ~= nil then - for _, test_definition in pairs(test_definitions) do - if test_definition._attr.id ~= nil then - if value._attr.testId == test_definition._attr.id then - local dot_index = string.find(test_definition._attr.name, "%.") - local bracket_index = string.find(test_definition._attr.name, "%(") - if dot_index ~= nil and (bracket_index == nil or dot_index < bracket_index) then - qualified_test_name = test_definition._attr.name - else - -- Fix for https://github.com/Issafalcon/neotest-dotnet/issues/79 - -- Modifying display name property on non-parameterized tests gives the 'name' attribute - -- the value of the display name, so we need to use the TestMethod name instead - if bracket_index == nil then - qualified_test_name = test_definition.TestMethod._attr.className - .. "." - .. test_definition.TestMethod._attr.name - else - qualified_test_name = test_definition.TestMethod._attr.className - .. "." - .. test_definition._attr.name - end - end - end - end - end - end - - if value._attr.testName ~= nil then - local error_info - local outcome = outcome_mapper[value._attr.outcome] - local has_errors = value.Output and value.Output.ErrorInfo or nil - - if has_errors and outcome == "failed" then - local stackTrace = value.Output.ErrorInfo.StackTrace or "" - error_info = value.Output.ErrorInfo.Message .. "\n" .. stackTrace - end - local intermediate_result = { - status = string.lower(outcome), - raw_output = value.Output and value.Output.StdOut or outcome, - test_name = qualified_test_name, - error_info = error_info, - } - table.insert(intermediate_results, intermediate_result) - end - end - - return intermediate_results -end - ----Converts and adds the results of the test_results list to the neotest_results table. ----@param intermediate_results DotnetResult[] The marshalled dotnet console outputs ----@param test_nodes neotest.Tree ----@return neotest.Result[] -function M.convert_intermediate_results(intermediate_results, test_nodes) - local neotest_results = {} - - for _, intermediate_result in ipairs(intermediate_results) do - for _, node in ipairs(test_nodes) do - local node_data = node:data() - -- The test name from the trx file uses the namespace to fully qualify the test name - local result_test_name = intermediate_result.test_name - - local is_dynamically_parameterized = #node:children() == 0 - and not string.find(node_data.name, "%(.*%)") - - if is_dynamically_parameterized then - -- Remove dynamically generated arguments as they are not in node_data - result_test_name = string.gsub(result_test_name, "%(.*%)", "") - end - - -- Use the full_name of the test, including namespace - local is_match = #result_test_name == #node_data.full_name - and string.find(result_test_name, node_data.full_name, 0, true) - - if is_match then - -- For non-inlined parameterized tests, check if we already have an entry for the test. - -- If so, we need to check for a failure, and ensure the entire group of tests is marked as failed. - neotest_results[node_data.id] = neotest_results[node_data.id] - or { - status = intermediate_result.status, - short = node_data.full_name .. ":" .. intermediate_result.status, - errors = {}, - } - - if intermediate_result.status == "failed" then - -- Mark as failed for the whole thing - neotest_results[node_data.id].status = "failed" - neotest_results[node_data.id].short = node_data.full_name .. ":failed" - end - - if intermediate_result.error_info then - table.insert(neotest_results[node_data.id].errors, { - message = intermediate_result.test_name .. ": " .. intermediate_result.error_info, - }) - - -- Mark as failed - neotest_results[node_data.id].status = "failed" - end - - break - end - end - end - - return neotest_results -end - -return M diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua new file mode 100644 index 0000000..ae2e737 --- /dev/null +++ b/lua/neotest-dotnet/xunit/init.lua @@ -0,0 +1,311 @@ +local logger = require("neotest.logging") +local lib = require("neotest.lib") +local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") +local types = require("neotest.types") +local NodeTreeUtils = require("neotest-dotnet.utils.neotest-node-tree-utils") +local Tree = types.Tree + +---@type FrameworkUtils +local M = {} + +function M.get_treesitter_queries(custom_attribute_args) + return require("neotest-dotnet.xunit.ts-queries").get_queries(custom_attribute_args) +end + +local get_node_math_type = function(captured_nodes) + if captured_nodes["test.name"] then + return "test" + end + if captured_nodes["namespace.name"] then + return "namespace" + end + if captured_nodes["class.name"] then + return "class" + end + if captured_nodes["test.parameterized.name"] then + return "test.parameterized" + end +end + +M.build_position = function(file_path, source, captured_nodes) + local match_type = get_node_math_type(captured_nodes) + + local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) + local display_name = nil + + if captured_nodes["display_name"] then + display_name = vim.treesitter.get_node_text(captured_nodes["display_name"], source) + end + + local definition = captured_nodes[match_type .. ".definition"] + + -- Introduce the C# concept of a "class" to the node, so we can distinguish between a class and a namespace. + -- Helps to determine if classes are nested, and therefore, if we need to modify the ID of the node (nested classes denoted by a '+' in C# test naming convention) + local is_class = match_type == "class" + + -- Swap the match type back to "namespace" so neotest core can handle it properly + if match_type == "class" then + match_type = "namespace" + end + + local node = { + type = match_type, + framework = "xunit", + is_class = is_class, + display_name = display_name, + path = file_path, + name = name, + range = { definition:range() }, + } + + if match_type and match_type ~= "test.parameterized" then + return node + end +end + +M.position_id = function(position, parents) + local original_id = position.path + local has_parent_class = false + local sep = "::" + + -- Build the original ID from the parents, changing the separator to "+" if any nodes are nested classes + for _, node in ipairs(parents) do + if has_parent_class and node.is_class then + sep = "+" + end + + if node.is_class then + has_parent_class = true + end + + original_id = original_id .. sep .. node.name + end + + -- Add the final leaf nodes name to the ID, again changing the separator to "+" if it is a nested class + sep = "::" + if has_parent_class and position.is_class then + sep = "+" + end + original_id = original_id .. sep .. position.name + + -- Check to see if the position is a test case and contains parentheses (meaning it is parameterized) + -- If it is, remove the duplicated parent test name from the ID, so that when reading the trx test name + -- it will be the same as the test name in the test explorer + -- Example: + -- When ID is "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName::ParentTestName(TestName)" + -- Then we need it to be converted to "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName(TestName)" + if position.type == "test" and position.name:find("%(") then + local id_segments = {} + for _, segment in ipairs(vim.split(original_id, "::")) do + table.insert(id_segments, segment) + end + + table.remove(id_segments, #id_segments - 1) + return table.concat(id_segments, "::") + end + + return original_id +end + +---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. +---@param base_node table The initial root node to build the positions from +---@param source any The source code to build the positions from +---@param captured_nodes any The nodes captured by the TS query +---@param match_type string The type of node that was matched by the TS query +---@return table +M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) + return base_node +end + +---Modifies the tree using supplementary information from dotnet test -t or other methods +---@param tree neotest.Tree The tree to modify +---@param path string The path to the file the tree was built from +M.post_process_tree_list = function(tree, path) + local proj_root = lib.files.match_root_pattern("*.csproj")(path) + local test_list_job = DotnetUtils.get_test_full_names(proj_root) + local dotnet_tests = test_list_job.result().output + local tree_as_list = tree:to_list() + + local function process_test_names(node_tree) + for _, node in ipairs(node_tree) do + if node.type == "test" then + local matched_tests = {} + local node_test_name = node.name + local running_id = node.id + + -- If node.display_name is not nil, use it to match the test name + if node.display_name ~= nil then + node_test_name = node.display_name + else + node_test_name = NodeTreeUtils.get_qualified_test_name_from_id(node.id) + end + + logger.debug("neotest-dotnet: Processing test name: " .. node_test_name) + + for _, dotnet_name in ipairs(dotnet_tests) do + -- First remove parameters from test name so we just match the "base" test name + if string.find(dotnet_name:gsub("%b()", ""), node_test_name, 0, true) then + table.insert(matched_tests, dotnet_name) + end + end + + if #matched_tests > 1 then + -- This is a parameterized test (multiple matches for the same test) + local parent_node_ranges = node.range + for j, matched_name in ipairs(matched_tests) do + local sub_id = path .. "::" .. string.gsub(matched_name, "%.", "::") + local sub_test = {} + local sub_node = { + id = sub_id, + is_class = false, + name = matched_name, + path = path, + range = { + parent_node_ranges[1] + j, + parent_node_ranges[2], + parent_node_ranges[1] + j, + parent_node_ranges[4], + }, + type = "test", + framework = "xunit", + running_id = running_id, + } + table.insert(sub_test, sub_node) + table.insert(node_tree, sub_test) + end + + node_tree[1] = vim.tbl_extend("force", node, { + name = matched_tests[1]:gsub("%b()", ""), + framework = "xunit", + running_id = running_id, + }) + + logger.debug("testing: node_tree after parameterized tests: ") + logger.debug(node_tree) + elseif #matched_tests == 1 then + logger.debug("testing: matched one test with name: " .. matched_tests[1]) + node_tree[1] = vim.tbl_extend( + "force", + node, + { name = matched_tests[1], framework = "xunit", running_id = running_id } + ) + end + end + + process_test_names(node) + end + end + + process_test_names(tree_as_list) + + logger.debug("neotest-dotnet: Processed tree before leaving method: ") + logger.debug(tree_as_list) + + return Tree.from_list(tree_as_list, function(pos) + return pos.id + end) +end + +---Generates test results from the node tree list and trx output file from the test run +---@param test_results any +---@param tree neotest.Tree +---@param context_id string The ID of the neotest context being run +---@return neotest.Result[] +function M.generate_test_results(test_results, tree, context_id) + local test_nodes = NodeTreeUtils.get_test_nodes_data(tree) + + logger.debug("neotest-dotnet: xUnit test Nodes: ") + logger.debug(test_nodes) + + local intermediate_results + + if test_results then + if #test_results.UnitTestResult > 1 then + test_results = test_results.UnitTestResult + end + + intermediate_results = {} + + local outcome_mapper = { + Passed = "passed", + Failed = "failed", + Skipped = "skipped", + NotExecuted = "skipped", + } + + for _, value in pairs(test_results) do + if value._attr.testName ~= nil then + local error_info + local outcome = outcome_mapper[value._attr.outcome] + local has_errors = value.Output and value.Output.ErrorInfo or nil + + if has_errors and outcome == "failed" then + local stackTrace = value.Output.ErrorInfo.StackTrace or "" + error_info = value.Output.ErrorInfo.Message .. "\n" .. stackTrace + end + local intermediate_result = { + status = string.lower(outcome), + raw_output = value.Output and value.Output.StdOut or outcome, + test_name = value._attr.testName, + error_info = error_info, + } + table.insert(intermediate_results, intermediate_result) + end + end + end + + -- No test results. Something went wrong. Check for runtime error + if not intermediate_results then + local run_outcome = {} + run_outcome[context_id] = { + status = "failed", + } + return run_outcome + end + + logger.debug("neotest-dotnet: Intermediate Results: ") + logger.debug(intermediate_results) + + local neotest_results = {} + + for _, intermediate_result in ipairs(intermediate_results) do + for _, node in ipairs(test_nodes) do + local node_data = node:data() + + if intermediate_result.test_name == node_data.full_name then + -- For non-inlined parameterized tests, check if we already have an entry for the test. + -- If so, we need to check for a failure, and ensure the entire group of tests is marked as failed. + neotest_results[node_data.id] = neotest_results[node_data.id] + or { + status = intermediate_result.status, + short = node_data.full_name .. ":" .. intermediate_result.status, + errors = {}, + } + + if intermediate_result.status == "failed" then + -- Mark as failed for the whole thing + neotest_results[node_data.id].status = "failed" + neotest_results[node_data.id].short = node_data.full_name .. ":failed" + end + + if intermediate_result.error_info then + table.insert(neotest_results[node_data.id].errors, { + message = intermediate_result.test_name .. ": " .. intermediate_result.error_info, + }) + + -- Mark as failed + neotest_results[node_data.id].status = "failed" + end + + break + end + end + end + + logger.debug("neotest-dotnet: Neotest Results after conversion of Intermediate Results: ") + logger.debug(neotest_results) + + return neotest_results +end + +return M diff --git a/lua/neotest-dotnet/tree-sitter/xunit-queries.lua b/lua/neotest-dotnet/xunit/ts-queries.lua similarity index 100% rename from lua/neotest-dotnet/tree-sitter/xunit-queries.lua rename to lua/neotest-dotnet/xunit/ts-queries.lua From 40e6c09aee581c1936b22716917a8a96e836d634 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Mon, 12 Feb 2024 20:24:45 +0000 Subject: [PATCH 09/17] chore(scope): Finishing off test framework type --- .../types/neotest-dotnet-types.lua | 2 ++ lua/neotest-dotnet/xunit/init.lua | 16 +--------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/lua/neotest-dotnet/types/neotest-dotnet-types.lua b/lua/neotest-dotnet/types/neotest-dotnet-types.lua index 98ac72e..a79a426 100644 --- a/lua/neotest-dotnet/types/neotest-dotnet-types.lua +++ b/lua/neotest-dotnet/types/neotest-dotnet-types.lua @@ -10,3 +10,5 @@ ---@field get_treesitter_queries fun(custom_attribute_args: any): string Gets the TS queries for the framework ---@field build_position fun(file_path: string, source: any, captured_nodes: any): any Builds a position from captured nodes ---@field position_id fun(position: any, parents: any): string Creates the id for a position based on the position node and parents +---@field post_process_tree_list fun(tree: neotest.Tree, path: string): neotest.Tree Post processes the tree list after initial position discovery +---@field generate_test_results fun(trx_results: any, tree: neotest.Tree, context_id: string): neotest:Result[] Generates test results from trx results diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua index ae2e737..c3ffa18 100644 --- a/lua/neotest-dotnet/xunit/init.lua +++ b/lua/neotest-dotnet/xunit/init.lua @@ -6,6 +6,7 @@ local NodeTreeUtils = require("neotest-dotnet.utils.neotest-node-tree-utils") local Tree = types.Tree ---@type FrameworkUtils +---@diagnostic disable-next-line: missing-fields local M = {} function M.get_treesitter_queries(custom_attribute_args) @@ -107,16 +108,6 @@ M.position_id = function(position, parents) return original_id end ----Builds a position from captured nodes, optionally parsing parameters to create sub-positions. ----@param base_node table The initial root node to build the positions from ----@param source any The source code to build the positions from ----@param captured_nodes any The nodes captured by the TS query ----@param match_type string The type of node that was matched by the TS query ----@return table -M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) - return base_node -end - ---Modifies the tree using supplementary information from dotnet test -t or other methods ---@param tree neotest.Tree The tree to modify ---@param path string The path to the file the tree was built from @@ -206,11 +197,6 @@ M.post_process_tree_list = function(tree, path) end) end ----Generates test results from the node tree list and trx output file from the test run ----@param test_results any ----@param tree neotest.Tree ----@param context_id string The ID of the neotest context being run ----@return neotest.Result[] function M.generate_test_results(test_results, tree, context_id) local test_nodes = NodeTreeUtils.get_test_nodes_data(tree) From 67ef38dc25b07b10285a5987861c8b111bf5b6b4 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Mon, 12 Feb 2024 20:38:06 +0000 Subject: [PATCH 10/17] chore(dotnet-test): Removes old code --- .../frameworks/test-attributes.lua | 69 ------- .../frameworks/test-framework-base.lua | 192 ------------------ lua/neotest-dotnet/init.lua | 3 +- lua/neotest-dotnet/mstest/init.lua | 2 +- lua/neotest-dotnet/mstest/ts-queries.lua | 4 +- lua/neotest-dotnet/nunit/init.lua | 2 +- lua/neotest-dotnet/nunit/ts-queries.lua | 4 +- lua/neotest-dotnet/xunit/ts-queries.lua | 4 +- 8 files changed, 10 insertions(+), 270 deletions(-) delete mode 100644 lua/neotest-dotnet/frameworks/test-attributes.lua delete mode 100644 lua/neotest-dotnet/frameworks/test-framework-base.lua diff --git a/lua/neotest-dotnet/frameworks/test-attributes.lua b/lua/neotest-dotnet/frameworks/test-attributes.lua deleted file mode 100644 index c33f6f9..0000000 --- a/lua/neotest-dotnet/frameworks/test-attributes.lua +++ /dev/null @@ -1,69 +0,0 @@ -local M = {} - -M.xunit_test_attributes = { - "Fact", - "Theory", -} - -M.nunit_test_attributes = { - "Test", - "TestCase", - "TestCaseSource", -} - -M.mstest_test_attributes = { - "TestMethod", - "DataTestMethod", -} - -M.specflow_test_attributes = { - "SkippableFactAttribute", - "TestMethodAttribute", - "TestAttribute", - "NUnit.Framework.TestAttribute", -} - -M.all_test_attributes = vim.tbl_flatten({ - M.xunit_test_attributes, - M.nunit_test_attributes, - M.mstest_test_attributes, - M.specflow_test_attributes, -}) - ---- Gets a list of the standard and customized test attributes for xUnit, for use in a tree-sitter predicates ----@param custom_attribute_args table The user configured mapping of the custom test attributes ----@param framework string The name of the test framework ----@return -function M.attribute_match_list(custom_attribute_args, framework) - local attribute_match_list = {} - if framework == "xunit" then - attribute_match_list = M.xunit_test_attributes - end - if framework == "mstest" then - attribute_match_list = M.mstest_test_attributes - end - if framework == "nunit" then - attribute_match_list = M.nunit_test_attributes - end - - if custom_attribute_args and custom_attribute_args[framework] then - attribute_match_list = - vim.tbl_flatten({ attribute_match_list, custom_attribute_args[framework] }) - end - - return M.join_test_attributes(attribute_match_list) -end - -function M.join_test_attributes(attributes) - local joined_attributes = attributes - and table.concat( - vim.tbl_map(function(attribute) - return '"' .. attribute .. '"' - end, attributes), - " " - ) - or "" - return joined_attributes -end - -return M diff --git a/lua/neotest-dotnet/frameworks/test-framework-base.lua b/lua/neotest-dotnet/frameworks/test-framework-base.lua deleted file mode 100644 index 292f896..0000000 --- a/lua/neotest-dotnet/frameworks/test-framework-base.lua +++ /dev/null @@ -1,192 +0,0 @@ -local xunit = require("neotest-dotnet.frameworks.xunit") -local nunit = require("neotest-dotnet.frameworks.nunit") -local mstest = require("neotest-dotnet.frameworks.mstest") -local attributes = require("neotest-dotnet.frameworks.test-attributes") -local async = require("neotest.async") - -local TestFrameworkBase = {} - -function TestFrameworkBase.get_match_type(captured_nodes) - if captured_nodes["test.name"] then - return "test" - end - if captured_nodes["namespace.name"] then - return "namespace" - end - if captured_nodes["class.name"] then - return "class" - end - if captured_nodes["test.parameterized.name"] then - return "test.parameterized" - end -end - -function TestFrameworkBase.position_id(position, parents) - local original_id = position.path - local has_parent_class = false - local sep = "::" - - -- Build the original ID from the parents, changing the separator to "+" if any nodes are nested classes - for _, node in ipairs(parents) do - if has_parent_class and node.is_class then - sep = "+" - end - - if node.is_class then - has_parent_class = true - end - - original_id = original_id .. sep .. node.name - end - - -- Add the final leaf nodes name to the ID, again changing the separator to "+" if it is a nested class - sep = "::" - if has_parent_class and position.is_class then - sep = "+" - end - original_id = original_id .. sep .. position.name - - -- Check to see if the position is a test case and contains parentheses (meaning it is parameterized) - -- If it is, remove the duplicated parent test name from the ID, so that when reading the trx test name - -- it will be the same as the test name in the test explorer - -- Example: - -- When ID is "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName::ParentTestName(TestName)" - -- Then we need it to be converted to "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName(TestName)" - if position.type == "test" and position.name:find("%(") then - local id_segments = {} - for _, segment in ipairs(vim.split(original_id, "::")) do - table.insert(id_segments, segment) - end - - table.remove(id_segments, #id_segments - 1) - return table.concat(id_segments, "::") - end - - return original_id -end - ----Builds a position from captured nodes, optionally parsing parameters to create sub-positions. ----@param file_path any ----@param source any ----@param captured_nodes any ----@return table -function TestFrameworkBase.build_position(file_path, source, captured_nodes) - local match_type = TestFrameworkBase.get_match_type(captured_nodes) - - local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) - local display_name = nil - - if captured_nodes["display_name"] then - display_name = vim.treesitter.get_node_text(captured_nodes["display_name"], source) - end - - local definition = captured_nodes[match_type .. ".definition"] - - -- Introduce the C# concept of a "class" to the node, so we can distinguish between a class and a namespace. - -- Helps to determine if classes are nested, and therefore, if we need to modify the ID of the node (nested classes denoted by a '+' in C# test naming convention) - local is_class = match_type == "class" - - -- Swap the match type back to "namespace" so neotest core can handle it properly - if match_type == "class" then - match_type = "namespace" - end - - local node = { - type = match_type, - is_class = is_class, - display_name = display_name, - path = file_path, - name = name, - range = { definition:range() }, - } - - if match_type and match_type ~= "test.parameterized" then - return node - end - - return TestFrameworkBase.get_test_framework_utils(source) - .build_parameterized_test_positions(node, source, captured_nodes, match_type) -end - ----Creates a table of intermediate results from the parsed xml result data ----@param test_results table ----@return DotnetResult[] -function TestFrameworkBase.create_intermediate_results(test_results) - ---@type DotnetResult[] - local intermediate_results = {} - - local outcome_mapper = { - Passed = "passed", - Failed = "failed", - Skipped = "skipped", - NotExecuted = "skipped", - } - - for _, value in pairs(test_results) do - if value._attr.testName ~= nil then - local error_info - local outcome = outcome_mapper[value._attr.outcome] - local has_errors = value.Output and value.Output.ErrorInfo or nil - - if has_errors and outcome == "failed" then - local stackTrace = value.Output.ErrorInfo.StackTrace or "" - error_info = value.Output.ErrorInfo.Message .. "\n" .. stackTrace - end - local intermediate_result = { - status = string.lower(outcome), - raw_output = value.Output and value.Output.StdOut or outcome, - test_name = value._attr.testName, - error_info = error_info, - } - table.insert(intermediate_results, intermediate_result) - end - end - - return intermediate_results -end - ----Converts and adds the results of the test_results list to the neotest_results table. ----@param intermediate_results DotnetResult[] The marshalled dotnet console outputs ----@param test_nodes neotest.Tree ----@return neotest.Result[] -function TestFrameworkBase.convert_intermediate_results(intermediate_results, test_nodes) - local neotest_results = {} - - for _, intermediate_result in ipairs(intermediate_results) do - for _, node in ipairs(test_nodes) do - local node_data = node:data() - - if intermediate_result.test_name == node_data.full_name then - -- For non-inlined parameterized tests, check if we already have an entry for the test. - -- If so, we need to check for a failure, and ensure the entire group of tests is marked as failed. - neotest_results[node_data.id] = neotest_results[node_data.id] - or { - status = intermediate_result.status, - short = node_data.full_name .. ":" .. intermediate_result.status, - errors = {}, - } - - if intermediate_result.status == "failed" then - -- Mark as failed for the whole thing - neotest_results[node_data.id].status = "failed" - neotest_results[node_data.id].short = node_data.full_name .. ":failed" - end - - if intermediate_result.error_info then - table.insert(neotest_results[node_data.id].errors, { - message = intermediate_result.test_name .. ": " .. intermediate_result.error_info, - }) - - -- Mark as failed - neotest_results[node_data.id].status = "failed" - end - - break - end - end - end - - return neotest_results -end - -return TestFrameworkBase diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index d63eb0a..406a739 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -74,7 +74,8 @@ end ---@return neotest.Tree DotnetNeotestAdapter.discover_positions = function(path) local content = lib.files.read(path) - local test_framework = FrameworkDiscovery.get_test_framework_utils(content, custom_attribute_args) + local test_framework = + FrameworkDiscovery.get_test_framework_utils_from_source(content, custom_attribute_args) local framework_queries = test_framework.get_treesitter_queries(custom_attribute_args) local query = [[ diff --git a/lua/neotest-dotnet/mstest/init.lua b/lua/neotest-dotnet/mstest/init.lua index c22c1d7..96b5901 100644 --- a/lua/neotest-dotnet/mstest/init.lua +++ b/lua/neotest-dotnet/mstest/init.lua @@ -4,7 +4,7 @@ local logger = require("neotest.logging") local M = {} function M.get_treesitter_queries(custom_attribute_args) - return require("neotest-dotnet.tree-sitter.mstest-queries").get_queries(custom_attribute_args) + return require("neotest-dotnet.mstest.ts-queries").get_queries(custom_attribute_args) end ---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. diff --git a/lua/neotest-dotnet/mstest/ts-queries.lua b/lua/neotest-dotnet/mstest/ts-queries.lua index 2c89a6b..ecb2155 100644 --- a/lua/neotest-dotnet/mstest/ts-queries.lua +++ b/lua/neotest-dotnet/mstest/ts-queries.lua @@ -1,11 +1,11 @@ -local attribute_utils = require("neotest-dotnet.frameworks.test-attributes") +local framework_discovery = require("neotest-dotnet.framework-discovery") local M = {} function M.get_queries(custom_attributes) -- Don't include parameterized test attribute indicators so we don't double count them local custom_fact_attributes = custom_attributes - and attribute_utils.join_test_attributes(custom_attributes.mstest) + and framework_discovery.join_test_attributes(custom_attributes.mstest) or "" return [[ diff --git a/lua/neotest-dotnet/nunit/init.lua b/lua/neotest-dotnet/nunit/init.lua index fc23e66..8b7335e 100644 --- a/lua/neotest-dotnet/nunit/init.lua +++ b/lua/neotest-dotnet/nunit/init.lua @@ -8,7 +8,7 @@ local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") local M = {} function M.get_treesitter_queries(custom_attribute_args) - return require("neotest-dotnet.tree-sitter.nunit-queries").get_queries(custom_attribute_args) + return require("neotest-dotnet.nunit.ts-queries").get_queries(custom_attribute_args) end ---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. diff --git a/lua/neotest-dotnet/nunit/ts-queries.lua b/lua/neotest-dotnet/nunit/ts-queries.lua index 3b9e524..9edcea2 100644 --- a/lua/neotest-dotnet/nunit/ts-queries.lua +++ b/lua/neotest-dotnet/nunit/ts-queries.lua @@ -1,11 +1,11 @@ -local attribute_utils = require("neotest-dotnet.frameworks.test-attributes") +local framework_discovery = require("neotest-dotnet.framework-discovery") local M = {} function M.get_queries(custom_attributes) -- Don't include parameterized test attribute indicators so we don't double count them local custom_test_attributes = custom_attributes - and attribute_utils.join_test_attributes(custom_attributes.nunit) + and framework_discovery.join_test_attributes(custom_attributes.nunit) or "" return [[ diff --git a/lua/neotest-dotnet/xunit/ts-queries.lua b/lua/neotest-dotnet/xunit/ts-queries.lua index 33a4ac5..1fbbe14 100644 --- a/lua/neotest-dotnet/xunit/ts-queries.lua +++ b/lua/neotest-dotnet/xunit/ts-queries.lua @@ -1,11 +1,11 @@ -local attribute_utils = require("neotest-dotnet.frameworks.test-attributes") +local framework_discovery = require("neotest-dotnet.framework-discovery") local M = {} function M.get_queries(custom_attributes) -- Don't include parameterized test attribute indicators so we don't double count them local custom_fact_attributes = custom_attributes - and attribute_utils.join_test_attributes(custom_attributes.xunit) + and framework_discovery.join_test_attributes(custom_attributes.xunit) or "" return [[ From d7f20f4790ae55ad7dd5f502e61b70afc3995cf7 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Thu, 15 Feb 2024 17:51:48 +0000 Subject: [PATCH 11/17] BREAKING CHANGE: Fixes NUnit framework file - Uses old treesitter method only --- lua/neotest-dotnet/init.lua | 25 +- lua/neotest-dotnet/nunit/init.lua | 327 +++++++++++++----- .../types/neotest-dotnet-types.lua | 2 +- .../utils/neotest-node-tree-utils.lua | 4 +- lua/neotest-dotnet/xunit/init.lua | 45 +-- 5 files changed, 279 insertions(+), 124 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 406a739..cf4156e 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -1,6 +1,5 @@ local lib = require("neotest.lib") local logger = require("neotest.logging") -local trx_utils = require("neotest-dotnet.utils.trx-utils") local FrameworkDiscovery = require("neotest-dotnet.framework-discovery") local build_spec_utils = require("neotest-dotnet.utils.build-spec-utils") @@ -58,8 +57,16 @@ end DotnetNeotestAdapter._build_position = function(...) local args = { ... } + + logger.debug("neotest-dotnet: Buil Position Args: ") + logger.debug(args) + local framework = FrameworkDiscovery.get_test_framework_utils_from_source(args[2], custom_attribute_args) -- args[2] is the content of the file + + logger.debug("neotest-dotnet: Framework: ") + logger.debug(framework) + return framework.build_position(...) end @@ -160,21 +167,7 @@ DotnetNeotestAdapter.results = function(spec, _, tree) logger.debug(tree:to_list()) local test_framework = FrameworkDiscovery.get_test_framework_utils_from_tree(tree) - - local parsed_data = trx_utils.parse_trx(output_file) - local test_results = parsed_data.TestRun and parsed_data.TestRun.Results - - logger.info( - "neotest-dotnet: Found " - .. #test_results - .. " test results when parsing TRX file: " - .. output_file - ) - - logger.debug("neotest-dotnet: TRX Results Output for" .. output_file .. ": ") - logger.debug(test_results) - - local results = test_framework.generate_test_results(test_results, tree, spec.context.id) + local results = test_framework.generate_test_results(output_file, tree, spec.context.id) return results end diff --git a/lua/neotest-dotnet/nunit/init.lua b/lua/neotest-dotnet/nunit/init.lua index 8b7335e..dff0cc4 100644 --- a/lua/neotest-dotnet/nunit/init.lua +++ b/lua/neotest-dotnet/nunit/init.lua @@ -1,23 +1,18 @@ local logger = require("neotest.logging") -local types = require("neotest.types") -local node_tree_utils = require("neotest-dotnet.utils.neotest-node-tree-utils") -local Tree = types.Tree -local lib = require("neotest.lib") -local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") +local TrxUtils = require("neotest-dotnet.utils.trx-utils") +local NodeTreeUtils = require("neotest-dotnet.utils.neotest-node-tree-utils") +---@type FrameworkUtils +---@diagnostic disable-next-line: missing-fields local M = {} -function M.get_treesitter_queries(custom_attribute_args) - return require("neotest-dotnet.nunit.ts-queries").get_queries(custom_attribute_args) -end - ---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. ---@param base_node table The initial root node to build the positions from ---@param source any The source code to build the positions from ---@param captured_nodes any The nodes captured by the TS query ---@param match_type string The type of node that was matched by the TS query ---@return table -M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) +local build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) logger.debug("neotest-dotnet(NUnit Utils): Building parameterized test positions from source") logger.debug("neotest-dotnet(NUnit Utils): Base node: ") logger.debug(base_node) @@ -64,93 +59,265 @@ M.build_parameterized_test_positions = function(base_node, source, captured_node return nodes end +local get_match_type = function(captured_nodes) + if captured_nodes["test.name"] then + return "test" + end + if captured_nodes["namespace.name"] then + return "namespace" + end + if captured_nodes["class.name"] then + return "class" + end + if captured_nodes["test.parameterized.name"] then + return "test.parameterized" + end +end + +function M.get_treesitter_queries(custom_attribute_args) + return require("neotest-dotnet.nunit.ts-queries").get_queries(custom_attribute_args) +end + +M.build_position = function(file_path, source, captured_nodes) + local match_type = get_match_type(captured_nodes) + + local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) + local definition = captured_nodes[match_type .. ".definition"] + + -- Introduce the C# concept of a "class" to the node, so we can distinguish between a class and a namespace. + -- Helps to determine if classes are nested, and therefore, if we need to modify the ID of the node (nested classes denoted by a '+' in C# test naming convention) + local is_class = match_type == "class" + + -- Swap the match type back to "namespace" so neotest core can handle it properly + if match_type == "class" then + match_type = "namespace" + end + + local node = { + type = match_type, + framework = "nunit", + is_class = is_class, + display_name = nil, + path = file_path, + name = name, + range = { definition:range() }, + } + + if match_type and match_type ~= "test.parameterized" then + return node + end + + return build_parameterized_test_positions(node, source, captured_nodes, match_type) +end + +M.position_id = function(position, parents) + local original_id = position.path + local has_parent_class = false + local sep = "::" + + -- Build the original ID from the parents, changing the separator to "+" if any nodes are nested classes + for _, node in ipairs(parents) do + if has_parent_class and node.is_class then + sep = "+" + end + + if node.is_class then + has_parent_class = true + end + + original_id = original_id .. sep .. node.name + end + + -- Add the final leaf nodes name to the ID, again changing the separator to "+" if it is a nested class + sep = "::" + if has_parent_class and position.is_class then + sep = "+" + end + original_id = original_id .. sep .. position.name + + -- Check to see if the position is a test case and contains parentheses (meaning it is parameterized) + -- If it is, remove the duplicated parent test name from the ID, so that when reading the trx test name + -- it will be the same as the test name in the test explorer + -- Example: + -- When ID is "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName::ParentTestName(TestName)" + -- Then we need it to be converted to "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName(TestName)" + if position.type == "test" and position.name:find("%(") then + local id_segments = {} + for _, segment in ipairs(vim.split(original_id, "::")) do + table.insert(id_segments, segment) + end + + table.remove(id_segments, #id_segments - 1) + return table.concat(id_segments, "::") + end + + return original_id +end + ---Modifies the tree using supplementary information from dotnet test -t or other methods ---@param tree neotest.Tree The tree to modify ---@param path string The path to the file the tree was built from M.post_process_tree_list = function(tree, path) - local proj_root = lib.files.match_root_pattern("*.csproj")(path) - local test_list_job = DotnetUtils.get_test_full_names(proj_root) - local dotnet_tests = test_list_job.result().output - local tree_as_list = tree:to_list() - - local function process_test_names(node_tree) - for _, node in ipairs(node_tree) do - if node.type == "test" then - local matched_tests = {} - local node_test_name = node.name - local running_id = node.id - - -- If node.display_name is not nil, use it to match the test name - if node.display_name ~= nil then - node_test_name = node.display_name - else - node_test_name = node_tree_utils.get_qualified_test_name_from_id(node.id) - end + return tree +end + +M.generate_test_results = function(output_file_path, tree, context_id) + local parsed_data = TrxUtils.parse_trx(output_file_path) + local test_results = parsed_data.TestRun and parsed_data.TestRun.Results + local test_definitions = parsed_data.TestRun and parsed_data.TestRun.TestDefinitions + + logger.info( + "neotest-dotnet: Found " + .. #test_results + .. " test results when parsing TRX file: " + .. output_file_path + ) - logger.debug("neotest-dotnet: Processing test name: " .. node_test_name) + logger.debug("neotest-dotnet: NUnit TRX Results Output for" .. output_file_path .. ": ") + logger.debug(test_results) - for _, dotnet_name in ipairs(dotnet_tests) do - -- First remove parameters from test name so we just match the "base" test name - if string.find(dotnet_name:gsub("%b()", ""), node_test_name, 0, true) then - table.insert(matched_tests, dotnet_name) + logger.debug("neotest-dotnet: NUnit TRX Test Definitions Output: ") + logger.debug(test_definitions) + + local test_nodes = NodeTreeUtils.get_test_nodes_data(tree) + + logger.debug("neotest-dotnet: NUnit test Nodes: ") + logger.debug(test_nodes) + + local intermediate_results + + if test_results and test_definitions then + if #test_results.UnitTestResult > 1 then + test_results = test_results.UnitTestResult + end + if #test_definitions.UnitTest > 1 then + test_definitions = test_definitions.UnitTest + end + + intermediate_results = {} + + local outcome_mapper = { + Passed = "passed", + Failed = "failed", + Skipped = "skipped", + NotExecuted = "skipped", + } + + for _, value in pairs(test_results) do + local qualified_test_name + + if value._attr.testId ~= nil then + for _, test_definition in pairs(test_definitions) do + if test_definition._attr.id ~= nil then + if value._attr.testId == test_definition._attr.id then + local dot_index = string.find(test_definition._attr.name, "%.") + local bracket_index = string.find(test_definition._attr.name, "%(") + if dot_index ~= nil and (bracket_index == nil or dot_index < bracket_index) then + qualified_test_name = test_definition._attr.name + else + -- Fix for https://github.com/Issafalcon/neotest-dotnet/issues/79 + -- Modifying display name property on non-parameterized tests gives the 'name' attribute + -- the value of the display name, so we need to use the TestMethod name instead + if bracket_index == nil then + qualified_test_name = test_definition.TestMethod._attr.className + .. "." + .. test_definition.TestMethod._attr.name + else + qualified_test_name = test_definition.TestMethod._attr.className + .. "." + .. test_definition._attr.name + end + end + end end end + end - if #matched_tests > 1 then - -- This is a parameterized test (multiple matches for the same test) - local parent_node_ranges = node.range - for j, matched_name in ipairs(matched_tests) do - local sub_id = path .. "::" .. string.gsub(matched_name, "%.", "::") - local sub_test = {} - local sub_node = { - id = sub_id, - is_class = false, - name = matched_name, - path = path, - range = { - parent_node_ranges[1] + j, - parent_node_ranges[2], - parent_node_ranges[1] + j, - parent_node_ranges[4], - }, - type = "test", - framework = "nunit", - running_id = running_id, - } - table.insert(sub_test, sub_node) - table.insert(node_tree, sub_test) - end + if value._attr.testName ~= nil then + local error_info + local outcome = outcome_mapper[value._attr.outcome] + local has_errors = value.Output and value.Output.ErrorInfo or nil + + if has_errors and outcome == "failed" then + local stackTrace = value.Output.ErrorInfo.StackTrace or "" + error_info = value.Output.ErrorInfo.Message .. "\n" .. stackTrace + end + local intermediate_result = { + status = string.lower(outcome), + raw_output = value.Output and value.Output.StdOut or outcome, + test_name = qualified_test_name, + error_info = error_info, + } + table.insert(intermediate_results, intermediate_result) + end + end + end + + -- No test results. Something went wrong. Check for runtime error + if not intermediate_results then + local run_outcome = {} + run_outcome[context_id] = { + status = "failed", + } + return run_outcome + end - node_tree[1] = vim.tbl_extend("force", node, { - name = matched_tests[1]:gsub("%b()", ""), - framework = "unit", - running_id = running_id, + logger.debug("neotest-dotnet: Intermediate Results: ") + logger.debug(intermediate_results) + + local neotest_results = {} + + for _, intermediate_result in ipairs(intermediate_results) do + for _, node in ipairs(test_nodes) do + local node_data = node:data() + -- The test name from the trx file uses the namespace to fully qualify the test name + local result_test_name = intermediate_result.test_name + + local is_dynamically_parameterized = #node:children() == 0 + and not string.find(node_data.name, "%(.*%)") + + if is_dynamically_parameterized then + -- Remove dynamically generated arguments as they are not in node_data + result_test_name = string.gsub(result_test_name, "%(.*%)", "") + end + + -- Use the full_name of the test, including namespace + local is_match = #result_test_name == #node_data.full_name + and string.find(result_test_name, node_data.full_name, 0, true) + + if is_match then + -- For non-inlined parameterized tests, check if we already have an entry for the test. + -- If so, we need to check for a failure, and ensure the entire group of tests is marked as failed. + neotest_results[node_data.id] = neotest_results[node_data.id] + or { + status = intermediate_result.status, + short = node_data.full_name .. ":" .. intermediate_result.status, + errors = {}, + } + + if intermediate_result.status == "failed" then + -- Mark as failed for the whole thing + neotest_results[node_data.id].status = "failed" + neotest_results[node_data.id].short = node_data.full_name .. ":failed" + end + + if intermediate_result.error_info then + table.insert(neotest_results[node_data.id].errors, { + message = intermediate_result.test_name .. ": " .. intermediate_result.error_info, }) - logger.debug("testing: node_tree after parameterized tests: ") - logger.debug(node_tree) - elseif #matched_tests == 1 then - logger.debug("testing: matched one test with name: " .. matched_tests[1]) - node_tree[1] = vim.tbl_extend( - "force", - node, - { name = matched_tests[1], framework = "nunit", running_id = running_id } - ) + -- Mark as failed + neotest_results[node_data.id].status = "failed" end - end - process_test_names(node) + break + end end end - process_test_names(tree_as_list) + logger.debug("neotest-dotnet: NUnit Neotest Results after conversion of Intermediate Results: ") + logger.debug(neotest_results) - logger.debug("neotest-dotnet: Processed tree before leaving method: ") - logger.debug(tree_as_list) - - return Tree.from_list(tree_as_list, function(pos) - return pos.id - end) + return neotest_results end - return M diff --git a/lua/neotest-dotnet/types/neotest-dotnet-types.lua b/lua/neotest-dotnet/types/neotest-dotnet-types.lua index a79a426..2e4c5a0 100644 --- a/lua/neotest-dotnet/types/neotest-dotnet-types.lua +++ b/lua/neotest-dotnet/types/neotest-dotnet-types.lua @@ -11,4 +11,4 @@ ---@field build_position fun(file_path: string, source: any, captured_nodes: any): any Builds a position from captured nodes ---@field position_id fun(position: any, parents: any): string Creates the id for a position based on the position node and parents ---@field post_process_tree_list fun(tree: neotest.Tree, path: string): neotest.Tree Post processes the tree list after initial position discovery ----@field generate_test_results fun(trx_results: any, tree: neotest.Tree, context_id: string): neotest:Result[] Generates test results from trx results +---@field generate_test_results fun(output_file_path: string, tree: neotest.Tree, context_id: string): neotest.Result[] Generates test results from trx results diff --git a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua index d8a1e3e..39e6991 100644 --- a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua +++ b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua @@ -22,7 +22,9 @@ function M.get_test_nodes_data(tree) -- Add an additional full_name property to the test nodes for _, node in ipairs(test_nodes) do - if node:data().framework == "xunit" or node:data().framework == "nunit" then + if + node:data().framework == "xunit" --[[ or node:data().framework == "nunit" ]] + then node:data().full_name = node:data().name else local full_name = M.get_qualified_test_name_from_id(node:data().id) diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua index c3ffa18..d4639d5 100644 --- a/lua/neotest-dotnet/xunit/init.lua +++ b/lua/neotest-dotnet/xunit/init.lua @@ -3,6 +3,7 @@ local lib = require("neotest.lib") local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") local types = require("neotest.types") local NodeTreeUtils = require("neotest-dotnet.utils.neotest-node-tree-utils") +local TrxUtils = require("neotest-dotnet.utils.trx-utils") local Tree = types.Tree ---@type FrameworkUtils @@ -13,7 +14,7 @@ function M.get_treesitter_queries(custom_attribute_args) return require("neotest-dotnet.xunit.ts-queries").get_queries(custom_attribute_args) end -local get_node_math_type = function(captured_nodes) +local get_node_type = function(captured_nodes) if captured_nodes["test.name"] then return "test" end @@ -23,13 +24,10 @@ local get_node_math_type = function(captured_nodes) if captured_nodes["class.name"] then return "class" end - if captured_nodes["test.parameterized.name"] then - return "test.parameterized" - end end M.build_position = function(file_path, source, captured_nodes) - local match_type = get_node_math_type(captured_nodes) + local match_type = get_node_type(captured_nodes) local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) local display_name = nil @@ -59,9 +57,7 @@ M.build_position = function(file_path, source, captured_nodes) range = { definition:range() }, } - if match_type and match_type ~= "test.parameterized" then - return node - end + return node end M.position_id = function(position, parents) @@ -89,22 +85,6 @@ M.position_id = function(position, parents) end original_id = original_id .. sep .. position.name - -- Check to see if the position is a test case and contains parentheses (meaning it is parameterized) - -- If it is, remove the duplicated parent test name from the ID, so that when reading the trx test name - -- it will be the same as the test name in the test explorer - -- Example: - -- When ID is "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName::ParentTestName(TestName)" - -- Then we need it to be converted to "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName(TestName)" - if position.type == "test" and position.name:find("%(") then - local id_segments = {} - for _, segment in ipairs(vim.split(original_id, "::")) do - table.insert(id_segments, segment) - end - - table.remove(id_segments, #id_segments - 1) - return table.concat(id_segments, "::") - end - return original_id end @@ -197,7 +177,20 @@ M.post_process_tree_list = function(tree, path) end) end -function M.generate_test_results(test_results, tree, context_id) +M.generate_test_results = function(output_file_path, tree, context_id) + local parsed_data = TrxUtils.parse_trx(output_file_path) + local test_results = parsed_data.TestRun and parsed_data.TestRun.Results + + logger.info( + "neotest-dotnet: Found " + .. #test_results + .. " test results when parsing TRX file: " + .. output_file_path + ) + + logger.debug("neotest-dotnet: TRX Results Output for" .. output_file_path .. ": ") + logger.debug(test_results) + local test_nodes = NodeTreeUtils.get_test_nodes_data(tree) logger.debug("neotest-dotnet: xUnit test Nodes: ") @@ -288,7 +281,7 @@ function M.generate_test_results(test_results, tree, context_id) end end - logger.debug("neotest-dotnet: Neotest Results after conversion of Intermediate Results: ") + logger.debug("neotest-dotnet: xUnit Neotest Results after conversion of Intermediate Results: ") logger.debug(neotest_results) return neotest_results From e72cdb6ef4c067b785dd55740d87b7d367ca7499 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Thu, 15 Feb 2024 18:18:10 +0000 Subject: [PATCH 12/17] chore(dotnet-test): Fixing unit tests for nunit and start of xunit --- .../test_attribute_spec.lua | 3 ++ .../testcasesource_attribute_spec.lua | 6 ++++ .../fact_attribute_spec.lua | 35 ++++++++++++++++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/tests/nunit/discover_positions/test_attribute_spec.lua b/tests/nunit/discover_positions/test_attribute_spec.lua index 2a31f3b..6d84bd7 100644 --- a/tests/nunit/discover_positions/test_attribute_spec.lua +++ b/tests/nunit/discover_positions/test_attribute_spec.lua @@ -28,6 +28,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = spec_file .. "::NUnitSamples", is_class = false, name = "NUnitSamples", @@ -37,6 +38,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = spec_file .. "::NUnitSamples::SingleTests", is_class = true, name = "SingleTests", @@ -46,6 +48,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = spec_file .. "::NUnitSamples::SingleTests::Test1", is_class = false, name = "Test1", diff --git a/tests/nunit/discover_positions/testcasesource_attribute_spec.lua b/tests/nunit/discover_positions/testcasesource_attribute_spec.lua index c34cb77..35116f9 100644 --- a/tests/nunit/discover_positions/testcasesource_attribute_spec.lua +++ b/tests/nunit/discover_positions/testcasesource_attribute_spec.lua @@ -30,6 +30,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = file_path .. "::NUnitSamples", is_class = false, name = "NUnitSamples", @@ -39,6 +40,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = file_path .. "::NUnitSamples::Tests", is_class = true, name = "Tests", @@ -48,6 +50,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = file_path .. "::NUnitSamples::Tests::DivideTest", is_class = false, name = "DivideTest", @@ -81,6 +84,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = file_path .. "::NUnitSamples", is_class = false, name = "NUnitSamples", @@ -90,6 +94,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = file_path .. "::NUnitSamples::DummyTestFeature", is_class = true, name = "DummyTestFeature", @@ -99,6 +104,7 @@ describe("discover_positions", function() }, { { + framework = "nunit", id = file_path .. "::NUnitSamples::DummyTestFeature::DummyScenario", is_class = false, name = "DummyScenario", diff --git a/tests/xunit/discover_positions/fact_attribute_spec.lua b/tests/xunit/discover_positions/fact_attribute_spec.lua index 3f991d7..e2c9c0d 100644 --- a/tests/xunit/discover_positions/fact_attribute_spec.lua +++ b/tests/xunit/discover_positions/fact_attribute_spec.lua @@ -1,6 +1,7 @@ local async = require("nio").tests local plugin = require("neotest-dotnet") -local Tree = require("neotest.types").Tree +local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") +local stub = require("luassert.stub") A = function(...) print(vim.inspect(...)) @@ -13,6 +14,22 @@ describe("discover_positions", function() }, }) + stub(DotnetUtils, "get_test_full_names", function() + return { + is_complete = true, + result = function() + return { + output = { + "XUnitSamples.UnitTest1.Test1", + "XUnitSamples.UnitTest1+NestedClass.Test1", + "XUnitSamples.UnitTest1+NestedClass.Test2", + }, + result_code = 0, + } + end, + } + end) + async.it("should discover single tests in sub-class", function() local spec_file = "./tests/xunit/specs/nested_class.cs" local spec_file_name = "nested_class.cs" @@ -28,6 +45,7 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = spec_file .. "::XUnitSamples", is_class = false, name = "XUnitSamples", @@ -37,6 +55,7 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = spec_file .. "::XUnitSamples::UnitTest1", is_class = true, name = "UnitTest1", @@ -46,16 +65,19 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = spec_file .. "::XUnitSamples::UnitTest1::Test1", is_class = false, - name = "Test1", + name = "XUnitSamples.UnitTest1.Test1", path = spec_file, range = { 6, 1, 10, 2 }, + running_id = "./tests/xunit/specs/nested_class.cs::XUnitSamples::UnitTest1::Test1", type = "test", }, }, { { + framework = "xunit", id = spec_file .. "::XUnitSamples::UnitTest1+NestedClass", is_class = true, name = "NestedClass", @@ -65,21 +87,25 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = spec_file .. "::XUnitSamples::UnitTest1+NestedClass::Test1", is_class = false, - name = "Test1", + name = "XUnitSamples.UnitTest1+NestedClass.Test1", path = spec_file, range = { 14, 2, 18, 3 }, + running_id = "./tests/xunit/specs/nested_class.cs::XUnitSamples::UnitTest1+NestedClass::Test1", type = "test", }, }, { { + framework = "xunit", id = spec_file .. "::XUnitSamples::UnitTest1+NestedClass::Test2", is_class = false, - name = "Test2", + name = "XUnitSamples.UnitTest1+NestedClass.Test2", path = spec_file, range = { 20, 2, 24, 3 }, + running_id = "./tests/xunit/specs/nested_class.cs::XUnitSamples::UnitTest1+NestedClass::Test2", type = "test", }, }, @@ -87,6 +113,7 @@ describe("discover_positions", function() }, }, } + assert.same(positions, expected_positions) end) end) From bdcb0ca0993b878a82f282a0bd91c33461b03c28 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Thu, 15 Feb 2024 18:50:12 +0000 Subject: [PATCH 13/17] chore(dotnet-tests): Fixes for specflow discovery in xunit --- lua/neotest-dotnet/framework-discovery.lua | 1 + lua/neotest-dotnet/xunit/init.lua | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lua/neotest-dotnet/framework-discovery.lua b/lua/neotest-dotnet/framework-discovery.lua index 1ecf8a8..da8ce7b 100644 --- a/lua/neotest-dotnet/framework-discovery.lua +++ b/lua/neotest-dotnet/framework-discovery.lua @@ -24,6 +24,7 @@ M.mstest_test_attributes = { M.specflow_test_attributes = { "SkippableFactAttribute", + "Xunit.SkippableFactAttribute", "TestMethodAttribute", "TestAttribute", "NUnit.Framework.TestAttribute", diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua index d4639d5..21465b8 100644 --- a/lua/neotest-dotnet/xunit/init.lua +++ b/lua/neotest-dotnet/xunit/init.lua @@ -181,13 +181,6 @@ M.generate_test_results = function(output_file_path, tree, context_id) local parsed_data = TrxUtils.parse_trx(output_file_path) local test_results = parsed_data.TestRun and parsed_data.TestRun.Results - logger.info( - "neotest-dotnet: Found " - .. #test_results - .. " test results when parsing TRX file: " - .. output_file_path - ) - logger.debug("neotest-dotnet: TRX Results Output for" .. output_file_path .. ": ") logger.debug(test_results) From 6e24029d4006feac6d69e43ff5302c926c303de9 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Fri, 16 Feb 2024 21:26:49 +0000 Subject: [PATCH 14/17] fix(dotnet-test): Fixes specflow for xunit --- lua/neotest-dotnet/nunit/init.lua | 7 ------ lua/neotest-dotnet/xunit/init.lua | 40 ++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/lua/neotest-dotnet/nunit/init.lua b/lua/neotest-dotnet/nunit/init.lua index dff0cc4..d4ea761 100644 --- a/lua/neotest-dotnet/nunit/init.lua +++ b/lua/neotest-dotnet/nunit/init.lua @@ -166,13 +166,6 @@ M.generate_test_results = function(output_file_path, tree, context_id) local test_results = parsed_data.TestRun and parsed_data.TestRun.Results local test_definitions = parsed_data.TestRun and parsed_data.TestRun.TestDefinitions - logger.info( - "neotest-dotnet: Found " - .. #test_results - .. " test results when parsing TRX file: " - .. output_file_path - ) - logger.debug("neotest-dotnet: NUnit TRX Results Output for" .. output_file_path .. ": ") logger.debug(test_results) diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua index 21465b8..218704c 100644 --- a/lua/neotest-dotnet/xunit/init.lua +++ b/lua/neotest-dotnet/xunit/init.lua @@ -1,6 +1,7 @@ local logger = require("neotest.logging") local lib = require("neotest.lib") local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") +local BuildSpecUtils = require("neotest-dotnet.utils.build-spec-utils") local types = require("neotest.types") local NodeTreeUtils = require("neotest-dotnet.utils.neotest-node-tree-utils") local TrxUtils = require("neotest-dotnet.utils.trx-utils") @@ -180,6 +181,7 @@ end M.generate_test_results = function(output_file_path, tree, context_id) local parsed_data = TrxUtils.parse_trx(output_file_path) local test_results = parsed_data.TestRun and parsed_data.TestRun.Results + local test_definitions = parsed_data.TestRun and parsed_data.TestRun.TestDefinitions logger.debug("neotest-dotnet: TRX Results Output for" .. output_file_path .. ": ") logger.debug(test_results) @@ -195,6 +197,9 @@ M.generate_test_results = function(output_file_path, tree, context_id) if #test_results.UnitTestResult > 1 then test_results = test_results.UnitTestResult end + if #test_definitions.UnitTest > 1 then + test_definitions = test_definitions.UnitTest + end intermediate_results = {} @@ -206,6 +211,35 @@ M.generate_test_results = function(output_file_path, tree, context_id) } for _, value in pairs(test_results) do + local qualified_test_name + + if value._attr.testId ~= nil then + for _, test_definition in pairs(test_definitions) do + if test_definition._attr.id ~= nil then + if value._attr.testId == test_definition._attr.id then + local dot_index = string.find(test_definition._attr.name, "%.") + local bracket_index = string.find(test_definition._attr.name, "%(") + if dot_index ~= nil and (bracket_index == nil or dot_index < bracket_index) then + qualified_test_name = test_definition._attr.name + else + -- For Specflow tests, the will be an inline DisplayName attribute. + -- This wrecks the test name for us, so we need to use the ClassName and + -- MethodName attributes to get the full test name to use when comparing the results with the node name. + if bracket_index == nil then + qualified_test_name = test_definition.TestMethod._attr.className + .. "." + .. test_definition.TestMethod._attr.name + else + qualified_test_name = test_definition.TestMethod._attr.className + .. "." + .. test_definition._attr.name + end + end + end + end + end + end + if value._attr.testName ~= nil then local error_info local outcome = outcome_mapper[value._attr.outcome] @@ -219,6 +253,7 @@ M.generate_test_results = function(output_file_path, tree, context_id) status = string.lower(outcome), raw_output = value.Output and value.Output.StdOut or outcome, test_name = value._attr.testName, + qualified_test_name = qualified_test_name, error_info = error_info, } table.insert(intermediate_results, intermediate_result) @@ -244,7 +279,10 @@ M.generate_test_results = function(output_file_path, tree, context_id) for _, node in ipairs(test_nodes) do local node_data = node:data() - if intermediate_result.test_name == node_data.full_name then + if + intermediate_result.test_name == node_data.full_name + or intermediate_result.qualified_test_name == BuildSpecUtils.build_test_fqn(node_data.id) + then -- For non-inlined parameterized tests, check if we already have an entry for the test. -- If so, we need to check for a failure, and ensure the entire group of tests is marked as failed. neotest_results[node_data.id] = neotest_results[node_data.id] From ed70202801619e2248f83698bcf8ebb22e7fc035 Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Fri, 16 Feb 2024 21:33:19 +0000 Subject: [PATCH 15/17] fix(dotnet-test): Updates mstest framework utils --- lua/neotest-dotnet/mstest/init.lua | 273 +++++++++++++++++++++++++++-- 1 file changed, 260 insertions(+), 13 deletions(-) diff --git a/lua/neotest-dotnet/mstest/init.lua b/lua/neotest-dotnet/mstest/init.lua index 96b5901..4915092 100644 --- a/lua/neotest-dotnet/mstest/init.lua +++ b/lua/neotest-dotnet/mstest/init.lua @@ -1,34 +1,34 @@ local logger = require("neotest.logging") +local TrxUtils = require("neotest-dotnet.utils.trx-utils") +local NodeTreeUtils = require("neotest-dotnet.utils.neotest-node-tree-utils") ---@type FrameworkUtils +---@diagnostic disable-next-line: missing-fields local M = {} -function M.get_treesitter_queries(custom_attribute_args) - return require("neotest-dotnet.mstest.ts-queries").get_queries(custom_attribute_args) -end - ---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. ---@param base_node table The initial root node to build the positions from ----@param source any ----@param captured_nodes any +---@param source any The source code to build the positions from +---@param captured_nodes any The nodes captured by the TS query ---@param match_type string The type of node that was matched by the TS query ---@return table -M.build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) - logger.debug("neotest-dotnet(MS-Test Utils): Building parameterized test positions from source") - logger.debug("neotest-dotnet(MS-Test Utils): Base node: ") +local build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) + logger.debug("neotest-dotnet(MSTest Utils): Building parameterized test positions from source") + logger.debug("neotest-dotnet(MSTest Utils): Base node: ") logger.debug(base_node) - logger.debug("neotest-dotnet(MS-Test Utils): Match Type: " .. match_type) + logger.debug("neotest-dotnet(MSTest Utils): Match Type: " .. match_type) local query = [[ ;;query (attribute_list (attribute - name: (identifier) @attribute_name (#any-of? @attribute_name "DataRow") + name: (identifier) @attribute_name (#any-of? @attribute_name "TestCase") ((attribute_argument_list) @arguments) ) ) ]] + local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse("c_sharp", query) or vim.treesitter.parse_query("c_sharp", query) @@ -48,17 +48,112 @@ M.build_parameterized_test_positions = function(base_node, source, captured_node local args_text = vim.treesitter.get_node_text(args_node, source):gsub("[()]", "") nodes[#nodes + 1] = vim.tbl_extend("force", parameterized_test_node, { - name = parameterized_test_node.name .. " (" .. args_text .. ")", + name = parameterized_test_node.name .. "(" .. args_text .. ")", range = { args_node:range() }, }) end - logger.debug("neotest-dotnet(MS-Test Utils): Built parameterized test positions: ") + logger.debug("neotest-dotnet(MSTest Utils): Built parameterized test positions: ") logger.debug(nodes) return nodes end +local get_match_type = function(captured_nodes) + if captured_nodes["test.name"] then + return "test" + end + if captured_nodes["namespace.name"] then + return "namespace" + end + if captured_nodes["class.name"] then + return "class" + end + if captured_nodes["test.parameterized.name"] then + return "test.parameterized" + end +end + +function M.get_treesitter_queries(custom_attribute_args) + return require("neotest-dotnet.mstest.ts-queries").get_queries(custom_attribute_args) +end + +M.build_position = function(file_path, source, captured_nodes) + local match_type = get_match_type(captured_nodes) + + local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) + local definition = captured_nodes[match_type .. ".definition"] + + -- Introduce the C# concept of a "class" to the node, so we can distinguish between a class and a namespace. + -- Helps to determine if classes are nested, and therefore, if we need to modify the ID of the node (nested classes denoted by a '+' in C# test naming convention) + local is_class = match_type == "class" + + -- Swap the match type back to "namespace" so neotest core can handle it properly + if match_type == "class" then + match_type = "namespace" + end + + local node = { + type = match_type, + framework = "mstest", + is_class = is_class, + display_name = nil, + path = file_path, + name = name, + range = { definition:range() }, + } + + if match_type and match_type ~= "test.parameterized" then + return node + end + + return build_parameterized_test_positions(node, source, captured_nodes, match_type) +end + +M.position_id = function(position, parents) + local original_id = position.path + local has_parent_class = false + local sep = "::" + + -- Build the original ID from the parents, changing the separator to "+" if any nodes are nested classes + for _, node in ipairs(parents) do + if has_parent_class and node.is_class then + sep = "+" + end + + if node.is_class then + has_parent_class = true + end + + original_id = original_id .. sep .. node.name + end + + -- Add the final leaf nodes name to the ID, again changing the separator to "+" if it is a nested class + sep = "::" + if has_parent_class and position.is_class then + sep = "+" + end + original_id = original_id .. sep .. position.name + + -- Check to see if the position is a test case and contains parentheses (meaning it is parameterized) + -- If it is, remove the duplicated parent test name from the ID, so that when reading the trx test name + -- it will be the same as the test name in the test explorer + -- Example: + -- When ID is "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName::ParentTestName(TestName)" + -- Then we need it to be converted to "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName(TestName)" + if position.type == "test" and position.name:find("%(") then + local id_segments = {} + for _, segment in ipairs(vim.split(original_id, "::")) do + table.insert(id_segments, segment) + end + + table.remove(id_segments, #id_segments - 1) + return table.concat(id_segments, "::") + end + + return original_id +end + ---Modifies the tree using supplementary information from dotnet test -t or other methods ---@param tree neotest.Tree The tree to modify ---@param path string The path to the file the tree was built from @@ -66,4 +161,156 @@ M.post_process_tree_list = function(tree, path) return tree end +M.generate_test_results = function(output_file_path, tree, context_id) + local parsed_data = TrxUtils.parse_trx(output_file_path) + local test_results = parsed_data.TestRun and parsed_data.TestRun.Results + local test_definitions = parsed_data.TestRun and parsed_data.TestRun.TestDefinitions + + logger.debug("neotest-dotnet: MSTest TRX Results Output for" .. output_file_path .. ": ") + logger.debug(test_results) + + logger.debug("neotest-dotnet: MSTest TRX Test Definitions Output: ") + logger.debug(test_definitions) + + local test_nodes = NodeTreeUtils.get_test_nodes_data(tree) + + logger.debug("neotest-dotnet: MSTest test Nodes: ") + logger.debug(test_nodes) + + local intermediate_results + + if test_results and test_definitions then + if #test_results.UnitTestResult > 1 then + test_results = test_results.UnitTestResult + end + if #test_definitions.UnitTest > 1 then + test_definitions = test_definitions.UnitTest + end + + intermediate_results = {} + + local outcome_mapper = { + Passed = "passed", + Failed = "failed", + Skipped = "skipped", + NotExecuted = "skipped", + } + + for _, value in pairs(test_results) do + local qualified_test_name + + if value._attr.testId ~= nil then + for _, test_definition in pairs(test_definitions) do + if test_definition._attr.id ~= nil then + if value._attr.testId == test_definition._attr.id then + local dot_index = string.find(test_definition._attr.name, "%.") + local bracket_index = string.find(test_definition._attr.name, "%(") + if dot_index ~= nil and (bracket_index == nil or dot_index < bracket_index) then + qualified_test_name = test_definition._attr.name + else + -- Fix for https://github.com/Issafalcon/neotest-dotnet/issues/79 + -- Modifying display name property on non-parameterized tests gives the 'name' attribute + -- the value of the display name, so we need to use the TestMethod name instead + if bracket_index == nil then + qualified_test_name = test_definition.TestMethod._attr.className + .. "." + .. test_definition.TestMethod._attr.name + else + qualified_test_name = test_definition.TestMethod._attr.className + .. "." + .. test_definition._attr.name + end + end + end + end + end + end + + if value._attr.testName ~= nil then + local error_info + local outcome = outcome_mapper[value._attr.outcome] + local has_errors = value.Output and value.Output.ErrorInfo or nil + + if has_errors and outcome == "failed" then + local stackTrace = value.Output.ErrorInfo.StackTrace or "" + error_info = value.Output.ErrorInfo.Message .. "\n" .. stackTrace + end + local intermediate_result = { + status = string.lower(outcome), + raw_output = value.Output and value.Output.StdOut or outcome, + test_name = qualified_test_name, + error_info = error_info, + } + table.insert(intermediate_results, intermediate_result) + end + end + end + + -- No test results. Something went wrong. Check for runtime error + if not intermediate_results then + local run_outcome = {} + run_outcome[context_id] = { + status = "failed", + } + return run_outcome + end + + logger.debug("neotest-dotnet: Intermediate Results: ") + logger.debug(intermediate_results) + + local neotest_results = {} + + for _, intermediate_result in ipairs(intermediate_results) do + for _, node in ipairs(test_nodes) do + local node_data = node:data() + -- The test name from the trx file uses the namespace to fully qualify the test name + local result_test_name = intermediate_result.test_name + + local is_dynamically_parameterized = #node:children() == 0 + and not string.find(node_data.name, "%(.*%)") + + if is_dynamically_parameterized then + -- Remove dynamically generated arguments as they are not in node_data + result_test_name = string.gsub(result_test_name, "%(.*%)", "") + end + + -- Use the full_name of the test, including namespace + local is_match = #result_test_name == #node_data.full_name + and string.find(result_test_name, node_data.full_name, 0, true) + + if is_match then + -- For non-inlined parameterized tests, check if we already have an entry for the test. + -- If so, we need to check for a failure, and ensure the entire group of tests is marked as failed. + neotest_results[node_data.id] = neotest_results[node_data.id] + or { + status = intermediate_result.status, + short = node_data.full_name .. ":" .. intermediate_result.status, + errors = {}, + } + + if intermediate_result.status == "failed" then + -- Mark as failed for the whole thing + neotest_results[node_data.id].status = "failed" + neotest_results[node_data.id].short = node_data.full_name .. ":failed" + end + + if intermediate_result.error_info then + table.insert(neotest_results[node_data.id].errors, { + message = intermediate_result.test_name .. ": " .. intermediate_result.error_info, + }) + + -- Mark as failed + neotest_results[node_data.id].status = "failed" + end + + break + end + end + end + + logger.debug("neotest-dotnet: MSTest Neotest Results after conversion of Intermediate Results: ") + logger.debug(neotest_results) + + return neotest_results +end return M From afdd1d4f54fc8e9ec6aeb7c7f5138fad97cdaf9a Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Thu, 22 Feb 2024 16:41:02 +0000 Subject: [PATCH 16/17] fix(dotnet-test): Fixes discover_positions tests --- lua/neotest-dotnet/xunit/ts-queries.lua | 3 + .../classdata_attribute_spec.lua | 84 +++++++++++++++--- .../custom_attribute_spec.lua | 26 ++++++ .../fact_attribute_spec.lua | 34 ++++--- .../theory_attribute_spec.lua | 88 +++++++++++++------ 5 files changed, 186 insertions(+), 49 deletions(-) diff --git a/lua/neotest-dotnet/xunit/ts-queries.lua b/lua/neotest-dotnet/xunit/ts-queries.lua index 1fbbe14..5cf115d 100644 --- a/lua/neotest-dotnet/xunit/ts-queries.lua +++ b/lua/neotest-dotnet/xunit/ts-queries.lua @@ -21,6 +21,9 @@ function M.get_queries(custom_attributes) name: (identifier) @attribute_name (#any-of? @attribute_name "Fact" "ClassData" ]] .. custom_fact_attributes .. [[) (attribute_argument_list (attribute_argument + (name_equals + (identifier) @property_name (#match? @property_name "DisplayName$") + ) (string_literal (string_literal_fragment) @display_name ) diff --git a/tests/xunit/discover_positions/classdata_attribute_spec.lua b/tests/xunit/discover_positions/classdata_attribute_spec.lua index 5bc353b..ecbe05a 100644 --- a/tests/xunit/discover_positions/classdata_attribute_spec.lua +++ b/tests/xunit/discover_positions/classdata_attribute_spec.lua @@ -1,5 +1,7 @@ local async = require("nio").tests local plugin = require("neotest-dotnet") +local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") +local stub = require("luassert.stub") A = function(...) print(vim.inspect(...)) @@ -12,6 +14,28 @@ describe("discover_positions", function() }, }) + before_each(function() + stub(DotnetUtils, "get_test_full_names", function() + return { + is_complete = true, + result = function() + return { + output = { + "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: 1, v2: 2)", + "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -4, v2: 6)", + "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -2, v2: 2)", + }, + result_code = 0, + } + end, + } + end) + end) + + after_each(function() + DotnetUtils.get_test_full_names:revert() + end) + async.it( "should discover tests with classdata attribute without creating nested parameterized tests", function() @@ -22,39 +46,79 @@ describe("discover_positions", function() local function get_expected_output(file_path, file_name) return { { - id = file_path, - name = file_name, - path = file_path, + id = "./tests/xunit/specs/classdata.cs", + name = "classdata.cs", + path = "./tests/xunit/specs/classdata.cs", range = { 0, 0, 28, 0 }, type = "file", }, { { - id = file_path .. "::XUnitSamples", + framework = "xunit", + id = "./tests/xunit/specs/classdata.cs::XUnitSamples", is_class = false, name = "XUnitSamples", - path = file_path, + path = "./tests/xunit/specs/classdata.cs", range = { 4, 0, 27, 1 }, type = "namespace", }, { { - id = file_path .. "::XUnitSamples::ClassDataTests", + framework = "xunit", + id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests", is_class = true, name = "ClassDataTests", - path = file_path, + path = "./tests/xunit/specs/classdata.cs", range = { 6, 0, 15, 1 }, type = "namespace", }, { { - id = file_path .. "::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test", + framework = "xunit", + id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test", is_class = false, - name = "Theory_With_Class_Data_Test", - path = file_path, + name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test", + path = "./tests/xunit/specs/classdata.cs", range = { 8, 1, 14, 2 }, + running_id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test", type = "test", }, + { + { + framework = "xunit", + id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test(v1: 1, v2: 2)", + is_class = false, + name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: 1, v2: 2)", + path = "./tests/xunit/specs/classdata.cs", + range = { 9, 1, 9, 2 }, + running_id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test", + type = "test", + }, + }, + { + { + framework = "xunit", + id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test(v1: -4, v2: 6)", + is_class = false, + name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -4, v2: 6)", + path = "./tests/xunit/specs/classdata.cs", + range = { 10, 1, 10, 2 }, + running_id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test", + type = "test", + }, + }, + { + { + framework = "xunit", + id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test(v1: -2, v2: 2)", + is_class = false, + name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -2, v2: 2)", + path = "./tests/xunit/specs/classdata.cs", + range = { 11, 1, 11, 2 }, + running_id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test", + type = "test", + }, + }, }, }, }, diff --git a/tests/xunit/discover_positions/custom_attribute_spec.lua b/tests/xunit/discover_positions/custom_attribute_spec.lua index ecc51d4..aa191dd 100644 --- a/tests/xunit/discover_positions/custom_attribute_spec.lua +++ b/tests/xunit/discover_positions/custom_attribute_spec.lua @@ -1,5 +1,7 @@ local async = require("nio").tests local plugin = require("neotest-dotnet") +local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") +local stub = require("luassert.stub") A = function(...) print(vim.inspect(...)) @@ -16,6 +18,26 @@ describe("discover_positions", function() }, }) + before_each(function() + stub(DotnetUtils, "get_test_full_names", function() + return { + is_complete = true, + result = function() + return { + output = { + "XUnitSamples.CosmosConnectorTest.Custom_Attribute_Tests", + }, + result_code = 0, + } + end, + } + end) + end) + + after_each(function() + DotnetUtils.get_test_full_names:revert() + end) + async.it( "should discover tests with custom attribute when no other xUnit tests are present", function() @@ -34,6 +56,7 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = "./tests/xunit/specs/custom_attribute.cs::XUnitSamples", is_class = false, name = "XUnitSamples", @@ -43,6 +66,7 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = "./tests/xunit/specs/custom_attribute.cs::XUnitSamples::CosmosConnectorTest", is_class = true, name = "CosmosConnectorTest", @@ -52,6 +76,8 @@ describe("discover_positions", function() }, { { + display_name = "Custom attribute works ok", + framework = "xunit", id = "./tests/xunit/specs/custom_attribute.cs::XUnitSamples::CosmosConnectorTest::Custom_Attribute_Tests", is_class = false, name = "Custom_Attribute_Tests", diff --git a/tests/xunit/discover_positions/fact_attribute_spec.lua b/tests/xunit/discover_positions/fact_attribute_spec.lua index e2c9c0d..1b53044 100644 --- a/tests/xunit/discover_positions/fact_attribute_spec.lua +++ b/tests/xunit/discover_positions/fact_attribute_spec.lua @@ -14,20 +14,26 @@ describe("discover_positions", function() }, }) - stub(DotnetUtils, "get_test_full_names", function() - return { - is_complete = true, - result = function() - return { - output = { - "XUnitSamples.UnitTest1.Test1", - "XUnitSamples.UnitTest1+NestedClass.Test1", - "XUnitSamples.UnitTest1+NestedClass.Test2", - }, - result_code = 0, - } - end, - } + before_each(function() + stub(DotnetUtils, "get_test_full_names", function() + return { + is_complete = true, + result = function() + return { + output = { + "XUnitSamples.UnitTest1.Test1", + "XUnitSamples.UnitTest1+NestedClass.Test1", + "XUnitSamples.UnitTest1+NestedClass.Test2", + }, + result_code = 0, + } + end, + } + end) + end) + + after_each(function() + DotnetUtils.get_test_full_names:revert() end) async.it("should discover single tests in sub-class", function() diff --git a/tests/xunit/discover_positions/theory_attribute_spec.lua b/tests/xunit/discover_positions/theory_attribute_spec.lua index 22afe71..d13136a 100644 --- a/tests/xunit/discover_positions/theory_attribute_spec.lua +++ b/tests/xunit/discover_positions/theory_attribute_spec.lua @@ -1,6 +1,7 @@ local async = require("nio").tests local plugin = require("neotest-dotnet") -local Tree = require("neotest.types").Tree +local DotnetUtils = require("neotest-dotnet.utils.dotnet-utils") +local stub = require("luassert.stub") A = function(...) print(vim.inspect(...)) @@ -13,74 +14,106 @@ describe("discover_positions", function() }, }) + before_each(function() + stub(DotnetUtils, "get_test_full_names", function() + return { + is_complete = true, + result = function() + return { + output = { + "xunit.testproj1.UnitTest1.Test1", + "xunit.testproj1.UnitTest1.Test2(a: 1)", + "xunit.testproj1.UnitTest1.Test2(a: 2)", + }, + result_code = 0, + } + end, + } + end) + end) + + after_each(function() + DotnetUtils.get_test_full_names:revert() + end) + async.it("should discover tests with inline parameters", function() local spec_file = "./tests/xunit/specs/theory_and_fact_mixed.cs" local spec_file_name = "theory_and_fact_mixed.cs" local positions = plugin.discover_positions(spec_file):to_list() - local function get_expected_output(file_path, file_name) + local function get_expected_output() return { { - id = file_path, - name = file_name, - path = file_path, + id = "./tests/xunit/specs/theory_and_fact_mixed.cs", + name = "theory_and_fact_mixed.cs", + path = "./tests/xunit/specs/theory_and_fact_mixed.cs", range = { 0, 0, 18, 0 }, type = "file", }, { { - id = file_path .. "::xunit.testproj1", + framework = "xunit", + id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1", is_class = false, name = "xunit.testproj1", - path = file_path, + path = "./tests/xunit/specs/theory_and_fact_mixed.cs", range = { 0, 0, 17, 1 }, type = "namespace", }, { { - id = file_path .. "::xunit.testproj1::UnitTest1", + framework = "xunit", + id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1", is_class = true, name = "UnitTest1", - path = file_path, + path = "./tests/xunit/specs/theory_and_fact_mixed.cs", range = { 2, 0, 17, 1 }, type = "namespace", }, { { - id = file_path .. "::xunit.testproj1::UnitTest1::Test1", + framework = "xunit", + id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1::Test1", is_class = false, - name = "Test1", - path = file_path, + name = "xunit.testproj1.UnitTest1.Test1", + path = "./tests/xunit/specs/theory_and_fact_mixed.cs", range = { 4, 1, 8, 2 }, + running_id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1::Test1", type = "test", }, }, { { - id = file_path .. "::xunit.testproj1::UnitTest1::Test2", + framework = "xunit", + id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1::Test2", is_class = false, - name = "Test2", - path = file_path, + name = "xunit.testproj1.UnitTest1.Test2", + path = "./tests/xunit/specs/theory_and_fact_mixed.cs", range = { 10, 1, 16, 2 }, + running_id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1::Test2", type = "test", }, { { - id = file_path .. "::xunit.testproj1::UnitTest1::Test2(a: 1)", + framework = "xunit", + id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit::testproj1::UnitTest1::Test2(a: 1)", is_class = false, - name = "Test2(a: 1)", - path = file_path, - range = { 11, 12, 11, 15 }, + name = "xunit.testproj1.UnitTest1.Test2(a: 1)", + path = "./tests/xunit/specs/theory_and_fact_mixed.cs", + range = { 11, 1, 11, 2 }, + running_id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1::Test2", type = "test", }, }, { { - id = file_path .. "::xunit.testproj1::UnitTest1::Test2(a: 2)", + framework = "xunit", + id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit::testproj1::UnitTest1::Test2(a: 2)", is_class = false, - name = "Test2(a: 2)", - path = file_path, - range = { 12, 12, 12, 15 }, + name = "xunit.testproj1.UnitTest1.Test2(a: 2)", + path = "./tests/xunit/specs/theory_and_fact_mixed.cs", + range = { 12, 1, 12, 2 }, + running_id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1::Test2", type = "test", }, }, @@ -90,7 +123,7 @@ describe("discover_positions", function() } end - assert.same(positions, get_expected_output(spec_file, spec_file_name)) + assert.same(positions, get_expected_output()) end) async.it("should discover tests in block scoped namespace", function() @@ -107,6 +140,7 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = "./tests/xunit/specs/block_scoped_namespace.cs::xunit.testproj1", is_class = false, name = "xunit.testproj1", @@ -116,6 +150,7 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = "./tests/xunit/specs/block_scoped_namespace.cs::xunit.testproj1::UnitTest1", is_class = true, name = "UnitTest1", @@ -125,17 +160,20 @@ describe("discover_positions", function() }, { { + framework = "xunit", id = "./tests/xunit/specs/block_scoped_namespace.cs::xunit.testproj1::UnitTest1::Test1", is_class = false, - name = "Test1", + name = "xunit.testproj1.UnitTest1.Test1", path = "./tests/xunit/specs/block_scoped_namespace.cs", range = { 4, 2, 8, 3 }, + running_id = "./tests/xunit/specs/block_scoped_namespace.cs::xunit.testproj1::UnitTest1::Test1", type = "test", }, }, }, }, } + assert.same(positions, expected_positions) end) end) From 4f156ea04aabc0a26ccc669dc1cec8a16ef5671b Mon Sep 17 00:00:00 2001 From: Adam Figgins Date: Thu, 22 Feb 2024 17:02:24 +0000 Subject: [PATCH 17/17] chore(dotnet-tests): Removing result_utils_tests --- tests/utils/mock_data/test_node_mocks.lua | 113 ------- tests/utils/mock_data/trx_results_mocks.lua | 331 -------------------- tests/utils/result_utils_spec.lua | 174 ---------- 3 files changed, 618 deletions(-) delete mode 100644 tests/utils/mock_data/test_node_mocks.lua delete mode 100644 tests/utils/mock_data/trx_results_mocks.lua delete mode 100644 tests/utils/result_utils_spec.lua diff --git a/tests/utils/mock_data/test_node_mocks.lua b/tests/utils/mock_data/test_node_mocks.lua deleted file mode 100644 index 6009023..0000000 --- a/tests/utils/mock_data/test_node_mocks.lua +++ /dev/null @@ -1,113 +0,0 @@ -local M = {} - ----@type TestNodeMockData -M.xunit_classdata_tests_simple = { - node_list = { - { - id = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs::XUnitSamples::ClassDataTests", - is_class = true, - name = "ClassDataTests", - path = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs", - range = { 6, 0, 15, 1 }, - type = "namespace", - }, - { - { - full_name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test", - id = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test", - is_class = false, - name = "Theory_With_Class_Data_Test", - path = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs", - range = { 8, 1, 14, 2 }, - type = "test", - }, - }, - }, - intermediate_results = { - { - error_info = "Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(Int32 v1, Int32 v2) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs:line 14", - raw_output = "failed", - status = "failed", - test_name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -2, v2: 2)", - }, - { - error_info = "Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(Int32 v1, Int32 v2) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs:line 14", - raw_output = "failed", - status = "failed", - test_name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -4, v2: 6)", - }, - { - raw_output = "passed", - status = "passed", - test_name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: 1, v2: 2)", - }, - }, -} - ----@type TestNodeMockData -M.xunit_parameterized_tests_simple = { - node_list = { - { - id = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs::XUnitSamples::ParameterizedTests::Test1", - full_name = "XUnitSamples.ParameterizedTests.Test1", - is_class = false, - name = "Test1", - path = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs", - range = { 6, 1, 13, 2 }, - type = "test", - }, - { - { - id = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs::XUnitSamples::ParameterizedTests::Test1(value: 1)", - is_class = false, - full_name = "XUnitSamples.ParameterizedTests.Test1(value: 1)", - name = "Test1(value: 1)", - path = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs", - range = { 7, 12, 7, 15 }, - type = "test", - }, - }, - { - { - id = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs::XUnitSamples::ParameterizedTests::Test1(value: 2)", - is_class = false, - full_name = "XUnitSamples.ParameterizedTests.Test1(value: 2)", - name = "Test1(value: 2)", - path = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs", - range = { 8, 12, 8, 15 }, - type = "test", - }, - }, - { - { - id = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs::XUnitSamples::ParameterizedTests::Test1(value: 3)", - is_class = false, - full_name = "XUnitSamples.ParameterizedTests.Test1(value: 3)", - name = "Test1(value: 3)", - path = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs", - range = { 9, 12, 9, 15 }, - type = "test", - }, - }, - }, - intermediate_results = { - { - raw_output = "passed", - status = "passed", - test_name = "XUnitSamples.ParameterizedTests.Test1(value: 1)", - }, - { - error_info = "Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ParameterizedTests.Test1(Int32 value) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs:line 13", - raw_output = "failed", - status = "failed", - test_name = "XUnitSamples.ParameterizedTests.Test1(value: 3)", - }, - { - raw_output = "passed", - status = "passed", - test_name = "XUnitSamples.ParameterizedTests.Test1(value: 2)", - }, - }, -} - -return M diff --git a/tests/utils/mock_data/trx_results_mocks.lua b/tests/utils/mock_data/trx_results_mocks.lua deleted file mode 100644 index d1169e1..0000000 --- a/tests/utils/mock_data/trx_results_mocks.lua +++ /dev/null @@ -1,331 +0,0 @@ -local M = {} - ----@type TrxMockData -M.xunit_simple_tests_with_displayname = { - trx_results = { - { - _attr = { - computerName = "pop-os", - duration = "00:00:00.0013299", - endTime = "2023-12-22T12:01:39.7370745+00:00", - executionId = "085adb48-a338-48ff-adbb-ea049c75f2ba", - outcome = "Passed", - relativeResultsDirectory = "085adb48-a338-48ff-adbb-ea049c75f2ba", - startTime = "2023-12-22T12:01:39.7370579+00:00", - testId = "930bb084-fb2f-ffb7-1091-b596a36c2fbd", - testListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d", - testName = "XUnitSamples.SingleTests.Test1", - testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", - }, - }, - { - Output = { - ErrorInfo = { - Message = "Assert.True() Failure\nExpected: True\nActual: False", - StackTrace = "at XUnitSamples.SingleTests.Test2() in /home/adam/repos/learning-dotnet/UnitTesting/XUnitSamples/SingleTests.cs:line 16\n at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)\n at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)", - }, - }, - _attr = { - computerName = "pop-os", - duration = "00:00:00.0007628", - endTime = "2023-12-22T12:01:39.7435917+00:00", - executionId = "0fdd90c3-a8d6-435e-a1dd-c3e1a52faf1e", - outcome = "Failed", - relativeResultsDirectory = "0fdd90c3-a8d6-435e-a1dd-c3e1a52faf1e", - startTime = "2023-12-22T12:01:39.7435917+00:00", - testId = "16e96eb4-1d8b-55da-3b5e-4a91699ccbb9", - testListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d", - testName = "SomethingElse", - testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", - }, - }, - }, - trx_test_definitions = { - { - Execution = { - _attr = { - id = "03c145a9-6b82-48b5-b6e1-ca47bc753473", - }, - }, - TestMethod = { - _attr = { - adapterTypeName = "executor://xunit/VsTestRunner2/netcoreapp", - className = "XUnitSamples.SingleTests", - codeBase = "/home/adam/repos/learning-dotnet/UnitTesting/XUnitSamples/bin/Debug/net7.0/XUnitSamples.dll", - name = "Test1", - }, - }, - _attr = { - id = "930bb084-fb2f-ffb7-1091-b596a36c2fbd", - name = "XUnitSamples.SingleTests.Test1", - storage = "/home/adam/repos/learning-dotnet/unittesting/xunitsamples/bin/debug/net7.0/xunitsamples.dll", - }, - }, - { - Execution = { - _attr = { - id = "796d3744-4e0a-4484-b20e-c0dd54421ae7", - }, - }, - TestMethod = { - _attr = { - adapterTypeName = "executor://xunit/VsTestRunner2/netcoreapp", - className = "XUnitSamples.SingleTests", - codeBase = "/home/adam/repos/learning-dotnet/UnitTesting/XUnitSamples/bin/Debug/net7.0/XUnitSamples.dll", - name = "Test2", - }, - }, - _attr = { - id = "16e96eb4-1d8b-55da-3b5e-4a91699ccbb9", - name = "SomethingElse", - storage = "/home/adam/repos/learning-dotnet/unittesting/xunitsamples/bin/debug/net7.0/xunitsamples.dll", - }, - }, - }, -} - ----@type TrxMockData -M.xunit_classdata_tests_simple = { - trx_results = { - { - Output = { - ErrorInfo = { - Message = "Assert.True() Failure\nExpected: True\nActual: False", - StackTrace = "at XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(Int32 v1, Int32 v2) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs:line 14", - }, - }, - _attr = { - computerName = "pop-os", - duration = "00:00:00.0018117", - endTime = "2023-06-04T16:57:09.8954313+01:00", - executionId = "671ed74f-42af-41b8-af88-d033def88221", - outcome = "Failed", - relativeResultsDirectory = "671ed74f-42af-41b8-af88-d033def88221", - startTime = "2023-06-04T16:57:09.8954091+01:00", - testId = "c01dbb41-e5dc-3e02-6d99-5f9c0dc3a7a0", - testListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d", - testName = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -2, v2: 2)", - testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", - }, - }, - { - _attr = { - computerName = "pop-os", - duration = "00:00:00.0008118", - endTime = "2023-06-04T16:57:09.8987894+01:00", - executionId = "05fc0384-54fe-4d2a-ab37-9f3b4ffa3f89", - outcome = "Passed", - relativeResultsDirectory = "05fc0384-54fe-4d2a-ab37-9f3b4ffa3f89", - startTime = "2023-06-04T16:57:09.8987894+01:00", - testId = "0b2c52ec-acbf-6d07-4538-9af65da3289d", - testListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d", - testName = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: 1, v2: 2)", - testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", - }, - }, - { - Output = { - ErrorInfo = { - Message = "Assert.True() Failure\nExpected: True\nActual: False", - StackTrace = "at XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(Int32 v1, Int32 v2) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs:line 14", - }, - }, - _attr = { - computerName = "pop-os", - duration = "00:00:00.0001265", - endTime = "2023-06-04T16:57:09.8990762+01:00", - executionId = "c1cda8ac-257f-45d2-9b6a-3278e919a2f9", - outcome = "Failed", - relativeResultsDirectory = "c1cda8ac-257f-45d2-9b6a-3278e919a2f9", - startTime = "2023-06-04T16:57:09.8990761+01:00", - testId = "ee4d605d-f7ca-954f-c294-c06c86468395", - testListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d", - testName = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -4, v2: 6)", - testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", - }, - }, - }, - trx_test_definitions = { - { - Execution = { - _attr = { - id = "05fc0384-54fe-4d2a-ab37-9f3b4ffa3f89", - }, - }, - TestMethod = { - _attr = { - adapterTypeName = "executor://xunit/VsTestRunner2/netcoreapp", - className = "XUnitSamples.ClassDataTests", - codeBase = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/bin/Debug/net6.0/XUnitSamples.dll", - name = "Theory_With_Class_Data_Test", - }, - }, - _attr = { - id = "0b2c52ec-acbf-6d07-4538-9af65da3289d", - name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: 1, v2: 2)", - storage = "/home/issafalcon/repos/learning-dotnet/unittesting/xunitsamples/bin/debug/net6.0/xunitsamples.dll", - }, - }, - { - Execution = { - _attr = { - id = "671ed74f-42af-41b8-af88-d033def88221", - }, - }, - TestMethod = { - _attr = { - adapterTypeName = "executor://xunit/VsTestRunner2/netcoreapp", - className = "XUnitSamples.ClassDataTests", - codeBase = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/bin/Debug/net6.0/XUnitSamples.dll", - name = "Theory_With_Class_Data_Test", - }, - }, - _attr = { - id = "c01dbb41-e5dc-3e02-6d99-5f9c0dc3a7a0", - name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -2, v2: 2)", - storage = "/home/issafalcon/repos/learning-dotnet/unittesting/xunitsamples/bin/debug/net6.0/xunitsamples.dll", - }, - }, - { - Execution = { - _attr = { - id = "c1cda8ac-257f-45d2-9b6a-3278e919a2f9", - }, - }, - TestMethod = { - _attr = { - adapterTypeName = "executor://xunit/VsTestRunner2/netcoreapp", - className = "XUnitSamples.ClassDataTests", - codeBase = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/bin/Debug/net6.0/XUnitSamples.dll", - name = "Theory_With_Class_Data_Test", - }, - }, - _attr = { - id = "ee4d605d-f7ca-954f-c294-c06c86468395", - name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -4, v2: 6)", - storage = "/home/issafalcon/repos/learning-dotnet/unittesting/xunitsamples/bin/debug/net6.0/xunitsamples.dll", - }, - }, - }, -} - ----@type TrxMockData -M.xunit_parameterized_tests_simple = { - trx_results = { - { - _attr = { - computerName = "pop-os", - duration = "00:00:00.0005921", - endTime = "2023-06-04T11:01:11.1960917+01:00", - executionId = "3c44b8df-784b-452d-866c-eea8c4189a5e", - outcome = "Passed", - relativeResultsDirectory = "3c44b8df-784b-452d-866c-eea8c4189a5e", - startTime = "2023-06-04T11:01:11.1960917+01:00", - testId = "e7aa3325-3472-fbf8-0fa4-7d51c72d859e", - testListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d", - testName = "XUnitSamples.ParameterizedTests.Test1(value: 1)", - testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", - }, - }, - { - Output = { - ErrorInfo = { - Message = "Assert.True() Failure\nExpected: True\nActual: False", - StackTrace = "at XUnitSamples.ParameterizedTests.Test1(Int32 value) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs:line 13", - }, - }, - _attr = { - computerName = "pop-os", - duration = "00:00:00.0028019", - endTime = "2023-06-04T11:01:11.1927613+01:00", - executionId = "d0a7aae8-7f4d-48d3-8793-38e40e3f4d7f", - outcome = "Failed", - relativeResultsDirectory = "d0a7aae8-7f4d-48d3-8793-38e40e3f4d7f", - startTime = "2023-06-04T11:01:11.1927396+01:00", - testId = "26bda926-2c36-936a-59ed-45ab600f3b44", - testListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d", - testName = "XUnitSamples.ParameterizedTests.Test1(value: 3)", - testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", - }, - }, - { - _attr = { - computerName = "pop-os", - duration = "00:00:00.0000046", - endTime = "2023-06-04T11:01:11.1962139+01:00", - executionId = "128f4f9e-f109-4904-9094-2676a497a3fe", - outcome = "Passed", - relativeResultsDirectory = "128f4f9e-f109-4904-9094-2676a497a3fe", - startTime = "2023-06-04T11:01:11.1962138+01:00", - testId = "29e20a39-c8e0-24b5-0775-a975ac621461", - testListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d", - testName = "XUnitSamples.ParameterizedTests.Test1(value: 2)", - testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", - }, - }, - }, - trx_test_definitions = { - { - Execution = { - _attr = { - id = "d0a7aae8-7f4d-48d3-8793-38e40e3f4d7f", - }, - }, - TestMethod = { - _attr = { - adapterTypeName = "executor://xunit/VsTestRunner2/netcoreapp", - className = "XUnitSamples.ParameterizedTests", - codeBase = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/bin/Debug/net6.0/XUnitSamples.dll", - name = "Test1", - }, - }, - _attr = { - id = "26bda926-2c36-936a-59ed-45ab600f3b44", - name = "XUnitSamples.ParameterizedTests.Test1(value: 3)", - storage = "/home/issafalcon/repos/learning-dotnet/unittesting/xunitsamples/bin/debug/net6.0/xunitsamples.dll", - }, - }, - { - Execution = { - _attr = { - id = "128f4f9e-f109-4904-9094-2676a497a3fe", - }, - }, - TestMethod = { - _attr = { - adapterTypeName = "executor://xunit/VsTestRunner2/netcoreapp", - className = "XUnitSamples.ParameterizedTests", - codeBase = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/bin/Debug/net6.0/XUnitSamples.dll", - name = "Test1", - }, - }, - _attr = { - id = "29e20a39-c8e0-24b5-0775-a975ac621461", - name = "XUnitSamples.ParameterizedTests.Test1(value: 2)", - storage = "/home/issafalcon/repos/learning-dotnet/unittesting/xunitsamples/bin/debug/net6.0/xunitsamples.dll", - }, - }, - { - Execution = { - _attr = { - id = "3c44b8df-784b-452d-866c-eea8c4189a5e", - }, - }, - TestMethod = { - _attr = { - adapterTypeName = "executor://xunit/VsTestRunner2/netcoreapp", - className = "XUnitSamples.ParameterizedTests", - codeBase = "/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/bin/Debug/net6.0/XUnitSamples.dll", - name = "Test1", - }, - }, - _attr = { - id = "e7aa3325-3472-fbf8-0fa4-7d51c72d859e", - name = "XUnitSamples.ParameterizedTests.Test1(value: 1)", - storage = "/home/issafalcon/repos/learning-dotnet/unittesting/xunitsamples/bin/debug/net6.0/xunitsamples.dll", - }, - }, - }, -} - -return M diff --git a/tests/utils/result_utils_spec.lua b/tests/utils/result_utils_spec.lua deleted file mode 100644 index 7f87f6f..0000000 --- a/tests/utils/result_utils_spec.lua +++ /dev/null @@ -1,174 +0,0 @@ -local Tree = require("neotest.types").Tree -local trx_result_mocks = require("tests.utils.mock_data.trx_results_mocks") -local test_node_mocks = require("tests.utils.mock_data.test_node_mocks") -local neotest_node_tree_utils = require("neotest-dotnet.utils.neotest-node-tree-utils") - -A = function(...) - print(vim.inspect(...)) -end - -describe("create_intermediate_results xUnit", function() - local ResultUtils = require("neotest-dotnet.utils.result-utils") - - it( - "should create correct intermediate results from simple Fact tests when one has 'DisplayName' set", - function() - local expected_results = { - { - raw_output = "passed", - status = "passed", - test_name = "XUnitSamples.SingleTests.Test1", - }, - { - error_info = "Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.SingleTests.Test2() in /home/adam/repos/learning-dotnet/UnitTesting/XUnitSamples/SingleTests.cs:line 16\n at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)\n at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)", - raw_output = "failed", - status = "failed", - test_name = "XUnitSamples.SingleTests.Test2", - }, - } - - local actual_results = ResultUtils.create_intermediate_results( - trx_result_mocks.xunit_simple_tests_with_displayname.trx_results, - trx_result_mocks.xunit_simple_tests_with_displayname.trx_test_definitions - ) - - assert.are.same(expected_results, actual_results) - end - ) - - it( - "should create correct intermediate results from simple inlined parameterized tests", - function() - local expected_results = { - { - raw_output = "passed", - status = "passed", - test_name = "XUnitSamples.ParameterizedTests.Test1(value: 1)", - }, - { - error_info = "Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ParameterizedTests.Test1(Int32 value) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs:line 13", - raw_output = "failed", - status = "failed", - test_name = "XUnitSamples.ParameterizedTests.Test1(value: 3)", - }, - { - raw_output = "passed", - status = "passed", - test_name = "XUnitSamples.ParameterizedTests.Test1(value: 2)", - }, - } - local actual_results = ResultUtils.create_intermediate_results( - trx_result_mocks.xunit_parameterized_tests_simple.trx_results, - trx_result_mocks.xunit_parameterized_tests_simple.trx_test_definitions - ) - - assert.are.same(expected_results, actual_results) - end - ) - - it("should create correct intermediate results from simple ClassData tests", function() - local expected_results = { - { - error_info = "Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(Int32 v1, Int32 v2) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs:line 14", - raw_output = "failed", - status = "failed", - test_name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -2, v2: 2)", - }, - { - raw_output = "passed", - status = "passed", - test_name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: 1, v2: 2)", - }, - { - error_info = "Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(Int32 v1, Int32 v2) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs:line 14", - raw_output = "failed", - status = "failed", - test_name = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -4, v2: 6)", - }, - } - local actual_results = ResultUtils.create_intermediate_results( - trx_result_mocks.xunit_classdata_tests_simple.trx_results, - trx_result_mocks.xunit_classdata_tests_simple.trx_test_definitions - ) - - assert.are.same(expected_results, actual_results) - end) -end) - -describe("convert_intermediate_results xUnit", function() - local ResultUtils = require("neotest-dotnet.utils.result-utils") - - it( - "should correctly convert simple inline parameterized intermediate results to neotest-results", - function() - local test_tree = Tree.from_list( - test_node_mocks.xunit_parameterized_tests_simple.node_list, - function(pos) - return pos.id - end - ) - - local test_nodes = neotest_node_tree_utils.get_test_nodes_data(test_tree) - - local expected_results = { - ["/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs::XUnitSamples::ParameterizedTests::Test1(value: 1)"] = { - errors = {}, - short = "XUnitSamples.ParameterizedTests.Test1(value: 1):passed", - status = "passed", - }, - ["/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs::XUnitSamples::ParameterizedTests::Test1(value: 2)"] = { - errors = {}, - short = "XUnitSamples.ParameterizedTests.Test1(value: 2):passed", - status = "passed", - }, - ["/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs::XUnitSamples::ParameterizedTests::Test1(value: 3)"] = { - errors = { - { - message = "XUnitSamples.ParameterizedTests.Test1(value: 3): Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ParameterizedTests.Test1(Int32 value) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ParameterizedTests.cs:line 13", - }, - }, - short = "XUnitSamples.ParameterizedTests.Test1(value: 3):failed", - status = "failed", - }, - } - local actual_results = ResultUtils.convert_intermediate_results( - test_node_mocks.xunit_parameterized_tests_simple.intermediate_results, - test_nodes - ) - - assert.are.same(expected_results, actual_results) - end - ) - - it("should correctly convert simple ClassData intermediate results to neotest-results", function() - local test_tree = Tree.from_list( - test_node_mocks.xunit_classdata_tests_simple.node_list, - function(pos) - return pos.id - end - ) - - local test_nodes = neotest_node_tree_utils.get_test_nodes_data(test_tree) - - local expected_results = { - ["/home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test"] = { - errors = { - { - message = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -2, v2: 2): Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(Int32 v1, Int32 v2) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs:line 14", - }, - { - message = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(v1: -4, v2: 6): Assert.True() Failure\nExpected: True\nActual: False\nat XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test(Int32 v1, Int32 v2) in /home/issafalcon/repos/learning-dotnet/UnitTesting/XUnitSamples/ClassDataTests.cs:line 14", - }, - }, - short = "XUnitSamples.ClassDataTests.Theory_With_Class_Data_Test:failed", - status = "failed", - }, - } - local actual_results = ResultUtils.convert_intermediate_results( - test_node_mocks.xunit_classdata_tests_simple.intermediate_results, - test_nodes - ) - - assert.are.same(expected_results, actual_results) - end) -end)