From f86006629ca36fc2d30dbaed274a4af29ad5c16e Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 25 Sep 2024 22:49:16 +0200 Subject: [PATCH 01/43] initial commit --- lua/neotest-dotnet/framework-discovery.lua | 19 ++++-- lua/neotest-dotnet/init.lua | 61 +++++++++++-------- lua/neotest-dotnet/mstest/init.lua | 2 +- lua/neotest-dotnet/nunit/init.lua | 2 +- .../types/neotest-dotnet-types.lua | 2 +- lua/neotest-dotnet/xunit/init.lua | 4 +- lua/neotest-dotnet/xunit/ts-queries.lua | 30 +++++++-- 7 files changed, 79 insertions(+), 41 deletions(-) diff --git a/lua/neotest-dotnet/framework-discovery.lua b/lua/neotest-dotnet/framework-discovery.lua index da8ce7b..7076201 100644 --- a/lua/neotest-dotnet/framework-discovery.lua +++ b/lua/neotest-dotnet/framework-discovery.lua @@ -73,12 +73,12 @@ function M.join_test_attributes(attributes) return joined_attributes end -function M.get_test_framework_utils_from_source(source, custom_attribute_args) +function M.get_test_framework_utils_from_source(lang, 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 = [[ + local c_sharp_query = [[ (attribute name: (identifier) @attribute_name (#any-of? @attribute_name ]] .. xunit_attributes .. " " .. nunit_attributes .. " " .. mstest_attributes .. [[) ) @@ -96,11 +96,20 @@ function M.get_test_framework_utils_from_source(source, custom_attribute_args) ) ]] + local fsharp_query = [[ + (attribute + (simple_type + (long_identifier (identifier) @attribute_name (#any-of? @attribute_name ]] .. xunit_attributes .. " " .. nunit_attributes .. " " .. mstest_attributes .. [[))) + ) + ]] + + local framework_query = lang == "fsharp" and fsharp_query or c_sharp_query + async.scheduler() - local root = vim.treesitter.get_string_parser(source, "c_sharp"):parse()[1]:root() + local root = vim.treesitter.get_string_parser(source, lang):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) + and vim.treesitter.query.parse(lang, framework_query) + or vim.treesitter.parse_query(lang, 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) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 8ff806b..30a434b 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -28,7 +28,7 @@ DotnetNeotestAdapter.is_test_file = function(file_path) local all_attributes = FrameworkDiscovery.all_test_attributes for _, test_attribute in ipairs(all_attributes) do - if string.find(content, "%[" .. test_attribute) then + if string.find(content, "%[ Date: Wed, 25 Sep 2024 23:11:29 +0200 Subject: [PATCH 02/43] fix tests --- lua/neotest-dotnet/framework-discovery.lua | 6 +-- lua/neotest-dotnet/init.lua | 55 ++++++++++------------ 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/lua/neotest-dotnet/framework-discovery.lua b/lua/neotest-dotnet/framework-discovery.lua index 7076201..ca85d7c 100644 --- a/lua/neotest-dotnet/framework-discovery.lua +++ b/lua/neotest-dotnet/framework-discovery.lua @@ -110,10 +110,10 @@ function M.get_test_framework_utils_from_source(lang, source, custom_attribute_a local parsed_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse(lang, framework_query) or vim.treesitter.parse_query(lang, framework_query) - for _, captures in parsed_query:iter_matches(root, source) do + for _, node, _, _ in parsed_query:iter_captures(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) + and vim.treesitter.get_node_text(node, source) + or vim.treesitter.query.get_node_text(node, source) if test_attribute then if string.find(xunit_attributes, test_attribute) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 30a434b..42fdd75 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -61,8 +61,10 @@ DotnetNeotestAdapter._build_position = function(...) logger.debug("neotest-dotnet: Buil Position Args: ") logger.debug(args) + local lang = lib.files.match_root_pattern("*.fsproj")(args[1]) and "fsharp" or "c_sharp" + local framework = - FrameworkDiscovery.get_test_framework_utils_from_source(args[2], custom_attribute_args) -- args[2] is the content of the file + FrameworkDiscovery.get_test_framework_utils_from_source(lang, args[2], custom_attribute_args) -- args[2] is the content of the file logger.debug("neotest-dotnet: Framework: ") logger.debug(framework) @@ -80,41 +82,34 @@ end ---@param path any The path to the file to discover positions in ---@return neotest.Tree DotnetNeotestAdapter.discover_positions = function(path) - local lang = nil - - if lib.files.match_root_pattern("*.fsproj")(path) then - lang = "fsharp" - else - lang = "c_sharp" - end + local lang = lib.files.match_root_pattern("*.fsproj")(path) and "fsharp" or "c_sharp" local content = lib.files.read(path) local test_framework = FrameworkDiscovery.get_test_framework_utils_from_source(lang, content, custom_attribute_args) local framework_queries = test_framework.get_treesitter_queries(lang, custom_attribute_args) - -- local query = [[ - -- ;; --Namespaces - -- ;; Matches namespace with a '.' in the name - -- (namespace_declaration - -- name: (qualified_name) @namespace.name - -- ) @namespace.definition - -- - -- ;; Matches namespace with a single identifier (no '.') - -- (namespace_declaration - -- name: (identifier) @namespace.name - -- ) @namespace.definition - -- - -- ;; Matches file-scoped namespaces (qualified and unqualified respectively) - -- (file_scoped_namespace_declaration - -- name: (qualified_name) @namespace.name - -- ) @namespace.definition - -- - -- (file_scoped_namespace_declaration - -- name: (identifier) @namespace.name - -- ) @namespace.definition - -- ]] .. framework_queries - local query = framework_queries + local query = [[ + ;; --Namespaces + ;; Matches namespace with a '.' in the name + (namespace_declaration + name: (qualified_name) @namespace.name + ) @namespace.definition + + ;; Matches namespace with a single identifier (no '.') + (namespace_declaration + name: (identifier) @namespace.name + ) @namespace.definition + + ;; Matches file-scoped namespaces (qualified and unqualified respectively) + (file_scoped_namespace_declaration + name: (qualified_name) @namespace.name + ) @namespace.definition + + (file_scoped_namespace_declaration + name: (identifier) @namespace.name + ) @namespace.definition + ]] .. framework_queries local tree = lib.treesitter.parse_positions(path, query, { nested_namespaces = true, From 0ebd107e748ccb217ed9d46c0fddb4ad3bb9014d Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Thu, 26 Sep 2024 22:37:21 +0200 Subject: [PATCH 03/43] add xunit fact queries --- lua/neotest-dotnet/init.lua | 26 +++++++++--- lua/neotest-dotnet/utils/build-spec-utils.lua | 6 +-- .../utils/neotest-node-tree-utils.lua | 2 +- lua/neotest-dotnet/xunit/init.lua | 2 +- lua/neotest-dotnet/xunit/ts-queries.lua | 27 ++++++++++++- .../fact_attribute_spec.lua | 40 +++++++++++++++++++ tests/xunit/specs/fact_and_trait.fs | 7 ++++ 7 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 tests/xunit/specs/fact_and_trait.fs diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 42fdd75..d3b750c 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -9,11 +9,19 @@ local custom_attribute_args local dotnet_additional_args local discovery_root = "project" +require("plenary.filetype").add_table({ + extension = { + ["fs"] = [[fsharp]], + ["fsx"] = [[fsharp]], + ["fsi"] = [[fsharp]], + }, +}) + DotnetNeotestAdapter.root = function(path) if discovery_root == "solution" then return lib.files.match_root_pattern("*.sln")(path) else - return lib.files.match_root_pattern("*.csproj", "*.fsproj")(path) + return lib.files.match_root_pattern("*.[cf]sproj")(path) end end @@ -61,7 +69,7 @@ DotnetNeotestAdapter._build_position = function(...) logger.debug("neotest-dotnet: Buil Position Args: ") logger.debug(args) - local lang = lib.files.match_root_pattern("*.fsproj")(args[1]) and "fsharp" or "c_sharp" + local lang = lib.files.match_root_pattern("*.fsproj")(args[1]) and "fsharp" or "c_sharp" -- args[1] is the file path local framework = FrameworkDiscovery.get_test_framework_utils_from_source(lang, args[2], custom_attribute_args) -- args[2] is the content of the file @@ -82,14 +90,14 @@ end ---@param path any The path to the file to discover positions in ---@return neotest.Tree DotnetNeotestAdapter.discover_positions = function(path) - local lang = lib.files.match_root_pattern("*.fsproj")(path) and "fsharp" or "c_sharp" + local lang = vim.endswith(path, ".fs") and "fsharp" or "c_sharp" local content = lib.files.read(path) local test_framework = FrameworkDiscovery.get_test_framework_utils_from_source(lang, content, custom_attribute_args) local framework_queries = test_framework.get_treesitter_queries(lang, custom_attribute_args) - local query = [[ + local csharp_query = [[ ;; --Namespaces ;; Matches namespace with a '.' in the name (namespace_declaration @@ -109,7 +117,15 @@ DotnetNeotestAdapter.discover_positions = function(path) (file_scoped_namespace_declaration name: (identifier) @namespace.name ) @namespace.definition - ]] .. framework_queries + ]] + + local fsharp_query = [[ + (namespace + name: (long_identifier) @namespace.name + ) @namespace.definition + ]] + + local query = (lang == "fsharp" and fsharp_query or csharp_query) .. framework_queries local tree = lib.treesitter.parse_positions(path, query, { nested_namespaces = true, diff --git a/lua/neotest-dotnet/utils/build-spec-utils.lua b/lua/neotest-dotnet/utils/build-spec-utils.lua index b272dd0..f1f84b5 100644 --- a/lua/neotest-dotnet/utils/build-spec-utils.lua +++ b/lua/neotest-dotnet/utils/build-spec-utils.lua @@ -74,7 +74,7 @@ function BuildSpecUtils.create_specs(tree, specs, dotnet_additional_args) -- Adapted from https://github.com/nvim-neotest/neotest/blob/392808a91d6ee28d27cbfb93c9fd9781759b5d00/lua/neotest/lib/file/init.lua#L341 if position.type == "dir" then -- Check to see if we are in a project root - local proj_files = async.fn.glob(Path:new(position.path, "*.csproj").filename, true, true) + local proj_files = async.fn.glob(Path:new(position.path, "*.[cf]sproj").filename, true, true) logger.debug("neotest-dotnet: Found " .. #proj_files .. " project files in " .. position.path) if #proj_files >= 1 then @@ -101,12 +101,12 @@ function BuildSpecUtils.create_specs(tree, specs, dotnet_additional_args) 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) + local proj_root = lib.files.match_root_pattern("*.[cf]sproj")(position.path) local spec = BuildSpecUtils.create_single_spec(position, proj_root, filter, dotnet_additional_args) table.insert(specs, spec) elseif position.type == "file" then - local proj_root = lib.files.match_root_pattern("*.csproj")(position.path) + local proj_root = lib.files.match_root_pattern("*.[cf]sproj")(position.path) local spec = BuildSpecUtils.create_single_spec(position, proj_root, "", dotnet_additional_args) table.insert(specs, spec) diff --git a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua index 39e6991..2cb5ed2 100644 --- a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua +++ b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua @@ -6,7 +6,7 @@ local M = {} ---@param position_id string The position_id of the neotest test node ---@return string The fully qualified name of the test function M.get_qualified_test_name_from_id(position_id) - local _, first_colon_end = string.find(position_id, ".cs::") + local _, first_colon_end = string.find(position_id, ".[cf]s::") local full_name = string.sub(position_id, first_colon_end + 1) full_name = string.gsub(full_name, "::", ".") return full_name diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua index f57a2a5..ce1b299 100644 --- a/lua/neotest-dotnet/xunit/init.lua +++ b/lua/neotest-dotnet/xunit/init.lua @@ -93,7 +93,7 @@ end ---@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 proj_root = lib.files.match_root_pattern("*.[cf]sproj")(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() diff --git a/lua/neotest-dotnet/xunit/ts-queries.lua b/lua/neotest-dotnet/xunit/ts-queries.lua index 43885bd..f2cf0b6 100644 --- a/lua/neotest-dotnet/xunit/ts-queries.lua +++ b/lua/neotest-dotnet/xunit/ts-queries.lua @@ -4,15 +4,38 @@ local M = {} local function get_fsharp_queries(custom_fact_attributes) return [[ - ;; Matches test methods + ;; Matches XUnit test class (has no specific attributes on class) + (anon_type_defn + (type_name (identifier) @class.name) + ) @class.definition + + (named_module + name: (long_identifier) @class.name + ) @class.definition + + (module_defn + (identifier) @class.name + ) @class.definition + + ;; Matches test functions (declaration_expression (attributes (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Fact"))))) + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Fact" "ClassData" ]] .. custom_fact_attributes .. [[))))) (function_or_value_defn (function_declaration_left (identifier) @test.name)) ) @test.definition + + ;; Matches test functions + (member_defn + (attributes + (attribute + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Fact" "ClassData" ]] .. custom_fact_attributes .. [[))))) + (method_or_prop_defn + (property_or_ident + (identifier) @test.name .)) + ) @test.definition ]] end diff --git a/tests/xunit/discover_positions/fact_attribute_spec.lua b/tests/xunit/discover_positions/fact_attribute_spec.lua index 56c768e..e4a9681 100644 --- a/tests/xunit/discover_positions/fact_attribute_spec.lua +++ b/tests/xunit/discover_positions/fact_attribute_spec.lua @@ -272,4 +272,44 @@ describe("discover_positions", function() assert.same(positions, expected_positions) end) + + async.it("should discover Fact tests in fsharp file when not the only attribute", function() + local spec_file = "./tests/xunit/specs/fact_and_trait.fs" + local spec_file_name = "fact_and_trait.fs" + local positions = plugin.discover_positions(spec_file):to_list() + + local expected_positions = { + { + id = spec_file, + name = spec_file_name, + path = spec_file, + range = { 0, 0, 11, 0 }, + type = "file", + }, + { + { + framework = "xunit", + id = spec_file .. "::UnitTest1", + is_class = true, + name = "UnitTest1", + path = spec_file, + range = { 2, 0, 10, 1 }, + type = "namespace", + }, + { + { + framework = "xunit", + id = spec_file .. "::UnitTest1::Test1", + is_class = false, + name = "XUnitSamples.UnitTest1.Test1", + path = spec_file, + range = { 4, 1, 9, 2 }, + running_id = "./tests/xunit/specs/fact_and_trait.fs::UnitTest1::Test1", + type = "test", + }, + }, + }, + } + assert.same(positions, expected_positions) + end) end) diff --git a/tests/xunit/specs/fact_and_trait.fs b/tests/xunit/specs/fact_and_trait.fs new file mode 100644 index 0000000..146cf69 --- /dev/null +++ b/tests/xunit/specs/fact_and_trait.fs @@ -0,0 +1,7 @@ +namespace xunit.testproj1 + +type UnitTest1() = + [] + [] + member _.Test1() = + Assert.Equal(1, 1) From d7821c51cb3d119f880fcd2b7e632150fa3a23e8 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sun, 29 Sep 2024 14:09:09 +0200 Subject: [PATCH 04/43] add xunit test --- lua/neotest-dotnet/init.lua | 2 +- lua/neotest-dotnet/xunit/init.lua | 5 +- lua/neotest-dotnet/xunit/ts-queries.lua | 29 ++++++++++-- tests/minimal_init.lua | 2 +- .../fact_attribute_spec.lua | 47 ++++++++++++------- 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index d3b750c..83eae27 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -69,7 +69,7 @@ DotnetNeotestAdapter._build_position = function(...) logger.debug("neotest-dotnet: Buil Position Args: ") logger.debug(args) - local lang = lib.files.match_root_pattern("*.fsproj")(args[1]) and "fsharp" or "c_sharp" -- args[1] is the file path + local lang = vim.endswith(args[1], ".fs") and "fsharp" or "c_sharp" -- args[1] is the file path local framework = FrameworkDiscovery.get_test_framework_utils_from_source(lang, args[2], custom_attribute_args) -- args[2] is the content of the file diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua index ce1b299..28b45a0 100644 --- a/lua/neotest-dotnet/xunit/init.lua +++ b/lua/neotest-dotnet/xunit/init.lua @@ -278,10 +278,11 @@ M.generate_test_results = function(output_file_path, tree, context_id) for _, intermediate_result in ipairs(intermediate_results) do for _, node in ipairs(test_nodes) do local node_data = node:data() + local full_name, _ = node_data.full_name:gsub("``(.*)``", "%1") if - intermediate_result.test_name == node_data.full_name - or string.find(intermediate_result.test_name, node_data.full_name, 0, true) + intermediate_result.test_name == full_name + or string.find(intermediate_result.test_name, full_name, 0, true) 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. diff --git a/lua/neotest-dotnet/xunit/ts-queries.lua b/lua/neotest-dotnet/xunit/ts-queries.lua index f2cf0b6..25e1699 100644 --- a/lua/neotest-dotnet/xunit/ts-queries.lua +++ b/lua/neotest-dotnet/xunit/ts-queries.lua @@ -27,7 +27,7 @@ local function get_fsharp_queries(custom_fact_attributes) (identifier) @test.name)) ) @test.definition - ;; Matches test functions + ;; Matches test methods (member_defn (attributes (attribute @@ -36,6 +36,28 @@ local function get_fsharp_queries(custom_fact_attributes) (property_or_ident (identifier) @test.name .)) ) @test.definition + + ;; Matches test parameterized function + (declaration_expression + (attributes + (attribute + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Theory"))))) + (function_or_value_defn + (function_declaration_left + (identifier) @test.name + (argument_patterns) @parameter_list)) + ) @test.definition + + ;; Matches test parameterized methods + (member_defn + (attributes + (attribute + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Theory"))))) + (method_or_prop_defn + (property_or_ident + (identifier) @test.name .) + args: (_) @parameter_list) + ) @test.definition ]] end @@ -114,9 +136,8 @@ function M.get_queries(lang, custom_attributes) and framework_discovery.join_test_attributes(custom_attributes.xunit) or "" - return (lang == "c_sharp" and get_csharp_queries(custom_fact_attributes)) - or (lang == "fsharp" and get_fsharp_queries(custom_fact_attributes)) - or "" + return lang == "fsharp" and get_fsharp_queries(custom_fact_attributes) + or get_csharp_queries(custom_fact_attributes) end return M diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index d97df72..6eeb1e0 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -23,7 +23,7 @@ if #vim.api.nvim_list_uis() == 0 then -- Setup test plugin dependencies require("nvim-treesitter.configs").setup({ - ensure_installed = "c_sharp", + ensure_installed = { "c_sharp", "fsharp" }, sync_install = true, highlight = { enable = false, diff --git a/tests/xunit/discover_positions/fact_attribute_spec.lua b/tests/xunit/discover_positions/fact_attribute_spec.lua index e4a9681..bc0e919 100644 --- a/tests/xunit/discover_positions/fact_attribute_spec.lua +++ b/tests/xunit/discover_positions/fact_attribute_spec.lua @@ -278,38 +278,51 @@ describe("discover_positions", function() local spec_file_name = "fact_and_trait.fs" local positions = plugin.discover_positions(spec_file):to_list() + vim.print(positions) + local expected_positions = { { - id = spec_file, - name = spec_file_name, - path = spec_file, - range = { 0, 0, 11, 0 }, + id = "./tests/xunit/specs/fact_and_trait.fs", + name = "fact_and_trait.fs", + path = "./tests/xunit/specs/fact_and_trait.fs", + range = { 0, 0, 7, 0 }, type = "file", }, { { framework = "xunit", - id = spec_file .. "::UnitTest1", - is_class = true, - name = "UnitTest1", - path = spec_file, - range = { 2, 0, 10, 1 }, + id = "./tests/xunit/specs/fact_and_trait.fs::xunit.testproj1", + is_class = false, + name = "xunit.testproj1", + path = "./tests/xunit/specs/fact_and_trait.fs", + range = { 0, 0, 6, 22 }, type = "namespace", }, { { framework = "xunit", - id = spec_file .. "::UnitTest1::Test1", - is_class = false, - name = "XUnitSamples.UnitTest1.Test1", - path = spec_file, - range = { 4, 1, 9, 2 }, - running_id = "./tests/xunit/specs/fact_and_trait.fs::UnitTest1::Test1", - type = "test", + id = "./tests/xunit/specs/fact_and_trait.fs::xunit.testproj1::UnitTest1", + is_class = true, + name = "UnitTest1", + path = "./tests/xunit/specs/fact_and_trait.fs", + range = { 2, 5, 6, 22 }, + type = "namespace", + }, + { + { + framework = "xunit", + id = "./tests/xunit/specs/fact_and_trait.fs::xunit.testproj1::UnitTest1::Test1", + is_class = false, + name = "Test1", + path = "./tests/xunit/specs/fact_and_trait.fs", + range = { 3, 1, 6, 22 }, + type = "test", + }, }, }, }, } - assert.same(positions, expected_positions) + + assert.same(expected_positions, positions) end) end) From 453e6774e477ffcd7037d8645c9dda06aaabe274 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sun, 29 Sep 2024 17:06:19 +0200 Subject: [PATCH 05/43] add nunit --- lua/neotest-dotnet/nunit/init.lua | 32 ++++++-- lua/neotest-dotnet/nunit/ts-queries.lua | 74 +++++++++++++++++-- .../utils/neotest-node-tree-utils.lua | 12 ++- lua/neotest-dotnet/xunit/init.lua | 5 +- .../fact_attribute_spec.lua | 4 +- 5 files changed, 107 insertions(+), 20 deletions(-) diff --git a/lua/neotest-dotnet/nunit/init.lua b/lua/neotest-dotnet/nunit/init.lua index a4b92f0..375fe4c 100644 --- a/lua/neotest-dotnet/nunit/init.lua +++ b/lua/neotest-dotnet/nunit/init.lua @@ -7,20 +7,35 @@ local NodeTreeUtils = require("neotest-dotnet.utils.neotest-node-tree-utils") local M = {} ---Builds a position from captured nodes, optionally parsing parameters to create sub-positions. +---@param lang string language of the treesitter parser to use ---@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 -local build_parameterized_test_positions = function(base_node, source, captured_nodes, match_type) +local build_parameterized_test_positions = function( + lang, + 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 + local fsharp_query = [[ + (attributes + (attribute + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "TestCase"))) + (_) @arguments + ) + ) + ]] + + local csharp_query = [[ (attribute_list (attribute name: (identifier) @attribute_name (#any-of? @attribute_name "TestCase") @@ -29,8 +44,10 @@ local build_parameterized_test_positions = function(base_node, source, captured_ ) ]] - 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) + local query = lang == "fsharp" and fsharp_query or csharp_query + + local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse(lang, query) + or vim.treesitter.parse_query(lang, query) -- Set type to test (otherwise it will be test.parameterized) local parameterized_test_node = vim.tbl_extend("force", base_node, { type = "test" }) @@ -75,10 +92,11 @@ local get_match_type = function(captured_nodes) end function M.get_treesitter_queries(lang, custom_attribute_args) - return require("neotest-dotnet.nunit.ts-queries").get_queries(custom_attribute_args) + return require("neotest-dotnet.nunit.ts-queries").get_queries(lang, custom_attribute_args) end M.build_position = function(file_path, source, captured_nodes) + local lang = vim.endswith(file_path, ".fs") and "fsharp" or "c_sharp" local match_type = get_match_type(captured_nodes) local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) @@ -107,7 +125,7 @@ M.build_position = function(file_path, source, captured_nodes) return node end - return build_parameterized_test_positions(node, source, captured_nodes, match_type) + return build_parameterized_test_positions(lang, node, source, captured_nodes, match_type) end M.position_id = function(position, parents) diff --git a/lua/neotest-dotnet/nunit/ts-queries.lua b/lua/neotest-dotnet/nunit/ts-queries.lua index 9edcea2..3980d7a 100644 --- a/lua/neotest-dotnet/nunit/ts-queries.lua +++ b/lua/neotest-dotnet/nunit/ts-queries.lua @@ -2,12 +2,66 @@ 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 framework_discovery.join_test_attributes(custom_attributes.nunit) - or "" +local function get_fsharp_queries(custom_test_attributes) + return [[ + ;; Matches XUnit test class (has no specific attributes on class) + (anon_type_defn + (type_name (identifier) @class.name) + ) @class.definition + + (named_module + name: (long_identifier) @class.name + ) @class.definition + (module_defn + (identifier) @class.name + ) @class.definition + + ;; Matches test functions + (declaration_expression + (attributes + (attribute + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Test" "TestCaseSource" ]] .. custom_test_attributes .. [[))))) + (function_or_value_defn + (function_declaration_left + (identifier) @test.name)) + ) @test.definition + + ;; Matches test methods + (member_defn + (attributes + (attribute + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Test" "TestCaseSource" ]] .. custom_test_attributes .. [[))))) + (method_or_prop_defn + (property_or_ident + (identifier) @test.name .)) + ) @test.definition + + ;; Matches test parameterized function + (declaration_expression + (attributes + (attribute + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "^TestCase"))))) + (function_or_value_defn + (function_declaration_left + (identifier) @test.name + (argument_patterns) @parameter_list)) + ) @test.definition + + ;; Matches test parameterized methods + (member_defn + (attributes + (attribute + (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "^TestCase"))))) + (method_or_prop_defn + (property_or_ident + (identifier) @test.name .) + args: (_) @parameter_list) + ) @test.definition + ]] +end + +local function get_csharp_queries(custom_test_attributes) return [[ ;; Wrap this in alternation (https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax) ;; otherwise Specflow generated classes will be picked up twice @@ -69,4 +123,14 @@ function M.get_queries(custom_attributes) ]] end +function M.get_queries(lang, custom_attributes) + -- Don't include parameterized test attribute indicators so we don't double count them + local custom_fact_attributes = custom_attributes + and framework_discovery.join_test_attributes(custom_attributes.xunit) + or "" + + return lang == "fsharp" and get_fsharp_queries(custom_fact_attributes) + or get_csharp_queries(custom_fact_attributes) +end + return M diff --git a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua index 2cb5ed2..65107f6 100644 --- a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua +++ b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua @@ -1,5 +1,12 @@ local M = {} +---@param identifier string the fsharp identifier to sanitize +---@return string The sanitized identifier +function M.sanitize_fsharp_identifiers(identifier) + local sanitized, _ = string.gsub(identifier, "``([^`]*)``", "%1") + return sanitized +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. @@ -25,10 +32,11 @@ function M.get_test_nodes_data(tree) if node:data().framework == "xunit" --[[ or node:data().framework == "nunit" ]] then - node:data().full_name = node:data().name + -- local full_name = string.gsub(node:data().name, "``(.*)``", "%1") + node:data().full_name = M.sanitize_fsharp_identifiers(node:data().name) else local full_name = M.get_qualified_test_name_from_id(node:data().id) - node:data().full_name = full_name + node:data().full_name = M.sanitize_fsharp_identifiers(full_name) end end diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua index 28b45a0..ce1b299 100644 --- a/lua/neotest-dotnet/xunit/init.lua +++ b/lua/neotest-dotnet/xunit/init.lua @@ -278,11 +278,10 @@ M.generate_test_results = function(output_file_path, tree, context_id) for _, intermediate_result in ipairs(intermediate_results) do for _, node in ipairs(test_nodes) do local node_data = node:data() - local full_name, _ = node_data.full_name:gsub("``(.*)``", "%1") if - intermediate_result.test_name == full_name - or string.find(intermediate_result.test_name, full_name, 0, true) + intermediate_result.test_name == node_data.full_name + or string.find(intermediate_result.test_name, node_data.full_name, 0, true) 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. diff --git a/tests/xunit/discover_positions/fact_attribute_spec.lua b/tests/xunit/discover_positions/fact_attribute_spec.lua index bc0e919..f6797d1 100644 --- a/tests/xunit/discover_positions/fact_attribute_spec.lua +++ b/tests/xunit/discover_positions/fact_attribute_spec.lua @@ -278,12 +278,10 @@ describe("discover_positions", function() local spec_file_name = "fact_and_trait.fs" local positions = plugin.discover_positions(spec_file):to_list() - vim.print(positions) - local expected_positions = { { id = "./tests/xunit/specs/fact_and_trait.fs", - name = "fact_and_trait.fs", + name = spec_file_name, path = "./tests/xunit/specs/fact_and_trait.fs", range = { 0, 0, 7, 0 }, type = "file", From 1175b13b8990aed2ffefcd9809947d788c5c78f0 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Tue, 1 Oct 2024 19:47:33 +0200 Subject: [PATCH 06/43] add nunit support wip allow nested tests misc improvements improve test filtering improve error handling vsconsole test runner --- README.md | 27 +- lua/neotest-dotnet/framework-discovery.lua | 157 ------- lua/neotest-dotnet/init.lua | 398 +++++++++++------- lua/neotest-dotnet/mstest/init.lua | 316 -------------- lua/neotest-dotnet/mstest/ts-queries.lua | 73 ---- lua/neotest-dotnet/nunit/init.lua | 334 --------------- lua/neotest-dotnet/nunit/ts-queries.lua | 136 ------ lua/neotest-dotnet/strategies/netcoredbg.lua | 137 ------ .../types/neotest-dotnet-types.lua | 14 - lua/neotest-dotnet/types/neotest-types.lua | 78 ---- lua/neotest-dotnet/utils/build-spec-utils.lua | 118 ------ lua/neotest-dotnet/utils/dotnet-utils.lua | 120 ------ .../utils/neotest-node-tree-utils.lua | 46 -- lua/neotest-dotnet/utils/trx-utils.lua | 33 -- lua/neotest-dotnet/xunit/init.lua | 322 -------------- lua/neotest-dotnet/xunit/ts-queries.lua | 143 ------- parse_tests.fsx | 74 ++++ run_tests.fsx | 126 ++++++ 18 files changed, 465 insertions(+), 2187 deletions(-) delete mode 100644 lua/neotest-dotnet/framework-discovery.lua delete mode 100644 lua/neotest-dotnet/mstest/init.lua delete mode 100644 lua/neotest-dotnet/mstest/ts-queries.lua delete mode 100644 lua/neotest-dotnet/nunit/init.lua delete mode 100644 lua/neotest-dotnet/nunit/ts-queries.lua delete mode 100644 lua/neotest-dotnet/strategies/netcoredbg.lua delete mode 100644 lua/neotest-dotnet/types/neotest-dotnet-types.lua delete mode 100644 lua/neotest-dotnet/types/neotest-types.lua delete mode 100644 lua/neotest-dotnet/utils/build-spec-utils.lua delete mode 100644 lua/neotest-dotnet/utils/dotnet-utils.lua delete mode 100644 lua/neotest-dotnet/utils/neotest-node-tree-utils.lua delete mode 100644 lua/neotest-dotnet/utils/trx-utils.lua delete mode 100644 lua/neotest-dotnet/xunit/init.lua delete mode 100644 lua/neotest-dotnet/xunit/ts-queries.lua create mode 100644 parse_tests.fsx create mode 100644 run_tests.fsx diff --git a/README.md b/README.md index 8700732..97f3a34 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Neotest adapter for dotnet tests - Covers the "majority" of use cases for the 3 major .NET test runners - Attempts to provide support for `SpecFlow` generated tests for the various test runners - Support for this may still be patchy, so please raise an issue if it doesn't behave as expected - - `RunNearest` or `RunInFile` functions will need to be run from the *generated* specflow tests (NOT the `.feature`) + - `RunNearest` or `RunInFile` functions will need to be run from the _generated_ specflow tests (NOT the `.feature`) # Pre-requisites @@ -23,7 +23,7 @@ neotest-dotnet requires makes a number of assumptions about your environment: 1. The `dotnet sdk` that is compatible with the current project is installed and the `dotnet` executable is on the users runtime path (future updates may allow customisation of the dotnet exe location) 2. The user is running tests using one of the supported test runners / frameworks (see support grid) 3. (For Debugging) `netcoredbg` is installed and `nvim-dap` plugin has been configured for `netcoredbg` (see debug config for more details) -4. Requires [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) and the parser for C#. +4. Requires [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) and the parser for C# or F#. # Installation @@ -106,7 +106,7 @@ To clear the runsettings file in the same session run: ## Additional `dotnet test` arguments As well as the `dotnet_additional_args` option in the adapter setup above, you may also provide additional CLI arguments as a table to each `neotest` command. -By doing this, the additional args provided in the setup function will be *replaced* in their entirety by the ones provided at the command level. +By doing this, the additional args provided in the setup function will be _replaced_ in their entirety by the ones provided at the command level. For example, to provide a `runtime` argument to the `dotnet test` command, for all the tests in the file, you can run: @@ -163,7 +163,7 @@ To see if your use case is supported, check the grids below. If it isn't there, ### NUnit | Framework Feature | Scope Level | Docs | Status | Notes | -| ------------------------- | ----------- | ------------------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------- | +| ---------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Test` (Attribute) | Method | [Test - Nunit](https://docs.nunit.org/articles/nunit/writing-tests/attributes/test.html) | :heavy_check_mark: | Supported when used inside a class with or without the `TestFixture` attribute decoration | | `TestFixture` (Attribute) | Class | [TestFixture - Nunit](https://docs.nunit.org/articles/nunit/writing-tests/attributes/testfixture.html) | :heavy_check_mark: | | | `TestCase()` (Attribute) | Method | [TestCase - Nunit](https://docs.nunit.org/articles/nunit/writing-tests/attributes/testcase.html) | :heavy_check_mark: | Support for parameterized tests with inline parameters. Supports neotest 'run nearest' and 'run file' functionality | @@ -183,13 +183,13 @@ To see if your use case is supported, check the grids below. If it isn't there, ### MSTest -| Framework Feature | Scope Level | Docs | Status | Notes | -| ------------------------- | ----------- | ------------------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------- | -| `TestMethod` (Attribute) | Method | [TestMethod - MSTest](https://docs.nunit.org/articles/nunit/writing-tests/attributes/test.html) | :heavy_check_mark: | | -| `TestClass` (Attribute) | Class | [TestClass - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.testclassattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | | -| Nested Classes | Class | | :heavy_check_mark: | Fully qualified name is corrected to include `+` when class is nested | -| `DataTestMethod` (Attribute) | Method | [DataTestMethod - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.datatestmethodattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | | -| `DataRow` (Attribute) | Method | [DataRow - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.datarowattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | Support for parameterized tests with inline parameters. Supports neotest 'run nearest' and 'run file' functionality | +| Framework Feature | Scope Level | Docs | Status | Notes | +| ---------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- | +| `TestMethod` (Attribute) | Method | [TestMethod - MSTest](https://docs.nunit.org/articles/nunit/writing-tests/attributes/test.html) | :heavy_check_mark: | | +| `TestClass` (Attribute) | Class | [TestClass - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.testclassattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | | +| Nested Classes | Class | | :heavy_check_mark: | Fully qualified name is corrected to include `+` when class is nested | +| `DataTestMethod` (Attribute) | Method | [DataTestMethod - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.datatestmethodattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | | +| `DataRow` (Attribute) | Method | [DataRow - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.datarowattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | Support for parameterized tests with inline parameters. Supports neotest 'run nearest' and 'run file' functionality | # Limitations @@ -198,10 +198,7 @@ To see if your use case is supported, check the grids below. If it isn't there, 2. Dynamically parameterized tests need to be grouped together as neotest-dotnet is unable to robustly match the full test names that the .NET test runner attaches to the tests at runtime. - An attempt was made to use `dotnet test -t` to extract the dynamic test names, but this was too unreliable (duplicate test names were indistinguishable, and xUnit was the only runner that provided fully qualified test names) 3. See the support guidance for feature and language support - -- F# is currently unsupported due to the fact there is no complete tree-sitter parser for F# available as yet () - -3. As mentioned in the **Debugging** section, there are some discrepancies in test output at the moment. +4. As mentioned in the **Debugging** section, there are some discrepancies in test output at the moment. ## NUnit Limitations diff --git a/lua/neotest-dotnet/framework-discovery.lua b/lua/neotest-dotnet/framework-discovery.lua deleted file mode 100644 index ca85d7c..0000000 --- a/lua/neotest-dotnet/framework-discovery.lua +++ /dev/null @@ -1,157 +0,0 @@ -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", - "Xunit.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(lang, 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 c_sharp_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$") - ) - ]] - - local fsharp_query = [[ - (attribute - (simple_type - (long_identifier (identifier) @attribute_name (#any-of? @attribute_name ]] .. xunit_attributes .. " " .. nunit_attributes .. " " .. mstest_attributes .. [[))) - ) - ]] - - local framework_query = lang == "fsharp" and fsharp_query or c_sharp_query - - async.scheduler() - local root = vim.treesitter.get_string_parser(source, lang):parse()[1]:root() - local parsed_query = vim.fn.has("nvim-0.9.0") == 1 - and vim.treesitter.query.parse(lang, framework_query) - or vim.treesitter.parse_query(lang, framework_query) - for _, node, _, _ in parsed_query:iter_captures(root, source) do - local test_attribute = vim.fn.has("nvim-0.9.0") == 1 - and vim.treesitter.get_node_text(node, source) - or vim.treesitter.query.get_node_text(node, 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/init.lua b/lua/neotest-dotnet/init.lua index 83eae27..e03d923 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -1,195 +1,303 @@ +local nio = require("nio") local lib = require("neotest.lib") local logger = require("neotest.logging") -local FrameworkDiscovery = require("neotest-dotnet.framework-discovery") -local build_spec_utils = require("neotest-dotnet.utils.build-spec-utils") local DotnetNeotestAdapter = { name = "neotest-dotnet" } local dap = { adapter_name = "netcoredbg" } -local custom_attribute_args -local dotnet_additional_args -local discovery_root = "project" - -require("plenary.filetype").add_table({ - extension = { - ["fs"] = [[fsharp]], - ["fsx"] = [[fsharp]], - ["fsi"] = [[fsharp]], - }, -}) -DotnetNeotestAdapter.root = function(path) - if discovery_root == "solution" then - return lib.files.match_root_pattern("*.sln")(path) - else - return lib.files.match_root_pattern("*.[cf]sproj")(path) +local function get_script(script_name) + local script_paths = vim.api.nvim_get_runtime_file(script_name, true) + for _, path in ipairs(script_paths) do + if vim.endswith(path, ("neotest-dotnet%s" .. script_name):format(lib.files.sep)) then + return path + end end end -DotnetNeotestAdapter.is_test_file = function(file_path) - if vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs") then - local content = lib.files.read(file_path) +local function test_discovery_args(test_application_dll) + local test_discovery_script = get_script("parse_tests.fsx") + local testhost_dll = "/usr/local/share/dotnet/sdk/8.0.401/vstest.console.dll" - local found_derived_attribute - local found_standard_test_attribute - - -- Combine all attribute list arrays into one - local all_attributes = FrameworkDiscovery.all_test_attributes + return { "fsi", test_discovery_script, testhost_dll, test_application_dll } +end - for _, test_attribute in ipairs(all_attributes) do - if string.find(content, "%[ 0 then + local content = lib.files.read(path) + local lang = vim.treesitter.language.get_lang(filetype) or filetype + nio.scheduler() + local lang_tree = + vim.treesitter.get_string_parser(content, lang, { injections = { [lang] = "" } }) + + local root = lib.treesitter.fast_parse(lang_tree):root() + + local query = lib.treesitter.normalise_query(lang, fsharp_query) + + local sep = lib.files.sep + local path_elems = vim.split(path, sep, { plain = true }) + local nodes = { + { + type = "file", + path = path, + name = path_elems[#path_elems], + range = { root:range() }, + }, + } + for _, match in query:iter_matches(root, content, nil, nil, { all = false }) do + local captured_nodes = {} + for i, capture in ipairs(query.captures) do + captured_nodes[capture] = match[i] + end + local res = build_position(content, captured_nodes) + if res then + for _, pos in ipairs(res) do + nodes[#nodes + 1] = pos + end + end + end + + tree = lib.positions.parse_tree(nodes, { + nested_tests = true, + require_namespaces = false, + position_id = function(position, parents) + return position.id + or vim + .iter({ + position.path, + vim.tbl_map(function(pos) + return pos.name + end, parents), + position.name, + }) + :flatten() + :join("::") + end, + }) + end + + return tree end ---@summary Neotest core interface method: Build specs for running tests ---@param args neotest.RunArgs ---@return nil | neotest.RunSpec | neotest.RunSpec[] DotnetNeotestAdapter.build_spec = function(args) - logger.debug("neotest-dotnet: Creating specs from Tree (as list): ") - logger.debug(args.tree:to_list()) - - local additional_args = args.dotnet_additional_args or dotnet_additional_args or nil - - local specs = build_spec_utils.create_specs(args.tree, nil, additional_args) - - logger.debug("neotest-dotnet: Created " .. #specs .. " specs, with contents: ") - logger.debug(specs) - - 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." - ) - args.strategy = "integrated" - return specs - else - specs[1].dap = dap - specs[1].strategy = require("neotest-dotnet.strategies.netcoredbg") - end + local results_path = nio.fn.tempname() + + local tree = args.tree + if not tree then + return + end + + local pos = args.tree:data() + + if pos.type ~= "test" then + return end - return specs + return { + command = run_test_command(pos.id, results_path, pos.proj_dll_path), + context = { + result_path = results_path, + file = pos.path, + id = pos.id, + }, + } end ---@async ---@param spec neotest.RunSpec ----@param _ neotest.StrategyResult +---@param run neotest.StrategyResult ---@param tree neotest.Tree ---@return neotest.Result[] -DotnetNeotestAdapter.results = function(spec, _, tree) - local output_file = spec.context.results_path +DotnetNeotestAdapter.results = function(spec, run, tree) + local success, data = pcall(lib.files.read, spec.context.result_path) - logger.debug("neotest-dotnet: Fetching results from neotest tree (as list): ") - logger.debug(tree:to_list()) + local results = {} - local test_framework = FrameworkDiscovery.get_test_framework_utils_from_tree(tree) - local results = test_framework.generate_test_results(output_file, tree, spec.context.id) + if not success then + local outcome = "skipped" + results[spec.context.id] = { + status = outcome, + errors = { + message = "failed to read result file: " .. data, + }, + } + + return results + end + + local parse_ok, parsed = pcall(vim.json.decode, data) + assert(parse_ok, "failed to parse result file") + + if not parse_ok then + local outcome = "skipped" + results[spec.context.id] = { + status = outcome, + errors = { + message = "failed to parse result file", + }, + } + + return results + end - return results + return parsed end setmetatable(DotnetNeotestAdapter, { diff --git a/lua/neotest-dotnet/mstest/init.lua b/lua/neotest-dotnet/mstest/init.lua deleted file mode 100644 index 1e7340c..0000000 --- a/lua/neotest-dotnet/mstest/init.lua +++ /dev/null @@ -1,316 +0,0 @@ -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 = {} - ----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 -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(MSTest 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(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(lang, 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 -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 diff --git a/lua/neotest-dotnet/mstest/ts-queries.lua b/lua/neotest-dotnet/mstest/ts-queries.lua deleted file mode 100644 index ecb2155..0000000 --- a/lua/neotest-dotnet/mstest/ts-queries.lua +++ /dev/null @@ -1,73 +0,0 @@ -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 framework_discovery.join_test_attributes(custom_attributes.mstest) - or "" - - return [[ - ;; Matches SpecFlow generated classes - (class_declaration - (attribute_list - (attribute - (attribute_argument_list - (attribute_argument - (string_literal) @attribute_argument (#match? @attribute_argument "SpecFlow\"$") - ) - ) - ) - ) - name: (identifier) @namespace.name - ) @namespace.definition - - ;; Specflow - MSTest - (method_declaration - (attribute_list - (attribute - name: (qualified_name) @attribute_name (#match? @attribute_name "TestMethodAttribute$") - ) - ) - name: (identifier) @test.name - ) @test.definition - - ;; Matches test classes - (class_declaration - (attribute_list - (attribute - name: (identifier) @attribute_name (#eq? @attribute_name "TestClass") - ) - ) - name: (identifier) @class.name - ) @class.definition - - ;; Matches test methods - (method_declaration - (attribute_list - (attribute - name: (identifier) @attribute_name (#eq? @attribute_name "TestMethod") - ) - ) - name: (identifier) @test.name - ) @test.definition - - ;; Matches parameterized test methods - (method_declaration - (attribute_list - (attribute - name: (identifier) @attribute_name (#any-of? @attribute_name "DataTestMethod") - ) - ) - name: (identifier) @test.parameterized.name - parameters: (parameter_list - (parameter - name: (identifier) - )* - ) @parameter_list - ) @test.parameterized.definition - ]] -end - -return M diff --git a/lua/neotest-dotnet/nunit/init.lua b/lua/neotest-dotnet/nunit/init.lua deleted file mode 100644 index 375fe4c..0000000 --- a/lua/neotest-dotnet/nunit/init.lua +++ /dev/null @@ -1,334 +0,0 @@ -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 = {} - ----Builds a position from captured nodes, optionally parsing parameters to create sub-positions. ----@param lang string language of the treesitter parser to use ----@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 -local build_parameterized_test_positions = function( - lang, - 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 fsharp_query = [[ - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "TestCase"))) - (_) @arguments - ) - ) - ]] - - local csharp_query = [[ - (attribute_list - (attribute - name: (identifier) @attribute_name (#any-of? @attribute_name "TestCase") - ((attribute_argument_list) @arguments) - ) - ) - ]] - - local query = lang == "fsharp" and fsharp_query or csharp_query - - local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse(lang, query) - or vim.treesitter.parse_query(lang, 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 - -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(lang, custom_attribute_args) - return require("neotest-dotnet.nunit.ts-queries").get_queries(lang, custom_attribute_args) -end - -M.build_position = function(file_path, source, captured_nodes) - local lang = vim.endswith(file_path, ".fs") and "fsharp" or "c_sharp" - 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(lang, 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) - 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: NUnit TRX Results Output for" .. output_file_path .. ": ") - logger.debug(test_results) - - 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 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 - or 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: NUnit Neotest Results after conversion of Intermediate Results: ") - logger.debug(neotest_results) - - return neotest_results -end -return M diff --git a/lua/neotest-dotnet/nunit/ts-queries.lua b/lua/neotest-dotnet/nunit/ts-queries.lua deleted file mode 100644 index 3980d7a..0000000 --- a/lua/neotest-dotnet/nunit/ts-queries.lua +++ /dev/null @@ -1,136 +0,0 @@ -local framework_discovery = require("neotest-dotnet.framework-discovery") - -local M = {} - -local function get_fsharp_queries(custom_test_attributes) - return [[ - ;; Matches XUnit test class (has no specific attributes on class) - (anon_type_defn - (type_name (identifier) @class.name) - ) @class.definition - - (named_module - name: (long_identifier) @class.name - ) @class.definition - - (module_defn - (identifier) @class.name - ) @class.definition - - ;; Matches test functions - (declaration_expression - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Test" "TestCaseSource" ]] .. custom_test_attributes .. [[))))) - (function_or_value_defn - (function_declaration_left - (identifier) @test.name)) - ) @test.definition - - ;; Matches test methods - (member_defn - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Test" "TestCaseSource" ]] .. custom_test_attributes .. [[))))) - (method_or_prop_defn - (property_or_ident - (identifier) @test.name .)) - ) @test.definition - - ;; Matches test parameterized function - (declaration_expression - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "^TestCase"))))) - (function_or_value_defn - (function_declaration_left - (identifier) @test.name - (argument_patterns) @parameter_list)) - ) @test.definition - - ;; Matches test parameterized methods - (member_defn - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "^TestCase"))))) - (method_or_prop_defn - (property_or_ident - (identifier) @test.name .) - args: (_) @parameter_list) - ) @test.definition - ]] -end - -local function get_csharp_queries(custom_test_attributes) - return [[ - ;; Wrap this in alternation (https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax) - ;; otherwise Specflow generated classes will be picked up twice - [ - ;; Matches SpecFlow generated classes - (class_declaration - (attribute_list - (attribute - (attribute_argument_list - (attribute_argument - (string_literal) @attribute_argument (#match? @attribute_argument "SpecFlow\"$") - ) - ) - ) - ) - name: (identifier) @class.name - ) @class.definition - - ;; Matches test classes - (class_declaration - name: (identifier) @class.name - ) @class.definition - ] - - ;; Specflow - NUnit - (method_declaration - (attribute_list - (attribute - name: (qualified_name) @attribute_name (#match? @attribute_name "TestAttribute$") - ) - ) - name: (identifier) @test.name - ) @test.definition - - ;; Matches test methods - (method_declaration - (attribute_list - (attribute - name: (identifier) @attribute_name (#eq? @attribute_name "Test" "TestCaseSource" ]] .. custom_test_attributes .. [[) - ) - ) - name: (identifier) @test.name - ) @test.definition - - ;; Matches parameterized test methods - (method_declaration - (attribute_list - (attribute - name: (identifier) @attribute_name (#match? @attribute_name "^TestCase") - ) - )+ - name: (identifier) @test.parameterized.name - parameters: (parameter_list - (parameter - name: (identifier) - )* - ) @parameter_list - ) @test.parameterized.definition - ]] -end - -function M.get_queries(lang, custom_attributes) - -- Don't include parameterized test attribute indicators so we don't double count them - local custom_fact_attributes = custom_attributes - and framework_discovery.join_test_attributes(custom_attributes.xunit) - or "" - - return lang == "fsharp" and get_fsharp_queries(custom_fact_attributes) - or get_csharp_queries(custom_fact_attributes) -end - -return M diff --git a/lua/neotest-dotnet/strategies/netcoredbg.lua b/lua/neotest-dotnet/strategies/netcoredbg.lua deleted file mode 100644 index 80bb293..0000000 --- a/lua/neotest-dotnet/strategies/netcoredbg.lua +++ /dev/null @@ -1,137 +0,0 @@ -local nio = require("nio") -local lib = require("neotest.lib") -local async = require("neotest.async") -local FanoutAccum = require("neotest.types").FanoutAccum -local logger = require("neotest.logging") - ----@param spec neotest.RunSpec ----@return neotest.StrategyResult? -return function(spec) - local dap = require("dap") - - 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 attach_win, attach_buf, attach_chan - local finish_future = async.control.future() - local debugStarted = false - local waitingForDebugger = false - local dotnet_test_pid - local result_code - - logger.info("neotest-dotnet: Running tests in debug mode") - - local success, job = pcall(nio.fn.jobstart, spec.command, { - cwd = spec.cwd, - env = { ["VSTEST_HOST_DEBUG"] = "1" }, - pty = true, - on_stdout = function(_, data) - nio.run(function() - data_accum:push(table.concat(data, "\n")) - end) - - if not debugStarted then - for _, output in ipairs(data) do - dotnet_test_pid = dotnet_test_pid or string.match(output, "Process Id%p%s(%d+)") - - if - string.find(output, "Waiting for debugger attach...") - or string.find(output, "Please attach debugger") - or string.find(output, "Process Id:") - then - waitingForDebugger = true - end - end - if dotnet_test_pid ~= nil and waitingForDebugger then - logger.debug("neotest-dotnet: Dotnet test process ID: " .. dotnet_test_pid) - debugStarted = true - - dap.run(vim.tbl_extend("keep", { - type = spec.dap.adapter_name, - name = "attach - netcoredbg", - request = "attach", - processId = dotnet_test_pid, - }, spec.dap.args or {})) - 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, - output = function() - return stream_path - end, - stop = function() - nio.fn.jobstop(job) - end, - output_stream = function() - local queue = nio.control.queue() - data_accum:subscribe(function(d) - queue.put(d) - end) - return function() - return nio.first({ finish_future.wait, queue.get }) - end - end, - attach = function() - if not attach_buf then - attach_buf = nio.api.nvim_create_buf(false, true) - attach_chan = lib.ui.open_term(attach_buf, { - on_input = function(_, _, _, data) - pcall(nio.api.nvim_chan_send, job, data) - end, - }) - data_accum:subscribe(function(data) - nio.api.nvim_chan_send(attach_chan, data) - end) - end - attach_win = lib.ui.float.open({ - buffer = attach_buf, - }) - vim.api.nvim_buf_set_option(attach_buf, "filetype", "neotest-attach") - attach_win:jump_to() - 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) - if attach_win then - attach_win:listen("close", function() - pcall(vim.api.nvim_buf_delete, attach_buf, { force = true }) - pcall(vim.fn.chanclose, attach_chan) - end) - end - return result_code - end, - } -end diff --git a/lua/neotest-dotnet/types/neotest-dotnet-types.lua b/lua/neotest-dotnet/types/neotest-dotnet-types.lua deleted file mode 100644 index 801eab7..0000000 --- a/lua/neotest-dotnet/types/neotest-dotnet-types.lua +++ /dev/null @@ -1,14 +0,0 @@ ----@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(lang: string, 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(output_file_path: string, tree: neotest.Tree, context_id: string): neotest.Result[] Generates test results from trx results diff --git a/lua/neotest-dotnet/types/neotest-types.lua b/lua/neotest-dotnet/types/neotest-types.lua deleted file mode 100644 index 1382860..0000000 --- a/lua/neotest-dotnet/types/neotest-types.lua +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index f1f84b5..0000000 --- a/lua/neotest-dotnet/utils/build-spec-utils.lua +++ /dev/null @@ -1,118 +0,0 @@ -local logger = require("neotest.logging") -local lib = require("neotest.lib") -local Path = require("plenary.path") -local async = require("neotest.async") -local neotest_node_tree_utils = require("neotest-dotnet.utils.neotest-node-tree-utils") - -local BuildSpecUtils = {} - ---- Takes a position id of the format such as: "C:\path\to\file.cs::namespace::class::method" (Windows) and returns the fully qualified name of the test. ---- The format may vary depending on the OS and the test framework, and whether the test has parameters. Other examples are: ---- "/home/user/repos/test-project/MyClassTests.cs::MyClassTests::MyTestMethod" (Linux) ---- "/home/user/repos/test-project/MyClassTests.cs::MyClassTests::MyParameterizedMethod(a: 1)" (Linux - Parameterized test) ----@param position_id string The position id to parse. ----@return string The fully qualified name of the test to be passed to the "dotnet test" command -function BuildSpecUtils.build_test_fqn(position_id) - local fqn = neotest_node_tree_utils.get_qualified_test_name_from_id(position_id) - -- Remove any test parameters as these don't work well with the dotnet filter formatting. - fqn = fqn:gsub("%b()", "") - - return fqn -end - ----Creates a single spec for neotest to run using the dotnet test CLI ----@param position table The position value of the neotest tree node ----@param proj_root string The path of the project root for this particular position ----@param filter_arg string The filter argument to pass to the dotnet test command ----@param dotnet_additional_args table Any additional arguments to pass to the dotnet test command -function BuildSpecUtils.create_single_spec(position, proj_root, filter_arg, dotnet_additional_args) - local results_path = async.fn.tempname() .. ".trx" - filter_arg = filter_arg or "" - - local command = { - "dotnet", - "test", - proj_root, - filter_arg, - "--results-directory", - vim.fn.fnamemodify(results_path, ":h"), - "--logger", - '"trx;logfilename=' .. vim.fn.fnamemodify(results_path, ":t:h") .. '"', - } - - if dotnet_additional_args then - -- Add the additional_args table to the command table - for _, arg in ipairs(dotnet_additional_args) do - table.insert(command, arg) - end - end - - if vim.g.neotest_dotnet_runsettings_path then - table.insert(command, "--settings") - table.insert(command, vim.g.neotest_dotnet_runsettings_path) - end - - local command_string = table.concat(command, " ") - - logger.debug("neotest-dotnet: Running tests using command: " .. command_string) - - return { - command = command_string, - context = { - results_path = results_path, - file = position.path, - id = position.id, - }, - } -end - -function BuildSpecUtils.create_specs(tree, specs, dotnet_additional_args) - local position = tree:data() - - specs = specs or {} - - -- Adapted from https://github.com/nvim-neotest/neotest/blob/392808a91d6ee28d27cbfb93c9fd9781759b5d00/lua/neotest/lib/file/init.lua#L341 - if position.type == "dir" then - -- Check to see if we are in a project root - local proj_files = async.fn.glob(Path:new(position.path, "*.[cf]sproj").filename, true, true) - logger.debug("neotest-dotnet: Found " .. #proj_files .. " project files in " .. position.path) - - if #proj_files >= 1 then - logger.debug(proj_files) - - for _, p in ipairs(proj_files) do - if lib.files.exists(p) then - local spec = - BuildSpecUtils.create_single_spec(position, position.path, "", dotnet_additional_args) - table.insert(specs, spec) - end - end - else - -- Not in a project root, so find all child dirs and recurse through them as well so we can - -- add all the specs for all projects in the solution dir. - for _, child in ipairs(tree:children()) do - BuildSpecUtils.create_specs(child, specs, dotnet_additional_args) - end - end - elseif position.type == "namespace" or position.type == "test" then - -- 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.running_id or position.id) - local filter = '--filter FullyQualifiedName~"' .. fqn .. '"' - - local proj_root = lib.files.match_root_pattern("*.[cf]sproj")(position.path) - local spec = - BuildSpecUtils.create_single_spec(position, proj_root, filter, dotnet_additional_args) - table.insert(specs, spec) - elseif position.type == "file" then - local proj_root = lib.files.match_root_pattern("*.[cf]sproj")(position.path) - - local spec = BuildSpecUtils.create_single_spec(position, proj_root, "", dotnet_additional_args) - table.insert(specs, spec) - end - - return #specs < 0 and nil or specs -end - -return BuildSpecUtils diff --git a/lua/neotest-dotnet/utils/dotnet-utils.lua b/lua/neotest-dotnet/utils/dotnet-utils.lua deleted file mode 100644 index 717d8c5..0000000 --- a/lua/neotest-dotnet/utils/dotnet-utils.lua +++ /dev/null @@ -1,120 +0,0 @@ -local nio = require("nio") -local async = require("neotest.async") -local FanoutAccum = require("neotest.types").FanoutAccum -local logger = require("neotest.logging") - -local DotNetUtils = {} - -function DotNetUtils.get_test_full_names(project_path) - vim.g.neotest_dotnet_test_full_names_cache = vim.g.neotest_dotnet_test_full_names_cache or {} - local cache = vim.g.neotest_dotnet_test_full_names_cache or {} - - if cache[project_path] then - return { - is_complete = function() - return cache[project_path].result_code ~= nil - end, - result = function() - logger.debug( - "neotest-dotnet: dotnet test already running for " - .. project_path - .. ". Awaiting results:" - ) - cache[project_path].finish_future:wait() - local output = nio.fn.readfile(cache[project_path].stream_path) - - logger.debug(output) - return { - result_code = cache[project_path].result_code, - output = output, - } - end, - } - end - - local finish_future = async.control.future() - local stream_path = vim.fn.tempname() - local result_code - - logger.debug("neotest-dotnet: Running dotnet test for " .. project_path .. " as no cache found.") - local cached_project = { - stream_path = stream_path, - finish_future = finish_future, - result_code = result_code, - } - - cache[project_path] = cached_project - vim.g.neotest_dotnet_test_full_names_cache = cache - - local data_accum = FanoutAccum(function(prev, new) - if not prev then - return new - end - return prev .. new - end, nil) - - local open_err, stream_fd = async.uv.fs_open(stream_path, "w", 438) - assert(not open_err, open_err) - - data_accum:subscribe(function(data) - 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 test_command = "dotnet test -t " .. project_path .. " -- NUnit.DisplayName=FullName" - local success, job = pcall(nio.fn.jobstart, test_command, { - pty = true, - on_stdout = function(_, data) - for _, line in ipairs(data) do - if test_names_started then - -- 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 = true - 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() - 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) - - cache[project_path] = nil - vim.g.neotest_dotnet_test_full_names_cache = cache - - return { - result_code = result_code, - output = output, - } - end, - } -end - -return DotNetUtils diff --git a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua b/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua deleted file mode 100644 index 65107f6..0000000 --- a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua +++ /dev/null @@ -1,46 +0,0 @@ -local M = {} - ----@param identifier string the fsharp identifier to sanitize ----@return string The sanitized identifier -function M.sanitize_fsharp_identifiers(identifier) - local sanitized, _ = string.gsub(identifier, "``([^`]*)``", "%1") - return sanitized -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 M.get_qualified_test_name_from_id(position_id) - local _, first_colon_end = string.find(position_id, ".[cf]s::") - local full_name = string.sub(position_id, first_colon_end + 1) - full_name = string.gsub(full_name, "::", ".") - return full_name -end - -function M.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 - if - node:data().framework == "xunit" --[[ or node:data().framework == "nunit" ]] - then - -- local full_name = string.gsub(node:data().name, "``(.*)``", "%1") - node:data().full_name = M.sanitize_fsharp_identifiers(node:data().name) - else - local full_name = M.get_qualified_test_name_from_id(node:data().id) - node:data().full_name = M.sanitize_fsharp_identifiers(full_name) - end - end - - return test_nodes -end - -return M diff --git a/lua/neotest-dotnet/utils/trx-utils.lua b/lua/neotest-dotnet/utils/trx-utils.lua deleted file mode 100644 index 650c31d..0000000 --- a/lua/neotest-dotnet/utils/trx-utils.lua +++ /dev/null @@ -1,33 +0,0 @@ -local lib = require("neotest.lib") -local logger = require("neotest.logging") - -local M = {} - -local function remove_bom(str) - if string.byte(str, 1) == 239 and string.byte(str, 2) == 187 and string.byte(str, 3) == 191 then - str = string.sub(str, 4) - end - return str -end - -M.parse_trx = function(output_file) - logger.info("Parsing trx file: " .. output_file) - local success, xml = pcall(lib.files.read, output_file) - - if not success then - logger.error("No test output file found ") - return {} - end - - local no_bom_xml = remove_bom(xml) - - local ok, parsed_data = pcall(lib.xml.parse, no_bom_xml) - if not ok then - logger.error("Failed to parse test output:", output_file) - return {} - end - - return parsed_data -end - -return M diff --git a/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua deleted file mode 100644 index ce1b299..0000000 --- a/lua/neotest-dotnet/xunit/init.lua +++ /dev/null @@ -1,322 +0,0 @@ -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") -local Tree = types.Tree - ----@type FrameworkUtils ----@diagnostic disable-next-line: missing-fields -local M = {} - -function M.get_treesitter_queries(lang, custom_attribute_args) - return require("neotest-dotnet.xunit.ts-queries").get_queries(lang, custom_attribute_args) -end - -local get_node_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 -end - -M.build_position = function(file_path, source, 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 - - 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() }, - } - - return node -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 - - 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("*.[cf]sproj")(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 - -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) - - 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 - 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 - -- 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] - 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, - qualified_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() - - if - intermediate_result.test_name == node_data.full_name - or string.find(intermediate_result.test_name, node_data.full_name, 0, true) - 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] - 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: xUnit Neotest Results after conversion of Intermediate Results: ") - logger.debug(neotest_results) - - return neotest_results -end - -return M diff --git a/lua/neotest-dotnet/xunit/ts-queries.lua b/lua/neotest-dotnet/xunit/ts-queries.lua deleted file mode 100644 index 25e1699..0000000 --- a/lua/neotest-dotnet/xunit/ts-queries.lua +++ /dev/null @@ -1,143 +0,0 @@ -local framework_discovery = require("neotest-dotnet.framework-discovery") - -local M = {} - -local function get_fsharp_queries(custom_fact_attributes) - return [[ - ;; Matches XUnit test class (has no specific attributes on class) - (anon_type_defn - (type_name (identifier) @class.name) - ) @class.definition - - (named_module - name: (long_identifier) @class.name - ) @class.definition - - (module_defn - (identifier) @class.name - ) @class.definition - - ;; Matches test functions - (declaration_expression - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Fact" "ClassData" ]] .. custom_fact_attributes .. [[))))) - (function_or_value_defn - (function_declaration_left - (identifier) @test.name)) - ) @test.definition - - ;; Matches test methods - (member_defn - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Fact" "ClassData" ]] .. custom_fact_attributes .. [[))))) - (method_or_prop_defn - (property_or_ident - (identifier) @test.name .)) - ) @test.definition - - ;; Matches test parameterized function - (declaration_expression - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Theory"))))) - (function_or_value_defn - (function_declaration_left - (identifier) @test.name - (argument_patterns) @parameter_list)) - ) @test.definition - - ;; Matches test parameterized methods - (member_defn - (attributes - (attribute - (simple_type (long_identifier (identifier) @attribute_name (#any-of? @attribute_name "Theory"))))) - (method_or_prop_defn - (property_or_ident - (identifier) @test.name .) - args: (_) @parameter_list) - ) @test.definition - ]] -end - -local function get_csharp_queries(custom_fact_attributes) - return [[ - ;; Matches XUnit test class (has no specific attributes on class) - (class_declaration - name: (identifier) @class.name - ) @class.definition - - ;; Matches test methods - (method_declaration - (attribute_list - (attribute - name: (identifier) @attribute_name (#any-of? @attribute_name "Fact" "ClassData" ]] .. custom_fact_attributes .. [[) - (attribute_argument_list - (attribute_argument - (assignment_expression - left: (identifier) @property_name (#match? @property_name "DisplayName$") - right: (string_literal - (string_literal_content) @display_name - ) - ) - ) - )? - ) - ) - name: (identifier) @test.name - ) @test.definition - - ;; Specflow - XUnit - (method_declaration - (attribute_list - (attribute - name: (qualified_name) @attribute_name (#match? @attribute_name "SkippableFactAttribute$") - ) - ) - name: (identifier) @test.name - ) @test.definition - - ;; Matches parameterized test methods - (method_declaration - (attribute_list - (attribute - name: (identifier) @attribute_name (#any-of? @attribute_name "Theory") - (attribute_argument_list - (attribute_argument - (assignment_expression - left: (identifier) @property_name (#match? @property_name "DisplayName$") - right: (string_literal - (string_literal_content) @display_name - ) - ) - ) - )* - ) - ) - (attribute_list - (attribute - name: (identifier) @extra_attributes (#not-any-of? @extra_attributes "ClassData") - ) - )* - name: (identifier) @test.name - parameters: (parameter_list - (parameter - name: (identifier) - )* - ) @parameter_list - ) @test.definition - ]] -end - -function M.get_queries(lang, custom_attributes) - -- Don't include parameterized test attribute indicators so we don't double count them - local custom_fact_attributes = custom_attributes - and framework_discovery.join_test_attributes(custom_attributes.xunit) - or "" - - return lang == "fsharp" and get_fsharp_queries(custom_fact_attributes) - or get_csharp_queries(custom_fact_attributes) -end - -return M diff --git a/parse_tests.fsx b/parse_tests.fsx new file mode 100644 index 0000000..ca53ef9 --- /dev/null +++ b/parse_tests.fsx @@ -0,0 +1,74 @@ +#r "nuget: Microsoft.TestPlatform.TranslationLayer, 17.11.0" +#r "nuget: Microsoft.VisualStudio.TestPlatform, 14.0.0" +#r "nuget: MSTest.TestAdapter, 3.3.1" +#r "nuget: MSTest.TestFramework, 3.3.1" +#r "nuget: Newtonsoft.Json, 13.0.0" + +open System +open System.Collections.Generic +open Newtonsoft.Json +open Microsoft.TestPlatform.VsTestConsole.TranslationLayer +open Microsoft.VisualStudio.TestPlatform.ObjectModel +open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client + +module TestDiscovery = + type Test = + { Id: Guid + Namespace: string + Name: string + FilePath: string + LineNumber: int } + + type PlaygroundTestDiscoveryHandler() = + interface ITestDiscoveryEventsHandler2 with + member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = + discoveredTestCases + |> Seq.map (fun testCase -> + { Id = testCase.Id + Namespace = testCase.FullyQualifiedName + Name = testCase.DisplayName + FilePath = testCase.CodeFilePath + LineNumber = testCase.LineNumber }) + |> JsonConvert.SerializeObject + |> Console.WriteLine + + member _.HandleDiscoveryComplete(_, _) = () + member _.HandleLogMessage(_, _) = () + member _.HandleRawMessage(_) = () + + let main (argv: string[]) = + if argv.Length <> 2 then + invalidArg "CommandLineArgs" "Usage: fsi script.fsx " + + let console = Array.head argv + + let sourceSettings = + """ + + + """ + + let sources = Array.tail argv + + let environmentVariables = + Map.empty + |> Map.add "VSTEST_CONNECTION_TIMEOUT" "999" + |> Map.add "VSTEST_DEBUG_NOBP" "1" + |> Map.add "VSTEST_RUNNER_DEBUG_ATTACHVS" "0" + |> Map.add "VSTEST_HOST_DEBUG_ATTACHVS" "0" + |> Map.add "VSTEST_DATACOLLECTOR_DEBUG_ATTACHVS" "0" + |> Dictionary + + let options = TestPlatformOptions(CollectMetrics = false) + + let r = + VsTestConsoleWrapper(console, ConsoleParameters(EnvironmentVariables = environmentVariables)) + + let discoveryHandler = PlaygroundTestDiscoveryHandler() + + let testSession = TestSessionInfo() + + r.DiscoverTests(sources, sourceSettings, options, testSession, discoveryHandler) + 0 + + main <| Array.tail fsi.CommandLineArgs diff --git a/run_tests.fsx b/run_tests.fsx new file mode 100644 index 0000000..2b80c74 --- /dev/null +++ b/run_tests.fsx @@ -0,0 +1,126 @@ +#r "nuget: Microsoft.TestPlatform.TranslationLayer, 17.11.0" +#r "nuget: Microsoft.TestPlatform.ObjectModel, 17.11.0" +#r "nuget: Microsoft.VisualStudio.TestPlatform, 14.0.0" +#r "nuget: MSTest.TestAdapter, 3.3.1" +#r "nuget: MSTest.TestFramework, 3.3.1" +#r "nuget: Newtonsoft.Json, 13.0.0" + +open System +open System.IO +open Newtonsoft.Json +open System.Collections.Generic +open Microsoft.TestPlatform.VsTestConsole.TranslationLayer +open Microsoft.VisualStudio.TestPlatform.ObjectModel +open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client + +module TestDiscovery = + + let mutable discoveredTests = Seq.empty + + type PlaygroundTestDiscoveryHandler() = + interface ITestDiscoveryEventsHandler2 with + member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = + discoveredTests <- discoveredTestCases + + member _.HandleDiscoveryComplete(_, _) = () + member _.HandleLogMessage(_, _) = () + member _.HandleRawMessage(_) = () + + type PlaygroundTestRunHandler(outputFilePath) = + interface ITestRunEventsHandler with + member _.HandleTestRunComplete + (_testRunCompleteArgs, _lastChunkArgs, _runContextAttachments, _executorUris) + = + () + + member __.HandleLogMessage(_level, _message) = () + + member __.HandleRawMessage(_rawMessage) = () + + member __.HandleTestRunStatsChange(testRunChangedArgs: TestRunChangedEventArgs) : unit = + use writer = new StreamWriter(outputFilePath, append = false) + + let toNeoTestStatus (outcome: TestOutcome) = + match outcome with + | TestOutcome.Passed -> "passed" + | TestOutcome.Failed -> "failed" + | TestOutcome.Skipped -> "skipped" + | TestOutcome.None -> "skipped" + | TestOutcome.NotFound -> "skipped" + | _ -> "skipped" + + testRunChangedArgs.NewTestResults + |> Seq.map (fun result -> + let outcome = toNeoTestStatus result.Outcome + + let errorMessage = + let message = result.ErrorMessage |> Option.ofObj |> Option.defaultValue "" + let stackTrace = result.ErrorStackTrace |> Option.ofObj |> Option.defaultValue "" + + [ message; stackTrace ] + |> List.filter (not << String.IsNullOrWhiteSpace) + |> String.concat Environment.NewLine + + result.TestCase.Id, + {| status = outcome + short = $"{result.TestCase.DisplayName}:{outcome}" + errors = [| {| message = errorMessage |} |] |}) + |> Map.ofSeq + |> JsonConvert.SerializeObject + |> writer.WriteLine + + member __.LaunchProcessWithDebuggerAttached(_testProcessStartInfo) = 1 + + let main (argv: string[]) = + if argv.Length <> 4 then + invalidArg + "CommandLineArgs" + "Usage: fsi script.fsx " + + let console = argv[0] + + let outputPath = argv[1] + + let sourceSettings = + """ + + + """ + + let testIds = + argv[2] + .Split(";", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) + |> Array.map Guid.Parse + |> Set + + let sources = argv[3..] + + let environmentVariables = + Map.empty + |> Map.add "VSTEST_CONNECTION_TIMEOUT" "999" + |> Map.add "VSTEST_DEBUG_NOBP" "1" + |> Map.add "VSTEST_RUNNER_DEBUG_ATTACHVS" "0" + |> Map.add "VSTEST_HOST_DEBUG_ATTACHVS" "0" + |> Map.add "VSTEST_DATACOLLECTOR_DEBUG_ATTACHVS" "0" + |> Dictionary + + let options = TestPlatformOptions(CollectMetrics = false) + + let r = + VsTestConsoleWrapper(console, ConsoleParameters(EnvironmentVariables = environmentVariables)) + + let discoveryHandler = PlaygroundTestDiscoveryHandler() + let testHandler = PlaygroundTestRunHandler(outputPath) + + let testSession = TestSessionInfo() + + r.DiscoverTests(sources, sourceSettings, options, testSession, discoveryHandler) + + let testsToRun = + discoveredTests |> Seq.filter (fun testCase -> Set.contains testCase.Id testIds) + + r.RunTests(testsToRun, sourceSettings, options, testHandler) + + 0 + + main <| Array.tail fsi.CommandLineArgs From 3ed800376b503505e6bf3bd29301d3a09badfd5c Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 9 Nov 2024 11:57:56 +0100 Subject: [PATCH 07/43] feat: test discovery --- lua/neotest-dotnet/init.lua | 91 +++++++++++------ lua/neotest-dotnet/vstest_wrapper.lua | 122 +++++++++++++++++++++++ out.txt | 1 + parse_tests.fsx | 4 + run_tests.fsx | 134 +++++++++++++++++--------- stream.json | 7 ++ test.json | 1 + 7 files changed, 286 insertions(+), 74 deletions(-) create mode 100644 lua/neotest-dotnet/vstest_wrapper.lua create mode 100644 out.txt create mode 100644 stream.json create mode 100644 test.json diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index e03d923..044ffbd 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -2,6 +2,8 @@ local nio = require("nio") local lib = require("neotest.lib") local logger = require("neotest.logging") +local vstest = require("neotest-dotnet.vstest_wrapper") + local DotnetNeotestAdapter = { name = "neotest-dotnet" } local dap = { adapter_name = "netcoredbg" } @@ -21,15 +23,19 @@ local function test_discovery_args(test_application_dll) return { "fsi", test_discovery_script, testhost_dll, test_application_dll } end -local function run_test_command(id, output_path, test_application_dll) +local function run_test_command(id, stream_path, output_path, test_application_dll) local test_discovery_script = get_script("run_tests.fsx") local testhost_dll = "/usr/local/share/dotnet/sdk/8.0.401/vstest.console.dll" return { "dotnet", "fsi", + "--exec", + "--nologo", + "--shadowcopyreferences+", test_discovery_script, testhost_dll, + stream_path, output_path, id, test_application_dll, @@ -80,21 +86,21 @@ local fsharp_query = [[ local cache = {} -local function discover_tests(proj_dll_path) - local open_err, file_fd = nio.uv.fs_open(proj_dll_path, "r", 444) - assert(not open_err, open_err) - local stat_err, stat = nio.uv.fs_fstat(file_fd) - assert(not stat_err, stat_err) - nio.uv.fs_close(file_fd) - cache[proj_dll_path] = cache[proj_dll_path] or {} - if cache.last_cached == nil or cache.last_cached < stat.mtime.sec then - local discovery = nio.process.run({ cmd = "dotnet", args = test_discovery_args(proj_dll_path) }) - cache[proj_dll_path].data = vim.json.decode(discovery.stdout.read()) - cache[proj_dll_path].last_cached = stat.mtime.sec - discovery.close() - end - return cache[proj_dll_path].data -end +-- local function discover_tests(proj_dll_path) +-- local open_err, file_fd = nio.uv.fs_open(proj_dll_path, "r", 444) +-- assert(not open_err, open_err) +-- local stat_err, stat = nio.uv.fs_fstat(file_fd) +-- assert(not stat_err, stat_err) +-- nio.uv.fs_close(file_fd) +-- cache[proj_dll_path] = cache[proj_dll_path] or {} +-- if cache.last_cached == nil or cache.last_cached < stat.mtime.sec then +-- local discovery = vstest.discover_tests +-- cache[proj_dll_path].data = vim.json.decode(discovery.stdout.read()) +-- cache[proj_dll_path].last_cached = stat.mtime.sec +-- discovery.close() +-- end +-- return cache[proj_dll_path].data +-- end local function get_match_type(captured_nodes) if captured_nodes["test.name"] then @@ -116,14 +122,14 @@ local function get_proj_file(path) return name:match("%.[cf]sproj$") end, { type = "file", path = vim.fs.dirname(path) })[1] - local dir_name = vim.fs.dirname(proj_file) - local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") - - local proj_dll_path = - vim.fs.find(proj_name .. ".dll", { upward = false, type = "file", path = dir_name })[1] - - proj_file_path_map[path] = proj_dll_path - return proj_dll_path + -- local dir_name = vim.fs.dirname(proj_file) + -- local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") + -- + -- local proj_dll_path = + -- vim.fs.find(proj_name .. ".dll", { upward = false, type = "file", path = dir_name })[1] + -- + proj_file_path_map[path] = proj_file + return proj_file end ---@param path any The path to the file to discover positions in @@ -133,13 +139,19 @@ DotnetNeotestAdapter.discover_positions = function(path) local proj_dll_path = get_proj_file(path) local tests_in_file = vim - .iter(discover_tests(proj_dll_path)) + .iter(vstest.discover_tests(proj_dll_path)) + :map(function(_, v) + return v + end) :filter(function(test) - return test.FilePath == path + return test.CodeFilePath == path end) :totable() - local tree = {} + logger.info("filtered test cases:") + logger.info(tests_in_file) + + local tree ---@return nil | neotest.Position | neotest.Position[] local function build_position(source, captured_nodes) @@ -156,9 +168,9 @@ DotnetNeotestAdapter.discover_positions = function(path) id = test.Id, type = match_type, path = path, - name = test.Name, - qualified_name = test.Namespace, - proj_dll_path = proj_dll_path, + name = test.DisplayName, + qualified_name = test.FullyQualifiedName, + proj_dll_path = test.Source, range = { definition:range() }, }) end @@ -238,6 +250,10 @@ end ---@return nil | neotest.RunSpec | neotest.RunSpec[] DotnetNeotestAdapter.build_spec = function(args) local results_path = nio.fn.tempname() + local stream_path = nio.fn.tempname() + lib.files.write(stream_path, "") + + local stream_data, stop_stream = lib.files.stream_lines(stream_path) local tree = args.tree if not tree then @@ -251,12 +267,24 @@ DotnetNeotestAdapter.build_spec = function(args) end return { - command = run_test_command(pos.id, results_path, pos.proj_dll_path), + command = run_test_command(pos.id, stream_path, results_path, pos.proj_dll_path), context = { result_path = results_path, + stop_stream = stop_stream, file = pos.path, id = pos.id, }, + stream = function() + return function() + local lines = stream_data() + local results = {} + for _, line in ipairs(lines) do + local result = vim.json.decode(line, { luanil = { object = true } }) + results[result.id] = result.result + end + return results + end + end, } end @@ -266,6 +294,7 @@ end ---@param tree neotest.Tree ---@return neotest.Result[] DotnetNeotestAdapter.results = function(spec, run, tree) + spec.context.stop_stream() local success, data = pcall(lib.files.read, spec.context.result_path) local results = {} diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua new file mode 100644 index 0000000..501b76a --- /dev/null +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -0,0 +1,122 @@ +local nio = require("nio") +local lib = require("neotest.lib") +local logger = require("neotest.logging") + +local M = {} + +local function get_script(script_name) + local script_paths = vim.api.nvim_get_runtime_file(script_name, true) + for _, path in ipairs(script_paths) do + if vim.endswith(path, ("neotest-dotnet%s" .. script_name):format(lib.files.sep)) then + return path + end + end +end + +local test_runner +local semaphore = nio.control.semaphore(1) + +local function invoke_test_runner(command) + semaphore.with(function() + if test_runner ~= nil then + return + end + + local test_discovery_script = get_script("run_tests.fsx") + local testhost_dll = "/usr/local/share/dotnet/sdk/8.0.401/vstest.console.dll" + + local process = vim.system({ "dotnet", "fsi", test_discovery_script, testhost_dll }, { + stdin = true, + stdout = function(err, data) + logger.trace(data) + logger.trace(err) + end, + }, function(obj) + logger.warn("process ded :(") + logger.warn(obj.code) + logger.warn(obj.signal) + logger.warn(obj.stdout) + logger.warn(obj.stderr) + end) + + logger.info(string.format("spawned vstest process with pid: %s", process.pid)) + + test_runner = function(content) + process:write(content .. "\n") + end + end) + + return test_runner(command) +end + +local discovery_semaphore = nio.control.semaphore(1) + +function M.discover_tests(proj_file) + local output_file = nio.fn.tempname() + + local dir_name = vim.fs.dirname(proj_file) + local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") + + local proj_dll_path = + vim.fs.find(proj_name .. ".dll", { upward = false, type = "file", path = dir_name })[1] + + lib.process.run({ "dotnet", "build", proj_file }) + + local command = vim + .iter({ + "discover", + output_file, + proj_dll_path, + }) + :flatten() + :join(" ") + + logger.debug("Discovering tests using:") + logger.debug(command) + + invoke_test_runner(command) + + logger.debug("Waiting for result file to populate...") + + local max_wait = 10 * 1000 -- 10 sec + local sleep_time = 25 -- scan every 25 ms + local tries = 1 + local file_exists = false + + local json = {} + + while not file_exists and tries * sleep_time < max_wait do + if vim.fn.filereadable(output_file) == 1 then + discovery_semaphore.with(function() + local file, open_err = nio.file.open(output_file) + assert(not open_err, open_err) + file_exists = true + json = vim.json.decode(file.read(), { luanil = { object = true } }) + file.close() + end) + else + tries = tries + 1 + nio.sleep(sleep_time) + end + end + + logger.debug("file has been populated. Extracting test cases") + + return json +end + +function M.run_tests(ids, output_path) + lib.process.run({ "dotnet", "build" }) + + local command = vim + .iter({ + "run-tests", + output_path, + ids, + }) + :flatten() + :join(" ") + invoke_test_runner(command) +end + +return M diff --git a/out.txt b/out.txt new file mode 100644 index 0000000..e61cf6b --- /dev/null +++ b/out.txt @@ -0,0 +1 @@ +{"9fbd6e5b-0c66-6588-db0d-84e4720856cd":{"Id":"9fbd6e5b-0c66-6588-db0d-84e4720856cd","FullyQualifiedName":"X.Tests.X Should.Pass cool test","DisplayName":"X.Tests.X Should.Pass cool test","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":26,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":":F:X.Tests.X Should:Pass cool test:1:0:6b2ef3f8e215450ab8c302f3e466a5e0"}]},"9919cbeb-7618-222e-2a2b-daaef52229bb":{"Id":"9919cbeb-7618-222e-2a2b-daaef52229bb","FullyQualifiedName":"X.Tests.X Should.Pass cool test parametrized","DisplayName":"X.Tests.X Should.Pass cool test parametrized(x: 10, _y: 20, _z: 30)","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":30,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":"Xunit.Sdk.XunitTestCase, xunit.execution.{Platform}:VGVzdE1ldGhvZDpYdW5pdC5TZGsuVGVzdE1ldGhvZCwgeHVuaXQuZXhlY3V0aW9uLntQbGF0Zm9ybX06VFdWMGFHOWtUbUZ0WlRwVGVYTjBaVzB1VTNSeWFXNW5PbFZIUm5wamVVSnFZakk1YzBsSVVteGpNMUZuWTBkR2VWbFhNV3hrU0Vwd1pXMVdhd3BVWlhOMFEyeGhjM002V0hWdWFYUXVVMlJyTGxSbGMzUkRiR0Z6Y3l3Z2VIVnVhWFF1WlhobFkzVjBhVzl1TG50UWJHRjBabTl5YlgwNlZrZFdlbVJGVG5aaVIzaHNXVE5TY0dJeU5EWlhTRloxWVZoUmRWVXlVbkpNYkZKc1l6TlNSR0l5ZUhOYVYwNHdZVmM1ZFV4RFFqUmtWelZ3WkVNMWJHVkhWbXBrV0ZKd1lqSTBkV1V4UW5OWldGSnRZak5LZEdaVWNGTlNNbmcyV1RCa05HRkhWbFpPVjJocFZqRlZNbFpVVG5ObGJWSklWbTVTVFdKRk5IZFpNakZ6WkZad05tTkdaRk5OVm04eVYydFdUMUV5Um5SVFdHeHNVMFUxYUZacVFUQmtNV3hYV1hwV2ExWlhlRWxXTWpWaFlXMUdWbE5zY0ZWU00yaFVXV3RrVG1Wc1ZuVmpSVEZwVWpKU2RWWnNVa3RpTWxKMFZXeG9iRkl6VGt4V2EyUlhaVzFTUmxKdWNHcE5iRm93VjFjeE5FNVZPWE5oUkVacFlsZDNkMVJIZUU5aE1rWTFUbFpXWVZkRk5IZFZWbWhQWld4d1dFMVhiR2xUUjNSNlUxVm9iMDFYU25SaVJFSk5ZbFpaTUZkc1pFOU5WMUpJWWtoYWFXRlVWVE5XVldRMFlVZFNTRmR1V21waVZFVTFWREo0UjFkV1VuVmpSMFpYWld0YWQxZFhkRzlqTVZaWFlrWnNWbUpVYkZGWmExVXdUVlpzTmxSc1RtbFNNSEJWVkd4YVUyRXhUa1pqU0dSYVlsUkdjVlJ0ZUZka1JUVldUMWRzVGxZemFHRldWRWt4WVRGWmVGTllhRmhoYkhCb1ZXeFZkMlZHYkZWVGEzUlVVakZKTWxSVlZqQlZhekZ4WWtSR1dHSlVSbnBaYlhoTFpFZEtTVlJ0UmxkV1JscDJWMWQ0YTFack5YTldXSEJwVTBoQ2NsVnFSbUZOUmxKSVkzcFdhRll3V2pCV2JURjNZVEZHV1ZGc2FGaGlSMmhNV2xjeFIxZEZPVmxXYkVKcFVsUlJlRmRZY0U5Vk1rcElVMnhTVDFac1NuSlZNRlozWkRGc2RFMVhjRTlpUmtwWVZrWlNRMkV3TVVsaFNHaFdWbTFvV0ZaSGVFZFdWVEZGWVRCMFYxWjZWbmRaTVdoWFlrWk9WbFZVV2xaTk1uZzJXa1ZrVjJSRmVITlVha0pxWWxkNE1WZHVjSGRVTWtwV1lrUldXR0pIVW1GYVYzaDNZMVp2ZWxWdFJsZFNWM2N3VmtkNFRtUXdNVVpPVmxaU1lsZG9UbFpxUW5KTlJtUlpZMFUxYTFKVVJrWlZNakI0VkdzeGMxWllaRlZpV0VKb1dWVlZNVmRHV2xsWk1IUlRVakZhY1ZsclpFZGxWbXhaVlc1Q2FVMXFWa05aZWs1UFlrZEtXRk51VG14V1ZGWnZXV3hrVms1c1ZYcGlTSEJyVWpGYU1GUkhjelZoVjBaMFZtMXdhMUZZUWtaWGJHUlBZekZzV1ZOdGFHdFNNbmd5V1cxNFUwNVhUa2hXYXpsYVZucEdjMVF5ZUU5T1YwMTZWVzE0YVZWNlZsRlhWekYzWWtacmVsVlVNRXRSTW5ob1l6Tk9RbU16VG14aVYwcHpaVlUxYUdKWFZUWlZNMng2WkVkV2RFeHNUakJqYld4MVducHdVMkpyTlhaWFZtaExaREZhU0ZadWNHdFJNMlJ1Vm0weFYyVlhUWGxpU0ZwcFlXcENORlJIY0VKa1ZURkVUa2hrVFZFd1NrVmFSbVEwVFVkU1dWTnRlRkZXZWxaeldrWm9VMlZXYkZoa00wNUtVbXRKZUZkWE1UUmpSbXQzWkVkNGJGWnNTakpaVkVwWFpGWkNXRTVVUm1sU00yTTVRMnRPYzFsWVRucFdTR3gzV2xVMWFHSlhWVFpWTTJ4NlpFZFdkRXhzVGpCamJXeDFXbnB3V0ZGNlZsWlhiR2hQVFVkT05VNVdiRXBTYXpWMldXcE9WMk14Y0VKUVZEQTkKVGVzdE1ldGhvZEFyZ3VtZW50czpTeXN0ZW0uT2JqZWN0W106Uld4bGJXVnVkRlI1Y0dVNlUzbHpkR1Z0TGxOMGNtbHVaenBWTTJ4NlpFZFdkRXhyT1dsaGJWWnFaRUU5UFFwU1lXNXJPbE41YzNSbGJTNUpiblF6TWpveENsUnZkR0ZzVEdWdVozUm9PbE41YzNSbGJTNUpiblF6TWpvekNreGxibWQwYURBNlUzbHpkR1Z0TGtsdWRETXlPak1LVEc5M1pYSkNiM1Z1WkRBNlUzbHpkR1Z0TGtsdWRETXlPakFLU1hSbGJUQTZVM2x6ZEdWdExrbHVkRE15T2pFd0NrbDBaVzB4T2xONWMzUmxiUzVKYm5Rek1qb3lNQXBKZEdWdE1qcFRlWE4wWlcwdVNXNTBNekk2TXpBPQpEZWZhdWx0TWV0aG9kRGlzcGxheTpTeXN0ZW0uU3RyaW5nOlEyeGhjM05CYm1STlpYUm9iMlE9CkRlZmF1bHRNZXRob2REaXNwbGF5T3B0aW9uczpTeXN0ZW0uU3RyaW5nOlRtOXVaUT09ClRpbWVvdXQ6U3lzdGVtLkludDMyOjA="}]},"5477ce6f-7feb-e409-69e2-c23ef65dbb34":{"Id":"5477ce6f-7feb-e409-69e2-c23ef65dbb34","FullyQualifiedName":"X.Tests.A.My test","DisplayName":"X.Tests.A.My test","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":8,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":":F:X.Tests.A:My test:1:0:23cd75c47eec429f8374bdb3e34fb8d8"}]},"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce":{"Id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","FullyQualifiedName":"X.Tests.A.Pass cool test parametrized function","DisplayName":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30)","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":13,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":"Xunit.Sdk.XunitTestCase, xunit.execution.{Platform}:VGVzdE1ldGhvZDpYdW5pdC5TZGsuVGVzdE1ldGhvZCwgeHVuaXQuZXhlY3V0aW9uLntQbGF0Zm9ybX06VFdWMGFHOWtUbUZ0WlRwVGVYTjBaVzB1VTNSeWFXNW5PbFZIUm5wamVVSnFZakk1YzBsSVVteGpNMUZuWTBkR2VWbFhNV3hrU0Vwd1pXMVdhMGxIV2pGaWJVNHdZVmM1ZFFwVVpYTjBRMnhoYzNNNldIVnVhWFF1VTJSckxsUmxjM1JEYkdGemN5d2dlSFZ1YVhRdVpYaGxZM1YwYVc5dUxudFFiR0YwWm05eWJYMDZWa2RXZW1SRlRuWmlSM2hzV1ROU2NHSXlORFpYU0ZaMVlWaFJkVlV5VW5KTWJGSnNZek5TUkdJeWVITmFWMDR3WVZjNWRVeERRalJrVnpWd1pFTTFiR1ZIVm1wa1dGSndZakkwZFdVeFFuTlpXRkp0WWpOS2RHWlVjRk5TTW5nMldUQmtOR0ZIVmxaT1YyaHBWakZWTWxaVVRuTmxiVkpJVm01U1RXSkZOSGRaTWpGelpGWndObU5HWkZOTlZtOHlWMnRXVDFFeVJuUlRXR3hzVTBVMWFGWnFRVEJrTVd4WFdYcFdhMVpYZUVsV01qVmhZVzFHVmxOc2NGVlNNMmhVV1d0a1RtVnNWblZqUlRGb1RVWlZOVkV5ZUZOaVIwMTZWV3RLYWswd05YTlpiR1JMWXpKV1ZXTkdiR3RXZWxaM1drVk5NVlpHY0Voak0xWlhVakZhTmxwRlZrZGxiVTE1Vm01U1dtSllaekZVUlU1RFRrZFNXRTVZUW10UmVsWnpXbFZrVjJGdFVsbFZia0pwVFdwU01WcFVSa05qTVd4WlZXMHhhVTB3Y0RCYWJGSjNWV3hrUms1VVdsaGlSMDQwV1Zaa1MxTlhTa2RTYkhCWVVtdHdNbFpFU2pSVU1EVllWRmh3Vm1KWWFIQldXSEJYVmxad1JtRkZkR3BTTUhBd1YxaHdZVlp0U2xWV2JFSmFZV3RhZWxZeFdrOWtWbkJIV2taT1RsWnRPSGxXTW5SWFZHc3hXRkpZYkZSaE1taHlXbGR3UTFSR1ZsVlRWRlpyVm01Q01GbHJZekZWTWtwWVpVaHdXR0pHVlhoWlZXUkxWMFphVlZkc1drNU5ibWN5VjJ0V2ExWXlVa1psU0VwUVZqSjRiMWxzV21GalZuQkdVbTVrVjAxWVFscFZNalYzWVVaYU5tSkVSbFZOYm1oUVZHeGtUbVZzVm5SbFIyeFdaV3hhVmxkclZtOVRNazVJVTI1U1dtVnNjRlpXYlhOM1pERndSVkZxVW1wV2ExcGFWbTF6TVZWc1drVlJWRlpFWWtaYU1WbFdhRWROVm5CV1lrVldVR0pGTkRGWmVrNVRZa2RLVkU1V1VtdFRSWEIzV1cweGFrNXNVbGhqUlRsb1lraENSbGRZY0VkWGJWWnpVbXBDVldGcmNGaFphMXB6VGxVeFJWRnJOV2hpV0dnd1ZrVmFVMkp0Vm5KT1ZGcFdWMFpLWVZsc1drdGpSbEpWVTJ4YWJHRjZWa2xXTWpGelZVWkplRkpVUWtSaE1VcHpWMVJLTkdGSFRuUlNha0pvVm5wc01WVldhRTlsYkhCWVRWZHNhVk5IZUZCWFZtTjRZa1U1YzFScVZtcE5NVXB6V1d4Tk1WVkdiSFJqUjNoYVRURkdURlZyWkZkaGJVcElVbTVzV2xkR1NuZFpha2t4VmxkV1dWRnRlRlZpVlZvd1YyeFNkMVpIVmxsVWFrSmhWbnBDTVZaRVNrdGpWbkJZVkdwQlMxRXllR2hqTTA1Q1l6Tk9iR0pYU25ObFZUVm9ZbGRWTmxVemJIcGtSMVowVEd4T01HTnRiSFZhZW5CVFltczFkbGRXYUV0a01WcElWbTV3YTFFelpHNVdiVEZYWlZkTmVXSklXbWxoYWtJMFZFZHdRbVJWTVVST1NHUk5VVEJLUlZwR1pEUk5SMUpaVTIxNFVWWjZWbk5hUm1oVFpWWnNXR1F6VGtwU2EwbDRWMWN4TkdOR2EzZGtSM2hzVm14S01sbFVTbGRrVmtKWVRsUkdhVkl6WXpsRGEwNXpXVmhPZWxaSWJIZGFWVFZvWWxkVk5sVXpiSHBrUjFaMFRHeE9NR050YkhWYWVuQllVWHBXVmxkc2FFOU5SMDQxVGxWSlBRPT0KVGVzdE1ldGhvZEFyZ3VtZW50czpTeXN0ZW0uT2JqZWN0W106Uld4bGJXVnVkRlI1Y0dVNlUzbHpkR1Z0TGxOMGNtbHVaenBWTTJ4NlpFZFdkRXhyT1dsaGJWWnFaRUU5UFFwU1lXNXJPbE41YzNSbGJTNUpiblF6TWpveENsUnZkR0ZzVEdWdVozUm9PbE41YzNSbGJTNUpiblF6TWpvekNreGxibWQwYURBNlUzbHpkR1Z0TGtsdWRETXlPak1LVEc5M1pYSkNiM1Z1WkRBNlUzbHpkR1Z0TGtsdWRETXlPakFLU1hSbGJUQTZVM2x6ZEdWdExrbHVkRE15T2pFd0NrbDBaVzB4T2xONWMzUmxiUzVKYm5Rek1qb3lNQXBKZEdWdE1qcFRlWE4wWlcwdVNXNTBNekk2TXpBPQpEZWZhdWx0TWV0aG9kRGlzcGxheTpTeXN0ZW0uU3RyaW5nOlEyeGhjM05CYm1STlpYUm9iMlE9CkRlZmF1bHRNZXRob2REaXNwbGF5T3B0aW9uczpTeXN0ZW0uU3RyaW5nOlRtOXVaUT09ClRpbWVvdXQ6U3lzdGVtLkludDMyOjA="}]},"d7a86b8a-8cc5-ea1a-e3da-57dcbc2424bf":{"Id":"d7a86b8a-8cc5-ea1a-e3da-57dcbc2424bf","FullyQualifiedName":"X.Tests.A.Pass cool test parametrized function","DisplayName":"X.Tests.A.Pass cool test parametrized function(x: 11, _y: 22, _z: 33)","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":13,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":"Xunit.Sdk.XunitTestCase, xunit.execution.{Platform}:VGVzdE1ldGhvZDpYdW5pdC5TZGsuVGVzdE1ldGhvZCwgeHVuaXQuZXhlY3V0aW9uLntQbGF0Zm9ybX06VFdWMGFHOWtUbUZ0WlRwVGVYTjBaVzB1VTNSeWFXNW5PbFZIUm5wamVVSnFZakk1YzBsSVVteGpNMUZuWTBkR2VWbFhNV3hrU0Vwd1pXMVdhMGxIV2pGaWJVNHdZVmM1ZFFwVVpYTjBRMnhoYzNNNldIVnVhWFF1VTJSckxsUmxjM1JEYkdGemN5d2dlSFZ1YVhRdVpYaGxZM1YwYVc5dUxudFFiR0YwWm05eWJYMDZWa2RXZW1SRlRuWmlSM2hzV1ROU2NHSXlORFpYU0ZaMVlWaFJkVlV5VW5KTWJGSnNZek5TUkdJeWVITmFWMDR3WVZjNWRVeERRalJrVnpWd1pFTTFiR1ZIVm1wa1dGSndZakkwZFdVeFFuTlpXRkp0WWpOS2RHWlVjRk5TTW5nMldUQmtOR0ZIVmxaT1YyaHBWakZWTWxaVVRuTmxiVkpJVm01U1RXSkZOSGRaTWpGelpGWndObU5HWkZOTlZtOHlWMnRXVDFFeVJuUlRXR3hzVTBVMWFGWnFRVEJrTVd4WFdYcFdhMVpYZUVsV01qVmhZVzFHVmxOc2NGVlNNMmhVV1d0a1RtVnNWblZqUlRGb1RVWlZOVkV5ZUZOaVIwMTZWV3RLYWswd05YTlpiR1JMWXpKV1ZXTkdiR3RXZWxaM1drVk5NVlpHY0Voak0xWlhVakZhTmxwRlZrZGxiVTE1Vm01U1dtSllaekZVUlU1RFRrZFNXRTVZUW10UmVsWnpXbFZrVjJGdFVsbFZia0pwVFdwU01WcFVSa05qTVd4WlZXMHhhVTB3Y0RCYWJGSjNWV3hrUms1VVdsaGlSMDQwV1Zaa1MxTlhTa2RTYkhCWVVtdHdNbFpFU2pSVU1EVllWRmh3Vm1KWWFIQldXSEJYVmxad1JtRkZkR3BTTUhBd1YxaHdZVlp0U2xWV2JFSmFZV3RhZWxZeFdrOWtWbkJIV2taT1RsWnRPSGxXTW5SWFZHc3hXRkpZYkZSaE1taHlXbGR3UTFSR1ZsVlRWRlpyVm01Q01GbHJZekZWTWtwWVpVaHdXR0pHVlhoWlZXUkxWMFphVlZkc1drNU5ibWN5VjJ0V2ExWXlVa1psU0VwUVZqSjRiMWxzV21GalZuQkdVbTVrVjAxWVFscFZNalYzWVVaYU5tSkVSbFZOYm1oUVZHeGtUbVZzVm5SbFIyeFdaV3hhVmxkclZtOVRNazVJVTI1U1dtVnNjRlpXYlhOM1pERndSVkZxVW1wV2ExcGFWbTF6TVZWc1drVlJWRlpFWWtaYU1WbFdhRWROVm5CV1lrVldVR0pGTkRGWmVrNVRZa2RLVkU1V1VtdFRSWEIzV1cweGFrNXNVbGhqUlRsb1lraENSbGRZY0VkWGJWWnpVbXBDVldGcmNGaFphMXB6VGxVeFJWRnJOV2hpV0dnd1ZrVmFVMkp0Vm5KT1ZGcFdWMFpLWVZsc1drdGpSbEpWVTJ4YWJHRjZWa2xXTWpGelZVWkplRkpVUWtSaE1VcHpWMVJLTkdGSFRuUlNha0pvVm5wc01WVldhRTlsYkhCWVRWZHNhVk5IZUZCWFZtTjRZa1U1YzFScVZtcE5NVXB6V1d4Tk1WVkdiSFJqUjNoYVRURkdURlZyWkZkaGJVcElVbTVzV2xkR1NuZFpha2t4VmxkV1dWRnRlRlZpVlZvd1YyeFNkMVpIVmxsVWFrSmhWbnBDTVZaRVNrdGpWbkJZVkdwQlMxRXllR2hqTTA1Q1l6Tk9iR0pYU25ObFZUVm9ZbGRWTmxVemJIcGtSMVowVEd4T01HTnRiSFZhZW5CVFltczFkbGRXYUV0a01WcElWbTV3YTFFelpHNVdiVEZYWlZkTmVXSklXbWxoYWtJMFZFZHdRbVJWTVVST1NHUk5VVEJLUlZwR1pEUk5SMUpaVTIxNFVWWjZWbk5hUm1oVFpWWnNXR1F6VGtwU2EwbDRWMWN4TkdOR2EzZGtSM2hzVm14S01sbFVTbGRrVmtKWVRsUkdhVkl6WXpsRGEwNXpXVmhPZWxaSWJIZGFWVFZvWWxkVk5sVXpiSHBrUjFaMFRHeE9NR050YkhWYWVuQllVWHBXVmxkc2FFOU5SMDQxVGxWSlBRPT0KVGVzdE1ldGhvZEFyZ3VtZW50czpTeXN0ZW0uT2JqZWN0W106Uld4bGJXVnVkRlI1Y0dVNlUzbHpkR1Z0TGxOMGNtbHVaenBWTTJ4NlpFZFdkRXhyT1dsaGJWWnFaRUU5UFFwU1lXNXJPbE41YzNSbGJTNUpiblF6TWpveENsUnZkR0ZzVEdWdVozUm9PbE41YzNSbGJTNUpiblF6TWpvekNreGxibWQwYURBNlUzbHpkR1Z0TGtsdWRETXlPak1LVEc5M1pYSkNiM1Z1WkRBNlUzbHpkR1Z0TGtsdWRETXlPakFLU1hSbGJUQTZVM2x6ZEdWdExrbHVkRE15T2pFeENrbDBaVzB4T2xONWMzUmxiUzVKYm5Rek1qb3lNZ3BKZEdWdE1qcFRlWE4wWlcwdVNXNTBNekk2TXpNPQpEZWZhdWx0TWV0aG9kRGlzcGxheTpTeXN0ZW0uU3RyaW5nOlEyeGhjM05CYm1STlpYUm9iMlE9CkRlZmF1bHRNZXRob2REaXNwbGF5T3B0aW9uczpTeXN0ZW0uU3RyaW5nOlRtOXVaUT09ClRpbWVvdXQ6U3lzdGVtLkludDMyOjA="}]},"62e68e30-b201-9dc5-5b0d-1c13bb8d501c":{"Id":"62e68e30-b201-9dc5-5b0d-1c13bb8d501c","FullyQualifiedName":"N.Tests.A.My test","DisplayName":"My test","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":8,"Properties":[]},"29a318a0-1aaf-d25a-1ab5-9f8d65626d88":{"Id":"29a318a0-1aaf-d25a-1ab5-9f8d65626d88","FullyQualifiedName":"N.Tests.A.Pass cool x parametrized function(10,20,30)","DisplayName":"Pass cool x parametrized function(10,20,30)","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":12,"Properties":[]},"6cbeb44d-186f-308e-2c13-f378ce9a1bb5":{"Id":"6cbeb44d-186f-308e-2c13-f378ce9a1bb5","FullyQualifiedName":"N.Tests.A.Pass cool x parametrized function(11,22,33)","DisplayName":"Pass cool x parametrized function(11,22,33)","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":12,"Properties":[]},"5591ae1f-c354-89c2-4dd2-a01c2d02610e":{"Id":"5591ae1f-c354-89c2-4dd2-a01c2d02610e","FullyQualifiedName":"N.Tests.X Should.Pass cool x","DisplayName":"Pass cool x","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":17,"Properties":[]},"2c591632-cbc6-8965-269f-5768f101a0f4":{"Id":"2c591632-cbc6-8965-269f-5768f101a0f4","FullyQualifiedName":"N.Tests.X Should.Pass cool x parametrized(11,22,33)","DisplayName":"Pass cool x parametrized(11,22,33)","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":20,"Properties":[]}} diff --git a/parse_tests.fsx b/parse_tests.fsx index ca53ef9..cad86ff 100644 --- a/parse_tests.fsx +++ b/parse_tests.fsx @@ -68,7 +68,11 @@ module TestDiscovery = let testSession = TestSessionInfo() + r.StartSession() + r.DiscoverTests(sources, sourceSettings, options, testSession, discoveryHandler) + + r.EndSession() 0 main <| Array.tail fsi.CommandLineArgs diff --git a/run_tests.fsx b/run_tests.fsx index 2b80c74..8415e0f 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -14,19 +14,46 @@ open Microsoft.VisualStudio.TestPlatform.ObjectModel open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client module TestDiscovery = - - let mutable discoveredTests = Seq.empty + [] + let (|DiscoveryRequest|_|) (str: string) = + if str.StartsWith("discover") then + let args = + str.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) + |> Array.tail + + {| OutputPath = Array.head args + Sources = args |> Array.tail |} + |> ValueOption.Some + else + ValueOption.None + + [] + let (|RunTests|_|) (str: string) = + if str.StartsWith("run-tests") then + let args = + str.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) + |> Array.tail + + {| OutputPath = Array.head args + Ids = args |> Array.tail |> Array.map Guid.Parse |} + |> ValueOption.Some + else + ValueOption.None + + let discoveredTests = Dictionary() type PlaygroundTestDiscoveryHandler() = interface ITestDiscoveryEventsHandler2 with member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = - discoveredTests <- discoveredTestCases + for testCase in discoveredTestCases do + discoveredTests.Add(testCase.Id, testCase) |> ignore member _.HandleDiscoveryComplete(_, _) = () + member _.HandleLogMessage(_, _) = () member _.HandleRawMessage(_) = () - type PlaygroundTestRunHandler(outputFilePath) = + type PlaygroundTestRunHandler(streamOutputPath, outputFilePath) = interface ITestRunEventsHandler with member _.HandleTestRunComplete (_testRunCompleteArgs, _lastChunkArgs, _runContextAttachments, _executorUris) @@ -38,8 +65,6 @@ module TestDiscovery = member __.HandleRawMessage(_rawMessage) = () member __.HandleTestRunStatsChange(testRunChangedArgs: TestRunChangedEventArgs) : unit = - use writer = new StreamWriter(outputFilePath, append = false) - let toNeoTestStatus (outcome: TestOutcome) = match outcome with | TestOutcome.Passed -> "passed" @@ -49,52 +74,55 @@ module TestDiscovery = | TestOutcome.NotFound -> "skipped" | _ -> "skipped" - testRunChangedArgs.NewTestResults - |> Seq.map (fun result -> - let outcome = toNeoTestStatus result.Outcome + let results = + testRunChangedArgs.NewTestResults + |> Seq.map (fun result -> + let outcome = toNeoTestStatus result.Outcome + + let errorMessage = + let message = result.ErrorMessage |> Option.ofObj + let stackTrace = result.ErrorStackTrace |> Option.ofObj - let errorMessage = - let message = result.ErrorMessage |> Option.ofObj |> Option.defaultValue "" - let stackTrace = result.ErrorStackTrace |> Option.ofObj |> Option.defaultValue "" + match message, stackTrace with + | Some message, Some stackTrace -> Some $"{message}{Environment.NewLine}{stackTrace}" + | Some message, None -> Some message + | None, Some stackTrace -> Some stackTrace + | None, None -> None - [ message; stackTrace ] - |> List.filter (not << String.IsNullOrWhiteSpace) - |> String.concat Environment.NewLine + let errors = + match errorMessage with + | Some error -> [| {| message = error |} |] + | None -> [||] - result.TestCase.Id, - {| status = outcome - short = $"{result.TestCase.DisplayName}:{outcome}" - errors = [| {| message = errorMessage |} |] |}) - |> Map.ofSeq - |> JsonConvert.SerializeObject - |> writer.WriteLine + result.TestCase.Id, + {| status = outcome + short = $"{result.TestCase.DisplayName}:{outcome}" + errors = errors |}) + + use streamWriter = new StreamWriter(streamOutputPath, append = true) + + for (id, result) in results do + {| id = id; result = result |} + |> JsonConvert.SerializeObject + |> streamWriter.WriteLine + + use outputWriter = new StreamWriter(outputFilePath, append = false) + outputWriter.WriteLine(JsonConvert.SerializeObject(Map.ofSeq results)) member __.LaunchProcessWithDebuggerAttached(_testProcessStartInfo) = 1 let main (argv: string[]) = - if argv.Length <> 4 then - invalidArg - "CommandLineArgs" - "Usage: fsi script.fsx " + if argv.Length <> 1 then + invalidArg "CommandLineArgs" "Usage: fsi script.fsx " let console = argv[0] - let outputPath = argv[1] - let sourceSettings = """ """ - let testIds = - argv[2] - .Split(";", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) - |> Array.map Guid.Parse - |> Set - - let sources = argv[3..] - let environmentVariables = Map.empty |> Map.add "VSTEST_CONNECTION_TIMEOUT" "999" @@ -109,18 +137,38 @@ module TestDiscovery = let r = VsTestConsoleWrapper(console, ConsoleParameters(EnvironmentVariables = environmentVariables)) + let testSession = TestSessionInfo() let discoveryHandler = PlaygroundTestDiscoveryHandler() - let testHandler = PlaygroundTestRunHandler(outputPath) - let testSession = TestSessionInfo() + r.StartSession() - r.DiscoverTests(sources, sourceSettings, options, testSession, discoveryHandler) + let mutable loop = true - let testsToRun = - discoveredTests |> Seq.filter (fun testCase -> Set.contains testCase.Id testIds) + while loop do + match Console.ReadLine() with + | DiscoveryRequest args -> + r.DiscoverTests(args.Sources, sourceSettings, options, testSession, discoveryHandler) - r.RunTests(testsToRun, sourceSettings, options, testHandler) + use streamWriter = new StreamWriter(args.OutputPath, append = false) + discoveredTests |> JsonConvert.SerializeObject |> streamWriter.WriteLine + + Console.WriteLine($"Wrote test results to {args.OutputPath}") + | RunTests args -> + let testCases = + args.Ids + |> Array.choose (fun id -> + match discoveredTests.TryGetValue(id) with + | true, testCase -> Some testCase + | false, _ -> None) + + let testHandler = PlaygroundTestRunHandler(args.OutputPath, args.OutputPath) + r.RunTests(testCases, sourceSettings, testHandler) + | _ -> loop <- false + + r.EndSession() 0 - main <| Array.tail fsi.CommandLineArgs + let args = fsi.CommandLineArgs |> Array.tail + + main args diff --git a/stream.json b/stream.json new file mode 100644 index 0000000..4ee5b6d --- /dev/null +++ b/stream.json @@ -0,0 +1,7 @@ +{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[{"message":""}],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} +{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} +{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} +{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} +{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} +{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} +{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} diff --git a/test.json b/test.json new file mode 100644 index 0000000..14b45fc --- /dev/null +++ b/test.json @@ -0,0 +1 @@ +{"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} From 2dfa5b3423c351e5d1b1d81a7b8fe7d5fe3bcc01 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 9 Nov 2024 14:39:08 +0100 Subject: [PATCH 08/43] feat: test runner --- lua/neotest-dotnet/init.lua | 7 +++- lua/neotest-dotnet/vstest_wrapper.lua | 53 +++++++++++++++------------ run_tests.fsx | 37 +++++++++++++------ 3 files changed, 60 insertions(+), 37 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 044ffbd..68a0a0c 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -267,7 +267,7 @@ DotnetNeotestAdapter.build_spec = function(args) end return { - command = run_test_command(pos.id, stream_path, results_path, pos.proj_dll_path), + command = { "dotnet", "build" }, context = { result_path = results_path, stop_stream = stop_stream, @@ -275,6 +275,7 @@ DotnetNeotestAdapter.build_spec = function(args) id = pos.id, }, stream = function() + vstest.run_tests(pos.id, stream_path, results_path) return function() local lines = stream_data() local results = {} @@ -294,8 +295,10 @@ end ---@param tree neotest.Tree ---@return neotest.Result[] DotnetNeotestAdapter.results = function(spec, run, tree) + local max_wait = 5 * 60 * 1000 -- 5 min + local success, data = pcall(vstest.spin_lock_wait_file, spec.context.result_path, max_wait) + spec.context.stop_stream() - local success, data = pcall(lib.files.read, spec.context.result_path) local results = {} diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 501b76a..a685641 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -49,7 +49,32 @@ local function invoke_test_runner(command) return test_runner(command) end -local discovery_semaphore = nio.control.semaphore(1) +local spin_lock = nio.control.semaphore(1) + +function M.spin_lock_wait_file(file_path, max_wait) + local json = {} + + local sleep_time = 25 -- scan every 25 ms + local tries = 1 + local file_exists = false + + while not file_exists and tries * sleep_time < max_wait do + if vim.fn.filereadable(file_path) == 1 then + spin_lock.with(function() + local file, open_err = nio.file.open(file_path) + assert(not open_err, open_err) + file_exists = true + json = file.read() + file.close() + end) + else + tries = tries + 1 + nio.sleep(sleep_time) + end + end + + return json +end function M.discover_tests(proj_file) local output_file = nio.fn.tempname() @@ -79,38 +104,20 @@ function M.discover_tests(proj_file) logger.debug("Waiting for result file to populate...") local max_wait = 10 * 1000 -- 10 sec - local sleep_time = 25 -- scan every 25 ms - local tries = 1 - local file_exists = false - local json = {} - - while not file_exists and tries * sleep_time < max_wait do - if vim.fn.filereadable(output_file) == 1 then - discovery_semaphore.with(function() - local file, open_err = nio.file.open(output_file) - assert(not open_err, open_err) - file_exists = true - json = vim.json.decode(file.read(), { luanil = { object = true } }) - file.close() - end) - else - tries = tries + 1 - nio.sleep(sleep_time) - end - end + local json = + vim.json.decode(M.spin_lock_wait_file(output_file, max_wait), { luanil = { object = true } }) logger.debug("file has been populated. Extracting test cases") return json end -function M.run_tests(ids, output_path) - lib.process.run({ "dotnet", "build" }) - +function M.run_tests(ids, stream_path, output_path) local command = vim .iter({ "run-tests", + stream_path, output_path, ids, }) diff --git a/run_tests.fsx b/run_tests.fsx index 8415e0f..76c63ee 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -34,19 +34,24 @@ module TestDiscovery = str.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) |> Array.tail - {| OutputPath = Array.head args - Ids = args |> Array.tail |> Array.map Guid.Parse |} + {| StreamPath = args[0] + OutputPath = args[1] + Ids = args[2..] |> Array.map Guid.Parse |} |> ValueOption.Some else ValueOption.None - let discoveredTests = Dictionary() + let discoveredTests = Dictionary() type PlaygroundTestDiscoveryHandler() = interface ITestDiscoveryEventsHandler2 with member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = - for testCase in discoveredTestCases do - discoveredTests.Add(testCase.Id, testCase) |> ignore + discoveredTestCases + |> Seq.groupBy _.CodeFilePath + |> Seq.iter (fun (file, testCase) -> + if discoveredTests.ContainsKey file then + discoveredTests.Remove(file) |> ignore + discoveredTests.Add(file, testCase)) member _.HandleDiscoveryComplete(_, _) = () @@ -150,18 +155,26 @@ module TestDiscovery = r.DiscoverTests(args.Sources, sourceSettings, options, testSession, discoveryHandler) use streamWriter = new StreamWriter(args.OutputPath, append = false) - discoveredTests |> JsonConvert.SerializeObject |> streamWriter.WriteLine + discoveredTests + |> _.Values + |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) + |> Map + |> JsonConvert.SerializeObject + |> streamWriter.WriteLine Console.WriteLine($"Wrote test results to {args.OutputPath}") | RunTests args -> + let idMap = + discoveredTests + |> _.Values + |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) + |> Map + let testCases = - args.Ids - |> Array.choose (fun id -> - match discoveredTests.TryGetValue(id) with - | true, testCase -> Some testCase - | false, _ -> None) + args.Ids + |> Array.choose (fun id -> Map.tryFind id idMap) - let testHandler = PlaygroundTestRunHandler(args.OutputPath, args.OutputPath) + let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath) r.RunTests(testCases, sourceSettings, testHandler) | _ -> loop <- false From 2c363c8e82668d3768c512cf724e915b84fe5ab1 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 9 Nov 2024 14:39:35 +0100 Subject: [PATCH 09/43] cleanup --- out.txt | 1 - parse_tests.fsx | 78 ------------------------------------------------- stream.json | 7 ----- test.json | 1 - 4 files changed, 87 deletions(-) delete mode 100644 out.txt delete mode 100644 parse_tests.fsx delete mode 100644 stream.json delete mode 100644 test.json diff --git a/out.txt b/out.txt deleted file mode 100644 index e61cf6b..0000000 --- a/out.txt +++ /dev/null @@ -1 +0,0 @@ -{"9fbd6e5b-0c66-6588-db0d-84e4720856cd":{"Id":"9fbd6e5b-0c66-6588-db0d-84e4720856cd","FullyQualifiedName":"X.Tests.X Should.Pass cool test","DisplayName":"X.Tests.X Should.Pass cool test","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":26,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":":F:X.Tests.X Should:Pass cool test:1:0:6b2ef3f8e215450ab8c302f3e466a5e0"}]},"9919cbeb-7618-222e-2a2b-daaef52229bb":{"Id":"9919cbeb-7618-222e-2a2b-daaef52229bb","FullyQualifiedName":"X.Tests.X Should.Pass cool test parametrized","DisplayName":"X.Tests.X Should.Pass cool test parametrized(x: 10, _y: 20, _z: 30)","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":30,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":"Xunit.Sdk.XunitTestCase, xunit.execution.{Platform}:VGVzdE1ldGhvZDpYdW5pdC5TZGsuVGVzdE1ldGhvZCwgeHVuaXQuZXhlY3V0aW9uLntQbGF0Zm9ybX06VFdWMGFHOWtUbUZ0WlRwVGVYTjBaVzB1VTNSeWFXNW5PbFZIUm5wamVVSnFZakk1YzBsSVVteGpNMUZuWTBkR2VWbFhNV3hrU0Vwd1pXMVdhd3BVWlhOMFEyeGhjM002V0hWdWFYUXVVMlJyTGxSbGMzUkRiR0Z6Y3l3Z2VIVnVhWFF1WlhobFkzVjBhVzl1TG50UWJHRjBabTl5YlgwNlZrZFdlbVJGVG5aaVIzaHNXVE5TY0dJeU5EWlhTRloxWVZoUmRWVXlVbkpNYkZKc1l6TlNSR0l5ZUhOYVYwNHdZVmM1ZFV4RFFqUmtWelZ3WkVNMWJHVkhWbXBrV0ZKd1lqSTBkV1V4UW5OWldGSnRZak5LZEdaVWNGTlNNbmcyV1RCa05HRkhWbFpPVjJocFZqRlZNbFpVVG5ObGJWSklWbTVTVFdKRk5IZFpNakZ6WkZad05tTkdaRk5OVm04eVYydFdUMUV5Um5SVFdHeHNVMFUxYUZacVFUQmtNV3hYV1hwV2ExWlhlRWxXTWpWaFlXMUdWbE5zY0ZWU00yaFVXV3RrVG1Wc1ZuVmpSVEZwVWpKU2RWWnNVa3RpTWxKMFZXeG9iRkl6VGt4V2EyUlhaVzFTUmxKdWNHcE5iRm93VjFjeE5FNVZPWE5oUkVacFlsZDNkMVJIZUU5aE1rWTFUbFpXWVZkRk5IZFZWbWhQWld4d1dFMVhiR2xUUjNSNlUxVm9iMDFYU25SaVJFSk5ZbFpaTUZkc1pFOU5WMUpJWWtoYWFXRlVWVE5XVldRMFlVZFNTRmR1V21waVZFVTFWREo0UjFkV1VuVmpSMFpYWld0YWQxZFhkRzlqTVZaWFlrWnNWbUpVYkZGWmExVXdUVlpzTmxSc1RtbFNNSEJWVkd4YVUyRXhUa1pqU0dSYVlsUkdjVlJ0ZUZka1JUVldUMWRzVGxZemFHRldWRWt4WVRGWmVGTllhRmhoYkhCb1ZXeFZkMlZHYkZWVGEzUlVVakZKTWxSVlZqQlZhekZ4WWtSR1dHSlVSbnBaYlhoTFpFZEtTVlJ0UmxkV1JscDJWMWQ0YTFack5YTldXSEJwVTBoQ2NsVnFSbUZOUmxKSVkzcFdhRll3V2pCV2JURjNZVEZHV1ZGc2FGaGlSMmhNV2xjeFIxZEZPVmxXYkVKcFVsUlJlRmRZY0U5Vk1rcElVMnhTVDFac1NuSlZNRlozWkRGc2RFMVhjRTlpUmtwWVZrWlNRMkV3TVVsaFNHaFdWbTFvV0ZaSGVFZFdWVEZGWVRCMFYxWjZWbmRaTVdoWFlrWk9WbFZVV2xaTk1uZzJXa1ZrVjJSRmVITlVha0pxWWxkNE1WZHVjSGRVTWtwV1lrUldXR0pIVW1GYVYzaDNZMVp2ZWxWdFJsZFNWM2N3VmtkNFRtUXdNVVpPVmxaU1lsZG9UbFpxUW5KTlJtUlpZMFUxYTFKVVJrWlZNakI0VkdzeGMxWllaRlZpV0VKb1dWVlZNVmRHV2xsWk1IUlRVakZhY1ZsclpFZGxWbXhaVlc1Q2FVMXFWa05aZWs1UFlrZEtXRk51VG14V1ZGWnZXV3hrVms1c1ZYcGlTSEJyVWpGYU1GUkhjelZoVjBaMFZtMXdhMUZZUWtaWGJHUlBZekZzV1ZOdGFHdFNNbmd5V1cxNFUwNVhUa2hXYXpsYVZucEdjMVF5ZUU5T1YwMTZWVzE0YVZWNlZsRlhWekYzWWtacmVsVlVNRXRSTW5ob1l6Tk9RbU16VG14aVYwcHpaVlUxYUdKWFZUWlZNMng2WkVkV2RFeHNUakJqYld4MVducHdVMkpyTlhaWFZtaExaREZhU0ZadWNHdFJNMlJ1Vm0weFYyVlhUWGxpU0ZwcFlXcENORlJIY0VKa1ZURkVUa2hrVFZFd1NrVmFSbVEwVFVkU1dWTnRlRkZXZWxaeldrWm9VMlZXYkZoa00wNUtVbXRKZUZkWE1UUmpSbXQzWkVkNGJGWnNTakpaVkVwWFpGWkNXRTVVUm1sU00yTTVRMnRPYzFsWVRucFdTR3gzV2xVMWFHSlhWVFpWTTJ4NlpFZFdkRXhzVGpCamJXeDFXbnB3V0ZGNlZsWlhiR2hQVFVkT05VNVdiRXBTYXpWMldXcE9WMk14Y0VKUVZEQTkKVGVzdE1ldGhvZEFyZ3VtZW50czpTeXN0ZW0uT2JqZWN0W106Uld4bGJXVnVkRlI1Y0dVNlUzbHpkR1Z0TGxOMGNtbHVaenBWTTJ4NlpFZFdkRXhyT1dsaGJWWnFaRUU5UFFwU1lXNXJPbE41YzNSbGJTNUpiblF6TWpveENsUnZkR0ZzVEdWdVozUm9PbE41YzNSbGJTNUpiblF6TWpvekNreGxibWQwYURBNlUzbHpkR1Z0TGtsdWRETXlPak1LVEc5M1pYSkNiM1Z1WkRBNlUzbHpkR1Z0TGtsdWRETXlPakFLU1hSbGJUQTZVM2x6ZEdWdExrbHVkRE15T2pFd0NrbDBaVzB4T2xONWMzUmxiUzVKYm5Rek1qb3lNQXBKZEdWdE1qcFRlWE4wWlcwdVNXNTBNekk2TXpBPQpEZWZhdWx0TWV0aG9kRGlzcGxheTpTeXN0ZW0uU3RyaW5nOlEyeGhjM05CYm1STlpYUm9iMlE9CkRlZmF1bHRNZXRob2REaXNwbGF5T3B0aW9uczpTeXN0ZW0uU3RyaW5nOlRtOXVaUT09ClRpbWVvdXQ6U3lzdGVtLkludDMyOjA="}]},"5477ce6f-7feb-e409-69e2-c23ef65dbb34":{"Id":"5477ce6f-7feb-e409-69e2-c23ef65dbb34","FullyQualifiedName":"X.Tests.A.My test","DisplayName":"X.Tests.A.My test","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":8,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":":F:X.Tests.A:My test:1:0:23cd75c47eec429f8374bdb3e34fb8d8"}]},"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce":{"Id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","FullyQualifiedName":"X.Tests.A.Pass cool test parametrized function","DisplayName":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30)","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":13,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":"Xunit.Sdk.XunitTestCase, xunit.execution.{Platform}:VGVzdE1ldGhvZDpYdW5pdC5TZGsuVGVzdE1ldGhvZCwgeHVuaXQuZXhlY3V0aW9uLntQbGF0Zm9ybX06VFdWMGFHOWtUbUZ0WlRwVGVYTjBaVzB1VTNSeWFXNW5PbFZIUm5wamVVSnFZakk1YzBsSVVteGpNMUZuWTBkR2VWbFhNV3hrU0Vwd1pXMVdhMGxIV2pGaWJVNHdZVmM1ZFFwVVpYTjBRMnhoYzNNNldIVnVhWFF1VTJSckxsUmxjM1JEYkdGemN5d2dlSFZ1YVhRdVpYaGxZM1YwYVc5dUxudFFiR0YwWm05eWJYMDZWa2RXZW1SRlRuWmlSM2hzV1ROU2NHSXlORFpYU0ZaMVlWaFJkVlV5VW5KTWJGSnNZek5TUkdJeWVITmFWMDR3WVZjNWRVeERRalJrVnpWd1pFTTFiR1ZIVm1wa1dGSndZakkwZFdVeFFuTlpXRkp0WWpOS2RHWlVjRk5TTW5nMldUQmtOR0ZIVmxaT1YyaHBWakZWTWxaVVRuTmxiVkpJVm01U1RXSkZOSGRaTWpGelpGWndObU5HWkZOTlZtOHlWMnRXVDFFeVJuUlRXR3hzVTBVMWFGWnFRVEJrTVd4WFdYcFdhMVpYZUVsV01qVmhZVzFHVmxOc2NGVlNNMmhVV1d0a1RtVnNWblZqUlRGb1RVWlZOVkV5ZUZOaVIwMTZWV3RLYWswd05YTlpiR1JMWXpKV1ZXTkdiR3RXZWxaM1drVk5NVlpHY0Voak0xWlhVakZhTmxwRlZrZGxiVTE1Vm01U1dtSllaekZVUlU1RFRrZFNXRTVZUW10UmVsWnpXbFZrVjJGdFVsbFZia0pwVFdwU01WcFVSa05qTVd4WlZXMHhhVTB3Y0RCYWJGSjNWV3hrUms1VVdsaGlSMDQwV1Zaa1MxTlhTa2RTYkhCWVVtdHdNbFpFU2pSVU1EVllWRmh3Vm1KWWFIQldXSEJYVmxad1JtRkZkR3BTTUhBd1YxaHdZVlp0U2xWV2JFSmFZV3RhZWxZeFdrOWtWbkJIV2taT1RsWnRPSGxXTW5SWFZHc3hXRkpZYkZSaE1taHlXbGR3UTFSR1ZsVlRWRlpyVm01Q01GbHJZekZWTWtwWVpVaHdXR0pHVlhoWlZXUkxWMFphVlZkc1drNU5ibWN5VjJ0V2ExWXlVa1psU0VwUVZqSjRiMWxzV21GalZuQkdVbTVrVjAxWVFscFZNalYzWVVaYU5tSkVSbFZOYm1oUVZHeGtUbVZzVm5SbFIyeFdaV3hhVmxkclZtOVRNazVJVTI1U1dtVnNjRlpXYlhOM1pERndSVkZxVW1wV2ExcGFWbTF6TVZWc1drVlJWRlpFWWtaYU1WbFdhRWROVm5CV1lrVldVR0pGTkRGWmVrNVRZa2RLVkU1V1VtdFRSWEIzV1cweGFrNXNVbGhqUlRsb1lraENSbGRZY0VkWGJWWnpVbXBDVldGcmNGaFphMXB6VGxVeFJWRnJOV2hpV0dnd1ZrVmFVMkp0Vm5KT1ZGcFdWMFpLWVZsc1drdGpSbEpWVTJ4YWJHRjZWa2xXTWpGelZVWkplRkpVUWtSaE1VcHpWMVJLTkdGSFRuUlNha0pvVm5wc01WVldhRTlsYkhCWVRWZHNhVk5IZUZCWFZtTjRZa1U1YzFScVZtcE5NVXB6V1d4Tk1WVkdiSFJqUjNoYVRURkdURlZyWkZkaGJVcElVbTVzV2xkR1NuZFpha2t4VmxkV1dWRnRlRlZpVlZvd1YyeFNkMVpIVmxsVWFrSmhWbnBDTVZaRVNrdGpWbkJZVkdwQlMxRXllR2hqTTA1Q1l6Tk9iR0pYU25ObFZUVm9ZbGRWTmxVemJIcGtSMVowVEd4T01HTnRiSFZhZW5CVFltczFkbGRXYUV0a01WcElWbTV3YTFFelpHNVdiVEZYWlZkTmVXSklXbWxoYWtJMFZFZHdRbVJWTVVST1NHUk5VVEJLUlZwR1pEUk5SMUpaVTIxNFVWWjZWbk5hUm1oVFpWWnNXR1F6VGtwU2EwbDRWMWN4TkdOR2EzZGtSM2hzVm14S01sbFVTbGRrVmtKWVRsUkdhVkl6WXpsRGEwNXpXVmhPZWxaSWJIZGFWVFZvWWxkVk5sVXpiSHBrUjFaMFRHeE9NR050YkhWYWVuQllVWHBXVmxkc2FFOU5SMDQxVGxWSlBRPT0KVGVzdE1ldGhvZEFyZ3VtZW50czpTeXN0ZW0uT2JqZWN0W106Uld4bGJXVnVkRlI1Y0dVNlUzbHpkR1Z0TGxOMGNtbHVaenBWTTJ4NlpFZFdkRXhyT1dsaGJWWnFaRUU5UFFwU1lXNXJPbE41YzNSbGJTNUpiblF6TWpveENsUnZkR0ZzVEdWdVozUm9PbE41YzNSbGJTNUpiblF6TWpvekNreGxibWQwYURBNlUzbHpkR1Z0TGtsdWRETXlPak1LVEc5M1pYSkNiM1Z1WkRBNlUzbHpkR1Z0TGtsdWRETXlPakFLU1hSbGJUQTZVM2x6ZEdWdExrbHVkRE15T2pFd0NrbDBaVzB4T2xONWMzUmxiUzVKYm5Rek1qb3lNQXBKZEdWdE1qcFRlWE4wWlcwdVNXNTBNekk2TXpBPQpEZWZhdWx0TWV0aG9kRGlzcGxheTpTeXN0ZW0uU3RyaW5nOlEyeGhjM05CYm1STlpYUm9iMlE9CkRlZmF1bHRNZXRob2REaXNwbGF5T3B0aW9uczpTeXN0ZW0uU3RyaW5nOlRtOXVaUT09ClRpbWVvdXQ6U3lzdGVtLkludDMyOjA="}]},"d7a86b8a-8cc5-ea1a-e3da-57dcbc2424bf":{"Id":"d7a86b8a-8cc5-ea1a-e3da-57dcbc2424bf","FullyQualifiedName":"X.Tests.A.Pass cool test parametrized function","DisplayName":"X.Tests.A.Pass cool test parametrized function(x: 11, _y: 22, _z: 33)","ExecutorUri":"executor://xunit/VsTestRunner2/netcoreapp","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/Tests.fs","LineNumber":13,"Properties":[{"Key":{"Id":"XunitTestCase","Label":"xUnit.net Test Case","Category":"","Description":"","Attributes":0,"ValueType":"System.String"},"Value":"Xunit.Sdk.XunitTestCase, xunit.execution.{Platform}:VGVzdE1ldGhvZDpYdW5pdC5TZGsuVGVzdE1ldGhvZCwgeHVuaXQuZXhlY3V0aW9uLntQbGF0Zm9ybX06VFdWMGFHOWtUbUZ0WlRwVGVYTjBaVzB1VTNSeWFXNW5PbFZIUm5wamVVSnFZakk1YzBsSVVteGpNMUZuWTBkR2VWbFhNV3hrU0Vwd1pXMVdhMGxIV2pGaWJVNHdZVmM1ZFFwVVpYTjBRMnhoYzNNNldIVnVhWFF1VTJSckxsUmxjM1JEYkdGemN5d2dlSFZ1YVhRdVpYaGxZM1YwYVc5dUxudFFiR0YwWm05eWJYMDZWa2RXZW1SRlRuWmlSM2hzV1ROU2NHSXlORFpYU0ZaMVlWaFJkVlV5VW5KTWJGSnNZek5TUkdJeWVITmFWMDR3WVZjNWRVeERRalJrVnpWd1pFTTFiR1ZIVm1wa1dGSndZakkwZFdVeFFuTlpXRkp0WWpOS2RHWlVjRk5TTW5nMldUQmtOR0ZIVmxaT1YyaHBWakZWTWxaVVRuTmxiVkpJVm01U1RXSkZOSGRaTWpGelpGWndObU5HWkZOTlZtOHlWMnRXVDFFeVJuUlRXR3hzVTBVMWFGWnFRVEJrTVd4WFdYcFdhMVpYZUVsV01qVmhZVzFHVmxOc2NGVlNNMmhVV1d0a1RtVnNWblZqUlRGb1RVWlZOVkV5ZUZOaVIwMTZWV3RLYWswd05YTlpiR1JMWXpKV1ZXTkdiR3RXZWxaM1drVk5NVlpHY0Voak0xWlhVakZhTmxwRlZrZGxiVTE1Vm01U1dtSllaekZVUlU1RFRrZFNXRTVZUW10UmVsWnpXbFZrVjJGdFVsbFZia0pwVFdwU01WcFVSa05qTVd4WlZXMHhhVTB3Y0RCYWJGSjNWV3hrUms1VVdsaGlSMDQwV1Zaa1MxTlhTa2RTYkhCWVVtdHdNbFpFU2pSVU1EVllWRmh3Vm1KWWFIQldXSEJYVmxad1JtRkZkR3BTTUhBd1YxaHdZVlp0U2xWV2JFSmFZV3RhZWxZeFdrOWtWbkJIV2taT1RsWnRPSGxXTW5SWFZHc3hXRkpZYkZSaE1taHlXbGR3UTFSR1ZsVlRWRlpyVm01Q01GbHJZekZWTWtwWVpVaHdXR0pHVlhoWlZXUkxWMFphVlZkc1drNU5ibWN5VjJ0V2ExWXlVa1psU0VwUVZqSjRiMWxzV21GalZuQkdVbTVrVjAxWVFscFZNalYzWVVaYU5tSkVSbFZOYm1oUVZHeGtUbVZzVm5SbFIyeFdaV3hhVmxkclZtOVRNazVJVTI1U1dtVnNjRlpXYlhOM1pERndSVkZxVW1wV2ExcGFWbTF6TVZWc1drVlJWRlpFWWtaYU1WbFdhRWROVm5CV1lrVldVR0pGTkRGWmVrNVRZa2RLVkU1V1VtdFRSWEIzV1cweGFrNXNVbGhqUlRsb1lraENSbGRZY0VkWGJWWnpVbXBDVldGcmNGaFphMXB6VGxVeFJWRnJOV2hpV0dnd1ZrVmFVMkp0Vm5KT1ZGcFdWMFpLWVZsc1drdGpSbEpWVTJ4YWJHRjZWa2xXTWpGelZVWkplRkpVUWtSaE1VcHpWMVJLTkdGSFRuUlNha0pvVm5wc01WVldhRTlsYkhCWVRWZHNhVk5IZUZCWFZtTjRZa1U1YzFScVZtcE5NVXB6V1d4Tk1WVkdiSFJqUjNoYVRURkdURlZyWkZkaGJVcElVbTVzV2xkR1NuZFpha2t4VmxkV1dWRnRlRlZpVlZvd1YyeFNkMVpIVmxsVWFrSmhWbnBDTVZaRVNrdGpWbkJZVkdwQlMxRXllR2hqTTA1Q1l6Tk9iR0pYU25ObFZUVm9ZbGRWTmxVemJIcGtSMVowVEd4T01HTnRiSFZhZW5CVFltczFkbGRXYUV0a01WcElWbTV3YTFFelpHNVdiVEZYWlZkTmVXSklXbWxoYWtJMFZFZHdRbVJWTVVST1NHUk5VVEJLUlZwR1pEUk5SMUpaVTIxNFVWWjZWbk5hUm1oVFpWWnNXR1F6VGtwU2EwbDRWMWN4TkdOR2EzZGtSM2hzVm14S01sbFVTbGRrVmtKWVRsUkdhVkl6WXpsRGEwNXpXVmhPZWxaSWJIZGFWVFZvWWxkVk5sVXpiSHBrUjFaMFRHeE9NR050YkhWYWVuQllVWHBXVmxkc2FFOU5SMDQxVGxWSlBRPT0KVGVzdE1ldGhvZEFyZ3VtZW50czpTeXN0ZW0uT2JqZWN0W106Uld4bGJXVnVkRlI1Y0dVNlUzbHpkR1Z0TGxOMGNtbHVaenBWTTJ4NlpFZFdkRXhyT1dsaGJWWnFaRUU5UFFwU1lXNXJPbE41YzNSbGJTNUpiblF6TWpveENsUnZkR0ZzVEdWdVozUm9PbE41YzNSbGJTNUpiblF6TWpvekNreGxibWQwYURBNlUzbHpkR1Z0TGtsdWRETXlPak1LVEc5M1pYSkNiM1Z1WkRBNlUzbHpkR1Z0TGtsdWRETXlPakFLU1hSbGJUQTZVM2x6ZEdWdExrbHVkRE15T2pFeENrbDBaVzB4T2xONWMzUmxiUzVKYm5Rek1qb3lNZ3BKZEdWdE1qcFRlWE4wWlcwdVNXNTBNekk2TXpNPQpEZWZhdWx0TWV0aG9kRGlzcGxheTpTeXN0ZW0uU3RyaW5nOlEyeGhjM05CYm1STlpYUm9iMlE9CkRlZmF1bHRNZXRob2REaXNwbGF5T3B0aW9uczpTeXN0ZW0uU3RyaW5nOlRtOXVaUT09ClRpbWVvdXQ6U3lzdGVtLkludDMyOjA="}]},"62e68e30-b201-9dc5-5b0d-1c13bb8d501c":{"Id":"62e68e30-b201-9dc5-5b0d-1c13bb8d501c","FullyQualifiedName":"N.Tests.A.My test","DisplayName":"My test","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":8,"Properties":[]},"29a318a0-1aaf-d25a-1ab5-9f8d65626d88":{"Id":"29a318a0-1aaf-d25a-1ab5-9f8d65626d88","FullyQualifiedName":"N.Tests.A.Pass cool x parametrized function(10,20,30)","DisplayName":"Pass cool x parametrized function(10,20,30)","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":12,"Properties":[]},"6cbeb44d-186f-308e-2c13-f378ce9a1bb5":{"Id":"6cbeb44d-186f-308e-2c13-f378ce9a1bb5","FullyQualifiedName":"N.Tests.A.Pass cool x parametrized function(11,22,33)","DisplayName":"Pass cool x parametrized function(11,22,33)","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":12,"Properties":[]},"5591ae1f-c354-89c2-4dd2-a01c2d02610e":{"Id":"5591ae1f-c354-89c2-4dd2-a01c2d02610e","FullyQualifiedName":"N.Tests.X Should.Pass cool x","DisplayName":"Pass cool x","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":17,"Properties":[]},"2c591632-cbc6-8965-269f-5768f101a0f4":{"Id":"2c591632-cbc6-8965-269f-5768f101a0f4","FullyQualifiedName":"N.Tests.X Should.Pass cool x parametrized(11,22,33)","DisplayName":"Pass cool x parametrized(11,22,33)","ExecutorUri":"executor://NUnit3TestExecutor","Source":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/bin/Debug/net8.0/FsharpTest.dll","CodeFilePath":"/Users/nikolaj/Documents/Code/fsharp-test/src/FsharpTest/TestsNUnit.fs","LineNumber":20,"Properties":[]}} diff --git a/parse_tests.fsx b/parse_tests.fsx deleted file mode 100644 index cad86ff..0000000 --- a/parse_tests.fsx +++ /dev/null @@ -1,78 +0,0 @@ -#r "nuget: Microsoft.TestPlatform.TranslationLayer, 17.11.0" -#r "nuget: Microsoft.VisualStudio.TestPlatform, 14.0.0" -#r "nuget: MSTest.TestAdapter, 3.3.1" -#r "nuget: MSTest.TestFramework, 3.3.1" -#r "nuget: Newtonsoft.Json, 13.0.0" - -open System -open System.Collections.Generic -open Newtonsoft.Json -open Microsoft.TestPlatform.VsTestConsole.TranslationLayer -open Microsoft.VisualStudio.TestPlatform.ObjectModel -open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client - -module TestDiscovery = - type Test = - { Id: Guid - Namespace: string - Name: string - FilePath: string - LineNumber: int } - - type PlaygroundTestDiscoveryHandler() = - interface ITestDiscoveryEventsHandler2 with - member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = - discoveredTestCases - |> Seq.map (fun testCase -> - { Id = testCase.Id - Namespace = testCase.FullyQualifiedName - Name = testCase.DisplayName - FilePath = testCase.CodeFilePath - LineNumber = testCase.LineNumber }) - |> JsonConvert.SerializeObject - |> Console.WriteLine - - member _.HandleDiscoveryComplete(_, _) = () - member _.HandleLogMessage(_, _) = () - member _.HandleRawMessage(_) = () - - let main (argv: string[]) = - if argv.Length <> 2 then - invalidArg "CommandLineArgs" "Usage: fsi script.fsx " - - let console = Array.head argv - - let sourceSettings = - """ - - - """ - - let sources = Array.tail argv - - let environmentVariables = - Map.empty - |> Map.add "VSTEST_CONNECTION_TIMEOUT" "999" - |> Map.add "VSTEST_DEBUG_NOBP" "1" - |> Map.add "VSTEST_RUNNER_DEBUG_ATTACHVS" "0" - |> Map.add "VSTEST_HOST_DEBUG_ATTACHVS" "0" - |> Map.add "VSTEST_DATACOLLECTOR_DEBUG_ATTACHVS" "0" - |> Dictionary - - let options = TestPlatformOptions(CollectMetrics = false) - - let r = - VsTestConsoleWrapper(console, ConsoleParameters(EnvironmentVariables = environmentVariables)) - - let discoveryHandler = PlaygroundTestDiscoveryHandler() - - let testSession = TestSessionInfo() - - r.StartSession() - - r.DiscoverTests(sources, sourceSettings, options, testSession, discoveryHandler) - - r.EndSession() - 0 - - main <| Array.tail fsi.CommandLineArgs diff --git a/stream.json b/stream.json deleted file mode 100644 index 4ee5b6d..0000000 --- a/stream.json +++ /dev/null @@ -1,7 +0,0 @@ -{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[{"message":""}],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} -{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} -{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} -{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} -{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} -{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} -{"id":"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce","result":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} diff --git a/test.json b/test.json deleted file mode 100644 index 14b45fc..0000000 --- a/test.json +++ /dev/null @@ -1 +0,0 @@ -{"d7e85bf3-19da-4d4a-2660-b1945bbbb5ce":{"errors":[],"short":"X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30):passed","status":"passed"}} From 913e4bce34aa671901d597664b6a2d40f500a4de Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 9 Nov 2024 14:55:20 +0100 Subject: [PATCH 10/43] improve test discovery --- lua/neotest-dotnet/init.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 68a0a0c..1ac321d 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -74,14 +74,19 @@ local fsharp_query = [[ (declaration_expression (function_or_value_defn - (function_declaration_left . (_) @test.name) - ) @test.definition) + (function_declaration_left + . + (_) @test.name) + body: (_) @test.definition)) (member_defn (method_or_prop_defn (property_or_ident (identifier) @test.name .) - ) @test.definition) + . + (_) + . + (_) @test.definition)) ]] local cache = {} @@ -148,8 +153,8 @@ DotnetNeotestAdapter.discover_positions = function(path) end) :totable() - logger.info("filtered test cases:") - logger.info(tests_in_file) + logger.debug("filtered test cases:") + logger.debug(tests_in_file) local tree From 2c470460af573cec678276702ecf15481d919070 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sun, 10 Nov 2024 12:18:58 +0100 Subject: [PATCH 11/43] improve test_spec --- lua/neotest-dotnet/init.lua | 6 +++--- lua/neotest-dotnet/vstest_wrapper.lua | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 1ac321d..b43838b 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -256,6 +256,7 @@ end DotnetNeotestAdapter.build_spec = function(args) local results_path = nio.fn.tempname() local stream_path = nio.fn.tempname() + lib.files.write(results_path, "") lib.files.write(stream_path, "") local stream_data, stop_stream = lib.files.stream_lines(stream_path) @@ -272,7 +273,7 @@ DotnetNeotestAdapter.build_spec = function(args) end return { - command = { "dotnet", "build" }, + command = vstest.run_tests(pos.id, stream_path, results_path), context = { result_path = results_path, stop_stream = stop_stream, @@ -280,7 +281,6 @@ DotnetNeotestAdapter.build_spec = function(args) id = pos.id, }, stream = function() - vstest.run_tests(pos.id, stream_path, results_path) return function() local lines = stream_data() local results = {} @@ -300,7 +300,7 @@ end ---@param tree neotest.Tree ---@return neotest.Result[] DotnetNeotestAdapter.results = function(spec, run, tree) - local max_wait = 5 * 60 * 1000 -- 5 min + local max_wait = 5 * 50 * 1000 -- 5 min local success, data = pcall(vstest.spin_lock_wait_file, spec.context.result_path, max_wait) spec.context.stop_stream() diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index a685641..e76fa0f 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -32,7 +32,7 @@ local function invoke_test_runner(command) logger.trace(err) end, }, function(obj) - logger.warn("process ded :(") + logger.warn("vstest process died :(") logger.warn(obj.code) logger.warn(obj.signal) logger.warn(obj.stdout) @@ -114,6 +114,8 @@ function M.discover_tests(proj_file) end function M.run_tests(ids, stream_path, output_path) + lib.process.run({ "dotnet", "build" }) + local command = vim .iter({ "run-tests", @@ -124,6 +126,8 @@ function M.run_tests(ids, stream_path, output_path) :flatten() :join(" ") invoke_test_runner(command) + + return string.format("tail -n 1 -f %s", output_path, output_path) end return M From 253e225f124c5eaecd4f0abf380be08f303e29b6 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sun, 10 Nov 2024 23:32:18 +0100 Subject: [PATCH 12/43] feat: dap strategy --- lua/neotest-dotnet/init.lua | 162 ++++++-------------------- lua/neotest-dotnet/vstest_wrapper.lua | 44 +++++++ run_tests.fsx | 104 ++++++++++++++--- 3 files changed, 172 insertions(+), 138 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index b43838b..0e8572a 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -5,42 +5,6 @@ local logger = require("neotest.logging") local vstest = require("neotest-dotnet.vstest_wrapper") local DotnetNeotestAdapter = { name = "neotest-dotnet" } -local dap = { adapter_name = "netcoredbg" } - -local function get_script(script_name) - local script_paths = vim.api.nvim_get_runtime_file(script_name, true) - for _, path in ipairs(script_paths) do - if vim.endswith(path, ("neotest-dotnet%s" .. script_name):format(lib.files.sep)) then - return path - end - end -end - -local function test_discovery_args(test_application_dll) - local test_discovery_script = get_script("parse_tests.fsx") - local testhost_dll = "/usr/local/share/dotnet/sdk/8.0.401/vstest.console.dll" - - return { "fsi", test_discovery_script, testhost_dll, test_application_dll } -end - -local function run_test_command(id, stream_path, output_path, test_application_dll) - local test_discovery_script = get_script("run_tests.fsx") - local testhost_dll = "/usr/local/share/dotnet/sdk/8.0.401/vstest.console.dll" - - return { - "dotnet", - "fsi", - "--exec", - "--nologo", - "--shadowcopyreferences+", - test_discovery_script, - testhost_dll, - stream_path, - output_path, - id, - test_application_dll, - } -end DotnetNeotestAdapter.root = function(path) return lib.files.match_root_pattern("*.sln")(path) @@ -89,24 +53,6 @@ local fsharp_query = [[ (_) @test.definition)) ]] -local cache = {} - --- local function discover_tests(proj_dll_path) --- local open_err, file_fd = nio.uv.fs_open(proj_dll_path, "r", 444) --- assert(not open_err, open_err) --- local stat_err, stat = nio.uv.fs_fstat(file_fd) --- assert(not stat_err, stat_err) --- nio.uv.fs_close(file_fd) --- cache[proj_dll_path] = cache[proj_dll_path] or {} --- if cache.last_cached == nil or cache.last_cached < stat.mtime.sec then --- local discovery = vstest.discover_tests --- cache[proj_dll_path].data = vim.json.decode(discovery.stdout.read()) --- cache[proj_dll_path].last_cached = stat.mtime.sec --- discovery.close() --- end --- return cache[proj_dll_path].data --- end - local function get_match_type(captured_nodes) if captured_nodes["test.name"] then return "test" @@ -127,12 +73,6 @@ local function get_proj_file(path) return name:match("%.[cf]sproj$") end, { type = "file", path = vim.fs.dirname(path) })[1] - -- local dir_name = vim.fs.dirname(proj_file) - -- local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") - -- - -- local proj_dll_path = - -- vim.fs.find(proj_name .. ".dll", { upward = false, type = "file", path = dir_name })[1] - -- proj_file_path_map[path] = proj_file return proj_file end @@ -168,6 +108,7 @@ DotnetNeotestAdapter.discover_positions = function(path) if match_type == "test" then for _, test in ipairs(tests_in_file) do + -- TODO: check if linenumber in start<->end range. if test.LineNumber == definition:start() + 1 then table.insert(positions, { id = test.Id, @@ -254,13 +195,6 @@ end ---@param args neotest.RunArgs ---@return nil | neotest.RunSpec | neotest.RunSpec[] DotnetNeotestAdapter.build_spec = function(args) - local results_path = nio.fn.tempname() - local stream_path = nio.fn.tempname() - lib.files.write(results_path, "") - lib.files.write(stream_path, "") - - local stream_data, stop_stream = lib.files.stream_lines(stream_path) - local tree = args.tree if not tree then return @@ -272,6 +206,40 @@ DotnetNeotestAdapter.build_spec = function(args) return end + local results_path = nio.fn.tempname() + local stream_path = nio.fn.tempname() + lib.files.write(results_path, "") + lib.files.write(stream_path, "") + + local stream_data, stop_stream = lib.files.stream_lines(stream_path) + + local strategy + if args.strategy == "dap" then + local pid_path = nio.fn.tempname() + local attached_path = nio.fn.tempname() + + local pid = vstest.debug_tests(pid_path, attached_path, stream_path, results_path, pos.id) + --- @type Configuration + strategy = { + type = "netcoredbg", + name = "netcoredbg - attach", + request = "attach", + cwd = vim.fs.dirname(get_proj_file(pos.path)), + env = { + DOTNET_ENVIRONMENT = "Development", + }, + processId = pid, + before = function() + local dap = require("dap") + dap.listeners.after.configurationDone["neotest-dotnet"] = function() + nio.run(function() + lib.files.write(attached_path, "1") + end) + end + end, + } + end + return { command = vstest.run_tests(pos.id, stream_path, results_path), context = { @@ -291,6 +259,7 @@ DotnetNeotestAdapter.build_spec = function(args) return results end end, + strategy = strategy, } end @@ -338,64 +307,7 @@ DotnetNeotestAdapter.results = function(spec, run, tree) end setmetatable(DotnetNeotestAdapter, { - __call = function(_, opts) - if type(opts.dap) == "table" then - for k, v in pairs(opts.dap) do - dap[k] = v - end - end - if type(opts.custom_attributes) == "table" then - custom_attribute_args = opts.custom_attributes - end - if type(opts.dotnet_additional_args) == "table" then - dotnet_additional_args = opts.dotnet_additional_args - end - if type(opts.discovery_root) == "string" then - discovery_root = opts.discovery_root - end - - local function find_runsettings_files() - local files = {} - for _, runsettingsFile in - ipairs(vim.fn.glob(vim.fn.getcwd() .. "**/*.runsettings", false, true)) - do - table.insert(files, runsettingsFile) - end - - for _, runsettingsFile in - ipairs(vim.fn.glob(vim.fn.getcwd() .. "**/.runsettings", false, true)) - do - table.insert(files, runsettingsFile) - end - - return files - end - - local function select_runsettings_file() - local files = find_runsettings_files() - if #files == 0 then - print("No .runsettings files found") - vim.g.neotest_dotnet_runsettings_path = nil - return - end - - vim.ui.select(files, { - prompt = "Select runsettings file:", - format_item = function(item) - return vim.fn.fnamemodify(item, ":p:.") - end, - }, function(choice) - if choice then - vim.g.neotest_dotnet_runsettings_path = choice - print("Selected runsettings file: " .. choice) - end - end) - end - - vim.api.nvim_create_user_command("NeotestSelectRunsettingsFile", select_runsettings_file, {}) - vim.api.nvim_create_user_command("NeotestClearRunsettings", function() - vim.g.neotest_dotnet_runsettings_path = nil - end, {}) + __call = function(_, _) return DotnetNeotestAdapter end, }) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index e76fa0f..690e5fd 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -51,6 +51,10 @@ end local spin_lock = nio.control.semaphore(1) +---Repeatly tries to read content. Repeats untill the file is non-empty or operation times out. +---@param file_path string +---@param max_wait integer maximal time to wait for the file to populated in miliseconds. +---@return table|string function M.spin_lock_wait_file(file_path, max_wait) local json = {} @@ -76,6 +80,8 @@ function M.spin_lock_wait_file(file_path, max_wait) return json end +---@param proj_file string +---@return table test_cases function M.discover_tests(proj_file) local output_file = nio.fn.tempname() @@ -113,6 +119,11 @@ function M.discover_tests(proj_file) return json end +---runs tests identified by ids. +---@param ids string|string[] +---@param stream_path string +---@param output_path string +---@return string command function M.run_tests(ids, stream_path, output_path) lib.process.run({ "dotnet", "build" }) @@ -130,4 +141,37 @@ function M.run_tests(ids, stream_path, output_path) return string.format("tail -n 1 -f %s", output_path, output_path) end +---Uses the vstest console to spawn a test process for the debugger to attach to. +---@param pid_path string +---@param attached_path string +---@param stream_path string +---@param output_path string +---@param ids string|string[] +---@return integer pid +function M.debug_tests(pid_path, attached_path, stream_path, output_path, ids) + lib.process.run({ "dotnet", "build" }) + + local command = vim + .iter({ + "debug-tests", + pid_path, + attached_path, + stream_path, + output_path, + ids, + }) + :flatten() + :join(" ") + logger.debug("starting test in debug mode using:") + logger.debug(command) + + invoke_test_runner(command) + + logger.debug("Waiting for pid file to populate...") + + local max_wait = 30 * 1000 -- 30 sec + + return M.spin_lock_wait_file(pid_path, max_wait) +end + return M diff --git a/run_tests.fsx b/run_tests.fsx index 76c63ee..f677096 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -7,13 +7,18 @@ open System open System.IO +open System.Threading +open System.Threading.Tasks open Newtonsoft.Json open System.Collections.Generic open Microsoft.TestPlatform.VsTestConsole.TranslationLayer open Microsoft.VisualStudio.TestPlatform.ObjectModel open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client +open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces module TestDiscovery = + open System.Threading + [] let (|DiscoveryRequest|_|) (str: string) = if str.StartsWith("discover") then @@ -41,6 +46,24 @@ module TestDiscovery = else ValueOption.None + [] + let (|DebugTests|_|) (str: string) = + if str.StartsWith("debug-tests") then + let args = + str.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) + |> Array.tail + + {| PidPath = args[0] + AttachedPath = args[1] + StreamPath = args[2] + OutputPath = args[3] + Ids = args[4..] |> Array.map Guid.Parse |} + |> ValueOption.Some + else + ValueOption.None + + let discoveryCompleteEvent = new ManualResetEventSlim() + let discoveredTests = Dictionary() type PlaygroundTestDiscoveryHandler() = @@ -48,10 +71,13 @@ module TestDiscovery = member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = discoveredTestCases |> Seq.groupBy _.CodeFilePath - |> Seq.iter (fun (file, testCase) -> - if discoveredTests.ContainsKey file then - discoveredTests.Remove(file) |> ignore - discoveredTests.Add(file, testCase)) + |> Seq.iter (fun (file, testCases) -> + if discoveredTests.ContainsKey file then + discoveredTests.Remove(file) |> ignore + + discoveredTests.Add(file, testCases)) + + discoveryCompleteEvent.Set() member _.HandleDiscoveryComplete(_, _) = () @@ -116,6 +142,35 @@ module TestDiscovery = member __.LaunchProcessWithDebuggerAttached(_testProcessStartInfo) = 1 + type DebugLauncher(pidFile: string, attachedFile: string) = + interface ITestHostLauncher2 with + member this.LaunchTestHost(defaultTestHostStartInfo: TestProcessStartInfo) = + (this :> ITestHostLauncher) + .LaunchTestHost(defaultTestHostStartInfo, CancellationToken.None) + + member _.LaunchTestHost(_defaultTestHostStartInfo: TestProcessStartInfo, _ct: CancellationToken) = 1 + + member this.AttachDebuggerToProcess(pid: int) = + (this :> ITestHostLauncher2) + .AttachDebuggerToProcess(pid, CancellationToken.None) + + member _.AttachDebuggerToProcess(pid: int, ct: CancellationToken) = + use cts = CancellationTokenSource.CreateLinkedTokenSource(ct) + cts.CancelAfter(TimeSpan.FromSeconds(450)) + + do + Console.WriteLine($"spawned test process with pid: {pid}") + use pidWriter = new StreamWriter(pidFile, append = false) + pidWriter.WriteLine(pid) + + while not (cts.Token.IsCancellationRequested || File.Exists(attachedFile)) do + () + + File.Exists(attachedFile) + + member __.IsDebug = true + + let main (argv: string[]) = if argv.Length <> 1 then invalidArg "CommandLineArgs" "Usage: fsi script.fsx " @@ -152,30 +207,53 @@ module TestDiscovery = while loop do match Console.ReadLine() with | DiscoveryRequest args -> + discoveryCompleteEvent.Reset() r.DiscoverTests(args.Sources, sourceSettings, options, testSession, discoveryHandler) + let _ = discoveryCompleteEvent.Wait(TimeSpan.FromSeconds(5)) use streamWriter = new StreamWriter(args.OutputPath, append = false) + discoveredTests |> _.Values |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) |> Map - |> JsonConvert.SerializeObject + |> JsonConvert.SerializeObject |> streamWriter.WriteLine Console.WriteLine($"Wrote test results to {args.OutputPath}") | RunTests args -> let idMap = - discoveredTests - |> _.Values - |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) - |> Map + discoveredTests + |> _.Values + |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) + |> Map - let testCases = - args.Ids - |> Array.choose (fun id -> Map.tryFind id idMap) + let testCases = args.Ids |> Array.choose (fun id -> Map.tryFind id idMap) let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath) - r.RunTests(testCases, sourceSettings, testHandler) + // spawn as task to allow running concurrent tests + r.RunTestsAsync(testCases, sourceSettings, testHandler) |> ignore + () + | DebugTests args -> + let idMap = + discoveredTests + |> _.Values + |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) + |> Map + + let testCases = args.Ids |> Array.choose (fun id -> Map.tryFind id idMap) + + let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath) + let debugLauncher = DebugLauncher(args.PidPath, args.AttachedPath) + Console.WriteLine($"Starting {testCases.Length} tests in debug-mode") + + task { + do! Task.Yield() + r.RunTestsWithCustomTestHost(testCases, sourceSettings, testHandler, debugLauncher) + } + |> ignore + + () | _ -> loop <- false r.EndSession() From 10705b34427a64eede6c504fc096746997ec7106 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sun, 10 Nov 2024 23:43:34 +0100 Subject: [PATCH 13/43] improve detection --- lua/neotest-dotnet/init.lua | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 0e8572a..f7a91d9 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -38,19 +38,14 @@ local fsharp_query = [[ (declaration_expression (function_or_value_defn - (function_declaration_left - . - (_) @test.name) - body: (_) @test.definition)) + (function_declaration_left . (_) @test.name)) +) @test.definition (member_defn (method_or_prop_defn (property_or_ident - (identifier) @test.name .) - . - (_) - . - (_) @test.definition)) + (identifier) @test.name .)) +) @test.definition ]] local function get_match_type(captured_nodes) @@ -108,8 +103,9 @@ DotnetNeotestAdapter.discover_positions = function(path) if match_type == "test" then for _, test in ipairs(tests_in_file) do - -- TODO: check if linenumber in start<->end range. - if test.LineNumber == definition:start() + 1 then + if + definition:start() <= test.LineNumber - 1 and test.LineNumber - 1 <= definition:end_() + then table.insert(positions, { id = test.Id, type = match_type, From b7bcb698c8e7971eeb6d19b8a9eb452750cbbb8d Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Mon, 11 Nov 2024 00:00:06 +0100 Subject: [PATCH 14/43] feat: csharp --- lua/neotest-dotnet/init.lua | 86 +++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index f7a91d9..64eb507 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -20,32 +20,63 @@ DotnetNeotestAdapter.filter_dir = function(name) end local fsharp_query = [[ -(namespace - name: (long_identifier) @namespace.name -) @namespace.definition - -(anon_type_defn - (type_name (identifier) @namespace.name) -) @namespace.definition - -(named_module - name: (long_identifier) @namespace.name -) @namespace.definition - -(module_defn - (identifier) @namespace.name -) @namespace.definition - -(declaration_expression - (function_or_value_defn - (function_declaration_left . (_) @test.name)) -) @test.definition - -(member_defn - (method_or_prop_defn - (property_or_ident - (identifier) @test.name .)) -) @test.definition + (namespace + name: (long_identifier) @namespace.name + ) @namespace.definition + + (anon_type_defn + (type_name (identifier) @namespace.name) + ) @namespace.definition + + (named_module + name: (long_identifier) @namespace.name + ) @namespace.definition + + (module_defn + (identifier) @namespace.name + ) @namespace.definition + + (declaration_expression + (function_or_value_defn + (function_declaration_left . (_) @test.name)) + ) @test.definition + + (member_defn + (method_or_prop_defn + (property_or_ident + (identifier) @test.name .)) + ) @test.definition +]] + +local c_sharp_query = [[ + ;; Matches namespace with a '.' in the name + (namespace_declaration + name: (qualified_name) @namespace.name + ) @namespace.definition + + ;; Matches namespace with a single identifier (no '.') + (namespace_declaration + name: (identifier) @namespace.name + ) @namespace.definition + + ;; Matches file-scoped namespaces (qualified and unqualified respectively) + (file_scoped_namespace_declaration + name: (qualified_name) @namespace.name + ) @namespace.definition + + (file_scoped_namespace_declaration + name: (identifier) @namespace.name + ) @namespace.definition + + ;; Matches XUnit test class (has no specific attributes on class) + (class_declaration + name: (identifier) @namespace.name + ) @namespace.definition + + ;; Matches test methods + (method_declaration + name: (identifier) @test.name + ) @test.definition ]] local function get_match_type(captured_nodes) @@ -140,7 +171,8 @@ DotnetNeotestAdapter.discover_positions = function(path) local root = lib.treesitter.fast_parse(lang_tree):root() - local query = lib.treesitter.normalise_query(lang, fsharp_query) + local query = + lib.treesitter.normalise_query(lang, filetype == "fsharp" and fsharp_query or c_sharp_query) local sep = lib.files.sep local path_elems = vim.split(path, sep, { plain = true }) From b3b1c65bb7d10413b17133b84cea6d7b6f058e90 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Mon, 11 Nov 2024 17:47:06 +0100 Subject: [PATCH 15/43] test discovery caching --- lua/neotest-dotnet/init.lua | 3 - lua/neotest-dotnet/vstest_wrapper.lua | 80 ++++++++++++++++++++------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 64eb507..dee0d36 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -119,9 +119,6 @@ DotnetNeotestAdapter.discover_positions = function(path) end) :totable() - logger.debug("filtered test cases:") - logger.debug(tests_in_file) - local tree ---@return nil | neotest.Position | neotest.Position[] diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 690e5fd..11958c5 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -54,9 +54,9 @@ local spin_lock = nio.control.semaphore(1) ---Repeatly tries to read content. Repeats untill the file is non-empty or operation times out. ---@param file_path string ---@param max_wait integer maximal time to wait for the file to populated in miliseconds. ----@return table|string +---@return string function M.spin_lock_wait_file(file_path, max_wait) - local json = {} + local content local sleep_time = 25 -- scan every 25 ms local tries = 1 @@ -68,7 +68,7 @@ function M.spin_lock_wait_file(file_path, max_wait) local file, open_err = nio.file.open(file_path) assert(not open_err, open_err) file_exists = true - json = file.read() + content = file.read() file.close() end) else @@ -77,9 +77,12 @@ function M.spin_lock_wait_file(file_path, max_wait) end end - return json + return content end +local discovery_cache = {} +local discovery_lock = nio.control.semaphore(1) + ---@param proj_file string ---@return table test_cases function M.discover_tests(proj_file) @@ -93,28 +96,65 @@ function M.discover_tests(proj_file) lib.process.run({ "dotnet", "build", proj_file }) - local command = vim - .iter({ - "discover", - output_file, - proj_dll_path, - }) - :flatten() - :join(" ") + local json - logger.debug("Discovering tests using:") - logger.debug(command) + discovery_lock.with(function() + local open_err, stats = nio.uv.fs_stat(proj_dll_path) + assert(not open_err, open_err) - invoke_test_runner(command) + local cached = discovery_cache[proj_dll_path] + local modified_time = stats.mtime and stats.mtime.sec - logger.debug("Waiting for result file to populate...") + if + cached + and cached.last_modified + and modified_time + and modified_time <= cached.last_modified + then + logger.debug("cache hit") + json = cached.content + return + end + + logger.debug( + string.format( + "cache not hit: %s %s %s", + proj_dll_path, + cached and cached.last_modified, + modified_time + ) + ) + + local command = vim + .iter({ + "discover", + output_file, + proj_dll_path, + }) + :flatten() + :join(" ") + + logger.debug("Discovering tests using:") + logger.debug(command) - local max_wait = 10 * 1000 -- 10 sec + invoke_test_runner(command) - local json = - vim.json.decode(M.spin_lock_wait_file(output_file, max_wait), { luanil = { object = true } }) + logger.debug("Waiting for result file to populate...") - logger.debug("file has been populated. Extracting test cases") + local max_wait = 10 * 1000 -- 10 sec + + json = vim.json.decode( + M.spin_lock_wait_file(output_file, max_wait), + { luanil = { object = true } } + ) or {} + + logger.debug("file has been populated. Extracting test cases") + + discovery_cache[proj_dll_path] = { + last_modified = modified_time, + content = json, + } + end) return json end From c8d558c96b17e471a0731a20aa3fb1fad5f9bc53 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Mon, 11 Nov 2024 19:13:07 +0100 Subject: [PATCH 16/43] vstest path detection --- lua/neotest-dotnet/vstest_wrapper.lua | 38 +++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 11958c5..f44fbf3 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -4,6 +4,34 @@ local logger = require("neotest.logging") local M = {} +M.sdk_path = nil + +local function get_vstest_path() + if not M.sdk_path then + local process, errors = nio.process.run({ + cmd = "dotnet", + args = { "--info" }, + }) + + if not process or errors then + if vim.fn.has("win32") then + M.sdk_path = "C:/Program Files/dotnet/sdk/" + else + M.sdk_path = "/usr/local/share/dotnet/sdk/" + end + + logger.info(string.format("failed to detect sdk path. falling back to %s", M.sdk_path)) + else + local out = process.stdout.read() + M.sdk_path = out:match("Base Path:%s*(%S+)") + logger.info(string.format("detected sdk path: %s", M.sdk_path)) + process.close() + end + end + + return vim.fs.find("vstest.console.dll", { upward = false, type = "file", path = M.sdk_path })[1] +end + local function get_script(script_name) local script_paths = vim.api.nvim_get_runtime_file(script_name, true) for _, path in ipairs(script_paths) do @@ -23,9 +51,14 @@ local function invoke_test_runner(command) end local test_discovery_script = get_script("run_tests.fsx") - local testhost_dll = "/usr/local/share/dotnet/sdk/8.0.401/vstest.console.dll" + local testhost_dll = get_vstest_path() + + local vstest_command = { "dotnet", "fsi", test_discovery_script, testhost_dll } + + logger.info("starting vstest console with:") + logger.info(vstest_command) - local process = vim.system({ "dotnet", "fsi", test_discovery_script, testhost_dll }, { + local process = vim.system(vstest_command, { stdin = true, stdout = function(err, data) logger.trace(data) @@ -92,6 +125,7 @@ function M.discover_tests(proj_file) local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") local proj_dll_path = + -- TODO: this might break if the project has been compiled as both Development and Release. vim.fs.find(proj_name .. ".dll", { upward = false, type = "file", path = dir_name })[1] lib.process.run({ "dotnet", "build", proj_file }) From 025d7dadf5eff750ed07b4c5aae4a2fce76534ee Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Mon, 11 Nov 2024 21:57:21 +0100 Subject: [PATCH 17/43] clean up --- lua/neotest-dotnet/init.lua | 22 +------- lua/neotest-dotnet/vstest_wrapper.lua | 72 ++++++++++++++++++--------- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index dee0d36..caf8f02 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -88,29 +88,13 @@ local function get_match_type(captured_nodes) end end -local proj_file_path_map = {} - -local function get_proj_file(path) - if proj_file_path_map[path] then - return proj_file_path_map[path] - end - - local proj_file = vim.fs.find(function(name, _) - return name:match("%.[cf]sproj$") - end, { type = "file", path = vim.fs.dirname(path) })[1] - - proj_file_path_map[path] = proj_file - return proj_file -end - ---@param path any The path to the file to discover positions in ---@return neotest.Tree DotnetNeotestAdapter.discover_positions = function(path) local filetype = (vim.endswith(path, ".fs") and "fsharp") or "c_sharp" - local proj_dll_path = get_proj_file(path) local tests_in_file = vim - .iter(vstest.discover_tests(proj_dll_path)) + .iter(vstest.discover_tests(path)) :map(function(_, v) return v end) @@ -140,7 +124,6 @@ DotnetNeotestAdapter.discover_positions = function(path) path = path, name = test.DisplayName, qualified_name = test.FullyQualifiedName, - proj_dll_path = test.Source, range = { definition:range() }, }) end @@ -151,7 +134,6 @@ DotnetNeotestAdapter.discover_positions = function(path) type = match_type, path = path, name = string.gsub(name, "``", ""), - proj_dll_path = proj_dll_path, range = { definition:range() }, }) end @@ -249,7 +231,7 @@ DotnetNeotestAdapter.build_spec = function(args) type = "netcoredbg", name = "netcoredbg - attach", request = "attach", - cwd = vim.fs.dirname(get_proj_file(pos.path)), + cwd = vstest.get_proj_info(pos.path).proj_dir, env = { DOTNET_ENVIRONMENT = "Development", }, diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index f44fbf3..e714849 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -23,7 +23,7 @@ local function get_vstest_path() logger.info(string.format("failed to detect sdk path. falling back to %s", M.sdk_path)) else local out = process.stdout.read() - M.sdk_path = out:match("Base Path:%s*(%S+)") + M.sdk_path = out and out:match("Base Path:%s*(%S+)") logger.info(string.format("detected sdk path: %s", M.sdk_path)) process.close() end @@ -41,6 +41,38 @@ local function get_script(script_name) end end +local proj_file_path_map = {} + +---collects project information based on file +---@param path string +---@return { proj_file: string, proj_name: string, dll_file: string, proj_dir: string } +function M.get_proj_info(path) + if proj_file_path_map[path] then + return proj_file_path_map[path] + end + + local proj_file = vim.fs.find(function(name, _) + return name:match("%.[cf]sproj$") + end, { type = "file", path = vim.fs.dirname(path) })[1] + + local dir_name = vim.fs.dirname(proj_file) + local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") + + local proj_dll_path = + -- TODO: this might break if the project has been compiled as both Development and Release. + vim.fs.find(proj_name .. ".dll", { upward = false, type = "file", path = dir_name })[1] + + local proj_data = { + proj_file = proj_file, + proj_name = proj_name, + dll_file = proj_dll_path, + proj_dir = dir_name, + } + + proj_file_path_map[path] = proj_data + return proj_data +end + local test_runner local semaphore = nio.control.semaphore(1) @@ -84,10 +116,10 @@ end local spin_lock = nio.control.semaphore(1) ----Repeatly tries to read content. Repeats untill the file is non-empty or operation times out. +---Repeatly tries to read content. Repeats until the file is non-empty or operation times out. ---@param file_path string ----@param max_wait integer maximal time to wait for the file to populated in miliseconds. ----@return string +---@param max_wait integer maximal time to wait for the file to populated in milliseconds. +---@return string? function M.spin_lock_wait_file(file_path, max_wait) local content @@ -116,27 +148,22 @@ end local discovery_cache = {} local discovery_lock = nio.control.semaphore(1) ----@param proj_file string +---@param path string ---@return table test_cases -function M.discover_tests(proj_file) +function M.discover_tests(path) local output_file = nio.fn.tempname() - local dir_name = vim.fs.dirname(proj_file) - local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") - - local proj_dll_path = - -- TODO: this might break if the project has been compiled as both Development and Release. - vim.fs.find(proj_name .. ".dll", { upward = false, type = "file", path = dir_name })[1] + local proj_info = M.get_proj_info(path) - lib.process.run({ "dotnet", "build", proj_file }) + lib.process.run({ "dotnet", "build", proj_info.proj_file }) local json discovery_lock.with(function() - local open_err, stats = nio.uv.fs_stat(proj_dll_path) + local open_err, stats = nio.uv.fs_stat(proj_info.dll_file) assert(not open_err, open_err) - local cached = discovery_cache[proj_dll_path] + local cached = discovery_cache[proj_info.dll_file] local modified_time = stats.mtime and stats.mtime.sec if @@ -153,7 +180,7 @@ function M.discover_tests(proj_file) logger.debug( string.format( "cache not hit: %s %s %s", - proj_dll_path, + proj_info.dll_file, cached and cached.last_modified, modified_time ) @@ -163,7 +190,7 @@ function M.discover_tests(proj_file) .iter({ "discover", output_file, - proj_dll_path, + proj_info.dll_file, }) :flatten() :join(" ") @@ -177,14 +204,13 @@ function M.discover_tests(proj_file) local max_wait = 10 * 1000 -- 10 sec - json = vim.json.decode( - M.spin_lock_wait_file(output_file, max_wait), - { luanil = { object = true } } - ) or {} + local content = M.spin_lock_wait_file(output_file, max_wait) + + json = (content and vim.json.decode(content, { luanil = { object = true } })) or {} logger.debug("file has been populated. Extracting test cases") - discovery_cache[proj_dll_path] = { + discovery_cache[proj_info.dll_file] = { last_modified = modified_time, content = json, } @@ -221,7 +247,7 @@ end ---@param stream_path string ---@param output_path string ---@param ids string|string[] ----@return integer pid +---@return string? pid function M.debug_tests(pid_path, attached_path, stream_path, output_path, ids) lib.process.run({ "dotnet", "build" }) From 0dc31d972505e4b3266084a8e7e6a40dfb12a95c Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Tue, 12 Nov 2024 17:19:10 +0100 Subject: [PATCH 18/43] improve detection stability --- lua/neotest-dotnet/vstest_wrapper.lua | 96 +++++++++++++++------------ run_tests.fsx | 20 +++--- 2 files changed, 63 insertions(+), 53 deletions(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index e714849..e865e26 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -45,7 +45,7 @@ local proj_file_path_map = {} ---collects project information based on file ---@param path string ----@return { proj_file: string, proj_name: string, dll_file: string, proj_dir: string } +---@return { proj_file: string, dll_file: string, proj_dir: string } function M.get_proj_info(path) if proj_file_path_map[path] then return proj_file_path_map[path] @@ -64,7 +64,6 @@ function M.get_proj_info(path) local proj_data = { proj_file = proj_file, - proj_name = proj_name, dll_file = proj_dll_path, proj_dir = dir_name, } @@ -151,59 +150,68 @@ local discovery_lock = nio.control.semaphore(1) ---@param path string ---@return table test_cases function M.discover_tests(path) - local output_file = nio.fn.tempname() - + local json = {} local proj_info = M.get_proj_info(path) - lib.process.run({ "dotnet", "build", proj_info.proj_file }) - - local json + discovery_lock.acquire() - discovery_lock.with(function() - local open_err, stats = nio.uv.fs_stat(proj_info.dll_file) - assert(not open_err, open_err) + if not proj_info.dll_file then + logger.warn(string.format("failed to find dll for file: %s", path)) + return json + end - local cached = discovery_cache[proj_info.dll_file] - local modified_time = stats.mtime and stats.mtime.sec + lib.process.run({ "dotnet", "build", proj_info.proj_file }) - if - cached - and cached.last_modified - and modified_time - and modified_time <= cached.last_modified - then - logger.debug("cache hit") - json = cached.content - return - end + local open_err, stats = nio.uv.fs_stat(proj_info.dll_file) + assert(not open_err, open_err) + + local cached = discovery_cache[proj_info.dll_file] + local modified_time = stats.mtime and stats.mtime.sec + + if + cached + and cached.last_modified + and modified_time + and modified_time <= cached.last_modified + then + logger.debug("cache hit") + discovery_lock.release() + return cached.content + end - logger.debug( - string.format( - "cache not hit: %s %s %s", - proj_info.dll_file, - cached and cached.last_modified, - modified_time - ) + logger.debug( + string.format( + "cache not hit: %s %s %s", + proj_info.dll_file, + cached and cached.last_modified, + modified_time ) + ) - local command = vim - .iter({ - "discover", - output_file, - proj_info.dll_file, - }) - :flatten() - :join(" ") + local wait_file = nio.fn.tempname() + local output_file = nio.fn.tempname() - logger.debug("Discovering tests using:") - logger.debug(command) + local command = vim + .iter({ + "discover", + output_file, + wait_file, + proj_info.dll_file, + }) + :flatten() + :join(" ") + + logger.debug("Discovering tests using:") + logger.debug(command) - invoke_test_runner(command) + invoke_test_runner(command) - logger.debug("Waiting for result file to populate...") + logger.debug("Waiting for result file to populate...") - local max_wait = 10 * 1000 -- 10 sec + local max_wait = 30 * 1000 -- 10 sec + local done = M.spin_lock_wait_file(wait_file, max_wait) + if done then local content = M.spin_lock_wait_file(output_file, max_wait) json = (content and vim.json.decode(content, { luanil = { object = true } })) or {} @@ -214,7 +222,9 @@ function M.discover_tests(path) last_modified = modified_time, content = json, } - end) + end + + discovery_lock.release() return json end diff --git a/run_tests.fsx b/run_tests.fsx index f677096..8a63bcd 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -17,8 +17,6 @@ open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces module TestDiscovery = - open System.Threading - [] let (|DiscoveryRequest|_|) (str: string) = if str.StartsWith("discover") then @@ -26,8 +24,9 @@ module TestDiscovery = str.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) |> Array.tail - {| OutputPath = Array.head args - Sources = args |> Array.tail |} + {| OutputPath = args[0] + WaitFile = args[1] + Sources = args[2..] |} |> ValueOption.Some else ValueOption.None @@ -77,9 +76,7 @@ module TestDiscovery = discoveredTests.Add(file, testCases)) - discoveryCompleteEvent.Set() - - member _.HandleDiscoveryComplete(_, _) = () + member _.HandleDiscoveryComplete(_, _) = discoveryCompleteEvent.Set() member _.HandleLogMessage(_, _) = () member _.HandleRawMessage(_) = () @@ -209,16 +206,19 @@ module TestDiscovery = | DiscoveryRequest args -> discoveryCompleteEvent.Reset() r.DiscoverTests(args.Sources, sourceSettings, options, testSession, discoveryHandler) - let _ = discoveryCompleteEvent.Wait(TimeSpan.FromSeconds(5)) + let _ = discoveryCompleteEvent.Wait(TimeSpan.FromSeconds(30)) - use streamWriter = new StreamWriter(args.OutputPath, append = false) + use testsWriter = new StreamWriter(args.OutputPath, append = false) discoveredTests |> _.Values |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) |> Map |> JsonConvert.SerializeObject - |> streamWriter.WriteLine + |> testsWriter.WriteLine + + use waitFileWriter = new StreamWriter(args.WaitFile, append = false) + waitFileWriter.WriteLine("1") Console.WriteLine($"Wrote test results to {args.OutputPath}") | RunTests args -> From 145eede5a053e22a4fe569b749c54e6193fe21ae Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Tue, 12 Nov 2024 22:08:07 +0100 Subject: [PATCH 19/43] improve test file filtering --- lua/neotest-dotnet/init.lua | 157 ++++++++----------------- lua/neotest-dotnet/queries/c_sharp.lua | 30 +++++ lua/neotest-dotnet/queries/fsharp.lua | 28 +++++ 3 files changed, 106 insertions(+), 109 deletions(-) create mode 100644 lua/neotest-dotnet/queries/c_sharp.lua create mode 100644 lua/neotest-dotnet/queries/fsharp.lua diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index caf8f02..d95cf07 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -4,6 +4,8 @@ local logger = require("neotest.logging") local vstest = require("neotest-dotnet.vstest_wrapper") +---@package +---@type neotest.Adapter local DotnetNeotestAdapter = { name = "neotest-dotnet" } DotnetNeotestAdapter.root = function(path) @@ -12,73 +14,16 @@ DotnetNeotestAdapter.root = function(path) end DotnetNeotestAdapter.is_test_file = function(file_path) - return vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs") + return (vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs")) + and vim.iter(vstest.discover_tests(file_path)):any(function(_, test) + return test.CodeFilePath == file_path + end) end DotnetNeotestAdapter.filter_dir = function(name) return name ~= "bin" and name ~= "obj" end -local fsharp_query = [[ - (namespace - name: (long_identifier) @namespace.name - ) @namespace.definition - - (anon_type_defn - (type_name (identifier) @namespace.name) - ) @namespace.definition - - (named_module - name: (long_identifier) @namespace.name - ) @namespace.definition - - (module_defn - (identifier) @namespace.name - ) @namespace.definition - - (declaration_expression - (function_or_value_defn - (function_declaration_left . (_) @test.name)) - ) @test.definition - - (member_defn - (method_or_prop_defn - (property_or_ident - (identifier) @test.name .)) - ) @test.definition -]] - -local c_sharp_query = [[ - ;; Matches namespace with a '.' in the name - (namespace_declaration - name: (qualified_name) @namespace.name - ) @namespace.definition - - ;; Matches namespace with a single identifier (no '.') - (namespace_declaration - name: (identifier) @namespace.name - ) @namespace.definition - - ;; Matches file-scoped namespaces (qualified and unqualified respectively) - (file_scoped_namespace_declaration - name: (qualified_name) @namespace.name - ) @namespace.definition - - (file_scoped_namespace_declaration - name: (identifier) @namespace.name - ) @namespace.definition - - ;; Matches XUnit test class (has no specific attributes on class) - (class_declaration - name: (identifier) @namespace.name - ) @namespace.definition - - ;; Matches test methods - (method_declaration - name: (identifier) @test.name - ) @test.definition -]] - local function get_match_type(captured_nodes) if captured_nodes["test.name"] then return "test" @@ -88,9 +33,46 @@ local function get_match_type(captured_nodes) end end ----@param path any The path to the file to discover positions in ----@return neotest.Tree +---@return nil | neotest.Position | neotest.Position[] +local function build_position(source, captured_nodes, tests_in_file, path) + local match_type = get_match_type(captured_nodes) + if match_type then + local definition = captured_nodes[match_type .. ".definition"] + + local positions = {} + + if match_type == "test" then + for _, test in ipairs(tests_in_file) do + if + definition:start() <= test.LineNumber - 1 and test.LineNumber - 1 <= definition:end_() + then + table.insert(positions, { + id = test.Id, + type = match_type, + path = path, + name = test.DisplayName, + qualified_name = test.FullyQualifiedName, + range = { definition:range() }, + }) + end + end + else + local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) + table.insert(positions, { + type = match_type, + path = path, + name = string.gsub(name, "``", ""), + range = { definition:range() }, + }) + end + return positions + end +end + DotnetNeotestAdapter.discover_positions = function(path) + local fsharp_query = require("neotest-dotnet.queries.fsharp") + local c_sharp_query = require("neotest-dotnet.queries.c_sharp") + local filetype = (vim.endswith(path, ".fs") and "fsharp") or "c_sharp" local tests_in_file = vim @@ -103,44 +85,9 @@ DotnetNeotestAdapter.discover_positions = function(path) end) :totable() + ---@type neotest.Tree? local tree - ---@return nil | neotest.Position | neotest.Position[] - local function build_position(source, captured_nodes) - local match_type = get_match_type(captured_nodes) - if match_type then - local definition = captured_nodes[match_type .. ".definition"] - - local positions = {} - - if match_type == "test" then - for _, test in ipairs(tests_in_file) do - if - definition:start() <= test.LineNumber - 1 and test.LineNumber - 1 <= definition:end_() - then - table.insert(positions, { - id = test.Id, - type = match_type, - path = path, - name = test.DisplayName, - qualified_name = test.FullyQualifiedName, - range = { definition:range() }, - }) - end - end - else - local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) - table.insert(positions, { - type = match_type, - path = path, - name = string.gsub(name, "``", ""), - range = { definition:range() }, - }) - end - return positions - end - end - if #tests_in_file > 0 then local content = lib.files.read(path) local lang = vim.treesitter.language.get_lang(filetype) or filetype @@ -168,7 +115,7 @@ DotnetNeotestAdapter.discover_positions = function(path) for i, capture in ipairs(query.captures) do captured_nodes[capture] = match[i] end - local res = build_position(content, captured_nodes) + local res = build_position(content, captured_nodes, tests_in_file, path) if res then for _, pos in ipairs(res) do nodes[#nodes + 1] = pos @@ -198,9 +145,6 @@ DotnetNeotestAdapter.discover_positions = function(path) return tree end ----@summary Neotest core interface method: Build specs for running tests ----@param args neotest.RunArgs ----@return nil | neotest.RunSpec | neotest.RunSpec[] DotnetNeotestAdapter.build_spec = function(args) local tree = args.tree if not tree then @@ -270,12 +214,7 @@ DotnetNeotestAdapter.build_spec = function(args) } end ----@async ----@param spec neotest.RunSpec ----@param run neotest.StrategyResult ----@param tree neotest.Tree ----@return neotest.Result[] -DotnetNeotestAdapter.results = function(spec, run, tree) +DotnetNeotestAdapter.results = function(spec) local max_wait = 5 * 50 * 1000 -- 5 min local success, data = pcall(vstest.spin_lock_wait_file, spec.context.result_path, max_wait) diff --git a/lua/neotest-dotnet/queries/c_sharp.lua b/lua/neotest-dotnet/queries/c_sharp.lua new file mode 100644 index 0000000..6ba0bcb --- /dev/null +++ b/lua/neotest-dotnet/queries/c_sharp.lua @@ -0,0 +1,30 @@ +return [[ + ;; Matches namespace with a '.' in the name + (namespace_declaration + name: (qualified_name) @namespace.name + ) @namespace.definition + + ;; Matches namespace with a single identifier (no '.') + (namespace_declaration + name: (identifier) @namespace.name + ) @namespace.definition + + ;; Matches file-scoped namespaces (qualified and unqualified respectively) + (file_scoped_namespace_declaration + name: (qualified_name) @namespace.name + ) @namespace.definition + + (file_scoped_namespace_declaration + name: (identifier) @namespace.name + ) @namespace.definition + + ;; Matches XUnit test class (has no specific attributes on class) + (class_declaration + name: (identifier) @namespace.name + ) @namespace.definition + + ;; Matches test methods + (method_declaration + name: (identifier) @test.name + ) @test.definition +]] diff --git a/lua/neotest-dotnet/queries/fsharp.lua b/lua/neotest-dotnet/queries/fsharp.lua new file mode 100644 index 0000000..f64f6f8 --- /dev/null +++ b/lua/neotest-dotnet/queries/fsharp.lua @@ -0,0 +1,28 @@ +return [[ + (namespace + name: (long_identifier) @namespace.name + ) @namespace.definition + + (anon_type_defn + (type_name (identifier) @namespace.name) + ) @namespace.definition + + (named_module + name: (long_identifier) @namespace.name + ) @namespace.definition + + (module_defn + (identifier) @namespace.name + ) @namespace.definition + + (declaration_expression + (function_or_value_defn + (function_declaration_left . (_) @test.name)) + ) @test.definition + + (member_defn + (method_or_prop_defn + (property_or_ident + (identifier) @test.name .)) + ) @test.definition +]] From 4b555f096c3a9aa1b1baeedb0cda37e1be60d2bb Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Tue, 12 Nov 2024 23:13:05 +0100 Subject: [PATCH 20/43] concurrent test discovery --- lua/neotest-dotnet/init.lua | 19 ++++++++-- lua/neotest-dotnet/vstest_wrapper.lua | 36 +++++++++--------- run_tests.fsx | 54 ++++++++++++++------------- 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index d95cf07..f1ae3ba 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -14,10 +14,17 @@ DotnetNeotestAdapter.root = function(path) end DotnetNeotestAdapter.is_test_file = function(file_path) - return (vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs")) - and vim.iter(vstest.discover_tests(file_path)):any(function(_, test) - return test.CodeFilePath == file_path - end) + if not (vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs")) then + return false + else + for _, test in pairs(vstest.discover_tests(file_path)) do + if test.CodeFilePath == file_path then + return true + end + end + end + + return false end DotnetNeotestAdapter.filter_dir = function(name) @@ -70,6 +77,8 @@ local function build_position(source, captured_nodes, tests_in_file, path) end DotnetNeotestAdapter.discover_positions = function(path) + logger.info(string.format("scanning %s for tests...", path)) + local fsharp_query = require("neotest-dotnet.queries.fsharp") local c_sharp_query = require("neotest-dotnet.queries.c_sharp") @@ -142,6 +151,8 @@ DotnetNeotestAdapter.discover_positions = function(path) }) end + logger.info(string.format("done scanning %s for tests", path)) + return tree end diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index e865e26..0bab64b 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -141,26 +141,37 @@ function M.spin_lock_wait_file(file_path, max_wait) end end + if not content then + logger.warn(string.format("timed out reading content of file %s", file_path)) + end + return content end local discovery_cache = {} -local discovery_lock = nio.control.semaphore(1) +local build_semaphore = nio.control.semaphore(1) + +---@class TestCase +---@field Id string +---@field CodeFilePath string +---@field DisplayName string +---@field FullyQualifiedName string +---@field Source string ---@param path string ----@return table test_cases +---@return table test_cases function M.discover_tests(path) local json = {} local proj_info = M.get_proj_info(path) - discovery_lock.acquire() - if not proj_info.dll_file then logger.warn(string.format("failed to find dll for file: %s", path)) return json end - lib.process.run({ "dotnet", "build", proj_info.proj_file }) + build_semaphore.with(function() + lib.process.run({ "dotnet", "build", proj_info.proj_file }) + end) local open_err, stats = nio.uv.fs_stat(proj_info.dll_file) assert(not open_err, open_err) @@ -174,20 +185,9 @@ function M.discover_tests(path) and modified_time and modified_time <= cached.last_modified then - logger.debug("cache hit") - discovery_lock.release() return cached.content end - logger.debug( - string.format( - "cache not hit: %s %s %s", - proj_info.dll_file, - cached and cached.last_modified, - modified_time - ) - ) - local wait_file = nio.fn.tempname() local output_file = nio.fn.tempname() @@ -206,7 +206,7 @@ function M.discover_tests(path) invoke_test_runner(command) - logger.debug("Waiting for result file to populate...") + logger.debug(string.format("Waiting for result file to populate for %s...", proj_info.proj_file)) local max_wait = 30 * 1000 -- 10 sec @@ -224,8 +224,6 @@ function M.discover_tests(path) } end - discovery_lock.release() - return json end diff --git a/run_tests.fsx b/run_tests.fsx index 8a63bcd..9ea8179 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -17,6 +17,8 @@ open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces module TestDiscovery = + open System.Collections.Concurrent + [] let (|DiscoveryRequest|_|) (str: string) = if str.StartsWith("discover") then @@ -61,25 +63,33 @@ module TestDiscovery = else ValueOption.None - let discoveryCompleteEvent = new ManualResetEventSlim() - - let discoveredTests = Dictionary() + let discoveredTests = ConcurrentDictionary() - type PlaygroundTestDiscoveryHandler() = + type PlaygroundTestDiscoveryHandler(resultFilePath, waitFilePath) = interface ITestDiscoveryEventsHandler2 with member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = discoveredTestCases |> Seq.groupBy _.CodeFilePath |> Seq.iter (fun (file, testCases) -> - if discoveredTests.ContainsKey file then - discoveredTests.Remove(file) |> ignore + discoveredTests.AddOrUpdate(file, testCases, (fun _ _ -> testCases)) |> ignore) + + use testsWriter = new StreamWriter(resultFilePath, append = false) + + discoveredTests + |> _.Values + |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) + |> Map + |> JsonConvert.SerializeObject + |> testsWriter.WriteLine - discoveredTests.Add(file, testCases)) + use waitFileWriter = new StreamWriter(waitFilePath, append = false) + waitFileWriter.WriteLine("1") - member _.HandleDiscoveryComplete(_, _) = discoveryCompleteEvent.Set() + Console.WriteLine($"Wrote test results to {resultFilePath}") - member _.HandleLogMessage(_, _) = () - member _.HandleRawMessage(_) = () + member _.HandleDiscoveryComplete(_, _) = () + member __.HandleLogMessage(_, _) = () + member __.HandleRawMessage(_) = () type PlaygroundTestRunHandler(streamOutputPath, outputFilePath) = interface ITestRunEventsHandler with @@ -195,7 +205,6 @@ module TestDiscovery = VsTestConsoleWrapper(console, ConsoleParameters(EnvironmentVariables = environmentVariables)) let testSession = TestSessionInfo() - let discoveryHandler = PlaygroundTestDiscoveryHandler() r.StartSession() @@ -204,23 +213,16 @@ module TestDiscovery = while loop do match Console.ReadLine() with | DiscoveryRequest args -> - discoveryCompleteEvent.Reset() - r.DiscoverTests(args.Sources, sourceSettings, options, testSession, discoveryHandler) - let _ = discoveryCompleteEvent.Wait(TimeSpan.FromSeconds(30)) - - use testsWriter = new StreamWriter(args.OutputPath, append = false) - - discoveredTests - |> _.Values - |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) - |> Map - |> JsonConvert.SerializeObject - |> testsWriter.WriteLine + // spawn as task to allow running discovery + task { + do! Task.Yield() - use waitFileWriter = new StreamWriter(args.WaitFile, append = false) - waitFileWriter.WriteLine("1") + let discoveryHandler = + PlaygroundTestDiscoveryHandler(args.OutputPath, args.WaitFile) :> ITestDiscoveryEventsHandler2 - Console.WriteLine($"Wrote test results to {args.OutputPath}") + r.DiscoverTests(args.Sources, sourceSettings, options, testSession, discoveryHandler) + } + |> ignore | RunTests args -> let idMap = discoveredTests From 5741e1530fd3024a9aa36edbd78cfb7176d9289e Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 13 Nov 2024 19:25:34 +0100 Subject: [PATCH 21/43] remove unused test case fields --- lua/neotest-dotnet/init.lua | 36 ++++++++------------------- lua/neotest-dotnet/vstest_wrapper.lua | 17 ++++++------- run_tests.fsx | 18 ++++++++++++-- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index f1ae3ba..621c86c 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -14,17 +14,8 @@ DotnetNeotestAdapter.root = function(path) end DotnetNeotestAdapter.is_test_file = function(file_path) - if not (vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs")) then - return false - else - for _, test in pairs(vstest.discover_tests(file_path)) do - if test.CodeFilePath == file_path then - return true - end - end - end - - return false + return (vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs")) + and vstest.discover_tests(file_path) end DotnetNeotestAdapter.filter_dir = function(name) @@ -40,6 +31,10 @@ local function get_match_type(captured_nodes) end end +---@param source string +---@param captured_nodes any +---@param tests_in_file table +---@param path string ---@return nil | neotest.Position | neotest.Position[] local function build_position(source, captured_nodes, tests_in_file, path) local match_type = get_match_type(captured_nodes) @@ -49,12 +44,12 @@ local function build_position(source, captured_nodes, tests_in_file, path) local positions = {} if match_type == "test" then - for _, test in ipairs(tests_in_file) do + for id, test in pairs(tests_in_file) do if definition:start() <= test.LineNumber - 1 and test.LineNumber - 1 <= definition:end_() then table.insert(positions, { - id = test.Id, + id = id, type = match_type, path = path, name = test.DisplayName, @@ -84,20 +79,11 @@ DotnetNeotestAdapter.discover_positions = function(path) local filetype = (vim.endswith(path, ".fs") and "fsharp") or "c_sharp" - local tests_in_file = vim - .iter(vstest.discover_tests(path)) - :map(function(_, v) - return v - end) - :filter(function(test) - return test.CodeFilePath == path - end) - :totable() - - ---@type neotest.Tree? + local tests_in_file = vstest.discover_tests(path) + local tree - if #tests_in_file > 0 then + if tests_in_file then local content = lib.files.read(path) local lang = vim.treesitter.language.get_lang(filetype) or filetype nio.scheduler() diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 0bab64b..79f8cb1 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -152,16 +152,15 @@ local discovery_cache = {} local build_semaphore = nio.control.semaphore(1) ---@class TestCase ----@field Id string ---@field CodeFilePath string ---@field DisplayName string ---@field FullyQualifiedName string ----@field Source string +---@field LineNumber integer ---@param path string ----@return table test_cases +---@return table | nil test_cases map from id -> test case function M.discover_tests(path) - local json = {} + local json local proj_info = M.get_proj_info(path) if not proj_info.dll_file then @@ -185,7 +184,7 @@ function M.discover_tests(path) and modified_time and modified_time <= cached.last_modified then - return cached.content + return cached.content and cached.content[path] end local wait_file = nio.fn.tempname() @@ -208,23 +207,23 @@ function M.discover_tests(path) logger.debug(string.format("Waiting for result file to populate for %s...", proj_info.proj_file)) - local max_wait = 30 * 1000 -- 10 sec + local max_wait = 30 * 1000 -- 30 sec local done = M.spin_lock_wait_file(wait_file, max_wait) if done then local content = M.spin_lock_wait_file(output_file, max_wait) - json = (content and vim.json.decode(content, { luanil = { object = true } })) or {} - logger.debug("file has been populated. Extracting test cases") + json = (content and vim.json.decode(content, { luanil = { object = true } })) or {} + discovery_cache[proj_info.dll_file] = { last_modified = modified_time, content = json, } end - return json + return json and json[path] end ---runs tests identified by ids. diff --git a/run_tests.fsx b/run_tests.fsx index 9ea8179..4c75665 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -63,6 +63,12 @@ module TestDiscovery = else ValueOption.None + type TestCaseDto = + { CodeFilePath: string + DisplayName: string + LineNumber: int + FullyQualifiedName: string } + let discoveredTests = ConcurrentDictionary() type PlaygroundTestDiscoveryHandler(resultFilePath, waitFilePath) = @@ -76,8 +82,16 @@ module TestDiscovery = use testsWriter = new StreamWriter(resultFilePath, append = false) discoveredTests - |> _.Values - |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) + |> Seq.map (fun x -> + (x.Key, + x.Value + |> Seq.map (fun testCase -> + testCase.Id, + { CodeFilePath = testCase.CodeFilePath + DisplayName = testCase.DisplayName + LineNumber = testCase.LineNumber + FullyQualifiedName = testCase.FullyQualifiedName }) + |> Map)) |> Map |> JsonConvert.SerializeObject |> testsWriter.WriteLine From ad180ed12c4983ada1d525b9463afc82e5fbb3ab Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 13 Nov 2024 23:14:23 +0100 Subject: [PATCH 22/43] discovery all projects at once --- lua/neotest-dotnet/vstest_wrapper.lua | 130 +++++++++++++++++++++----- run_tests.fsx | 46 ++++----- 2 files changed, 128 insertions(+), 48 deletions(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 79f8cb1..4b7ad32 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -53,14 +53,16 @@ function M.get_proj_info(path) local proj_file = vim.fs.find(function(name, _) return name:match("%.[cf]sproj$") - end, { type = "file", path = vim.fs.dirname(path) })[1] + end, { upward = true, type = "file", path = vim.fs.dirname(path) })[1] local dir_name = vim.fs.dirname(proj_file) local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") local proj_dll_path = -- TODO: this might break if the project has been compiled as both Development and Release. - vim.fs.find(proj_name .. ".dll", { upward = false, type = "file", path = dir_name })[1] + vim.fs.find(function(name) + return string.lower(name) == string.lower(proj_name .. ".dll") + end, { type = "file", path = dir_name })[1] local proj_data = { proj_file = proj_file, @@ -149,7 +151,7 @@ function M.spin_lock_wait_file(file_path, max_wait) end local discovery_cache = {} -local build_semaphore = nio.control.semaphore(1) +local last_discovery = {} ---@class TestCase ---@field CodeFilePath string @@ -163,39 +165,116 @@ function M.discover_tests(path) local json local proj_info = M.get_proj_info(path) - if not proj_info.dll_file then - logger.warn(string.format("failed to find dll for file: %s", path)) - return json + if not (proj_info.proj_file and proj_info.dll_file) then + logger.warn(string.format("failed to find project file for %s", path)) + return {} end - build_semaphore.with(function() - lib.process.run({ "dotnet", "build", proj_info.proj_file }) - end) + local path_open_err, path_stats = nio.uv.fs_stat(path) + + if + not ( + not path_open_err + and path_stats + and path_stats.mtime + and last_discovery[proj_info.proj_file] + and path_stats.mtime.sec <= last_discovery[proj_info.proj_file] + ) + then + local exitCode, stdout = lib.process.run( + { "dotnet", "build", proj_info.proj_file }, + { stdout = true, stderr = true } + ) + logger.debug(string.format("dotnet build status code: %s", exitCode)) + logger.debug(stdout) + end - local open_err, stats = nio.uv.fs_stat(proj_info.dll_file) - assert(not open_err, open_err) + local dll_open_err, dll_stats = nio.uv.fs_stat(proj_info.dll_file) + assert(not dll_open_err, dll_open_err) - local cached = discovery_cache[proj_info.dll_file] - local modified_time = stats.mtime and stats.mtime.sec + local path_modified_time = dll_stats and dll_stats.mtime and dll_stats.mtime.sec if - cached - and cached.last_modified - and modified_time - and modified_time <= cached.last_modified + last_discovery[proj_info.proj_file] + and path_modified_time + and path_modified_time <= last_discovery[proj_info.proj_file] then - return cached.content and cached.content[path] + logger.debug( + string.format( + "cache hit for %s. %s - %s", + proj_info.proj_file, + path_modified_time, + last_discovery[proj_info.proj_file] + ) + ) + return discovery_cache[path] + else + logger.debug( + string.format( + "cache miss for %s... path: %s cache: %s - %s", + path, + path_modified_time, + proj_info.proj_file, + last_discovery[proj_info.dll_file] + ) + ) + logger.debug(last_discovery) + end + + local dlls = {} + + if vim.tbl_isempty(discovery_cache) then + local root = lib.files.match_root_pattern("*.sln")(path) + or lib.files.match_root_pattern("*.[cf]sproj")(path) + + logger.debug(string.format("root: %s", root)) + + local projects = vim.fs.find(function(name, _) + return name:match("%.[cf]sproj$") + end, { type = "file", path = root, limit = math.huge }) + + for _, project in ipairs(projects) do + local dir_name = vim.fs.dirname(project) + local proj_name = vim.fn.fnamemodify(project, ":t:r") + + local proj_dll_path = + -- TODO: this might break if the project has been compiled as both Development and Release. + vim.fs.find(function(name) + return string.lower(name) == string.lower(proj_name .. ".dll") + end, { type = "file", path = dir_name })[1] + + if proj_dll_path then + dlls[#dlls + 1] = proj_dll_path + local project_open_err, project_stats = nio.uv.fs_stat(proj_dll_path) + last_discovery[project] = not project_open_err + and project_stats + and project_stats.mtime + and project_stats.mtime.sec + else + logger.warn(string.format("failed to find dll for %s", project)) + end + end + else + dlls = { proj_info.dll_file } + last_discovery[proj_info.proj_file] = path_modified_time + end + + if vim.tbl_isempty(dlls) then + return {} end local wait_file = nio.fn.tempname() local output_file = nio.fn.tempname() + logger.debug("found dlls:") + logger.debug(dlls) + local command = vim .iter({ "discover", output_file, wait_file, - proj_info.dll_file, + dlls, }) :flatten() :join(" ") @@ -205,7 +284,7 @@ function M.discover_tests(path) invoke_test_runner(command) - logger.debug(string.format("Waiting for result file to populate for %s...", proj_info.proj_file)) + logger.debug("Waiting for result file to populated...") local max_wait = 30 * 1000 -- 30 sec @@ -213,14 +292,15 @@ function M.discover_tests(path) if done then local content = M.spin_lock_wait_file(output_file, max_wait) - logger.debug("file has been populated. Extracting test cases") + logger.debug("file has been populated. Extracting test cases...") json = (content and vim.json.decode(content, { luanil = { object = true } })) or {} - discovery_cache[proj_info.dll_file] = { - last_modified = modified_time, - content = json, - } + logger.debug("done decoding test cases.") + + for file_path, test_map in pairs(json) do + discovery_cache[file_path] = test_map + end end return json and json[path] diff --git a/run_tests.fsx b/run_tests.fsx index 4c75665..8020469 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -71,7 +71,7 @@ module TestDiscovery = let discoveredTests = ConcurrentDictionary() - type PlaygroundTestDiscoveryHandler(resultFilePath, waitFilePath) = + type PlaygroundTestDiscoveryHandler() = interface ITestDiscoveryEventsHandler2 with member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = discoveredTestCases @@ -79,27 +79,6 @@ module TestDiscovery = |> Seq.iter (fun (file, testCases) -> discoveredTests.AddOrUpdate(file, testCases, (fun _ _ -> testCases)) |> ignore) - use testsWriter = new StreamWriter(resultFilePath, append = false) - - discoveredTests - |> Seq.map (fun x -> - (x.Key, - x.Value - |> Seq.map (fun testCase -> - testCase.Id, - { CodeFilePath = testCase.CodeFilePath - DisplayName = testCase.DisplayName - LineNumber = testCase.LineNumber - FullyQualifiedName = testCase.FullyQualifiedName }) - |> Map)) - |> Map - |> JsonConvert.SerializeObject - |> testsWriter.WriteLine - - use waitFileWriter = new StreamWriter(waitFilePath, append = false) - waitFileWriter.WriteLine("1") - - Console.WriteLine($"Wrote test results to {resultFilePath}") member _.HandleDiscoveryComplete(_, _) = () member __.HandleLogMessage(_, _) = () @@ -232,9 +211,30 @@ module TestDiscovery = do! Task.Yield() let discoveryHandler = - PlaygroundTestDiscoveryHandler(args.OutputPath, args.WaitFile) :> ITestDiscoveryEventsHandler2 + PlaygroundTestDiscoveryHandler() :> ITestDiscoveryEventsHandler2 r.DiscoverTests(args.Sources, sourceSettings, options, testSession, discoveryHandler) + use testsWriter = new StreamWriter(args.OutputPath, append = false) + + discoveredTests + |> Seq.map (fun x -> + (x.Key, + x.Value + |> Seq.map (fun testCase -> + testCase.Id, + { CodeFilePath = testCase.CodeFilePath + DisplayName = testCase.DisplayName + LineNumber = testCase.LineNumber + FullyQualifiedName = testCase.FullyQualifiedName }) + |> Map)) + |> Map + |> JsonConvert.SerializeObject + |> testsWriter.WriteLine + + use waitFileWriter = new StreamWriter(args.WaitFile, append = false) + waitFileWriter.WriteLine("1") + + Console.WriteLine($"Wrote test results to {args.WaitFile}") } |> ignore | RunTests args -> From bea87f9362487a8633a36e3a3dd0c6dccca70dc8 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Thu, 14 Nov 2024 17:40:27 +0100 Subject: [PATCH 23/43] handle running directories --- lua/neotest-dotnet/init.lua | 26 ++++++++++++++------------ run_tests.fsx | 12 ++++++++---- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 621c86c..295bcf9 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -150,10 +150,22 @@ DotnetNeotestAdapter.build_spec = function(args) local pos = args.tree:data() + local ids = {} + if pos.type ~= "test" then - return + ids = {} + for _, position in tree:iter() do + if position.type == "test" then + ids[#ids + 1] = position.id + end + end + else + ids = { pos.id } end + logger.debug("ids:") + logger.debug(ids) + local results_path = nio.fn.tempname() local stream_path = nio.fn.tempname() lib.files.write(results_path, "") @@ -189,12 +201,10 @@ DotnetNeotestAdapter.build_spec = function(args) end return { - command = vstest.run_tests(pos.id, stream_path, results_path), + command = vstest.run_tests(ids, stream_path, results_path), context = { result_path = results_path, stop_stream = stop_stream, - file = pos.path, - id = pos.id, }, stream = function() return function() @@ -220,14 +230,6 @@ DotnetNeotestAdapter.results = function(spec) local results = {} if not success then - local outcome = "skipped" - results[spec.context.id] = { - status = outcome, - errors = { - message = "failed to read result file: " .. data, - }, - } - return results end diff --git a/run_tests.fsx b/run_tests.fsx index 8020469..edcccd2 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -18,6 +18,7 @@ open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces module TestDiscovery = open System.Collections.Concurrent + open System.Collections.Concurrent [] let (|DiscoveryRequest|_|) (str: string) = @@ -85,11 +86,14 @@ module TestDiscovery = member __.HandleRawMessage(_) = () type PlaygroundTestRunHandler(streamOutputPath, outputFilePath) = + let resultsDictionary = ConcurrentDictionary() + interface ITestRunEventsHandler with member _.HandleTestRunComplete (_testRunCompleteArgs, _lastChunkArgs, _runContextAttachments, _executorUris) = - () + use outputWriter = new StreamWriter(outputFilePath, append = false) + outputWriter.WriteLine(JsonConvert.SerializeObject(resultsDictionary)) member __.HandleLogMessage(_level, _message) = () @@ -130,6 +134,9 @@ module TestDiscovery = short = $"{result.TestCase.DisplayName}:{outcome}" errors = errors |}) + for (id, result) in results do + resultsDictionary.AddOrUpdate(id, result, (fun _ _ -> result)) |> ignore + use streamWriter = new StreamWriter(streamOutputPath, append = true) for (id, result) in results do @@ -137,9 +144,6 @@ module TestDiscovery = |> JsonConvert.SerializeObject |> streamWriter.WriteLine - use outputWriter = new StreamWriter(outputFilePath, append = false) - outputWriter.WriteLine(JsonConvert.SerializeObject(Map.ofSeq results)) - member __.LaunchProcessWithDebuggerAttached(_testProcessStartInfo) = 1 type DebugLauncher(pidFile: string, attachedFile: string) = From 90485b4f20ae25ca7be72192e6ed0ae9f41ea521 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Fri, 15 Nov 2024 15:27:22 +0100 Subject: [PATCH 24/43] remove old tests --- lua/neotest-dotnet/vstest_wrapper.lua | 2 +- run_tests.fsx | 3 +- tests/adapter_is_test_file_spec.lua | 20 - tests/adapter_root_spec.lua | 75 ---- tests/minimal_init.lua | 68 ---- .../test_attribute_spec.lua | 60 --- .../testcasesource_attribute_spec.lua | 157 -------- tests/nunit/specs/specflow.cs | 108 ------ tests/nunit/specs/test_simple.cs | 17 - tests/nunit/specs/testcasesource.cs | 25 -- tests/project_dir/dummy.test.csproj | 0 tests/solution_dir/dummy.test.sln | 0 .../project1/dummy.project1.csproj | 0 .../project2/dummy.project2.csproj | 0 tests/test.sh | 25 -- tests/types/mock_data.lua | 7 - tests/utils/build_spec_utils_spec.lua | 354 ------------------ .../classdata_attribute_spec.lua | 199 ---------- .../custom_attribute_spec.lua | 129 ------- .../fact_attribute_spec.lua | 326 ---------------- .../theory_attribute_spec.lua | 247 ------------ tests/xunit/specs/block_scoped_namespace.cs | 11 - tests/xunit/specs/classdata.cs | 28 -- tests/xunit/specs/custom_attribute.cs | 16 - tests/xunit/specs/fact_and_trait.cs | 11 - tests/xunit/specs/fact_and_trait.fs | 7 - tests/xunit/specs/nested_class.cs | 27 -- tests/xunit/specs/theory_and_fact_mixed.cs | 18 - 28 files changed, 2 insertions(+), 1938 deletions(-) delete mode 100644 tests/adapter_is_test_file_spec.lua delete mode 100644 tests/adapter_root_spec.lua delete mode 100644 tests/minimal_init.lua delete mode 100644 tests/nunit/discover_positions/test_attribute_spec.lua delete mode 100644 tests/nunit/discover_positions/testcasesource_attribute_spec.lua delete mode 100644 tests/nunit/specs/specflow.cs delete mode 100644 tests/nunit/specs/test_simple.cs delete mode 100644 tests/nunit/specs/testcasesource.cs delete mode 100644 tests/project_dir/dummy.test.csproj delete mode 100644 tests/solution_dir/dummy.test.sln delete mode 100644 tests/solution_dir/project1/dummy.project1.csproj delete mode 100644 tests/solution_dir/project2/dummy.project2.csproj delete mode 100755 tests/test.sh delete mode 100644 tests/types/mock_data.lua delete mode 100644 tests/utils/build_spec_utils_spec.lua delete mode 100644 tests/xunit/discover_positions/classdata_attribute_spec.lua delete mode 100644 tests/xunit/discover_positions/custom_attribute_spec.lua delete mode 100644 tests/xunit/discover_positions/fact_attribute_spec.lua delete mode 100644 tests/xunit/discover_positions/theory_attribute_spec.lua delete mode 100644 tests/xunit/specs/block_scoped_namespace.cs delete mode 100644 tests/xunit/specs/classdata.cs delete mode 100644 tests/xunit/specs/custom_attribute.cs delete mode 100644 tests/xunit/specs/fact_and_trait.cs delete mode 100644 tests/xunit/specs/fact_and_trait.fs delete mode 100644 tests/xunit/specs/nested_class.cs delete mode 100644 tests/xunit/specs/theory_and_fact_mixed.cs diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 4b7ad32..4398e9e 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -328,7 +328,7 @@ function M.run_tests(ids, stream_path, output_path) return string.format("tail -n 1 -f %s", output_path, output_path) end ----Uses the vstest console to spawn a test process for the debugger to attach to. +--- Uses the vstest console to spawn a test process for the debugger to attach to. ---@param pid_path string ---@param attached_path string ---@param stream_path string diff --git a/run_tests.fsx b/run_tests.fsx index edcccd2..10f759d 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -11,14 +11,13 @@ open System.Threading open System.Threading.Tasks open Newtonsoft.Json open System.Collections.Generic +open System.Collections.Concurrent open Microsoft.TestPlatform.VsTestConsole.TranslationLayer open Microsoft.VisualStudio.TestPlatform.ObjectModel open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces module TestDiscovery = - open System.Collections.Concurrent - open System.Collections.Concurrent [] let (|DiscoveryRequest|_|) (str: string) = diff --git a/tests/adapter_is_test_file_spec.lua b/tests/adapter_is_test_file_spec.lua deleted file mode 100644 index 6ac2d96..0000000 --- a/tests/adapter_is_test_file_spec.lua +++ /dev/null @@ -1,20 +0,0 @@ -local async = require("nio").tests - -describe("is_test_file", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet")({ - discovery_root = "solution", - }), - }, - }) - - async.it("should return true for NUnit Specflow Generated File", function() - local plugin = require("neotest-dotnet") - local dir = "./tests/nunit/specs/specflow.cs" - - local result = plugin.is_test_file(dir) - - assert.equal(true, result) - end) -end) diff --git a/tests/adapter_root_spec.lua b/tests/adapter_root_spec.lua deleted file mode 100644 index bb01b05..0000000 --- a/tests/adapter_root_spec.lua +++ /dev/null @@ -1,75 +0,0 @@ -local async = require("nio").tests - -describe("root when using solution option", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet")({ - discovery_root = "solution", - }), - }, - }) - - async.it("should return .sln dir when it exists and path contains it", function() - local plugin = require("neotest-dotnet") - local dir = "./tests/solution_dir" - local root = plugin.root(dir) - - assert.equal(dir, root) - end) - - async.it("should return nil when neither path nor parents contain .sln file", function() - local plugin = require("neotest-dotnet") - local dir = "./tests/project_dir" - local root = plugin.root(dir) - - assert.equal(nil, root) - end) - - async.it("should return .sln dir when parent dir contains .sln file", function() - local plugin = require("neotest-dotnet") - local dir = "./tests/solution_dir/project1/tests" - local parent_sln_dir = "/tests/solution_dir" - local root = plugin.root(dir) - - -- Check the end of the root matches the test dir as the function - -- in neotest will use the fully qualified path (which will vary) - assert.is.True(string.find(root, parent_sln_dir .. "$") ~= nil) - end) -end) - -describe("root when using project option", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet")({ - discovery_root = "project", - }), - }, - }) - - async.it("should return .csproj dir when it exists and path contains it", function() - local plugin = require("neotest-dotnet") - local dir = "./tests/project_dir" - local root = plugin.root(dir) - - assert.equal(dir, root) - end) - - async.it("should return nil when neither path nor parents contain .csproj file", function() - local plugin = require("neotest-dotnet") - local dir = "./tests/solution_dir" - local root = plugin.root(dir) - - assert.equal(nil, root) - end) - - async.it("should return .csproj dir when parent dir contains .csproj file", function() - local plugin = require("neotest-dotnet") - local dir = "./tests/project_dir/tests" - local parent_proj_dir = "/tests/project_dir" - local root = plugin.root(dir) - - -- Check the end of the root matches the test dir as the function - -- in neotest will use the fully qualified path (which will vary) - assert.is.True(string.find(root, parent_proj_dir .. "$") ~= nil) - end) -end) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua deleted file mode 100644 index 6eeb1e0..0000000 --- a/tests/minimal_init.lua +++ /dev/null @@ -1,68 +0,0 @@ --- Add current directory to 'runtimepath' to be able to use 'lua' files -vim.cmd([[let &rtp.=','.getcwd()]]) - --- When running headless only (i.e. via Makefile command) -if #vim.api.nvim_list_uis() == 0 then - -- Add dependenices to rtp (installed via the Makefile 'deps' command) - local neotest_path = vim.fn.getcwd() .. "/deps/neotest" - local plenary_path = vim.fn.getcwd() .. "/deps/plenary" - local treesitter_path = vim.fn.getcwd() .. "/deps/nvim-treesitter" - local mini_path = vim.fn.getcwd() .. "/deps/mini.doc.nvim" - local nio_path = vim.fn.getcwd() .. "/deps/nvim-nio" - - vim.cmd("set rtp+=" .. neotest_path) - vim.cmd("set rtp+=" .. plenary_path) - vim.cmd("set rtp+=" .. treesitter_path) - vim.cmd("set rtp+=" .. mini_path) - vim.cmd("set rtp+=" .. nio_path) - - -- Source the plugin dependency files - vim.cmd("runtime plugin/nvim-treesitter.lua") - vim.cmd("runtime plugin/plenary.vim") - vim.cmd("runtime lua/mini/doc.lua") - - -- Setup test plugin dependencies - require("nvim-treesitter.configs").setup({ - ensure_installed = { "c_sharp", "fsharp" }, - sync_install = true, - highlight = { - enable = false, - }, - }) -end --- local M = {} --- --- function M.root(root) --- local f = debug.getinfo(1, "S").source:sub(2) --- return vim.fn.fnamemodify(f, ":p:h:h") .. "/" .. (root or "") --- end --- --- ---@param plugin string --- function M.load(plugin) --- local name = plugin:match(".*/(.*)") --- local package_root = M.root(".tests/site/pack/deps/start/") --- if not vim.loop.fs_stat(package_root .. name) then --- print("Installing " .. plugin) --- vim.fn.mkdir(package_root, "p") --- vim.fn.system({ --- "git", --- "clone", --- "--depth=1", --- "https://github.com/" .. plugin .. ".git", --- package_root .. "/" .. name, --- }) --- end --- end --- --- function M.setup() --- vim.cmd([[set runtimepath=$VIMRUNTIME]]) --- vim.opt.runtimepath:append(M.root()) --- vim.opt.packpath = { M.root(".tests/site") } --- --- M.load("nvim-treesitter/nvim-treesitter") --- M.load("nvim-lua/plenary.nvim") --- M.load("Issafalcon/neotest-dotnet") --- M.load("echasnovski/mini.doc") --- end --- --- M.setup() diff --git a/tests/nunit/discover_positions/test_attribute_spec.lua b/tests/nunit/discover_positions/test_attribute_spec.lua deleted file mode 100644 index daa70e4..0000000 --- a/tests/nunit/discover_positions/test_attribute_spec.lua +++ /dev/null @@ -1,60 +0,0 @@ -local async = require("nio").tests -local plugin = require("neotest-dotnet") -local Tree = require("neotest.types").Tree - -A = function(...) - print(vim.inspect(...)) -end - -describe("discover_positions", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet"), - }, - }) - - async.it("should discover non parameterized tests without TestFixture", function() - local spec_file = "./tests/nunit/specs/test_simple.cs" - local spec_file_name = "test_simple.cs" - local positions = plugin.discover_positions(spec_file):to_list() - - local expected_positions = { - { - id = spec_file, - name = spec_file_name, - path = spec_file, - range = { 0, 0, 17, 0 }, - type = "file", - }, - { - { - framework = "nunit", - id = spec_file .. "::SingleTests", - is_class = true, - name = "SingleTests", - path = spec_file, - range = { 4, 0, 16, 1 }, - type = "namespace", - }, - { - { - framework = "nunit", - id = spec_file .. "::SingleTests::Test1", - is_class = false, - name = "Test1", - path = spec_file, - range = { 11, 1, 15, 2 }, - type = "test", - }, - }, - }, - } - - assert.same(positions, expected_positions) - end) - - -- TODO: - -- 1. Write tests for non-inline parameterized tests - -- 2. Write tests for nested namespaces - -- 3. Write tests for nested classes -end) diff --git a/tests/nunit/discover_positions/testcasesource_attribute_spec.lua b/tests/nunit/discover_positions/testcasesource_attribute_spec.lua deleted file mode 100644 index 674dbce..0000000 --- a/tests/nunit/discover_positions/testcasesource_attribute_spec.lua +++ /dev/null @@ -1,157 +0,0 @@ -local async = require("nio").tests -local plugin = require("neotest-dotnet") - -A = function(...) - print(vim.inspect(...)) -end - -describe("discover_positions", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet"), - }, - }) - - async.it( - "should discover tests with TestCaseSource attribute without creating nested parameterized tests", - function() - local spec_file = "./tests/nunit/specs/testcasesource.cs" - local spec_file_name = "testcasesource.cs" - local positions = plugin.discover_positions(spec_file):to_list() - - local function get_expected_output(file_path, file_name) - return { - { - id = file_path, - name = file_name, - path = file_path, - range = { 0, 0, 25, 0 }, - type = "file", - }, - { - { - framework = "nunit", - id = file_path .. "::Tests", - is_class = true, - name = "Tests", - path = file_path, - range = { 4, 0, 24, 1 }, - type = "namespace", - }, - { - { - framework = "nunit", - id = file_path .. "::Tests::DivideTest", - is_class = false, - name = "DivideTest", - path = file_path, - range = { 12, 4, 16, 5 }, - type = "test", - }, - }, - }, - } - - -- 01-06-2024: c_sharp treesitter parser changes mean file scoped namespaces don't include content of file as their range anymore - -- - Other spec files have been modified accoridingly until parse has been fixed - -- return { - -- { - -- id = file_path, - -- name = file_name, - -- path = file_path, - -- range = { 0, 0, 25, 0 }, - -- type = "file", - -- }, - -- { - -- { - -- framework = "nunit", - -- id = file_path .. "::NUnitSamples", - -- is_class = false, - -- name = "NUnitSamples", - -- path = file_path, - -- range = { 2, 0, 24, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "nunit", - -- id = file_path .. "::NUnitSamples::Tests", - -- is_class = true, - -- name = "Tests", - -- path = file_path, - -- range = { 4, 0, 24, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "nunit", - -- id = file_path .. "::NUnitSamples::Tests::DivideTest", - -- is_class = false, - -- name = "DivideTest", - -- path = file_path, - -- range = { 12, 4, 16, 5 }, - -- type = "test", - -- }, - -- }, - -- }, - -- }, - -- } - end - - assert.same(positions, get_expected_output(spec_file, spec_file_name)) - end - ) - - async.it("should discover Specflow Generate tests", function() - local spec_file = "./tests/nunit/specs/specflow.cs" - local spec_file_name = "specflow.cs" - local positions = plugin.discover_positions(spec_file):to_list() - - local function get_expected_output(file_path, file_name) - return { - { - id = file_path, - name = file_name, - path = file_path, - range = { 0, 0, 108, 0 }, - type = "file", - }, - { - { - framework = "nunit", - id = file_path .. "::NUnitSamples", - is_class = false, - name = "NUnitSamples", - path = file_path, - range = { 12, 0, 105, 1 }, - type = "namespace", - }, - { - { - framework = "nunit", - id = file_path .. "::NUnitSamples::DummyTestFeature", - is_class = true, - name = "DummyTestFeature", - path = file_path, - range = { 19, 4, 104, 5 }, - type = "namespace", - }, - { - { - framework = "nunit", - id = file_path .. "::NUnitSamples::DummyTestFeature::DummyScenario", - is_class = false, - name = "DummyScenario", - path = file_path, - range = { 75, 8, 103, 9 }, - type = "test", - }, - }, - }, - }, - } - end - - assert.same(positions, get_expected_output(spec_file, spec_file_name)) - end) -end) diff --git a/tests/nunit/specs/specflow.cs b/tests/nunit/specs/specflow.cs deleted file mode 100644 index 4c53342..0000000 --- a/tests/nunit/specs/specflow.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ------------------------------------------------------------------------------ -// -// This code was generated by SpecFlow (https://www.specflow.org/). -// SpecFlow Version:3.9.0.0 -// SpecFlow Generator Version:3.9.0.0 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#region Designer generated code -#pragma warning disable -namespace NUnitSamples -{ - using System; - using System.Linq; - using TechTalk.SpecFlow; - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [NUnit.Framework.TestFixtureAttribute()] - [NUnit.Framework.DescriptionAttribute("Dummy test")] - public partial class DummyTestFeature - { - - private TechTalk.SpecFlow.ITestRunner testRunner; - - private static string[] featureTags = ((string[])(null)); - -#line 1 "Tester.feature" -#line hidden - - [NUnit.Framework.OneTimeSetUpAttribute()] - public virtual void FeatureSetup() - { - testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); - TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Dummy test", null, ProgrammingLanguage.CSharp, featureTags); - testRunner.OnFeatureStart(featureInfo); - } - - [NUnit.Framework.OneTimeTearDownAttribute()] - public virtual void FeatureTearDown() - { - testRunner.OnFeatureEnd(); - testRunner = null; - } - - [NUnit.Framework.SetUpAttribute()] - public void TestInitialize() - { - } - - [NUnit.Framework.TearDownAttribute()] - public void TestTearDown() - { - testRunner.OnScenarioEnd(); - } - - public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) - { - testRunner.OnScenarioInitialize(scenarioInfo); - testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext); - } - - public void ScenarioStart() - { - testRunner.OnScenarioStart(); - } - - public void ScenarioCleanup() - { - testRunner.CollectScenarioErrors(); - } - - [NUnit.Framework.TestAttribute()] - [NUnit.Framework.DescriptionAttribute("Dummy scenario")] - public void DummyScenario() - { - string[] tagsOfScenario = ((string[])(null)); - System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Dummy scenario", null, tagsOfScenario, argumentsOfScenario, featureTags); -#line 3 - this.ScenarioInitialize(scenarioInfo); -#line hidden - if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) - { - testRunner.SkipScenario(); - } - else - { - this.ScenarioStart(); -#line 4 - testRunner.Given("Dummy reason", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); -#line hidden -#line 5 - testRunner.When("Dummy execution", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); -#line hidden -#line 6 - testRunner.Then("Dummy compare", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); -#line hidden - } - this.ScenarioCleanup(); - } - } -} -#pragma warning restore -#endregion diff --git a/tests/nunit/specs/test_simple.cs b/tests/nunit/specs/test_simple.cs deleted file mode 100644 index 7e1f948..0000000 --- a/tests/nunit/specs/test_simple.cs +++ /dev/null @@ -1,17 +0,0 @@ -using NUnit.Framework; - -namespace NUnitSamples; - -public class SingleTests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} diff --git a/tests/nunit/specs/testcasesource.cs b/tests/nunit/specs/testcasesource.cs deleted file mode 100644 index 7a02aad..0000000 --- a/tests/nunit/specs/testcasesource.cs +++ /dev/null @@ -1,25 +0,0 @@ -using NUnit.Framework; - -namespace NUnitSamples; - -[TestFixture] -public class Tests -{ - [SetUp] - public void Setup() - { - } - - [TestCaseSource(nameof(DivideCases))] - public void DivideTest(int n, int d, int q) - { - Assert.AreEqual(q, n / d); - } - - public static object[] DivideCases = - { - new object[] { 12, 4, 4 }, - new object[] { 12, 2, 6 }, - new object[] { 12, 4, 3 } - }; -} diff --git a/tests/project_dir/dummy.test.csproj b/tests/project_dir/dummy.test.csproj deleted file mode 100644 index e69de29..0000000 diff --git a/tests/solution_dir/dummy.test.sln b/tests/solution_dir/dummy.test.sln deleted file mode 100644 index e69de29..0000000 diff --git a/tests/solution_dir/project1/dummy.project1.csproj b/tests/solution_dir/project1/dummy.project1.csproj deleted file mode 100644 index e69de29..0000000 diff --git a/tests/solution_dir/project2/dummy.project2.csproj b/tests/solution_dir/project2/dummy.project2.csproj deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test.sh b/tests/test.sh deleted file mode 100755 index 81100d4..0000000 --- a/tests/test.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -tempfile=".test_output.tmp" -TEST_INIT=tests/minimal_init.lua -TEST_DIR=tests/ - -if [[ -n $1 ]]; then - nvim --headless --noplugin -u ${TEST_INIT} \ - -c "PlenaryBustedFile $1" | tee "${tempfile}" -else - nvim --headless --clean --noplugin -u ${TEST_INIT} \ - -c "set rtp?" \ - -c "lua vim.cmd([[PlenaryBustedDirectory ${TEST_DIR} { minimal_init = '${TEST_INIT}', sequential = true}]])" | tee "${tempfile}" -fi - -# Plenary doesn't emit exit code 1 when tests have errors during setup -errors=$(sed 's/\x1b\[[0-9;]*m//g' "${tempfile}" | awk '/(Errors|Failed) :/ {print $3}' | grep -v '0') - -rm "${tempfile}" - -if [[ -n $errors ]]; then - echo "Tests failed" - exit 1 -fi - -exit 0 diff --git a/tests/types/mock_data.lua b/tests/types/mock_data.lua deleted file mode 100644 index 4ae496c..0000000 --- a/tests/types/mock_data.lua +++ /dev/null @@ -1,7 +0,0 @@ ----@class TrxMockData ----@field trx_results table ----@field trx_test_definitions table - ----@class TestNodeMockData ----@field node_list table ----@field intermediate_results table diff --git a/tests/utils/build_spec_utils_spec.lua b/tests/utils/build_spec_utils_spec.lua deleted file mode 100644 index c37b62e..0000000 --- a/tests/utils/build_spec_utils_spec.lua +++ /dev/null @@ -1,354 +0,0 @@ -local async = require("nio").tests -local mock = require("luassert.mock") -local stub = require("luassert.stub") -local lib = require("neotest.lib") -local Tree = require("neotest.types").Tree - -describe("build_test_fqn windows_os", function() - local BuildSpecUtils = require("neotest-dotnet.utils.build-spec-utils") - local fn_mock = mock(vim.fn, true) - fn_mock.has.returns(true) - - it("should return the fully qualified name of the test", function() - local fqn = BuildSpecUtils.build_test_fqn("C:\\path\\to\\file.cs::namespace::class::method") - assert.are.equals(fqn, "namespace.class.method") - end) - - it("should return the fully qualified name of the test when the test has parameters", function() - local fqn = - BuildSpecUtils.build_test_fqn("C:\\path\\to\\file.cs::namespace::class::method(a: 1)") - assert.are.equals(fqn, "namespace.class.method") - end) - - it( - "should return the fully qualified name of the test when the test has multiple parameters", - function() - local fqn = - BuildSpecUtils.build_test_fqn("C:\\path\\to\\file.cs::namespace::class::method(a: 1, b: 2)") - assert.are.equals(fqn, "namespace.class.method") - end - ) - mock.revert(fn_mock) -end) - -describe("build_test_fqn linux", function() - local BuildSpecUtils = require("neotest-dotnet.utils.build-spec-utils") - local mock = require("luassert.mock") - local fn_mock = mock(vim.fn, true) - fn_mock.has.returns(false) - - it("should return the fully qualified name of the test", function() - local fqn = BuildSpecUtils.build_test_fqn("/path/to/file.cs::namespace::class::method") - assert.are.equals(fqn, "namespace.class.method") - end) - - it("should return the fully qualified name of the test when the test has parameters", function() - local fqn = BuildSpecUtils.build_test_fqn("/path/to/file.cs::namespace::class::method(a: 1)") - assert.are.equals(fqn, "namespace.class.method") - end) - - it( - "should return the fully qualified name of the test when the test has multiple parameters", - function() - local fqn = - BuildSpecUtils.build_test_fqn("/path/to/file.cs::namespace::class::method(a: 1, b: 2)") - assert.are.equals(fqn, "namespace.class.method") - end - ) - mock.revert(fn_mock) -end) - -describe("create_specs", function() - local BuildSpecUtils = require("neotest-dotnet.utils.build-spec-utils") - local test_result_path = "/tmp/output/test_result" - local test_root_path = "/dummy/path/to/proj" - - local function assert_spec_matches(expected, actual) - assert.equal(expected.command, actual.command) - assert.equal(expected.context.file, actual.context.file) - assert.equal(expected.context.id, actual.context.id) - assert.equal(expected.context.results_path, actual.context.results_path) - end - - before_each(function() - -- fn_mock.tempname.returns(test_result_path) - - stub(vim.fn, "tempname", function() - return test_result_path - end) - stub(lib.files, "match_root_pattern", function(_) - return function(_) - return test_root_path - end - end) - end) - - after_each(function() - lib.files.match_root_pattern:revert() - vim.fn.tempname:revert() - end) - - it("should return correct spec when position is 'file' type", function() - local expected_specs = { - { - command = "dotnet test " - .. test_root_path - .. ' --results-directory /tmp/output --logger "trx;logfilename=test_result.trx"', - context = { - file = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - results_path = test_result_path .. ".trx", - }, - }, - } - - local tree = Tree.from_list({ - { - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - name = "UnitTest1.cs", - path = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - range = { 0, 0, 19, 0 }, - type = "file", - }, - { - { - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs::xunit.testproj1", - name = "xunit.testproj1", - path = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - range = { 0, 0, 18, 1 }, - type = "namespace", - }, - }, - }, function(pos) - return pos.id - end) - - local result = BuildSpecUtils.create_specs(tree) - - assert.equal(#expected_specs, #result) - assert_spec_matches(expected_specs[1], result[1]) - end) - - async.it("should return the correct specs when the position is 'namespace' type", function() - local expected_specs = { - { - command = "dotnet test " - .. test_root_path - .. ' --filter FullyQualifiedName~"xunit.testproj1"' - .. ' --results-directory /tmp/output --logger "trx;logfilename=test_result.trx"', - context = { - file = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs::xunit.testproj1", - results_path = test_result_path .. ".trx", - }, - }, - } - - local tree = Tree.from_list({ - { - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs::xunit.testproj1", - name = "xunit.testproj1", - path = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - range = { 0, 0, 18, 1 }, - type = "namespace", - }, - { - { - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs::xunit.testproj1::UnitTest1", - name = "UnitTest1", - path = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - range = { 2, 0, 18, 1 }, - type = "namespace", - }, - { - { - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs::xunit.testproj1::UnitTest1::Test1", - name = "Test1", - path = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - range = { 4, 1, 8, 2 }, - type = "test", - }, - }, - }, - }, function(pos) - return pos.id - end) - - local result = BuildSpecUtils.create_specs(tree) - - assert.equal(#expected_specs, #result) - assert_spec_matches(expected_specs[1], result[1]) - end) - - async.it("should return the correct specs when the position is 'test' type", function() - local expected_specs = { - { - command = "dotnet test " - .. test_root_path - .. ' --filter FullyQualifiedName~"xunit.testproj1.UnitTest1.Test1"' - .. ' --results-directory /tmp/output --logger "trx;logfilename=test_result.trx"', - context = { - file = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs::xunit.testproj1::UnitTest1::Test1", - results_path = test_result_path .. ".trx", - }, - }, - } - - local tree = Tree.from_list({ - { - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs::xunit.testproj1::UnitTest1::Test1", - name = "Test1", - path = "/home/issafalcon/repos/neotest-dotnet-tests/xunit/testproj1/UnitTest1.cs", - range = { 4, 1, 8, 2 }, - type = "test", - }, - }, function(pos) - return pos.id - end) - - local result = BuildSpecUtils.create_specs(tree) - - assert.equal(#expected_specs, #result) - assert_spec_matches(expected_specs[1], result[1]) - end) - - async.it( - "should return the correct specs when the position is 'test' type and the test is in a nested namespace", - function() - local expected_specs = { - { - command = "dotnet test " - .. test_root_path - .. ' --filter FullyQualifiedName~"XUnitSamples.UnitTest1+NestedClass.Test1"' - .. ' --results-directory /tmp/output --logger "trx;logfilename=test_result.trx"', - context = { - file = "./tests/xunit/specs/nested_class.cs", - id = "./tests/xunit/specs/nested_class.cs::XUnitSamples::UnitTest1+NestedClass::Test1", - results_path = test_result_path .. ".trx", - }, - }, - } - - local tree = Tree.from_list({ - { - id = "./tests/xunit/specs/nested_class.cs::XUnitSamples::UnitTest1+NestedClass::Test1", - is_class = false, - name = "Test1", - path = "./tests/xunit/specs/nested_class.cs", - range = { 14, 2, 18, 3 }, - type = "test", - }, - }, function(pos) - return pos.id - end) - - local result = BuildSpecUtils.create_specs(tree) - - assert.equal(#expected_specs, #result) - assert_spec_matches(expected_specs[1], result[1]) - end - ) - - -- Caters for situation where root directory contains a .sln file, and there are nested dirs with .csproj files in them - async.it( - "should return multiple specs when the position is 'dir' type and contains nested project roots", - function() - local solution_dir = vim.fn.expand("%:p:h") .. "/tests/solution_dir" - local project1_dir = vim.fn.expand("%:p:h") .. "/tests/solution_dir/project1" - local project2_dir = vim.fn.expand("%:p:h") .. "/tests/solution_dir/project2" - - local expected_specs = { - { - command = "dotnet test " - .. project1_dir - .. ' --results-directory /tmp/output --logger "trx;logfilename=test_result.trx"', - context = { - file = project1_dir, - id = project1_dir, - results_path = test_result_path .. ".trx", - }, - }, - { - command = "dotnet test " - .. project2_dir - .. ' --results-directory /tmp/output --logger "trx;logfilename=test_result.trx"', - context = { - file = project2_dir, - id = project2_dir, - results_path = test_result_path .. ".trx", - }, - }, - } - - local tree = Tree.from_list({ - { - id = "/home/issafalcon/repos/neotest-dotnet-tests/xunit", - name = "xunit", - path = solution_dir, - type = "dir", - }, - { - { - id = project1_dir, - name = "testproj1", - path = project1_dir, - type = "dir", - }, - }, - { - { - id = project2_dir, - name = "testproj2", - path = project2_dir, - type = "dir", - }, - }, - }, function(pos) - return pos.id - end) - - local result = BuildSpecUtils.create_specs(tree) - - assert.equal(#expected_specs, #result) - assert_spec_matches(expected_specs[1], result[1]) - assert_spec_matches(expected_specs[2], result[2]) - end - ) - - async.it( - "should return single spec when the position is 'dir' type and contains a single project root", - function() - local project1_dir = vim.fn.expand("%:p:h") .. "/tests/solution_dir/project1" - - local expected_specs = { - { - command = "dotnet test " - .. project1_dir - .. ' --results-directory /tmp/output --logger "trx;logfilename=test_result.trx"', - context = { - file = project1_dir, - id = project1_dir, - results_path = test_result_path .. ".trx", - }, - }, - } - - local tree = Tree.from_list({ - { - id = project1_dir, - name = "testproj1", - path = project1_dir, - type = "dir", - }, - }, function(pos) - return pos.id - end) - - local result = BuildSpecUtils.create_specs(tree) - - assert.equal(#expected_specs, #result) - assert_spec_matches(expected_specs[1], result[1]) - end - ) -end) diff --git a/tests/xunit/discover_positions/classdata_attribute_spec.lua b/tests/xunit/discover_positions/classdata_attribute_spec.lua deleted file mode 100644 index 6b1eeef..0000000 --- a/tests/xunit/discover_positions/classdata_attribute_spec.lua +++ /dev/null @@ -1,199 +0,0 @@ -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(...)) -end - -describe("discover_positions", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet"), - }, - }) - - 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() - local spec_file = "./tests/xunit/specs/classdata.cs" - local spec_file_name = "classdata.cs" - local positions = plugin.discover_positions(spec_file):to_list() - - local function get_expected_output(file_path, file_name) - return { - { - id = "./tests/xunit/specs/classdata.cs", - name = "classdata.cs", - path = "./tests/xunit/specs/classdata.cs", - range = { 0, 0, 28, 0 }, - type = "file", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/classdata.cs::ClassDataTests", - is_class = true, - name = "ClassDataTests", - path = "./tests/xunit/specs/classdata.cs", - range = { 6, 0, 15, 1 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/classdata.cs::ClassDataTests::Theory_With_Class_Data_Test", - is_class = false, - 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::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::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::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::ClassDataTests::Theory_With_Class_Data_Test", - type = "test", - }, - }, - }, - }, - } - -- return { - -- { - -- id = "./tests/xunit/specs/classdata.cs", - -- name = "classdata.cs", - -- path = "./tests/xunit/specs/classdata.cs", - -- range = { 0, 0, 28, 0 }, - -- type = "file", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/classdata.cs::XUnitSamples", - -- is_class = false, - -- name = "XUnitSamples", - -- path = "./tests/xunit/specs/classdata.cs", - -- range = { 4, 0, 27, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests", - -- is_class = true, - -- name = "ClassDataTests", - -- path = "./tests/xunit/specs/classdata.cs", - -- range = { 6, 0, 15, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/classdata.cs::XUnitSamples::ClassDataTests::Theory_With_Class_Data_Test", - -- is_class = false, - -- 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", - -- }, - -- }, - -- }, - -- }, - -- }, - -- } - end - - assert.same(positions, get_expected_output(spec_file, spec_file_name)) - end - ) -end) diff --git a/tests/xunit/discover_positions/custom_attribute_spec.lua b/tests/xunit/discover_positions/custom_attribute_spec.lua deleted file mode 100644 index d2ebd04..0000000 --- a/tests/xunit/discover_positions/custom_attribute_spec.lua +++ /dev/null @@ -1,129 +0,0 @@ -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(...)) -end - -describe("discover_positions", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet")({ - custom_attributes = { - xunit = { "SkippableEnvironmentFact" }, - }, - }), - }, - }) - - 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() - local spec_file = "./tests/xunit/specs/custom_attribute.cs" - local spec_file_name = "custom_attribute.cs" - local positions = plugin.discover_positions(spec_file):to_list() - - local function get_expected_output(file_path, file_name) - return { - { - id = "./tests/xunit/specs/custom_attribute.cs", - name = "custom_attribute.cs", - path = "./tests/xunit/specs/custom_attribute.cs", - range = { 0, 0, 16, 0 }, - type = "file", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/custom_attribute.cs::CosmosConnectorTest", - is_class = true, - name = "CosmosConnectorTest", - path = "./tests/xunit/specs/custom_attribute.cs", - range = { 6, 0, 15, 1 }, - type = "namespace", - }, - { - { - display_name = "Custom attribute works ok", - framework = "xunit", - id = "./tests/xunit/specs/custom_attribute.cs::CosmosConnectorTest::Custom_Attribute_Tests", - is_class = false, - name = "Custom_Attribute_Tests", - path = "./tests/xunit/specs/custom_attribute.cs", - range = { 9, 4, 14, 5 }, - type = "test", - }, - }, - }, - } - -- return { - -- { - -- id = "./tests/xunit/specs/custom_attribute.cs", - -- name = "custom_attribute.cs", - -- path = "./tests/xunit/specs/custom_attribute.cs", - -- range = { 0, 0, 16, 0 }, - -- type = "file", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/custom_attribute.cs::XUnitSamples", - -- is_class = false, - -- name = "XUnitSamples", - -- path = "./tests/xunit/specs/custom_attribute.cs", - -- range = { 4, 0, 15, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/custom_attribute.cs::XUnitSamples::CosmosConnectorTest", - -- is_class = true, - -- name = "CosmosConnectorTest", - -- path = "./tests/xunit/specs/custom_attribute.cs", - -- range = { 6, 0, 15, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- 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", - -- path = "./tests/xunit/specs/custom_attribute.cs", - -- range = { 9, 4, 14, 5 }, - -- type = "test", - -- }, - -- }, - -- }, - -- }, - -- } - end - - assert.same(positions, get_expected_output(spec_file, spec_file_name)) - end - ) -end) diff --git a/tests/xunit/discover_positions/fact_attribute_spec.lua b/tests/xunit/discover_positions/fact_attribute_spec.lua deleted file mode 100644 index f6797d1..0000000 --- a/tests/xunit/discover_positions/fact_attribute_spec.lua +++ /dev/null @@ -1,326 +0,0 @@ -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(...)) -end - -describe("discover_positions", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet"), - }, - }) - - 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 Fact tests when not the only attribute", function() - local spec_file = "./tests/xunit/specs/fact_and_trait.cs" - local spec_file_name = "fact_and_trait.cs" - local positions = plugin.discover_positions(spec_file):to_list() - - local expected_positions = { - { - id = spec_file, - name = spec_file_name, - path = spec_file, - range = { 0, 0, 11, 0 }, - type = "file", - }, - { - { - framework = "xunit", - id = spec_file .. "::UnitTest1", - is_class = true, - name = "UnitTest1", - path = spec_file, - range = { 2, 0, 10, 1 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = spec_file .. "::UnitTest1::Test1", - is_class = false, - name = "XUnitSamples.UnitTest1.Test1", - path = spec_file, - range = { 4, 1, 9, 2 }, - running_id = "./tests/xunit/specs/fact_and_trait.cs::UnitTest1::Test1", - type = "test", - }, - }, - }, - } - -- local expected_positions = { - -- { - -- id = spec_file, - -- name = spec_file_name, - -- path = spec_file, - -- range = { 0, 0, 11, 0 }, - -- type = "file", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = spec_file .. "::xunit.testproj1", - -- is_class = false, - -- name = "xunit.testproj1", - -- path = spec_file, - -- range = { 0, 0, 10, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = spec_file .. "::xunit.testproj1::UnitTest1", - -- is_class = true, - -- name = "UnitTest1", - -- path = spec_file, - -- range = { 2, 0, 10, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = spec_file .. "::xunit.testproj1::UnitTest1::Test1", - -- is_class = false, - -- name = "Test1", - -- path = spec_file, - -- range = { 4, 1, 9, 2 }, - -- type = "test", - -- }, - -- }, - -- }, - -- }, - -- } - - assert.same(positions, expected_positions) - 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" - local positions = plugin.discover_positions(spec_file):to_list() - - local expected_positions = { - { - id = spec_file, - name = spec_file_name, - path = spec_file, - range = { 0, 0, 27, 0 }, - type = "file", - }, - { - { - framework = "xunit", - id = spec_file .. "::UnitTest1", - is_class = true, - name = "UnitTest1", - path = spec_file, - range = { 4, 0, 26, 1 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = spec_file .. "::UnitTest1::Test1", - is_class = false, - name = "XUnitSamples.UnitTest1.Test1", - path = spec_file, - range = { 6, 1, 10, 2 }, - running_id = "./tests/xunit/specs/nested_class.cs::UnitTest1::Test1", - type = "test", - }, - }, - { - { - framework = "xunit", - id = spec_file .. "::UnitTest1+NestedClass", - is_class = true, - name = "NestedClass", - path = spec_file, - range = { 12, 1, 25, 2 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = spec_file .. "::UnitTest1+NestedClass::Test1", - is_class = false, - name = "XUnitSamples.UnitTest1+NestedClass.Test1", - path = spec_file, - range = { 14, 2, 18, 3 }, - running_id = "./tests/xunit/specs/nested_class.cs::UnitTest1+NestedClass::Test1", - type = "test", - }, - }, - { - { - framework = "xunit", - id = spec_file .. "::UnitTest1+NestedClass::Test2", - is_class = false, - name = "XUnitSamples.UnitTest1+NestedClass.Test2", - path = spec_file, - range = { 20, 2, 24, 3 }, - running_id = "./tests/xunit/specs/nested_class.cs::UnitTest1+NestedClass::Test2", - type = "test", - }, - }, - }, - }, - } - -- local expected_positions = { - -- { - -- id = spec_file, - -- name = spec_file_name, - -- path = spec_file, - -- range = { 0, 0, 27, 0 }, - -- type = "file", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = spec_file .. "::XUnitSamples", - -- is_class = false, - -- name = "XUnitSamples", - -- path = spec_file, - -- range = { 2, 0, 26, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = spec_file .. "::XUnitSamples::UnitTest1", - -- is_class = true, - -- name = "UnitTest1", - -- path = spec_file, - -- range = { 4, 0, 26, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = spec_file .. "::XUnitSamples::UnitTest1::Test1", - -- is_class = false, - -- 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", - -- path = spec_file, - -- range = { 12, 1, 25, 2 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = spec_file .. "::XUnitSamples::UnitTest1+NestedClass::Test1", - -- is_class = false, - -- 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 = "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", - -- }, - -- }, - -- }, - -- }, - -- }, - -- } - - assert.same(positions, expected_positions) - end) - - async.it("should discover Fact tests in fsharp file when not the only attribute", function() - local spec_file = "./tests/xunit/specs/fact_and_trait.fs" - local spec_file_name = "fact_and_trait.fs" - local positions = plugin.discover_positions(spec_file):to_list() - - local expected_positions = { - { - id = "./tests/xunit/specs/fact_and_trait.fs", - name = spec_file_name, - path = "./tests/xunit/specs/fact_and_trait.fs", - range = { 0, 0, 7, 0 }, - type = "file", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/fact_and_trait.fs::xunit.testproj1", - is_class = false, - name = "xunit.testproj1", - path = "./tests/xunit/specs/fact_and_trait.fs", - range = { 0, 0, 6, 22 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/fact_and_trait.fs::xunit.testproj1::UnitTest1", - is_class = true, - name = "UnitTest1", - path = "./tests/xunit/specs/fact_and_trait.fs", - range = { 2, 5, 6, 22 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/fact_and_trait.fs::xunit.testproj1::UnitTest1::Test1", - is_class = false, - name = "Test1", - path = "./tests/xunit/specs/fact_and_trait.fs", - range = { 3, 1, 6, 22 }, - type = "test", - }, - }, - }, - }, - } - - assert.same(expected_positions, positions) - end) -end) diff --git a/tests/xunit/discover_positions/theory_attribute_spec.lua b/tests/xunit/discover_positions/theory_attribute_spec.lua deleted file mode 100644 index 572be36..0000000 --- a/tests/xunit/discover_positions/theory_attribute_spec.lua +++ /dev/null @@ -1,247 +0,0 @@ -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(...)) -end - -describe("discover_positions", function() - require("neotest").setup({ - adapters = { - require("neotest-dotnet"), - }, - }) - - 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() - -- return { - -- { - -- 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", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1", - -- is_class = false, - -- name = "xunit.testproj1", - -- path = "./tests/xunit/specs/theory_and_fact_mixed.cs", - -- range = { 0, 0, 17, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1", - -- is_class = true, - -- name = "UnitTest1", - -- path = "./tests/xunit/specs/theory_and_fact_mixed.cs", - -- range = { 2, 0, 17, 1 }, - -- type = "namespace", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1::Test1", - -- is_class = false, - -- 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", - -- }, - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit.testproj1::UnitTest1::Test2", - -- is_class = false, - -- 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", - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit::testproj1::UnitTest1::Test2(a: 1)", - -- is_class = false, - -- 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", - -- }, - -- }, - -- { - -- { - -- framework = "xunit", - -- id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit::testproj1::UnitTest1::Test2(a: 2)", - -- is_class = false, - -- 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", - -- }, - -- }, - -- }, - -- }, - -- }, - -- } - return { - { - 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", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/theory_and_fact_mixed.cs::UnitTest1", - is_class = true, - name = "UnitTest1", - path = "./tests/xunit/specs/theory_and_fact_mixed.cs", - range = { 2, 0, 17, 1 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/theory_and_fact_mixed.cs::UnitTest1::Test1", - is_class = false, - 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::UnitTest1::Test1", - type = "test", - }, - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/theory_and_fact_mixed.cs::UnitTest1::Test2", - is_class = false, - 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::UnitTest1::Test2", - type = "test", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit::testproj1::UnitTest1::Test2(a: 1)", - is_class = false, - 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::UnitTest1::Test2", - type = "test", - }, - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/theory_and_fact_mixed.cs::xunit::testproj1::UnitTest1::Test2(a: 2)", - is_class = false, - 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::UnitTest1::Test2", - type = "test", - }, - }, - }, - }, - } - end - - assert.same(positions, get_expected_output()) - end) - - async.it("should discover tests in block scoped namespace", function() - local spec_file = "./tests/xunit/specs/block_scoped_namespace.cs" - local positions = plugin.discover_positions(spec_file):to_list() - - local expected_positions = { - { - id = "./tests/xunit/specs/block_scoped_namespace.cs", - name = "block_scoped_namespace.cs", - path = "./tests/xunit/specs/block_scoped_namespace.cs", - range = { 0, 0, 11, 0 }, - type = "file", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/block_scoped_namespace.cs::xunit.testproj1", - is_class = false, - name = "xunit.testproj1", - path = "./tests/xunit/specs/block_scoped_namespace.cs", - range = { 0, 0, 10, 1 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/block_scoped_namespace.cs::xunit.testproj1::UnitTest1", - is_class = true, - name = "UnitTest1", - path = "./tests/xunit/specs/block_scoped_namespace.cs", - range = { 2, 1, 9, 2 }, - type = "namespace", - }, - { - { - framework = "xunit", - id = "./tests/xunit/specs/block_scoped_namespace.cs::xunit.testproj1::UnitTest1::Test1", - is_class = false, - 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) diff --git a/tests/xunit/specs/block_scoped_namespace.cs b/tests/xunit/specs/block_scoped_namespace.cs deleted file mode 100644 index 816d04a..0000000 --- a/tests/xunit/specs/block_scoped_namespace.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace xunit.testproj1 -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - Assert.Equal(1, 1); - } - } -} diff --git a/tests/xunit/specs/classdata.cs b/tests/xunit/specs/classdata.cs deleted file mode 100644 index 09549e5..0000000 --- a/tests/xunit/specs/classdata.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Xunit; - -namespace XUnitSamples; - -public class ClassDataTests -{ - [Theory] - [ClassData(typeof(NumericTestData))] - public void Theory_With_Class_Data_Test(int v1, int v2) - { - var sum = v1 + v2; - Assert.True(sum == 3); - } -} - -public class NumericTestData : IEnumerable -{ - public IEnumerator GetEnumerator() - { - yield return new object[] { 1, 2 }; - yield return new object[] { -4, 6 }; - yield return new object[] { -2, 2 }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/tests/xunit/specs/custom_attribute.cs b/tests/xunit/specs/custom_attribute.cs deleted file mode 100644 index 14a1728..0000000 --- a/tests/xunit/specs/custom_attribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Xunit; - -namespace XUnitSamples; - -[CollectionDefinition(nameof(CosmosContainer), DisableParallelization = true)] -public class CosmosConnectorTest(CosmosContainer CosmosContainer) : IClassFixture -{ - [SkippableEnvironmentFact("TEST", DisplayName = "Custom attribute works ok")] - public async void Custom_Attribute_Tests() - { - ConnectionInfo connectionInfo = await CosmosContainer.StartContainerOrGetConnectionInfo(); - Assert.Equal("127.0.0.1", connectionInfo.Host); - } -} diff --git a/tests/xunit/specs/fact_and_trait.cs b/tests/xunit/specs/fact_and_trait.cs deleted file mode 100644 index e1ff469..0000000 --- a/tests/xunit/specs/fact_and_trait.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace xunit.testproj1; - -public class UnitTest1 -{ - [Fact] - [Trait("Category", "Integration")] - public void Test1() - { - Assert.Equal(1, 1); - } -} diff --git a/tests/xunit/specs/fact_and_trait.fs b/tests/xunit/specs/fact_and_trait.fs deleted file mode 100644 index 146cf69..0000000 --- a/tests/xunit/specs/fact_and_trait.fs +++ /dev/null @@ -1,7 +0,0 @@ -namespace xunit.testproj1 - -type UnitTest1() = - [] - [] - member _.Test1() = - Assert.Equal(1, 1) diff --git a/tests/xunit/specs/nested_class.cs b/tests/xunit/specs/nested_class.cs deleted file mode 100644 index 2cb4160..0000000 --- a/tests/xunit/specs/nested_class.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Xunit; - -namespace XUnitSamples; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - Assert.Equal(1, 1); - } - - public class NestedClass - { - [Fact] - public void Test1() - { - Assert.Equal(1, 0); - } - - [Fact] - public void Test2() - { - Assert.Equal(1, 1); - } - } -} diff --git a/tests/xunit/specs/theory_and_fact_mixed.cs b/tests/xunit/specs/theory_and_fact_mixed.cs deleted file mode 100644 index 1cbba9f..0000000 --- a/tests/xunit/specs/theory_and_fact_mixed.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace xunit.testproj1; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - Assert.Equal(1, 1); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - public void Test2(int a) - { - Assert.Equal(1, a); - } -} From efb9e336b9107c1a3b0204ae91dc4420c3be1c0e Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 20 Nov 2024 19:38:47 +0100 Subject: [PATCH 25/43] handle roll forward dotnet versions --- run_tests.fsx | 58 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/run_tests.fsx b/run_tests.fsx index 10f759d..f691fe0 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -16,6 +16,7 @@ open Microsoft.TestPlatform.VsTestConsole.TranslationLayer open Microsoft.VisualStudio.TestPlatform.ObjectModel open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces +open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging module TestDiscovery = @@ -42,7 +43,7 @@ module TestDiscovery = {| StreamPath = args[0] OutputPath = args[1] - Ids = args[2..] |> Array.map Guid.Parse |} + Ids = args[3..] |> Array.map Guid.Parse |} |> ValueOption.Some else ValueOption.None @@ -79,9 +80,14 @@ module TestDiscovery = |> Seq.iter (fun (file, testCases) -> discoveredTests.AddOrUpdate(file, testCases, (fun _ _ -> testCases)) |> ignore) - member _.HandleDiscoveryComplete(_, _) = () - member __.HandleLogMessage(_, _) = () + + member __.HandleLogMessage(level, message) = + if level = TestMessageLevel.Error then + Console.Error.WriteLine(message) + else + Console.WriteLine(message) + member __.HandleRawMessage(_) = () type PlaygroundTestRunHandler(streamOutputPath, outputFilePath) = @@ -193,6 +199,7 @@ module TestDiscovery = |> Map.add "VSTEST_RUNNER_DEBUG_ATTACHVS" "0" |> Map.add "VSTEST_HOST_DEBUG_ATTACHVS" "0" |> Map.add "VSTEST_DATACOLLECTOR_DEBUG_ATTACHVS" "0" + |> Map.add "DOTNET_ROLL_FORWARD" "Major" |> Dictionary let options = TestPlatformOptions(CollectMetrics = false) @@ -213,26 +220,31 @@ module TestDiscovery = task { do! Task.Yield() - let discoveryHandler = - PlaygroundTestDiscoveryHandler() :> ITestDiscoveryEventsHandler2 - - r.DiscoverTests(args.Sources, sourceSettings, options, testSession, discoveryHandler) - use testsWriter = new StreamWriter(args.OutputPath, append = false) - - discoveredTests - |> Seq.map (fun x -> - (x.Key, - x.Value - |> Seq.map (fun testCase -> - testCase.Id, - { CodeFilePath = testCase.CodeFilePath - DisplayName = testCase.DisplayName - LineNumber = testCase.LineNumber - FullyQualifiedName = testCase.FullyQualifiedName }) - |> Map)) - |> Map - |> JsonConvert.SerializeObject - |> testsWriter.WriteLine + try + let discoveryHandler = + PlaygroundTestDiscoveryHandler() :> ITestDiscoveryEventsHandler2 + + for source in args.Sources do + r.DiscoverTests([| source |], sourceSettings, options, testSession, discoveryHandler) + + use testsWriter = new StreamWriter(args.OutputPath, append = false) + + discoveredTests + |> Seq.map (fun x -> + (x.Key, + x.Value + |> Seq.map (fun testCase -> + testCase.Id, + { CodeFilePath = testCase.CodeFilePath + DisplayName = testCase.DisplayName + LineNumber = testCase.LineNumber + FullyQualifiedName = testCase.FullyQualifiedName }) + |> Map)) + |> Map + |> JsonConvert.SerializeObject + |> testsWriter.WriteLine + with e -> + Console.WriteLine($"failed to discovery tests for {args.Sources}. Exception: {e}") use waitFileWriter = new StreamWriter(args.WaitFile, append = false) waitFileWriter.WriteLine("1") From dd15f3dbb30888c85b756b3bf62ae462b2b4ca2d Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 20 Nov 2024 20:03:41 +0100 Subject: [PATCH 26/43] simplify id collection --- lua/neotest-dotnet/init.lua | 11 +++-------- run_tests.fsx | 16 +++++++++------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 295bcf9..2d73ebd 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -152,15 +152,10 @@ DotnetNeotestAdapter.build_spec = function(args) local ids = {} - if pos.type ~= "test" then - ids = {} - for _, position in tree:iter() do - if position.type == "test" then - ids[#ids + 1] = position.id - end + for _, position in tree:iter() do + if position.type == "test" then + ids[#ids + 1] = position.id end - else - ids = { pos.id } end logger.debug("ids:") diff --git a/run_tests.fsx b/run_tests.fsx index f691fe0..c6ec8c3 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -43,7 +43,7 @@ module TestDiscovery = {| StreamPath = args[0] OutputPath = args[1] - Ids = args[3..] |> Array.map Guid.Parse |} + Ids = args[2..] |> Array.map Guid.Parse |} |> ValueOption.Some else ValueOption.None @@ -64,6 +64,12 @@ module TestDiscovery = else ValueOption.None + let logHandler (level: TestMessageLevel) (message: string) = + if level = TestMessageLevel.Error then + Console.Error.WriteLine(message) + else + Console.WriteLine(message) + type TestCaseDto = { CodeFilePath: string DisplayName: string @@ -82,11 +88,7 @@ module TestDiscovery = member _.HandleDiscoveryComplete(_, _) = () - member __.HandleLogMessage(level, message) = - if level = TestMessageLevel.Error then - Console.Error.WriteLine(message) - else - Console.WriteLine(message) + member __.HandleLogMessage(level, message) = logHandler level message member __.HandleRawMessage(_) = () @@ -100,7 +102,7 @@ module TestDiscovery = use outputWriter = new StreamWriter(outputFilePath, append = false) outputWriter.WriteLine(JsonConvert.SerializeObject(resultsDictionary)) - member __.HandleLogMessage(_level, _message) = () + member __.HandleLogMessage(level, message) = logHandler level message member __.HandleRawMessage(_rawMessage) = () From 5aaabc5e4d7f3c7464aacd3e29dbc04914ba3fe0 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 23 Nov 2024 11:31:46 +0100 Subject: [PATCH 27/43] clean up --- lua/neotest-dotnet/init.lua | 3 +++ run_tests.fsx | 45 +++++++++++++++---------------------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 2d73ebd..6bcc7ea 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -105,6 +105,9 @@ DotnetNeotestAdapter.discover_positions = function(path) range = { root:range() }, }, } + -- TODO: invert logic so we loop test in tests_in_file rather than treesitter nodes. + -- tests_in_file is our source of truth of test cases. + -- the treesitter nodes are there to get the correct range for the test case. for _, match in query:iter_matches(root, content, nil, nil, { all = false }) do local captured_nodes = {} for i, capture in ipairs(query.captures) do diff --git a/run_tests.fsx b/run_tests.fsx index c6ec8c3..9b3c995 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -19,13 +19,14 @@ open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging module TestDiscovery = + let parseArgs (args: string) = + args.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) + |> Array.tail [] let (|DiscoveryRequest|_|) (str: string) = if str.StartsWith("discover") then - let args = - str.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) - |> Array.tail + let args = parseArgs str {| OutputPath = args[0] WaitFile = args[1] @@ -37,9 +38,7 @@ module TestDiscovery = [] let (|RunTests|_|) (str: string) = if str.StartsWith("run-tests") then - let args = - str.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) - |> Array.tail + let args = parseArgs str {| StreamPath = args[0] OutputPath = args[1] @@ -51,9 +50,7 @@ module TestDiscovery = [] let (|DebugTests|_|) (str: string) = if str.StartsWith("debug-tests") then - let args = - str.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) - |> Array.tail + let args = parseArgs str {| PidPath = args[0] AttachedPath = args[1] @@ -78,6 +75,15 @@ module TestDiscovery = let discoveredTests = ConcurrentDictionary() + let getTestCases ids = + let idMap = + discoveredTests + |> _.Values + |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) + |> Map + + ids |> Array.choose (fun id -> Map.tryFind id idMap) + type PlaygroundTestDiscoveryHandler() = interface ITestDiscoveryEventsHandler2 with member _.HandleDiscoveredTests(discoveredTestCases: IEnumerable) = @@ -111,9 +117,6 @@ module TestDiscovery = match outcome with | TestOutcome.Passed -> "passed" | TestOutcome.Failed -> "failed" - | TestOutcome.Skipped -> "skipped" - | TestOutcome.None -> "skipped" - | TestOutcome.NotFound -> "skipped" | _ -> "skipped" let results = @@ -218,7 +221,7 @@ module TestDiscovery = while loop do match Console.ReadLine() with | DiscoveryRequest args -> - // spawn as task to allow running discovery + // spawn as task to allow running discovery concurrently task { do! Task.Yield() @@ -255,26 +258,14 @@ module TestDiscovery = } |> ignore | RunTests args -> - let idMap = - discoveredTests - |> _.Values - |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) - |> Map - - let testCases = args.Ids |> Array.choose (fun id -> Map.tryFind id idMap) + let testCases = getTestCases args.Ids let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath) // spawn as task to allow running concurrent tests r.RunTestsAsync(testCases, sourceSettings, testHandler) |> ignore () | DebugTests args -> - let idMap = - discoveredTests - |> _.Values - |> Seq.collect (Seq.map (fun testCase -> testCase.Id, testCase)) - |> Map - - let testCases = args.Ids |> Array.choose (fun id -> Map.tryFind id idMap) + let testCases = getTestCases args.Ids let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath) let debugLauncher = DebugLauncher(args.PidPath, args.AttachedPath) From 8b3f6d7f283e093b8f79443c407d6892fc7c617e Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sun, 24 Nov 2024 13:18:50 +0100 Subject: [PATCH 28/43] improve position detection --- lua/neotest-dotnet/init.lua | 10 ++++----- lua/neotest-dotnet/vstest_wrapper.lua | 29 +++++++++++++++------------ run_tests.fsx | 6 +++++- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 6bcc7ea..40265c8 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -56,6 +56,7 @@ local function build_position(source, captured_nodes, tests_in_file, path) qualified_name = test.FullyQualifiedName, range = { definition:range() }, }) + tests_in_file[id] = nil end end else @@ -87,6 +88,7 @@ DotnetNeotestAdapter.discover_positions = function(path) local content = lib.files.read(path) local lang = vim.treesitter.language.get_lang(filetype) or filetype nio.scheduler() + tests_in_file = vim.fn.deepcopy(tests_in_file) local lang_tree = vim.treesitter.get_string_parser(content, lang, { injections = { [lang] = "" } }) @@ -105,9 +107,6 @@ DotnetNeotestAdapter.discover_positions = function(path) range = { root:range() }, }, } - -- TODO: invert logic so we loop test in tests_in_file rather than treesitter nodes. - -- tests_in_file is our source of truth of test cases. - -- the treesitter nodes are there to get the correct range for the test case. for _, match in query:iter_matches(root, content, nil, nil, { all = false }) do local captured_nodes = {} for i, capture in ipairs(query.captures) do @@ -176,7 +175,7 @@ DotnetNeotestAdapter.build_spec = function(args) local pid_path = nio.fn.tempname() local attached_path = nio.fn.tempname() - local pid = vstest.debug_tests(pid_path, attached_path, stream_path, results_path, pos.id) + local pid = vstest.debug_tests(pid_path, attached_path, stream_path, results_path, ids) --- @type Configuration strategy = { type = "netcoredbg", @@ -191,6 +190,7 @@ DotnetNeotestAdapter.build_spec = function(args) local dap = require("dap") dap.listeners.after.configurationDone["neotest-dotnet"] = function() nio.run(function() + logger.debug("attached to debug test runner") lib.files.write(attached_path, "1") end) end @@ -199,7 +199,7 @@ DotnetNeotestAdapter.build_spec = function(args) end return { - command = vstest.run_tests(ids, stream_path, results_path), + command = vstest.run_tests(args.strategy == "dap", ids, stream_path, results_path), context = { result_path = results_path, stop_stream = stop_stream, diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 4398e9e..0d37382 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -307,23 +307,26 @@ function M.discover_tests(path) end ---runs tests identified by ids. +---@param dap boolean true if normal test runner should be skipped ---@param ids string|string[] ---@param stream_path string ---@param output_path string ---@return string command -function M.run_tests(ids, stream_path, output_path) - lib.process.run({ "dotnet", "build" }) - - local command = vim - .iter({ - "run-tests", - stream_path, - output_path, - ids, - }) - :flatten() - :join(" ") - invoke_test_runner(command) +function M.run_tests(dap, ids, stream_path, output_path) + if not dap then + lib.process.run({ "dotnet", "build" }) + + local command = vim + .iter({ + "run-tests", + stream_path, + output_path, + ids, + }) + :flatten() + :join(" ") + invoke_test_runner(command) + end return string.format("tail -n 1 -f %s", output_path, output_path) end diff --git a/run_tests.fsx b/run_tests.fsx index 9b3c995..738b03e 100644 --- a/run_tests.fsx +++ b/run_tests.fsx @@ -180,7 +180,11 @@ module TestDiscovery = while not (cts.Token.IsCancellationRequested || File.Exists(attachedFile)) do () - File.Exists(attachedFile) + let attached = File.Exists(attachedFile) + + Console.WriteLine($"Debugger attached: {attached}") + + attached member __.IsDebug = true From 109dd722f1edd574950f13908f3c586b85eff0c4 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Tue, 26 Nov 2024 22:43:15 +0100 Subject: [PATCH 29/43] improve dap strategy --- lua/neotest-dotnet/init.lua | 8 +++----- lua/neotest-dotnet/vstest_wrapper.lua | 9 +++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 40265c8..0b37dbb 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -165,17 +165,15 @@ DotnetNeotestAdapter.build_spec = function(args) local results_path = nio.fn.tempname() local stream_path = nio.fn.tempname() - lib.files.write(results_path, "") lib.files.write(stream_path, "") local stream_data, stop_stream = lib.files.stream_lines(stream_path) local strategy if args.strategy == "dap" then - local pid_path = nio.fn.tempname() local attached_path = nio.fn.tempname() - local pid = vstest.debug_tests(pid_path, attached_path, stream_path, results_path, ids) + local pid = vstest.debug_tests(attached_path, stream_path, results_path, ids) --- @type Configuration strategy = { type = "netcoredbg", @@ -185,7 +183,7 @@ DotnetNeotestAdapter.build_spec = function(args) env = { DOTNET_ENVIRONMENT = "Development", }, - processId = pid, + processId = vim.trim(pid), before = function() local dap = require("dap") dap.listeners.after.configurationDone["neotest-dotnet"] = function() @@ -199,7 +197,7 @@ DotnetNeotestAdapter.build_spec = function(args) end return { - command = vstest.run_tests(args.strategy == "dap", ids, stream_path, results_path), + command = vstest.run_tests(args.strategy == "dap", stream_path, results_path, ids), context = { result_path = results_path, stop_stream = stop_stream, diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 0d37382..5b786a2 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -308,11 +308,11 @@ end ---runs tests identified by ids. ---@param dap boolean true if normal test runner should be skipped ----@param ids string|string[] ---@param stream_path string ---@param output_path string +---@param ids string|string[] ---@return string command -function M.run_tests(dap, ids, stream_path, output_path) +function M.run_tests(dap, stream_path, output_path, ids) if not dap then lib.process.run({ "dotnet", "build" }) @@ -332,15 +332,16 @@ function M.run_tests(dap, ids, stream_path, output_path) end --- Uses the vstest console to spawn a test process for the debugger to attach to. ----@param pid_path string ---@param attached_path string ---@param stream_path string ---@param output_path string ---@param ids string|string[] ---@return string? pid -function M.debug_tests(pid_path, attached_path, stream_path, output_path, ids) +function M.debug_tests(attached_path, stream_path, output_path, ids) lib.process.run({ "dotnet", "build" }) + local pid_path = nio.fn.tempname() + local command = vim .iter({ "debug-tests", From c5c6e10963375ab799b39f334d75b5e91b70298d Mon Sep 17 00:00:00 2001 From: Nikolaj Sidorenco Date: Mon, 2 Dec 2024 19:19:55 +0100 Subject: [PATCH 30/43] Add testing * improve position detection * add tests * update workflow * add treesitter cli to workflow * bump lower bound nvim version * test on all os * set dotnet version --- .busted | 13 ++++ .github/workflows/main.yml | 59 ++++++++-------- .gitignore | 7 ++ .luacheckrc | 10 +++ Makefile | 12 +--- lua/neotest-dotnet/init.lua | 10 +-- lua/neotest-dotnet/vstest_wrapper.lua | 33 +++++---- neotest-dotnet-scm-1.rockspec | 27 ++++++++ run_tests.fsx => scripts/run_tests.fsx | 12 ++-- spec/installation_spec.lua | 12 ++++ spec/root_detection_spec.lua | 20 ++++++ spec/samples/test_project/project.fsproj | 0 spec/samples/test_solution/fsharp-test.sln | 34 ++++++++++ .../src/CSharpTest/CSharpTest.csproj | 23 +++++++ .../test_solution/src/CSharpTest/UnitTest1.cs | 10 +++ .../src/FsharpTest/FsharpTest.fsproj | 27 ++++++++ .../test_solution/src/FsharpTest/Program.fs | 1 + .../test_solution/src/FsharpTest/Tests.fs | 53 +++++++++++++++ .../src/FsharpTest/TestsNUnit.fs | 23 +++++++ spec/test_detection_spec.lua | 67 +++++++++++++++++++ 20 files changed, 388 insertions(+), 65 deletions(-) create mode 100644 .busted create mode 100644 .luacheckrc create mode 100644 neotest-dotnet-scm-1.rockspec rename run_tests.fsx => scripts/run_tests.fsx (96%) create mode 100644 spec/installation_spec.lua create mode 100644 spec/root_detection_spec.lua create mode 100644 spec/samples/test_project/project.fsproj create mode 100644 spec/samples/test_solution/fsharp-test.sln create mode 100644 spec/samples/test_solution/src/CSharpTest/CSharpTest.csproj create mode 100644 spec/samples/test_solution/src/CSharpTest/UnitTest1.cs create mode 100644 spec/samples/test_solution/src/FsharpTest/FsharpTest.fsproj create mode 100644 spec/samples/test_solution/src/FsharpTest/Program.fs create mode 100644 spec/samples/test_solution/src/FsharpTest/Tests.fs create mode 100644 spec/samples/test_solution/src/FsharpTest/TestsNUnit.fs create mode 100644 spec/test_detection_spec.lua diff --git a/.busted b/.busted new file mode 100644 index 0000000..ed81890 --- /dev/null +++ b/.busted @@ -0,0 +1,13 @@ +return { + _all = { + coverage = false, + lpath = "lua/?.lua;lua/?/init.lua", + lua = "nlua", + }, + default = { + verbose = true, + }, + tests = { + verbose = true, + }, +} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d69f201..091452d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,53 +23,48 @@ jobs: version: latest args: --check . - #documentation: - # runs-on: ubuntu-latest - # name: documentation - # steps: - # - uses: actions/checkout@v3 -# - # - name: setup neovim - # uses: rhysd/action-setup-vim@v1 - # with: - # neovim: true - # version: v0.8.2 - - # - name: generate documentation - # run: make documentation-ci - - # - name: check docs diff - # run: exit $(git diff --name-only origin/main -- doc | wc -l) - tests: - needs: + needs: - lint #- documentation runs-on: ubuntu-latest timeout-minutes: 2 strategy: matrix: - neovim_version: ['v0.9.1', 'v0.9.4', 'v0.10.0', 'nightly'] + os: [ubuntu-latest, macos-latest, windows-latest] + neovim_version: ["v0.10.0"] + include: + - os: ubuntu-latest + neovim_version: "nightly" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "9.0.x" - - run: date +%F > todays-date + - name: Install C/C++ Compiler + uses: rlalik/setup-cpp-compiler@master + with: + compiler: clang-latest - - name: restore cache for today's nightly. - uses: actions/cache@v3 + - name: Install tree-sitter CLI + uses: baptiste0928/cargo-install@v3 with: - path: _neovim - key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }} + crate: tree-sitter-cli - - name: setup neovim - uses: rhysd/action-setup-vim@v1 + - name: Run tests + id: test + uses: nvim-neorocks/nvim-busted-action@v1 with: - neovim: true - version: ${{ matrix.neovim_version }} + nvim_version: ${{ matrix.neovim_version }} - - name: run tests - run: make test-ci + - name: Save neotest log + if: always() && steps.test.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: neotest-log-${{ matrix.neovim_version }}-${{ matrix.os }} + path: ~/.local/state/nvim/neotest.log release: name: release diff --git a/.gitignore b/.gitignore index ea910b6..8465e2a 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,10 @@ luac.out # Test dependencies deps/ **/obj/* +/luarocks +/lua +/lua_modules +/.luarocks + +obj/ +bin/ diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..8a1f518 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,10 @@ +ignore = { + "631", -- max_line_length + "122", -- read-only field of global variable +} +read_globals = { + "vim", + "describe", + "it", + "assert", +} diff --git a/Makefile b/Makefile index 6a586d5..7bc3cba 100644 --- a/Makefile +++ b/Makefile @@ -5,17 +5,7 @@ all: # runs all the test files. test: - nvim --version | head -n 1 && echo '' - ./tests/test.sh - -# installs `mini.nvim`, used for both the tests and documentation. -deps: - @mkdir -p deps - git clone --depth 1 https://github.com/echasnovski/mini.doc.git deps/mini.doc.nvim - git clone --depth 1 https://github.com/nvim-neotest/neotest.git deps/neotest - git clone --depth 1 https://github.com/nvim-lua/plenary.nvim.git deps/plenary - git clone --depth 1 https://github.com/nvim-treesitter/nvim-treesitter.git deps/nvim-treesitter - git clone --depth 1 https://github.com/nvim-neotest/nvim-nio deps/nvim-nio + luarocks test --local # installs deps before running tests, useful for the CI. test-ci: deps test diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 0b37dbb..2043562 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -75,9 +75,6 @@ end DotnetNeotestAdapter.discover_positions = function(path) logger.info(string.format("scanning %s for tests...", path)) - local fsharp_query = require("neotest-dotnet.queries.fsharp") - local c_sharp_query = require("neotest-dotnet.queries.c_sharp") - local filetype = (vim.endswith(path, ".fs") and "fsharp") or "c_sharp" local tests_in_file = vstest.discover_tests(path) @@ -94,8 +91,11 @@ DotnetNeotestAdapter.discover_positions = function(path) local root = lib.treesitter.fast_parse(lang_tree):root() - local query = - lib.treesitter.normalise_query(lang, filetype == "fsharp" and fsharp_query or c_sharp_query) + local query = lib.treesitter.normalise_query( + lang, + filetype == "fsharp" and require("neotest-dotnet.queries.fsharp") + or require("neotest-dotnet.queries.c_sharp") + ) local sep = lib.files.sep local path_elems = vim.split(path, sep, { plain = true }) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 5b786a2..410f79f 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -34,6 +34,8 @@ end local function get_script(script_name) local script_paths = vim.api.nvim_get_runtime_file(script_name, true) + logger.debug("possible scripts:") + logger.debug(script_paths) for _, path in ipairs(script_paths) do if vim.endswith(path, ("neotest-dotnet%s" .. script_name):format(lib.files.sep)) then return path @@ -41,16 +43,10 @@ local function get_script(script_name) end end -local proj_file_path_map = {} - ---collects project information based on file ---@param path string ---@return { proj_file: string, dll_file: string, proj_dir: string } function M.get_proj_info(path) - if proj_file_path_map[path] then - return proj_file_path_map[path] - end - local proj_file = vim.fs.find(function(name, _) return name:match("%.[cf]sproj$") end, { upward = true, type = "file", path = vim.fs.dirname(path) })[1] @@ -70,7 +66,6 @@ function M.get_proj_info(path) proj_dir = dir_name, } - proj_file_path_map[path] = proj_data return proj_data end @@ -83,9 +78,12 @@ local function invoke_test_runner(command) return end - local test_discovery_script = get_script("run_tests.fsx") + local test_discovery_script = get_script("scripts/run_tests.fsx") local testhost_dll = get_vstest_path() + logger.debug("found discovery script: " .. test_discovery_script) + logger.debug("found testhost dll: " .. testhost_dll) + local vstest_command = { "dotnet", "fsi", test_discovery_script, testhost_dll } logger.info("starting vstest console with:") @@ -94,8 +92,12 @@ local function invoke_test_runner(command) local process = vim.system(vstest_command, { stdin = true, stdout = function(err, data) - logger.trace(data) - logger.trace(err) + if data then + logger.trace(data) + end + if err then + logger.trace(err) + end end, }, function(obj) logger.warn("vstest process died :(") @@ -165,7 +167,7 @@ function M.discover_tests(path) local json local proj_info = M.get_proj_info(path) - if not (proj_info.proj_file and proj_info.dll_file) then + if not proj_info.proj_file then logger.warn(string.format("failed to find project file for %s", path)) return {} end @@ -189,6 +191,13 @@ function M.discover_tests(path) logger.debug(stdout) end + proj_info = M.get_proj_info(path) + + if not proj_info.dll_file then + logger.warn(string.format("failed to find project dll for %s", path)) + return {} + end + local dll_open_err, dll_stats = nio.uv.fs_stat(proj_info.dll_file) assert(not dll_open_err, dll_open_err) @@ -286,7 +295,7 @@ function M.discover_tests(path) logger.debug("Waiting for result file to populated...") - local max_wait = 30 * 1000 -- 30 sec + local max_wait = 60 * 1000 -- 60 sec local done = M.spin_lock_wait_file(wait_file, max_wait) if done then diff --git a/neotest-dotnet-scm-1.rockspec b/neotest-dotnet-scm-1.rockspec new file mode 100644 index 0000000..b81ec30 --- /dev/null +++ b/neotest-dotnet-scm-1.rockspec @@ -0,0 +1,27 @@ +rockspec_format = "3.0" +package = "neotest-dotnet" +version = "scm-1" + +dependencies = { + "lua >= 5.1", + "neotest", + "tree-sitter-fsharp", + "tree-sitter-c_sharp", +} + +test_dependencies = { + "lua >= 5.1", + "busted", + "nlua", +} + +source = { + url = "git://github.com/issafalcon/neotest-dotnet", +} + +build = { + type = "builtin", + copy_directories = { + "scripts", + }, +} diff --git a/run_tests.fsx b/scripts/run_tests.fsx similarity index 96% rename from run_tests.fsx rename to scripts/run_tests.fsx index 738b03e..731a451 100644 --- a/run_tests.fsx +++ b/scripts/run_tests.fsx @@ -62,10 +62,11 @@ module TestDiscovery = ValueOption.None let logHandler (level: TestMessageLevel) (message: string) = - if level = TestMessageLevel.Error then - Console.Error.WriteLine(message) - else - Console.WriteLine(message) + if not <| String.IsNullOrWhiteSpace message then + if level = TestMessageLevel.Error then + Console.Error.WriteLine(message) + else + Console.WriteLine(message) type TestCaseDto = { CodeFilePath: string @@ -170,7 +171,7 @@ module TestDiscovery = member _.AttachDebuggerToProcess(pid: int, ct: CancellationToken) = use cts = CancellationTokenSource.CreateLinkedTokenSource(ct) - cts.CancelAfter(TimeSpan.FromSeconds(450)) + cts.CancelAfter(TimeSpan.FromSeconds(450.)) do Console.WriteLine($"spawned test process with pid: {pid}") @@ -234,6 +235,7 @@ module TestDiscovery = PlaygroundTestDiscoveryHandler() :> ITestDiscoveryEventsHandler2 for source in args.Sources do + Console.WriteLine($"Discovering tests for: {source}") r.DiscoverTests([| source |], sourceSettings, options, testSession, discoveryHandler) use testsWriter = new StreamWriter(args.OutputPath, append = false) diff --git a/spec/installation_spec.lua b/spec/installation_spec.lua new file mode 100644 index 0000000..01ce53b --- /dev/null +++ b/spec/installation_spec.lua @@ -0,0 +1,12 @@ +describe("Test environment", function() + it("Test can access vim namespace", function() + assert(vim, "Cannot access vim namespace") + assert.are.same(vim.trim(" a "), "a") + end) + it("Test can access neotest dependency", function() + assert(require("neotest"), "neotest") + end) + it("Test can access module in lua/neotest-dotnet", function() + assert(require("neotest-dotnet"), "Could not access main module") + end) +end) diff --git a/spec/root_detection_spec.lua b/spec/root_detection_spec.lua new file mode 100644 index 0000000..d64ecd4 --- /dev/null +++ b/spec/root_detection_spec.lua @@ -0,0 +1,20 @@ +describe("Test root detection", function() + it("Detect .sln file as root", function() + local plugin = require("neotest-dotnet") + local dir = vim.fn.getcwd() .. "/spec/samples/test_solution" + local root = plugin.root(dir) + assert.are_equal(dir, root) + end) + it("Detect .sln file as root from project dir", function() + local plugin = require("neotest-dotnet") + local dir = vim.fn.getcwd() .. "/spec/samples/test_solution" + local root = plugin.root(dir .. "/src/FsharpTest") + assert.are_equal(dir, root) + end) + it("Detect .fsproj file as root from project dir with no .sln file", function() + local plugin = require("neotest-dotnet") + local dir = vim.fn.getcwd() .. "/spec/samples/test_project" + local root = plugin.root(dir) + assert.are_equal(dir, root) + end) +end) diff --git a/spec/samples/test_project/project.fsproj b/spec/samples/test_project/project.fsproj new file mode 100644 index 0000000..e69de29 diff --git a/spec/samples/test_solution/fsharp-test.sln b/spec/samples/test_solution/fsharp-test.sln new file mode 100644 index 0000000..ddbc660 --- /dev/null +++ b/spec/samples/test_solution/fsharp-test.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{364BD0DC-1C6E-4811-BC58-D543DB1E67D2}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsharpTest", "src\FsharpTest\FsharpTest.fsproj", "{FFB89E81-0B57-4A30-9836-DC83EFD2ADA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpTest", "src\CSharpTest\CSharpTest.csproj", "{D0B0861B-D9E5-4EA5-8CAE-0CDCF0054021}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FFB89E81-0B57-4A30-9836-DC83EFD2ADA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFB89E81-0B57-4A30-9836-DC83EFD2ADA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFB89E81-0B57-4A30-9836-DC83EFD2ADA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFB89E81-0B57-4A30-9836-DC83EFD2ADA3}.Release|Any CPU.Build.0 = Release|Any CPU + {D0B0861B-D9E5-4EA5-8CAE-0CDCF0054021}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0B0861B-D9E5-4EA5-8CAE-0CDCF0054021}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0B0861B-D9E5-4EA5-8CAE-0CDCF0054021}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0B0861B-D9E5-4EA5-8CAE-0CDCF0054021}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FFB89E81-0B57-4A30-9836-DC83EFD2ADA3} = {364BD0DC-1C6E-4811-BC58-D543DB1E67D2} + {D0B0861B-D9E5-4EA5-8CAE-0CDCF0054021} = {364BD0DC-1C6E-4811-BC58-D543DB1E67D2} + EndGlobalSection +EndGlobal diff --git a/spec/samples/test_solution/src/CSharpTest/CSharpTest.csproj b/spec/samples/test_solution/src/CSharpTest/CSharpTest.csproj new file mode 100644 index 0000000..3aa9860 --- /dev/null +++ b/spec/samples/test_solution/src/CSharpTest/CSharpTest.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + diff --git a/spec/samples/test_solution/src/CSharpTest/UnitTest1.cs b/spec/samples/test_solution/src/CSharpTest/UnitTest1.cs new file mode 100644 index 0000000..5df4325 --- /dev/null +++ b/spec/samples/test_solution/src/CSharpTest/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace CSharpTest; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/spec/samples/test_solution/src/FsharpTest/FsharpTest.fsproj b/spec/samples/test_solution/src/FsharpTest/FsharpTest.fsproj new file mode 100644 index 0000000..7fc752d --- /dev/null +++ b/spec/samples/test_solution/src/FsharpTest/FsharpTest.fsproj @@ -0,0 +1,27 @@ + + + + net8.0 + + false + false + true + + + + + + + + + + + + + + + + + + + diff --git a/spec/samples/test_solution/src/FsharpTest/Program.fs b/spec/samples/test_solution/src/FsharpTest/Program.fs new file mode 100644 index 0000000..fdc31cd --- /dev/null +++ b/spec/samples/test_solution/src/FsharpTest/Program.fs @@ -0,0 +1 @@ +module Program = let [] main _ = 0 diff --git a/spec/samples/test_solution/src/FsharpTest/Tests.fs b/spec/samples/test_solution/src/FsharpTest/Tests.fs new file mode 100644 index 0000000..ab7a263 --- /dev/null +++ b/spec/samples/test_solution/src/FsharpTest/Tests.fs @@ -0,0 +1,53 @@ +namespace X.Tests + +open Xunit +open System.Threading.Tasks + +module A = + + [] + let ``My test`` () = + let fx x = + let x = 1 + Assert.True(false) + + fx () + + [] + let ``My test 2`` () = + let x = 1 + Assert.True(false) + + [] + let ``My test 3`` () = + let x = 1 + Assert.True(false) + + [] + let ``My slow test`` () = + task { + do! Task.Delay(10000) + Assert.True(true) + } + + [] + [] + [] + let ``Pass cool test parametrized function`` x _y _z = Assert.True(x > 0) + + + let notATest () = () + + +type ``X Should``() = + [] + member _.``Pass cool test``() = + do () + do () + do () + do () + Assert.True(true) + + [] + [] + member _.``Pass cool test parametrized``(x, _y, _z) = Assert.True(x > 0) diff --git a/spec/samples/test_solution/src/FsharpTest/TestsNUnit.fs b/spec/samples/test_solution/src/FsharpTest/TestsNUnit.fs new file mode 100644 index 0000000..1a83690 --- /dev/null +++ b/spec/samples/test_solution/src/FsharpTest/TestsNUnit.fs @@ -0,0 +1,23 @@ +namespace N.Tests + +open NUnit.Framework + +module A = + + [] + let ``My test`` () = + let x = 1 + let y = 2 + Assert.Pass() + + [] + [] + let ``Pass cool x parametrized function`` x _y _z = Assert.That(x > 0) + +[] +type ``X Should``() = + [] + member _.``Pass cool x``() = Assert.Pass() + + [] + member _.``Pass cool x parametrized``(x, _y, _z) = Assert.That(x > 0) diff --git a/spec/test_detection_spec.lua b/spec/test_detection_spec.lua new file mode 100644 index 0000000..aead8cf --- /dev/null +++ b/spec/test_detection_spec.lua @@ -0,0 +1,67 @@ +describe("Test test detection", function() + -- increase nio.test timeout + vim.env.PLENARY_TEST_TIMEOUT = 20000 + -- add test_discovery script and treesitter parsers installed with luarocks + vim.opt.runtimepath:append(vim.fn.getcwd()) + vim.opt.runtimepath:append(vim.fn.expand("~/.luarocks/lib/lua/5.1/")) + + local nio = require("nio") + + require("neotest").setup({ + adapters = { require("neotest-dotnet") }, + log_level = 0, + }) + + nio.tests.it("detect tests in fsharp file", function() + local plugin = require("neotest-dotnet") + local dir = vim.fn.getcwd() .. "/spec/samples/test_solution" + local test_file = dir .. "/src/FsharpTest/Tests.fs" + local positions = plugin.discover_positions(test_file) + + local tests = {} + + for _, position in positions:iter() do + if position.type == "test" then + tests[#tests + 1] = position.name + end + end + + local expected_tests = { + "X.Tests.A.My test", + "X.Tests.A.My test 2", + "X.Tests.A.My test 3", + "X.Tests.A.My slow test", + "X.Tests.A.Pass cool test parametrized function(x: 11, _y: 22, _z: 33)", + "X.Tests.A.Pass cool test parametrized function(x: 10, _y: 20, _z: 30)", + "X.Tests.X Should.Pass cool test", + "X.Tests.X Should.Pass cool test parametrized(x: 10, _y: 20, _z: 30)", + } + + table.sort(expected_tests) + table.sort(tests) + + assert.are_same(expected_tests, tests) + end) + + nio.tests.it("detect tests in c_sharp file", function() + local plugin = require("neotest-dotnet") + local dir = vim.fn.getcwd() .. "/spec/samples/test_solution" + local test_file = dir .. "/src/CSharpTest/UnitTest1.cs" + local positions = plugin.discover_positions(test_file) + + local tests = {} + + for _, position in positions:iter() do + if position.type == "test" then + tests[#tests + 1] = position.name + end + end + + local expected_tests = { "CSharpTest.UnitTest1.Test1" } + + table.sort(expected_tests) + table.sort(tests) + + assert.are_same(expected_tests, tests) + end) +end) From 543ac69d3c46722d42464f47a1902329fea99009 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 4 Dec 2024 16:58:58 +0100 Subject: [PATCH 31/43] improve scripts detection --- lua/neotest-dotnet/vstest_wrapper.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 410f79f..cc9ed80 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -33,11 +33,11 @@ local function get_vstest_path() end local function get_script(script_name) - local script_paths = vim.api.nvim_get_runtime_file(script_name, true) + local script_paths = vim.api.nvim_get_runtime_file(vim.fs.joinpath("scripts", script_name), true) logger.debug("possible scripts:") logger.debug(script_paths) for _, path in ipairs(script_paths) do - if vim.endswith(path, ("neotest-dotnet%s" .. script_name):format(lib.files.sep)) then + if vim.endswith(path, vim.fs.joinpath("neotest-dotnet", "scripts", script_name)) then return path end end @@ -78,7 +78,7 @@ local function invoke_test_runner(command) return end - local test_discovery_script = get_script("scripts/run_tests.fsx") + local test_discovery_script = get_script("run_tests.fsx") local testhost_dll = get_vstest_path() logger.debug("found discovery script: " .. test_discovery_script) From 682cc9471f9fde418a524bbb4e2179980a159853 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 4 Dec 2024 19:17:29 +0100 Subject: [PATCH 32/43] improve scripts detection --- lua/neotest-dotnet/vstest_wrapper.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index cc9ed80..90b7681 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -37,7 +37,7 @@ local function get_script(script_name) logger.debug("possible scripts:") logger.debug(script_paths) for _, path in ipairs(script_paths) do - if vim.endswith(path, vim.fs.joinpath("neotest-dotnet", "scripts", script_name)) then + if path:match("neotest%-dotnet") ~= nil then return path end end From 135ee327d796420a0e965678ad949974abf76642 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 4 Dec 2024 20:30:03 +0100 Subject: [PATCH 33/43] allow using vstest.console.exe on windows --- .gitignore | 1 - lua/neotest-dotnet/vstest_wrapper.lua | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8465e2a..3e54649 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ luac.out deps/ **/obj/* /luarocks -/lua /lua_modules /.luarocks diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 90b7681..f49d956 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -20,7 +20,10 @@ local function get_vstest_path() M.sdk_path = "/usr/local/share/dotnet/sdk/" end - logger.info(string.format("failed to detect sdk path. falling back to %s", M.sdk_path)) + local log_string = string.format("failed to detect sdk path. falling back to %s", M.sdk_path) + + vim.notify_once("neotest-dotnet: " .. log_string) + logger.info(log_string) else local out = process.stdout.read() M.sdk_path = out and out:match("Base Path:%s*(%S+)") @@ -30,6 +33,7 @@ local function get_vstest_path() end return vim.fs.find("vstest.console.dll", { upward = false, type = "file", path = M.sdk_path })[1] + or vim.fs.find("vstest.console.exe", { upward = false, type = "file", path = M.sdk_path })[1] end local function get_script(script_name) From 1e95b39f92ca7ba00214059e204d6813f3b9ebcc Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Wed, 4 Dec 2024 21:30:39 +0100 Subject: [PATCH 34/43] clean up in file reading code --- lua/neotest-dotnet/vstest_wrapper.lua | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index f49d956..fe2241f 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -13,27 +13,37 @@ local function get_vstest_path() args = { "--info" }, }) - if not process or errors then - if vim.fn.has("win32") then - M.sdk_path = "C:/Program Files/dotnet/sdk/" - else - M.sdk_path = "/usr/local/share/dotnet/sdk/" - end + local default_sdk_path + if vim.fn.has("win32") then + default_sdk_path = "C:/Program Files/dotnet/sdk/" + else + default_sdk_path = "/usr/local/share/dotnet/sdk/" + end + if not process or errors then + M.sdk_path = default_sdk_path local log_string = string.format("failed to detect sdk path. falling back to %s", M.sdk_path) vim.notify_once("neotest-dotnet: " .. log_string) logger.info(log_string) else local out = process.stdout.read() - M.sdk_path = out and out:match("Base Path:%s*(%S+)") - logger.info(string.format("detected sdk path: %s", M.sdk_path)) + local match = out and out:match("Base Path:%s*(%S+)") + if match then + M.sdk_path = match + logger.info(string.format("detected sdk path: %s", M.sdk_path)) + else + M.sdk_path = default_sdk_path + local log_string = + string.format("failed to detect sdk path. falling back to %s", M.sdk_path) + vim.notify_once("neotest-dotnet: " .. log_string) + logger.info(log_string) + end process.close() end end return vim.fs.find("vstest.console.dll", { upward = false, type = "file", path = M.sdk_path })[1] - or vim.fs.find("vstest.console.exe", { upward = false, type = "file", path = M.sdk_path })[1] end local function get_script(script_name) @@ -135,13 +145,10 @@ function M.spin_lock_wait_file(file_path, max_wait) local file_exists = false while not file_exists and tries * sleep_time < max_wait do - if vim.fn.filereadable(file_path) == 1 then + if lib.files.exists(file_path) then spin_lock.with(function() - local file, open_err = nio.file.open(file_path) - assert(not open_err, open_err) file_exists = true - content = file.read() - file.close() + content = require("neotest.lib").files.read(file_path) end) else tries = tries + 1 From 827a21f863e37a5ed52d704d77d54ee7e7c892b6 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 7 Dec 2024 13:47:11 +0100 Subject: [PATCH 35/43] fix path detection on windows --- lua/neotest-dotnet/vstest_wrapper.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index fe2241f..b2b5765 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -28,9 +28,9 @@ local function get_vstest_path() logger.info(log_string) else local out = process.stdout.read() - local match = out and out:match("Base Path:%s*(%S+)") + local match = out and out:match("Base Path:%s*(%S+[^\n]*)") if match then - M.sdk_path = match + M.sdk_path = vim.trim(match) logger.info(string.format("detected sdk path: %s", M.sdk_path)) else M.sdk_path = default_sdk_path From 05d7eda02dc83b4b9f3240ec34d8ad4c31bacf00 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sun, 22 Dec 2024 21:48:28 +0100 Subject: [PATCH 36/43] update readme --- README.md | 183 ++++---------------------- lua/neotest-dotnet/init.lua | 14 +- lua/neotest-dotnet/vstest_wrapper.lua | 57 ++++---- 3 files changed, 64 insertions(+), 190 deletions(-) diff --git a/README.md b/README.md index 97f3a34..947794d 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,16 @@ Neotest adapter for dotnet tests -- Covers the "majority" of use cases for the 3 major .NET test runners -- Attempts to provide support for `SpecFlow` generated tests for the various test runners - - Support for this may still be patchy, so please raise an issue if it doesn't behave as expected - - `RunNearest` or `RunInFile` functions will need to be run from the _generated_ specflow tests (NOT the `.feature`) +- Integrates with the VSTest runner to support all testing frameworks. +- DAP strategy for attaching debug adapter to test execution. # Pre-requisites neotest-dotnet requires makes a number of assumptions about your environment: -1. The `dotnet sdk` that is compatible with the current project is installed and the `dotnet` executable is on the users runtime path (future updates may allow customisation of the dotnet exe location) -2. The user is running tests using one of the supported test runners / frameworks (see support grid) +1. The `dotnet sdk` that is compatible with the current project is installed and the `dotnet` executable is on the users runtime path. 3. (For Debugging) `netcoredbg` is installed and `nvim-dap` plugin has been configured for `netcoredbg` (see debug config for more details) -4. Requires [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) and the parser for C# or F#. +4. Requires treesitter parser for either `C#` or `F#` # Installation @@ -63,63 +60,15 @@ Additional configuration settings can be provided: require("neotest").setup({ adapters = { require("neotest-dotnet")({ - dap = { - -- Extra arguments for nvim-dap configuration - -- See https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings for values - args = {justMyCode = false }, - -- Enter the name of your dap adapter, the default value is netcoredbg - adapter_name = "netcoredbg" - }, - -- Let the test-discovery know about your custom attributes (otherwise tests will not be picked up) - -- Note: Only custom attributes for non-parameterized tests should be added here. See the support note about parameterized tests - custom_attributes = { - xunit = { "MyCustomFactAttribute" }, - nunit = { "MyCustomTestAttribute" }, - mstest = { "MyCustomTestMethodAttribute" } - }, - -- Provide any additional "dotnet test" CLI commands here. These will be applied to ALL test runs performed via neotest. These need to be a table of strings, ideally with one key-value pair per item. - dotnet_additional_args = { - "--verbosity detailed" - }, - -- Tell neotest-dotnet to use either solution (requires .sln file) or project (requires .csproj or .fsproj file) as project root - -- Note: If neovim is opened from the solution root, using the 'project' setting may sometimes find all nested projects, however, - -- to locate all test projects in the solution more reliably (if a .sln file is present) then 'solution' is better. - discovery_root = "project" -- Default + -- Path to dotnet sdk path. + -- Used in cases where the sdk path cannot be auto discovered. + sdk_path = "/usr/local/dotnet/sdk/9.0.101/" }) } }) ``` -## Using `.runsettings` files - -The plugin provides commands to select and clear the runsettings files (if any are available in the Neovim working director tree). - -To select the runsettings file in a Neovim session run: -`:NeotestSelectRunsettingsFile` - -- This will apply the runsettings to all tests run via the neotest-adapter - -To clear the runsettings file in the same session run: -`:NeotestClearRunsettings` - - -## Additional `dotnet test` arguments - -As well as the `dotnet_additional_args` option in the adapter setup above, you may also provide additional CLI arguments as a table to each `neotest` command. -By doing this, the additional args provided in the setup function will be _replaced_ in their entirety by the ones provided at the command level. - -For example, to provide a `runtime` argument to the `dotnet test` command, for all the tests in the file, you can run: - -```lua -require("neotest").run.run({ vim.fn.expand("%"), dotnet_additional_args = { "--runtime win-x64" } }) -``` - -**NOTE**: - -- The `--logger` and `--results-directory` arguments, as well as the `--filter` expression are all added by the adapter, so changing any of these will likely result in errors in the adapter. -- Not all possible combinations of arguments will work with the adapter, as you might expect, given the way that output is specifically parsed and handled by the adapter. - -# Debugging +# Debugging adapter [Debugging Using neotest dap strategy](https://user-images.githubusercontent.com/19861614/232598584-4d673050-989d-4a3e-ae67-8969821898ce.mp4) @@ -136,92 +85,15 @@ dap.adapters.netcoredbg = { } ``` -Neotest-Dotnet uses a custom strategy for debugging, as `netcoredbg` needs to attach to the running test. The test command is modified by setting the `VSTEST_HOST_DEBUG` env variable, which then waits for the debugger to attach. - -To use the custom strategy, you no longer need to provide a custom command other than the standard neotest recommended one for debugging: +This adapter uses that standard dap strategy from `neotest`, which is run like so: - `lua require("neotest").run.run({strategy = "dap"})` -The adapter will replace the standard `dap` strategy with the custom one automatically. - -# Framework Support - -The adapter supports `NUnit`, `xUnit` and `MSTest` frameworks, to varying degrees. Given each framework has their own test runner, and specific features and attributes, it is a difficult task to support all the possible use cases for each one. - -To see if your use case is supported, check the grids below. If it isn't there, feel free to raise a ticket, or better yet, take a look at [how to contribute](#contributing) and raise a PR to support your use case! - -## Key - -:heavy_check_mark: = Fully supported - -:part_alternation_mark: = Partially Supported (functionality might behave unusually) - -:interrobang: = As yet untested - -:x: = Unsupported (tested) - -### NUnit - -| Framework Feature | Scope Level | Docs | Status | Notes | -| ---------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Test` (Attribute) | Method | [Test - Nunit](https://docs.nunit.org/articles/nunit/writing-tests/attributes/test.html) | :heavy_check_mark: | Supported when used inside a class with or without the `TestFixture` attribute decoration | -| `TestFixture` (Attribute) | Class | [TestFixture - Nunit](https://docs.nunit.org/articles/nunit/writing-tests/attributes/testfixture.html) | :heavy_check_mark: | | -| `TestCase()` (Attribute) | Method | [TestCase - Nunit](https://docs.nunit.org/articles/nunit/writing-tests/attributes/testcase.html) | :heavy_check_mark: | Support for parameterized tests with inline parameters. Supports neotest 'run nearest' and 'run file' functionality | -| Nested Classes | Class | | :heavy_check_mark: | Fully qualified name is corrected to include `+` when class is nested | -| `Theory` (Attribute) | Method | [Theory - Nunit](https://docs.nunit.org/articles/nunit/writing-tests/attributes/theory.html) | :x: | Currently has conflicts with XUnits `Theory` which is more commonly used | -| `TestCaseSource` (Attribute) | Method | [TestCaseSource - NUnit](https://docs.nunit.org/articles/nunit/writing-tests/attributes/testcasesource.html) | :heavy_check_mark: | Bundles all dynamically parameterized tests under one neotest listing (short output contains errors for all tests. One test failure displays failure indicator for entire test "grouping"). Supports neotest 'run nearest' and 'run file' functionality | - -### xUnit - -| Framework Feature | Scope Level | Docs | Status | Notes | -| -------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Fact` (Attribute) | Method | [Fact - xUnit](https://xunit.net/docs/getting-started/netcore/cmdline#write-first-tests) | :heavy_check_mark: | | -| `Theory` (Attribute) | Method | [Theory - xUnit](https://xunit.net/docs/getting-started/netcore/cmdline#write-first-theory) | :heavy_check_mark: | Used in conjunction with the `InlineData()` attribute | -| `InlineData()` (Attribute) | Method | [Theory - xUnit](https://xunit.net/docs/getting-started/netcore/cmdline#write-first-theory) | :heavy_check_mark: | Support for parameterized tests with inline parameters. Supports neotest 'run nearest' and 'run file' functionality | -| `ClassData()` (Attribute) | Method | [ClassData - xUnit](https://andrewlock.net/creating-parameterised-tests-in-xunit-with-inlinedata-classdata-and-memberdata/) | :heavy_check_mark: | Bundles all dynamically parameterized tests under one neotest listing (short output contains errors for all tests. One test failure displays failure indicator for entire test "grouping"). Supports neotest 'run nearest' and 'run file' functionality | -| Nested Classes | Class | | :heavy_check_mark: | Fully qualified name is corrected to include `+` when class is nested | - -### MSTest - -| Framework Feature | Scope Level | Docs | Status | Notes | -| ---------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- | -| `TestMethod` (Attribute) | Method | [TestMethod - MSTest](https://docs.nunit.org/articles/nunit/writing-tests/attributes/test.html) | :heavy_check_mark: | | -| `TestClass` (Attribute) | Class | [TestClass - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.testclassattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | | -| Nested Classes | Class | | :heavy_check_mark: | Fully qualified name is corrected to include `+` when class is nested | -| `DataTestMethod` (Attribute) | Method | [DataTestMethod - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.datatestmethodattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | | -| `DataRow` (Attribute) | Method | [DataRow - MSTest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.datarowattribute?view=visualstudiosdk-2022) | :heavy_check_mark: | Support for parameterized tests with inline parameters. Supports neotest 'run nearest' and 'run file' functionality | - -# Limitations - -1. A tradeoff was made between being able to run parameterized tests and the specificity of the `dotnet --filter` command options. A more lenient 'contains' type filter is used - in order for the adapter to be able to work with parameterized tests. Unfortunately, no amount of formatting would support specific `FullyQualifiedName` filters for the dotnet test command for parameterized tests. -2. Dynamically parameterized tests need to be grouped together as neotest-dotnet is unable to robustly match the full test names that the .NET test runner attaches to the tests at runtime. - - An attempt was made to use `dotnet test -t` to extract the dynamic test names, but this was too unreliable (duplicate test names were indistinguishable, and xUnit was the only runner that provided fully qualified test names) -3. See the support guidance for feature and language support -4. As mentioned in the **Debugging** section, there are some discrepancies in test output at the moment. - -## NUnit Limitations - -1. Using the `[Test]` attribute alongside `[TestCase]` attributes on the same method will cause `neotest-dotnet` to duplicate the item with erroneous nesting in the test structure. This will also break the ability of neotest to run the test cases e.g: - -```c_sharp - [Test] - [TestCase(1)] - [TestCase(2)] - public void Test_With_Parameters(int a) - { - Assert.AreEqual(2, a); - } -``` - -- The workaround is to instead, remove the redundant `[Test]` attribute. - # Contributing -Any help on this plugin would be very much appreciated. It has turned out to be a more significant effort to account for all the Microsoft `dotnet test` quirks -and various differences between each test runner, than I had initially imagined. +Any help on this plugin would be very much appreciated. -## First Steps +## First steps If you have a use case that the adapter isn't quite able to cover, a more detailed understanding of why can be achieved by following these steps: @@ -230,26 +102,23 @@ If you have a use case that the adapter isn't quite able to cover, a more detail 3. Look through the neotest log files for logs prefixed with `neotest-dotnet` (can be found by running the command `echo stdpath("log")`) 4. You should be able to piece together how the nodes in the neotest summary window are created (Using logs from tests that are "Found") -- The Tree for each test run is printed as a list (search for `Creating specs from tree`) from each test run -- The individual specs usually follow after in the log list, showing the command and context for each spec -- `TRX Results Output` can be searched to find out how neotest-dotnet is parsing the test output files -- Final results are tied back to the original list of discovered tests by using a set of conversion functions: -- `Test Nodes` are logged - these are taken from the original node tree list, and filtered to include only the test nodes and their children (if any) -- `Intermediate Results` are obtained and logged by parsing the TRX output into a list of test results -- The test nodes and intermediate results are passed to a function to correlate them with each other. If the test names in the nodes match the test names from the intermediate results, a final neotest-result for that test is returned and matched to the original test position from the very initial tree of nodes +The general flow for test discovery and execution is as follows: +1. Spawn VSTest instance at start-up. +2. On test discovery: Send list of files to VSTest instance. + - Once tests have been discovered the VSTest instance will write the discovered test cases to a file. +3. Read result file and parse tests. +4. Use treesitter to determine line ranges for test cases. +5. On test execution: Send list of test ids to VSTest instance. + - Once test results are in the VSTest instance will write the results to a file. +6. Read test result file and parse results. -Usually, if tests are not appearing in the `neotest` summary window, or are failing to be discovered by individual or grouped test runs, there will usually be an issue with one of the above steps. Carefully examining the names in the original node list and the names of the tests in each of the result lists, usually highighlights a mismatch. +## Running tests -5. Narrow down the function where you think the issue is. -6. Look through the unit tests (named by convention using ``) and check if there is a test case covering the use case for your situation -7. Write a test case that would enable your use case to be satisfied -8. See that the test fails -9. Try to fix the issue until the test passes +To run the tests from CLI, make sure that `luarocks` is installed and executable. +Then, Run `luarocks test` from the project root. -## Running Tests +If you see a module 'busted.runner' not found error you need to update your `LUA_PATH`: -To run the plenary tests from CLI, in the root folder, run - -``` -make test +```sh +eval $(luarocks path --no-bin) ``` diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 2043562..20ddbbf 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -73,7 +73,7 @@ local function build_position(source, captured_nodes, tests_in_file, path) end DotnetNeotestAdapter.discover_positions = function(path) - logger.info(string.format("scanning %s for tests...", path)) + logger.info(string.format("neotest-dotnet: scanning %s for tests...", path)) local filetype = (vim.endswith(path, ".fs") and "fsharp") or "c_sharp" @@ -139,7 +139,7 @@ DotnetNeotestAdapter.discover_positions = function(path) }) end - logger.info(string.format("done scanning %s for tests", path)) + logger.info(string.format("neotest-dotnet: done scanning %s for tests", path)) return tree end @@ -160,7 +160,7 @@ DotnetNeotestAdapter.build_spec = function(args) end end - logger.debug("ids:") + logger.debug("neotest-dotnet: ids:") logger.debug(ids) local results_path = nio.fn.tempname() @@ -188,7 +188,7 @@ DotnetNeotestAdapter.build_spec = function(args) local dap = require("dap") dap.listeners.after.configurationDone["neotest-dotnet"] = function() nio.run(function() - logger.debug("attached to debug test runner") + logger.debug("neotest-dotnet: attached to debug test runner") lib.files.write(attached_path, "1") end) end @@ -247,8 +247,12 @@ DotnetNeotestAdapter.results = function(spec) return parsed end +---@class neotest-dotnet.Config +---@field sdk_path? string path to dotnet sdk. Example: /usr/local/share/dotnet/sdk/9.0.101/ + setmetatable(DotnetNeotestAdapter, { - __call = function(_, _) + __call = function(_, opts) + vstest.sdk_path = opts.sdk_path return DotnetNeotestAdapter end, }) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index b2b5765..a81d7f1 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -22,21 +22,22 @@ local function get_vstest_path() if not process or errors then M.sdk_path = default_sdk_path - local log_string = string.format("failed to detect sdk path. falling back to %s", M.sdk_path) + local log_string = + string.format("neotest-dotnet: failed to detect sdk path. falling back to %s", M.sdk_path) - vim.notify_once("neotest-dotnet: " .. log_string) + vim.notify_once(log_string) logger.info(log_string) else local out = process.stdout.read() local match = out and out:match("Base Path:%s*(%S+[^\n]*)") if match then M.sdk_path = vim.trim(match) - logger.info(string.format("detected sdk path: %s", M.sdk_path)) + logger.info(string.format("neotest-dotnet: detected sdk path: %s", M.sdk_path)) else M.sdk_path = default_sdk_path local log_string = - string.format("failed to detect sdk path. falling back to %s", M.sdk_path) - vim.notify_once("neotest-dotnet: " .. log_string) + string.format("neotest-dotnet: failed to detect sdk path. falling back to %s", M.sdk_path) + vim.notify_once(log_string) logger.info(log_string) end process.close() @@ -48,7 +49,7 @@ end local function get_script(script_name) local script_paths = vim.api.nvim_get_runtime_file(vim.fs.joinpath("scripts", script_name), true) - logger.debug("possible scripts:") + logger.debug("neotest-dotnet: possible scripts:") logger.debug(script_paths) for _, path in ipairs(script_paths) do if path:match("neotest%-dotnet") ~= nil then @@ -95,33 +96,33 @@ local function invoke_test_runner(command) local test_discovery_script = get_script("run_tests.fsx") local testhost_dll = get_vstest_path() - logger.debug("found discovery script: " .. test_discovery_script) - logger.debug("found testhost dll: " .. testhost_dll) + logger.debug("neotest-dotnet: found discovery script: " .. test_discovery_script) + logger.debug("neotest-dotnet: found testhost dll: " .. testhost_dll) local vstest_command = { "dotnet", "fsi", test_discovery_script, testhost_dll } - logger.info("starting vstest console with:") + logger.info("neotest-dotnet: starting vstest console with:") logger.info(vstest_command) local process = vim.system(vstest_command, { stdin = true, stdout = function(err, data) if data then - logger.trace(data) + logger.trace("neotest-dotnet: " .. data) end if err then - logger.trace(err) + logger.trace("neotest-dotnet " .. err) end end, }, function(obj) - logger.warn("vstest process died :(") + logger.warn("neotest-dotnet: vstest process died :(") logger.warn(obj.code) logger.warn(obj.signal) logger.warn(obj.stdout) logger.warn(obj.stderr) end) - logger.info(string.format("spawned vstest process with pid: %s", process.pid)) + logger.info(string.format("neotest-dotnet: spawned vstest process with pid: %s", process.pid)) test_runner = function(content) process:write(content .. "\n") @@ -157,7 +158,7 @@ function M.spin_lock_wait_file(file_path, max_wait) end if not content then - logger.warn(string.format("timed out reading content of file %s", file_path)) + logger.warn(string.format("neotest-dotnet: timed out reading content of file %s", file_path)) end return content @@ -179,7 +180,7 @@ function M.discover_tests(path) local proj_info = M.get_proj_info(path) if not proj_info.proj_file then - logger.warn(string.format("failed to find project file for %s", path)) + logger.warn(string.format("neotest-dotnet: failed to find project file for %s", path)) return {} end @@ -198,14 +199,14 @@ function M.discover_tests(path) { "dotnet", "build", proj_info.proj_file }, { stdout = true, stderr = true } ) - logger.debug(string.format("dotnet build status code: %s", exitCode)) + logger.debug(string.format("neotest-dotnet: dotnet build status code: %s", exitCode)) logger.debug(stdout) end proj_info = M.get_proj_info(path) if not proj_info.dll_file then - logger.warn(string.format("failed to find project dll for %s", path)) + logger.warn(string.format("neotest-dotnet: failed to find project dll for %s", path)) return {} end @@ -221,7 +222,7 @@ function M.discover_tests(path) then logger.debug( string.format( - "cache hit for %s. %s - %s", + "neotest-dotnet: cache hit for %s. %s - %s", proj_info.proj_file, path_modified_time, last_discovery[proj_info.proj_file] @@ -231,7 +232,7 @@ function M.discover_tests(path) else logger.debug( string.format( - "cache miss for %s... path: %s cache: %s - %s", + "neotest-dotnet: cache miss for %s... path: %s cache: %s - %s", path, path_modified_time, proj_info.proj_file, @@ -247,7 +248,7 @@ function M.discover_tests(path) local root = lib.files.match_root_pattern("*.sln")(path) or lib.files.match_root_pattern("*.[cf]sproj")(path) - logger.debug(string.format("root: %s", root)) + logger.debug(string.format("neotest-dotnet: root: %s", root)) local projects = vim.fs.find(function(name, _) return name:match("%.[cf]sproj$") @@ -271,7 +272,7 @@ function M.discover_tests(path) and project_stats.mtime and project_stats.mtime.sec else - logger.warn(string.format("failed to find dll for %s", project)) + logger.warn(string.format("neotest-dotnet: failed to find dll for %s", project)) end end else @@ -286,7 +287,7 @@ function M.discover_tests(path) local wait_file = nio.fn.tempname() local output_file = nio.fn.tempname() - logger.debug("found dlls:") + logger.debug("neotest-dotnet: found dlls:") logger.debug(dlls) local command = vim @@ -299,12 +300,12 @@ function M.discover_tests(path) :flatten() :join(" ") - logger.debug("Discovering tests using:") + logger.debug("neotest-dotnet: Discovering tests using:") logger.debug(command) invoke_test_runner(command) - logger.debug("Waiting for result file to populated...") + logger.debug("neotest-dotnet: Waiting for result file to populated...") local max_wait = 60 * 1000 -- 60 sec @@ -312,11 +313,11 @@ function M.discover_tests(path) if done then local content = M.spin_lock_wait_file(output_file, max_wait) - logger.debug("file has been populated. Extracting test cases...") + logger.debug("neotest-dotnet: file has been populated. Extracting test cases...") json = (content and vim.json.decode(content, { luanil = { object = true } })) or {} - logger.debug("done decoding test cases.") + logger.debug("neotest-dotnet: done decoding test cases.") for file_path, test_map in pairs(json) do discovery_cache[file_path] = test_map @@ -373,12 +374,12 @@ function M.debug_tests(attached_path, stream_path, output_path, ids) }) :flatten() :join(" ") - logger.debug("starting test in debug mode using:") + logger.debug("neotest-dotnet: starting test in debug mode using:") logger.debug(command) invoke_test_runner(command) - logger.debug("Waiting for pid file to populate...") + logger.debug("neotest-dotnet: Waiting for pid file to populate...") local max_wait = 30 * 1000 -- 30 sec From 294d8946919461a2bada50ca72e0b465f51c9193 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 28 Dec 2024 12:28:13 +0100 Subject: [PATCH 37/43] feat: nesting of parameterized test cases --- .github/workflows/main.yml | 2 +- lua/neotest-dotnet/init.lua | 75 +++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71b767d..7c19987 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: - lint #- documentation runs-on: ubuntu-latest - timeout-minutes: 2 + timeout-minutes: 4 strategy: matrix: neovim_version: ["v0.10.0", "v0.10.1", "v0.10.2", "v0.10.3", "nightly"] diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 20ddbbf..ab81e70 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -1,5 +1,7 @@ local nio = require("nio") local lib = require("neotest.lib") +local utils = require("neotest.utils") +local types = require("neotest.types") local logger = require("neotest.logging") local vstest = require("neotest-dotnet.vstest_wrapper") @@ -31,6 +33,50 @@ local function get_match_type(captured_nodes) end end +local function build_structure(positions, namespaces, opts) + ---@type neotest.Position + local parent = table.remove(positions, 1) + if not parent then + return nil + end + parent.id = parent.type == "file" and parent.path or opts.position_id(parent, namespaces) + local current_level = { parent } + local child_namespaces = vim.list_extend({}, namespaces) + if + parent.type == "namespace" + or parent.type == "parameterized" + or (opts.nested_tests and parent.type == "test") + then + child_namespaces[#child_namespaces + 1] = parent + end + if not parent.range then + return current_level + end + while true do + local next_pos = positions[1] + if not next_pos or (next_pos.range and not lib.positions.contains(parent, next_pos)) then + -- Don't preserve empty namespaces + if #current_level == 1 and parent.type == "namespace" then + return nil + end + if opts.require_namespaces and parent.type == "test" and #namespaces == 0 then + return nil + end + return current_level + end + + if parent.type == "parameterized" then + local pos = table.remove(positions, 1) + current_level[#current_level + 1] = pos + else + local sub_tree = build_structure(positions, child_namespaces, opts) + if opts.nested_tests or parent.type ~= "test" then + current_level[#current_level + 1] = sub_tree + end + end + end +end + ---@param source string ---@param captured_nodes any ---@param tests_in_file table @@ -41,6 +87,7 @@ local function build_position(source, captured_nodes, tests_in_file, path) if match_type then local definition = captured_nodes[match_type .. ".definition"] + ---@type neotest.Position[] local positions = {} if match_type == "test" then @@ -68,6 +115,18 @@ local function build_position(source, captured_nodes, tests_in_file, path) range = { definition:range() }, }) end + + if #positions > 1 then + local pos = positions[1] + table.insert(positions, 1, { + type = "parameterized", + path = pos.path, + -- remove parameterized part of test name + name = pos.name:gsub("<.*>", ""):gsub("%(.*%)", ""), + range = pos.range, + }) + end + return positions end end @@ -120,8 +179,11 @@ DotnetNeotestAdapter.discover_positions = function(path) end end - tree = lib.positions.parse_tree(nodes, { - nested_tests = true, + logger.info("neotest-dotnet: sorted test cases:") + logger.info(nodes) + + local structure = assert(build_structure(nodes, {}, { + nested_tests = false, require_namespaces = false, position_id = function(position, parents) return position.id @@ -136,9 +198,16 @@ DotnetNeotestAdapter.discover_positions = function(path) :flatten() :join("::") end, - }) + })) + + tree = types.Tree.from_list(structure, function(pos) + return pos.id + end) end + logger.info("neotest-dotnet: test case tree:") + logger.info(tree) + logger.info(string.format("neotest-dotnet: done scanning %s for tests", path)) return tree From a43e3529112317b2149ecd90b14a7d308f632e31 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 28 Dec 2024 14:57:50 +0100 Subject: [PATCH 38/43] use custom neotest strategy --- README.md | 1 - lua/neotest-dotnet/init.lua | 57 ++++++++++++++---------- lua/neotest-dotnet/strategies/vstest.lua | 50 +++++++++++++++++++++ lua/neotest-dotnet/vstest_wrapper.lua | 40 +++++++++-------- scripts/run_tests.fsx | 49 +++++++++++++------- 5 files changed, 137 insertions(+), 60 deletions(-) create mode 100644 lua/neotest-dotnet/strategies/vstest.lua diff --git a/README.md b/README.md index 27ace59..5b65f90 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ neotest-dotnet requires makes a number of assumptions about your environment: 2. (For Debugging) `netcoredbg` is installed and `nvim-dap` plugin has been configured for `netcoredbg` (see debug config for more details) 3. Requires treesitter parser for either `C#` or `F#` 4. Requires `neovim v0.10.0` or later - > > > > > > > refs/rewritten/onto # Installation diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index ab81e70..4a77fdf 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -1,26 +1,26 @@ local nio = require("nio") local lib = require("neotest.lib") -local utils = require("neotest.utils") local types = require("neotest.types") local logger = require("neotest.logging") local vstest = require("neotest-dotnet.vstest_wrapper") +local vstest_strategy = require("neotest-dotnet.strategies.vstest") ---@package ---@type neotest.Adapter local DotnetNeotestAdapter = { name = "neotest-dotnet" } -DotnetNeotestAdapter.root = function(path) +function DotnetNeotestAdapter.root(path) return lib.files.match_root_pattern("*.sln")(path) or lib.files.match_root_pattern("*.[cf]sproj")(path) end -DotnetNeotestAdapter.is_test_file = function(file_path) +function DotnetNeotestAdapter.is_test_file(file_path) return (vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs")) and vstest.discover_tests(file_path) end -DotnetNeotestAdapter.filter_dir = function(name) +function DotnetNeotestAdapter.filter_dir(name) return name ~= "bin" and name ~= "obj" end @@ -131,7 +131,7 @@ local function build_position(source, captured_nodes, tests_in_file, path) end end -DotnetNeotestAdapter.discover_positions = function(path) +function DotnetNeotestAdapter.discover_positions(path) logger.info(string.format("neotest-dotnet: scanning %s for tests...", path)) local filetype = (vim.endswith(path, ".fs") and "fsharp") or "c_sharp" @@ -179,9 +179,6 @@ DotnetNeotestAdapter.discover_positions = function(path) end end - logger.info("neotest-dotnet: sorted test cases:") - logger.info(nodes) - local structure = assert(build_structure(nodes, {}, { nested_tests = false, require_namespaces = false, @@ -205,15 +202,12 @@ DotnetNeotestAdapter.discover_positions = function(path) end) end - logger.info("neotest-dotnet: test case tree:") - logger.info(tree) - logger.info(string.format("neotest-dotnet: done scanning %s for tests", path)) return tree end -DotnetNeotestAdapter.build_spec = function(args) +function DotnetNeotestAdapter.build_spec(args) local tree = args.tree if not tree then return @@ -243,7 +237,7 @@ DotnetNeotestAdapter.build_spec = function(args) local attached_path = nio.fn.tempname() local pid = vstest.debug_tests(attached_path, stream_path, results_path, ids) - --- @type Configuration + --- @type dap.Configuration strategy = { type = "netcoredbg", name = "netcoredbg - attach", @@ -252,7 +246,7 @@ DotnetNeotestAdapter.build_spec = function(args) env = { DOTNET_ENVIRONMENT = "Development", }, - processId = vim.trim(pid), + processId = pid and vim.trim(pid), before = function() local dap = require("dap") dap.listeners.after.configurationDone["neotest-dotnet"] = function() @@ -266,10 +260,11 @@ DotnetNeotestAdapter.build_spec = function(args) end return { - command = vstest.run_tests(args.strategy == "dap", stream_path, results_path, ids), context = { result_path = results_path, + stream_path = stream_path, stop_stream = stop_stream, + ids = ids, }, stream = function() return function() @@ -282,19 +277,31 @@ DotnetNeotestAdapter.build_spec = function(args) return results end end, - strategy = strategy, + strategy = strategy or vstest_strategy, } end -DotnetNeotestAdapter.results = function(spec) +function DotnetNeotestAdapter.results(spec, _result, _tree) local max_wait = 5 * 50 * 1000 -- 5 min + logger.info("neotest-dotnet: waiting for test results") local success, data = pcall(vstest.spin_lock_wait_file, spec.context.result_path, max_wait) spec.context.stop_stream() + logger.info("neotest-dotnet: parsing test results") + local results = {} if not success then + for _, id in ipairs(spec.context.ids) do + results[id] = { + status = "skipped", + output = spec.context.result_path, + errors = { + message = "failed to read result file", + }, + } + end return results end @@ -302,13 +309,15 @@ DotnetNeotestAdapter.results = function(spec) assert(parse_ok, "failed to parse result file") if not parse_ok then - local outcome = "skipped" - results[spec.context.id] = { - status = outcome, - errors = { - message = "failed to parse result file", - }, - } + for _, id in ipairs(spec.context.ids) do + results[id] = { + status = "skipped", + output = spec.context.result_path, + errors = { + message = "failed to parse result file", + }, + } + end return results end diff --git a/lua/neotest-dotnet/strategies/vstest.lua b/lua/neotest-dotnet/strategies/vstest.lua new file mode 100644 index 0000000..472bc92 --- /dev/null +++ b/lua/neotest-dotnet/strategies/vstest.lua @@ -0,0 +1,50 @@ +local nio = require("nio") +local lib = require("neotest.lib") +local vstest = require("neotest-dotnet.vstest_wrapper") + +---@async +---@param spec neotest.RunSpec +---@return neotest.Process +return function(spec) + local process_output = nio.fn.tempname() + lib.files.write(process_output, "") + + local wait_file = vstest.run_tests( + spec.context.stream_path, + spec.context.result_path, + process_output, + spec.context.ids + ) + + local result_future = nio.control.future() + + nio.run(function() + vstest.spin_lock_wait_file(wait_file, 5 * 30 * 1000) + result_future:set() + end) + + local stream_data, stop_stream = lib.files.stream_lines(process_output) + + return { + is_complete = function() + return result_future.is_set() + end, + output = function() + return process_output + end, + stop = function() + stop_stream() + end, + output_stream = function() + return function() + local lines = stream_data() + return table.concat(lines, "\n") + end + end, + attach = function() end, + result = function() + result_future:wait() + return 1 + end, + } +end diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index a81d7f1..fb326fc 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -149,7 +149,7 @@ function M.spin_lock_wait_file(file_path, max_wait) if lib.files.exists(file_path) then spin_lock.with(function() file_exists = true - content = require("neotest.lib").files.read(file_path) + content = lib.files.read(file_path) end) else tries = tries + 1 @@ -328,28 +328,27 @@ function M.discover_tests(path) end ---runs tests identified by ids. ----@param dap boolean true if normal test runner should be skipped ---@param stream_path string ---@param output_path string +---@param process_output_path string ---@param ids string|string[] ----@return string command -function M.run_tests(dap, stream_path, output_path, ids) - if not dap then - lib.process.run({ "dotnet", "build" }) - - local command = vim - .iter({ - "run-tests", - stream_path, - output_path, - ids, - }) - :flatten() - :join(" ") - invoke_test_runner(command) - end +---@return string wait_file +function M.run_tests(stream_path, output_path, process_output_path, ids) + lib.process.run({ "dotnet", "build" }) - return string.format("tail -n 1 -f %s", output_path, output_path) + local command = vim + .iter({ + "run-tests", + stream_path, + output_path, + process_output_path, + ids, + }) + :flatten() + :join(" ") + invoke_test_runner(command) + + return output_path end --- Uses the vstest console to spawn a test process for the debugger to attach to. @@ -361,6 +360,8 @@ end function M.debug_tests(attached_path, stream_path, output_path, ids) lib.process.run({ "dotnet", "build" }) + local process_output = nio.fn.tempname() + local pid_path = nio.fn.tempname() local command = vim @@ -370,6 +371,7 @@ function M.debug_tests(attached_path, stream_path, output_path, ids) attached_path, stream_path, output_path, + process_output, ids, }) :flatten() diff --git a/scripts/run_tests.fsx b/scripts/run_tests.fsx index 731a451..dc4c653 100644 --- a/scripts/run_tests.fsx +++ b/scripts/run_tests.fsx @@ -42,7 +42,8 @@ module TestDiscovery = {| StreamPath = args[0] OutputPath = args[1] - Ids = args[2..] |> Array.map Guid.Parse |} + ProcessOutput = args[2] + Ids = args[3..] |> Array.map Guid.Parse |} |> ValueOption.Some else ValueOption.None @@ -56,7 +57,8 @@ module TestDiscovery = AttachedPath = args[1] StreamPath = args[2] OutputPath = args[3] - Ids = args[4..] |> Array.map Guid.Parse |} + ProcessOutput = args[4] + Ids = args[5..] |> Array.map Guid.Parse |} |> ValueOption.Some else ValueOption.None @@ -99,8 +101,9 @@ module TestDiscovery = member __.HandleRawMessage(_) = () - type PlaygroundTestRunHandler(streamOutputPath, outputFilePath) = + type PlaygroundTestRunHandler(streamOutputPath, outputFilePath, processOutputPath) = let resultsDictionary = ConcurrentDictionary() + let processOutputWriter = new StreamWriter(processOutputPath, append = true) interface ITestRunEventsHandler with member _.HandleTestRunComplete @@ -109,7 +112,9 @@ module TestDiscovery = use outputWriter = new StreamWriter(outputFilePath, append = false) outputWriter.WriteLine(JsonConvert.SerializeObject(resultsDictionary)) - member __.HandleLogMessage(level, message) = logHandler level message + member __.HandleLogMessage(_level, message) = + if not <| String.IsNullOrWhiteSpace message then + processOutputWriter.WriteLine(message) member __.HandleRawMessage(_rawMessage) = () @@ -157,6 +162,9 @@ module TestDiscovery = member __.LaunchProcessWithDebuggerAttached(_testProcessStartInfo) = 1 + interface IDisposable with + member _.Dispose() = processOutputWriter.Dispose() + type DebugLauncher(pidFile: string, attachedFile: string) = interface ITestHostLauncher2 with member this.LaunchTestHost(defaultTestHostStartInfo: TestProcessStartInfo) = @@ -264,26 +272,35 @@ module TestDiscovery = } |> ignore | RunTests args -> - let testCases = getTestCases args.Ids + task { + let testCases = getTestCases args.Ids - let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath) - // spawn as task to allow running concurrent tests - r.RunTestsAsync(testCases, sourceSettings, testHandler) |> ignore - () - | DebugTests args -> - let testCases = getTestCases args.Ids + use testHandler = + new PlaygroundTestRunHandler(args.StreamPath, args.OutputPath, args.ProcessOutput) + // spawn as task to allow running concurrent tests + do! r.RunTestsAsync(testCases, sourceSettings, testHandler) + Console.WriteLine($"Done running tests for ids: ") - let testHandler = PlaygroundTestRunHandler(args.StreamPath, args.OutputPath) - let debugLauncher = DebugLauncher(args.PidPath, args.AttachedPath) - Console.WriteLine($"Starting {testCases.Length} tests in debug-mode") + for id in args.Ids do + Console.Write($"{id} ") + return () + } + |> ignore + | DebugTests args -> task { + let testCases = getTestCases args.Ids + + use testHandler = + new PlaygroundTestRunHandler(args.StreamPath, args.OutputPath, args.ProcessOutput) + + let debugLauncher = DebugLauncher(args.PidPath, args.AttachedPath) + Console.WriteLine($"Starting {testCases.Length} tests in debug-mode") + do! Task.Yield() r.RunTestsWithCustomTestHost(testCases, sourceSettings, testHandler, debugLauncher) } |> ignore - - () | _ -> loop <- false r.EndSession() From c2bad801d0d26a19770659a7fa1b0750be324cde Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 11 Jan 2025 13:40:10 +0100 Subject: [PATCH 39/43] feat: use msbuild to find dll path --- lua/neotest-dotnet/vstest_wrapper.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index fb326fc..4747503 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -66,18 +66,26 @@ function M.get_proj_info(path) return name:match("%.[cf]sproj$") end, { upward = true, type = "file", path = vim.fs.dirname(path) })[1] - local dir_name = vim.fs.dirname(proj_file) - local proj_name = vim.fn.fnamemodify(proj_file, ":t:r") + local _, res = lib.process.run({ + "dotnet", + "msbuild", + proj_file, + "-getProperty:OutputPath", + "-getProperty:AssemblyName", + "-getProperty:TargetExt", + }, { + stderr = false, + stdout = true, + }) + + local info = nio.fn.json_decode(res.stdout).Properties - local proj_dll_path = - -- TODO: this might break if the project has been compiled as both Development and Release. - vim.fs.find(function(name) - return string.lower(name) == string.lower(proj_name .. ".dll") - end, { type = "file", path = dir_name })[1] + local dir_name = vim.fs.dirname(proj_file) local proj_data = { proj_file = proj_file, - dll_file = proj_dll_path, + dll_file = vim.fs.joinpath(dir_name, info.OutputPath:gsub("\\", "/"), info.AssemblyName) + .. info.TargetExt, proj_dir = dir_name, } From 19f8ec95fadb648d1a3c0cbee647585a58d471ea Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 18 Jan 2025 10:10:01 +0100 Subject: [PATCH 40/43] update CI --- .github/workflows/main.yml | 45 +++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7c19987..ba4911e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,22 +33,41 @@ jobs: timeout-minutes: 4 strategy: matrix: - neovim_version: ["v0.10.0", "v0.10.1", "v0.10.2", "v0.10.3", "nightly"] + os: [ubuntu-latest, macos-latest, windows-latest] + neovim_version: ["v0.10.0"] + include: + - os: ubuntu-latest + neovim_version: "nightly" + steps: - - uses: actions/checkout@v3 - - run: date +%F > todays-date - - name: restore cache for today's nightly. - uses: actions/cache@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "9.0.x" + + - name: Install C/C++ Compiler + uses: rlalik/setup-cpp-compiler@master + with: + compiler: clang-latest + + - name: Install tree-sitter CLI + uses: baptiste0928/cargo-install@v3 with: - path: _neovim - key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }} - - name: setup neovim - uses: rhysd/action-setup-vim@v1 + crate: tree-sitter-cli + + - name: Run tests + id: test + uses: nvim-neorocks/nvim-busted-action@v1 with: - neovim: true - version: ${{ matrix.neovim_version }} - - name: run tests - run: make test-ci + nvim_version: ${{ matrix.neovim_version }} + + - name: Save neotest log + if: always() && steps.test.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: neotest-log-${{ matrix.neovim_version }}-${{ matrix.os }} + path: ~/.local/state/nvim/neotest.log + release: name: release if: ${{ github.ref == 'refs/heads/main' }} From ee0f925f1642d2f388a0c973eb15e8db18a42f99 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Sat, 18 Jan 2025 10:28:34 +0100 Subject: [PATCH 41/43] improve results reporting --- lua/neotest-dotnet/init.lua | 13 ++++++++----- scripts/run_tests.fsx | 29 ++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 4a77fdf..5781692 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -281,7 +281,7 @@ function DotnetNeotestAdapter.build_spec(args) } end -function DotnetNeotestAdapter.results(spec, _result, _tree) +function DotnetNeotestAdapter.results(spec, result, _tree) local max_wait = 5 * 50 * 1000 -- 5 min logger.info("neotest-dotnet: waiting for test results") local success, data = pcall(vstest.spin_lock_wait_file, spec.context.result_path, max_wait) @@ -290,15 +290,17 @@ function DotnetNeotestAdapter.results(spec, _result, _tree) logger.info("neotest-dotnet: parsing test results") + ---@type table local results = {} if not success then for _, id in ipairs(spec.context.ids) do results[id] = { - status = "skipped", + status = types.ResultStatus.skipped, output = spec.context.result_path, errors = { - message = "failed to read result file", + { message = result.output }, + { message = "failed to read result file" }, }, } end @@ -311,10 +313,11 @@ function DotnetNeotestAdapter.results(spec, _result, _tree) if not parse_ok then for _, id in ipairs(spec.context.ids) do results[id] = { - status = "skipped", + status = types.ResultStatus.skipped, output = spec.context.result_path, errors = { - message = "failed to parse result file", + { message = result.output }, + { message = "failed to parse result file" }, }, } end diff --git a/scripts/run_tests.fsx b/scripts/run_tests.fsx index dc4c653..6024ebc 100644 --- a/scripts/run_tests.fsx +++ b/scripts/run_tests.fsx @@ -18,6 +18,14 @@ open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging +type NeoTestResultError = { message: string } + +type NeotestResult = + { status: string + short: string + output: string + errors: NeoTestResultError array } + module TestDiscovery = let parseArgs (args: string) = args.Split(" ", StringSplitOptions.TrimEntries &&& StringSplitOptions.RemoveEmptyEntries) @@ -142,16 +150,23 @@ module TestDiscovery = let errors = match errorMessage with - | Some error -> [| {| message = error |} |] + | Some error -> [| { message = error } |] | None -> [||] - result.TestCase.Id, - {| status = outcome - short = $"{result.TestCase.DisplayName}:{outcome}" - errors = errors |}) + let id = result.TestCase.Id - for (id, result) in results do - resultsDictionary.AddOrUpdate(id, result, (fun _ _ -> result)) |> ignore + let neoTestResult = + { status = outcome + short = $"{result.TestCase.DisplayName}:{outcome}" + output = Path.GetTempPath() + Guid.NewGuid().ToString() + errors = errors } + + File.WriteAllText(neoTestResult.output, result.ToString()) + + resultsDictionary.AddOrUpdate(id, neoTestResult, (fun _ _ -> neoTestResult)) + |> ignore + + (id, neoTestResult)) use streamWriter = new StreamWriter(streamOutputPath, append = true) From d12537485ed6e94f051f62f79fd5495864dcb37b Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Mon, 20 Jan 2025 19:48:41 +0100 Subject: [PATCH 42/43] use $TargetPath over $OutputPath --- lua/neotest-dotnet/vstest_wrapper.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lua/neotest-dotnet/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua index 4747503..a8fbfc6 100644 --- a/lua/neotest-dotnet/vstest_wrapper.lua +++ b/lua/neotest-dotnet/vstest_wrapper.lua @@ -70,9 +70,8 @@ function M.get_proj_info(path) "dotnet", "msbuild", proj_file, - "-getProperty:OutputPath", - "-getProperty:AssemblyName", - "-getProperty:TargetExt", + "-getProperty:TargetPath", + "-getProperty:MSBuildProjectDirectory", }, { stderr = false, stdout = true, @@ -80,13 +79,10 @@ function M.get_proj_info(path) local info = nio.fn.json_decode(res.stdout).Properties - local dir_name = vim.fs.dirname(proj_file) - local proj_data = { proj_file = proj_file, - dll_file = vim.fs.joinpath(dir_name, info.OutputPath:gsub("\\", "/"), info.AssemblyName) - .. info.TargetExt, - proj_dir = dir_name, + dll_file = info.TargetPath, + proj_dir = info.MSBuildProjectDirectory, } return proj_data From e416caec4d07abd540c84506baedfb8399803a18 Mon Sep 17 00:00:00 2001 From: nsidorenco Date: Mon, 20 Jan 2025 23:16:00 +0100 Subject: [PATCH 43/43] fix nightly CI --- lua/neotest-dotnet/init.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua index 5781692..827d2c7 100644 --- a/lua/neotest-dotnet/init.lua +++ b/lua/neotest-dotnet/init.lua @@ -142,16 +142,15 @@ function DotnetNeotestAdapter.discover_positions(path) if tests_in_file then local content = lib.files.read(path) - local lang = vim.treesitter.language.get_lang(filetype) or filetype nio.scheduler() tests_in_file = vim.fn.deepcopy(tests_in_file) local lang_tree = - vim.treesitter.get_string_parser(content, lang, { injections = { [lang] = "" } }) + vim.treesitter.get_string_parser(content, filetype, { injections = { [filetype] = "" } }) local root = lib.treesitter.fast_parse(lang_tree):root() local query = lib.treesitter.normalise_query( - lang, + filetype, filetype == "fsharp" and require("neotest-dotnet.queries.fsharp") or require("neotest-dotnet.queries.c_sharp") )