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 cca7603..ba4911e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -25,41 +25,49 @@ jobs:
# - 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:
- 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']
+ 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:
- path: _neovim
- key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }}
- - name: setup neovim
- uses: rhysd/action-setup-vim@v1
+ compiler: clang-latest
+
+ - name: Install tree-sitter CLI
+ uses: baptiste0928/cargo-install@v3
+ with:
+ 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' }}
diff --git a/.gitignore b/.gitignore
index ea910b6..3e54649 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,9 @@ luac.out
# Test dependencies
deps/
**/obj/*
+/luarocks
+/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/README.md b/README.md
index 6a1cc1b..5b65f90 100644
--- a/README.md
+++ b/README.md
@@ -10,25 +10,21 @@
-
# Neotest .NET
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)
-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#.
-5. Requires `neovim v0.10.0` or later
+1. The `dotnet sdk` that is compatible with the current project is installed and the `dotnet` executable is on the users runtime path.
+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
# Installation
@@ -68,63 +64,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)
@@ -141,95 +89,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
-
-- 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.
-
-## 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:
@@ -238,26 +106,24 @@ 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:
-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.
+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.
-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
+## Running tests
-## Running Tests
+To run the tests from CLI, make sure that `luarocks` is installed and executable.
+Then, Run `luarocks test` from the project root.
-To run the plenary tests from CLI, in the root folder, run
+If you see a module 'busted.runner' not found error you need to update your `LUA_PATH`:
-```
-make test
+```sh
+eval $(luarocks path --no-bin)
```
diff --git a/lua/neotest-dotnet/framework-discovery.lua b/lua/neotest-dotnet/framework-discovery.lua
deleted file mode 100644
index 2e639f0..0000000
--- a/lua/neotest-dotnet/framework-discovery.lua
+++ /dev/null
@@ -1,159 +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.fn.has("nvim-0.11") == 1
- and vim
- .iter({
- M.xunit_test_attributes,
- M.nunit_test_attributes,
- M.mstest_test_attributes,
- M.specflow_test_attributes,
- })
- :flatten()
- :totable()
- or 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.fn.has("nvim-0.11") == 1
- and vim.iter({ attribute_match_list, custom_attribute_args[framework] }):flatten():totable()
- or vim.tbl_flatten({ attribute_match_list, custom_attribute_args[framework] })
- end
-
- return M.join_test_attributes(attribute_match_list)
-end
-
-function M.join_test_attributes(attributes)
- local joined_attributes = attributes
- and table.concat(
- vim.tbl_map(function(attribute)
- return '"' .. attribute .. '"'
- end, attributes),
- " "
- )
- or ""
- return joined_attributes
-end
-
-function M.get_test_framework_utils_from_source(source, custom_attribute_args)
- local xunit_attributes = M.attribute_match_list(custom_attribute_args, "xunit")
- local mstest_attributes = M.attribute_match_list(custom_attribute_args, "mstest")
- local nunit_attributes = M.attribute_match_list(custom_attribute_args, "nunit")
-
- local framework_query = [[
- (attribute
- name: (identifier) @attribute_name (#any-of? @attribute_name ]] .. xunit_attributes .. " " .. nunit_attributes .. " " .. mstest_attributes .. [[)
- )
-
- (attribute
- name: (qualified_name) @attribute_name (#match? @attribute_name "SkippableFactAttribute$")
- )
-
- (attribute
- name: (qualified_name) @attribute_name (#match? @attribute_name "TestMethodAttribute$")
- )
-
- (attribute
- name: (qualified_name) @attribute_name (#match? @attribute_name "TestAttribute$")
- )
- ]]
-
- async.scheduler()
- local root = vim.treesitter.get_string_parser(source, "c_sharp"):parse()[1]:root()
- local parsed_query = vim.fn.has("nvim-0.9.0") == 1
- and vim.treesitter.query.parse("c_sharp", framework_query)
- or vim.treesitter.parse_query("c_sharp", framework_query)
- for _, captures, _ in parsed_query:iter_matches(root, source, nil, nil, { all = false }) do
- local test_attribute = vim.fn.has("nvim-0.9.0") == 1
- and vim.treesitter.get_node_text(captures[1], source)
- or vim.treesitter.query.get_node_text(captures[1], source)
- if test_attribute then
- if
- string.find(xunit_attributes, test_attribute)
- or string.find(test_attribute, "SkippableFactAttribute")
- then
- return xunit
- elseif
- string.find(nunit_attributes, test_attribute)
- or string.find(test_attribute, "TestAttribute")
- then
- return nunit
- elseif
- string.find(mstest_attributes, test_attribute)
- or string.find(test_attribute, "TestMethodAttribute")
- then
- return mstest
- else
- -- Default fallback
- return xunit
- end
- end
- end
-end
-
-function M.get_test_framework_utils_from_tree(tree)
- for _, node in tree:iter_nodes() do
- local framework = node:data().framework
- if framework == "xunit" then
- return xunit
- elseif framework == "nunit" then
- return nunit
- elseif framework == "mstest" then
- return mstest
- end
- end
-
- -- Default fallback (no test nodes anyway)
- return xunit
-end
-
-return M
diff --git a/lua/neotest-dotnet/init.lua b/lua/neotest-dotnet/init.lua
index 8ff806b..827d2c7 100644
--- a/lua/neotest-dotnet/init.lua
+++ b/lua/neotest-dotnet/init.lua
@@ -1,236 +1,338 @@
+local nio = require("nio")
local lib = require("neotest.lib")
+local types = require("neotest.types")
local logger = require("neotest.logging")
-local FrameworkDiscovery = require("neotest-dotnet.framework-discovery")
-local build_spec_utils = require("neotest-dotnet.utils.build-spec-utils")
+local vstest = require("neotest-dotnet.vstest_wrapper")
+local vstest_strategy = require("neotest-dotnet.strategies.vstest")
+
+---@package
+---@type neotest.Adapter
local DotnetNeotestAdapter = { name = "neotest-dotnet" }
-local dap = { adapter_name = "netcoredbg" }
-local custom_attribute_args
-local dotnet_additional_args
-local discovery_root = "project"
-
-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)
- end
+
+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)
- if vim.endswith(file_path, ".cs") or vim.endswith(file_path, ".fs") then
- local content = lib.files.read(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
- local found_derived_attribute
- local found_standard_test_attribute
+function DotnetNeotestAdapter.filter_dir(name)
+ return name ~= "bin" and name ~= "obj"
+end
- -- Combine all attribute list arrays into one
- local all_attributes = FrameworkDiscovery.all_test_attributes
+local function get_match_type(captured_nodes)
+ if captured_nodes["test.name"] then
+ return "test"
+ end
+ if captured_nodes["namespace.name"] then
+ return "namespace"
+ end
+end
- for _, test_attribute in ipairs(all_attributes) do
- if string.find(content, "%[" .. test_attribute) then
- found_standard_test_attribute = true
- break
+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 custom_attribute_args then
- for _, framework_attrs in pairs(custom_attribute_args) do
- for _, value in ipairs(framework_attrs) do
- if string.find(content, "%[" .. value) then
- found_derived_attribute = true
- break
- end
- 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
-
- return found_standard_test_attribute or found_derived_attribute
- else
- return false
end
end
-DotnetNeotestAdapter.filter_dir = function(name)
- return name ~= "bin" and name ~= "obj"
-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)
+ if match_type then
+ local definition = captured_nodes[match_type .. ".definition"]
+
+ ---@type neotest.Position[]
+ local positions = {}
+
+ if match_type == "test" then
+ 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 = id,
+ type = match_type,
+ path = path,
+ name = test.DisplayName,
+ qualified_name = test.FullyQualifiedName,
+ range = { definition:range() },
+ })
+ tests_in_file[id] = nil
+ 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
-DotnetNeotestAdapter._build_position = function(...)
- local args = { ... }
+ 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
- logger.debug("neotest-dotnet: Buil Position Args: ")
- logger.debug(args)
+ return positions
+ end
+end
- local framework =
- FrameworkDiscovery.get_test_framework_utils_from_source(args[2], custom_attribute_args) -- args[2] is the content of the file
+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"
+
+ local tests_in_file = vstest.discover_tests(path)
+
+ local tree
+
+ if tests_in_file then
+ local content = lib.files.read(path)
+ nio.scheduler()
+ tests_in_file = vim.fn.deepcopy(tests_in_file)
+ local lang_tree =
+ vim.treesitter.get_string_parser(content, filetype, { injections = { [filetype] = "" } })
+
+ local root = lib.treesitter.fast_parse(lang_tree):root()
+
+ local query = lib.treesitter.normalise_query(
+ filetype,
+ 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 })
+ 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, tests_in_file, path)
+ if res then
+ for _, pos in ipairs(res) do
+ nodes[#nodes + 1] = pos
+ end
+ end
+ end
- logger.debug("neotest-dotnet: Framework: ")
- logger.debug(framework)
+ local structure = assert(build_structure(nodes, {}, {
+ nested_tests = false,
+ 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,
+ }))
+
+ tree = types.Tree.from_list(structure, function(pos)
+ return pos.id
+ end)
+ end
- return framework.build_position(...)
-end
+ logger.info(string.format("neotest-dotnet: done scanning %s for tests", path))
-DotnetNeotestAdapter._position_id = function(...)
- local args = { ... }
- local framework = args[1].framework and require("neotest-dotnet." .. args[1].framework)
- or require("neotest-dotnet.xunit")
- return framework.position_id(...)
+ return tree
end
----@param path any The path to the file to discover positions in
----@return neotest.Tree
-DotnetNeotestAdapter.discover_positions = function(path)
- local content = lib.files.read(path)
- local test_framework =
- FrameworkDiscovery.get_test_framework_utils_from_source(content, custom_attribute_args)
- local framework_queries = test_framework.get_treesitter_queries(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 tree = lib.treesitter.parse_positions(path, query, {
- nested_namespaces = true,
- nested_tests = true,
- build_position = "require('neotest-dotnet')._build_position",
- position_id = "require('neotest-dotnet')._position_id",
- })
-
- logger.debug("neotest-dotnet: Original Position Tree: ")
- logger.debug(tree:to_list())
-
- local modified_tree = test_framework.post_process_tree_list(tree, path)
-
- logger.debug("neotest-dotnet: Post-processed Position Tree: ")
- logger.debug(modified_tree:to_list())
-
- return modified_tree
-end
+function DotnetNeotestAdapter.build_spec(args)
+ local tree = args.tree
+ if not tree then
+ return
+ end
+
+ local pos = args.tree:data()
+
+ local ids = {}
----@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())
+ for _, position in tree:iter() do
+ if position.type == "test" then
+ ids[#ids + 1] = position.id
+ end
+ end
- local additional_args = args.dotnet_additional_args or dotnet_additional_args or nil
+ logger.debug("neotest-dotnet: ids:")
+ logger.debug(ids)
- local specs = build_spec_utils.create_specs(args.tree, nil, additional_args)
+ local results_path = nio.fn.tempname()
+ local stream_path = nio.fn.tempname()
+ lib.files.write(stream_path, "")
- logger.debug("neotest-dotnet: Created " .. #specs .. " specs, with contents: ")
- logger.debug(specs)
+ local stream_data, stop_stream = lib.files.stream_lines(stream_path)
+ local strategy
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 attached_path = nio.fn.tempname()
+
+ local pid = vstest.debug_tests(attached_path, stream_path, results_path, ids)
+ --- @type dap.Configuration
+ strategy = {
+ type = "netcoredbg",
+ name = "netcoredbg - attach",
+ request = "attach",
+ cwd = vstest.get_proj_info(pos.path).proj_dir,
+ env = {
+ DOTNET_ENVIRONMENT = "Development",
+ },
+ processId = pid and vim.trim(pid),
+ before = function()
+ local dap = require("dap")
+ dap.listeners.after.configurationDone["neotest-dotnet"] = function()
+ nio.run(function()
+ logger.debug("neotest-dotnet: attached to debug test runner")
+ lib.files.write(attached_path, "1")
+ end)
+ end
+ end,
+ }
end
- return specs
+ return {
+ context = {
+ result_path = results_path,
+ stream_path = stream_path,
+ stop_stream = stop_stream,
+ ids = ids,
+ },
+ 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,
+ strategy = strategy or vstest_strategy,
+ }
end
----@async
----@param spec neotest.RunSpec
----@param _ neotest.StrategyResult
----@param tree neotest.Tree
----@return neotest.Result[]
-DotnetNeotestAdapter.results = function(spec, _, tree)
- local output_file = spec.context.results_path
+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)
- logger.debug("neotest-dotnet: Fetching results from neotest tree (as list): ")
- logger.debug(tree:to_list())
+ spec.context.stop_stream()
- local test_framework = FrameworkDiscovery.get_test_framework_utils_from_tree(tree)
- local results = test_framework.generate_test_results(output_file, tree, spec.context.id)
+ logger.info("neotest-dotnet: parsing test results")
- return results
-end
+ ---@type table
+ local results = {}
-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
+ if not success then
+ for _, id in ipairs(spec.context.ids) do
+ results[id] = {
+ status = types.ResultStatus.skipped,
+ output = spec.context.result_path,
+ errors = {
+ { message = result.output },
+ { message = "failed to read result file" },
+ },
+ }
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 results
+ end
- return files
+ local parse_ok, parsed = pcall(vim.json.decode, data)
+ assert(parse_ok, "failed to parse result file")
+
+ if not parse_ok then
+ for _, id in ipairs(spec.context.ids) do
+ results[id] = {
+ status = types.ResultStatus.skipped,
+ output = spec.context.result_path,
+ errors = {
+ { message = result.output },
+ { message = "failed to parse result file" },
+ },
+ }
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
+ return results
+ 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
+ return parsed
+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, {})
+---@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(_, opts)
+ vstest.sdk_path = opts.sdk_path
return DotnetNeotestAdapter
end,
})
diff --git a/lua/neotest-dotnet/mstest/init.lua b/lua/neotest-dotnet/mstest/init.lua
deleted file mode 100644
index 4915092..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(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 4c81108..0000000
--- a/lua/neotest-dotnet/nunit/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(NUnit Utils): Building parameterized test positions from source")
- logger.debug("neotest-dotnet(NUnit Utils): Base node: ")
- logger.debug(base_node)
-
- logger.debug("neotest-dotnet(NUnit Utils): Match Type: " .. match_type)
-
- local query = [[
- ;;query
- (attribute_list
- (attribute
- name: (identifier) @attribute_name (#any-of? @attribute_name "TestCase")
- ((attribute_argument_list) @arguments)
- )
- )
- ]]
-
- local param_query = vim.fn.has("nvim-0.9.0") == 1 and vim.treesitter.query.parse("c_sharp", query)
- or vim.treesitter.parse_query("c_sharp", query)
-
- -- Set type to test (otherwise it will be test.parameterized)
- local parameterized_test_node = vim.tbl_extend("force", base_node, { type = "test" })
- local nodes = { parameterized_test_node }
-
- -- Test method has parameters, so we need to create a sub-position for each test case
- local capture_indices = {}
- for i, capture in ipairs(param_query.captures) do
- capture_indices[capture] = i
- end
- local arguments_index = capture_indices["arguments"]
-
- for _, match in param_query:iter_matches(captured_nodes[match_type .. ".definition"], source) do
- local args_node = match[arguments_index]
- local args_text = vim.treesitter.get_node_text(args_node, source):gsub("[()]", "")
-
- nodes[#nodes + 1] = vim.tbl_extend("force", parameterized_test_node, {
- name = parameterized_test_node.name .. "(" .. args_text .. ")",
- range = { args_node:range() },
- })
- end
-
- logger.debug("neotest-dotnet(NUnit Utils): Built parameterized test positions: ")
- logger.debug(nodes)
-
- return nodes
-end
-
-local get_match_type = function(captured_nodes)
- if captured_nodes["test.name"] then
- return "test"
- end
- if captured_nodes["namespace.name"] then
- return "namespace"
- end
- if captured_nodes["class.name"] then
- return "class"
- end
- if captured_nodes["test.parameterized.name"] then
- return "test.parameterized"
- end
-end
-
-function M.get_treesitter_queries(custom_attribute_args)
- return require("neotest-dotnet.nunit.ts-queries").get_queries(custom_attribute_args)
-end
-
-M.build_position = function(file_path, source, captured_nodes)
- local match_type = get_match_type(captured_nodes)
-
- local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source)
- local definition = captured_nodes[match_type .. ".definition"]
-
- -- Introduce the C# concept of a "class" to the node, so we can distinguish between a class and a namespace.
- -- Helps to determine if classes are nested, and therefore, if we need to modify the ID of the node (nested classes denoted by a '+' in C# test naming convention)
- local is_class = match_type == "class"
-
- -- Swap the match type back to "namespace" so neotest core can handle it properly
- if match_type == "class" then
- match_type = "namespace"
- end
-
- local node = {
- type = match_type,
- framework = "nunit",
- is_class = is_class,
- display_name = nil,
- path = file_path,
- name = name,
- range = { definition:range() },
- }
-
- if match_type and match_type ~= "test.parameterized" then
- return node
- end
-
- return build_parameterized_test_positions(node, source, captured_nodes, match_type)
-end
-
-M.position_id = function(position, parents)
- local original_id = position.path
- local has_parent_class = false
- local sep = "::"
-
- -- Build the original ID from the parents, changing the separator to "+" if any nodes are nested classes
- for _, node in ipairs(parents) do
- if has_parent_class and node.is_class then
- sep = "+"
- end
-
- if node.is_class then
- has_parent_class = true
- end
-
- original_id = original_id .. sep .. node.name
- end
-
- -- Add the final leaf nodes name to the ID, again changing the separator to "+" if it is a nested class
- sep = "::"
- if has_parent_class and position.is_class then
- sep = "+"
- end
- original_id = original_id .. sep .. position.name
-
- -- Check to see if the position is a test case and contains parentheses (meaning it is parameterized)
- -- If it is, remove the duplicated parent test name from the ID, so that when reading the trx test name
- -- it will be the same as the test name in the test explorer
- -- Example:
- -- When ID is "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName::ParentTestName(TestName)"
- -- Then we need it to be converted to "/path/to/test_file.cs::TestNamespace::TestClassName::ParentTestName(TestName)"
- if position.type == "test" and position.name:find("%(") then
- local id_segments = {}
- for _, segment in ipairs(vim.split(original_id, "::")) do
- table.insert(id_segments, segment)
- end
-
- table.remove(id_segments, #id_segments - 1)
- return table.concat(id_segments, "::")
- end
-
- return original_id
-end
-
----Modifies the tree using supplementary information from dotnet test -t or other methods
----@param tree neotest.Tree The tree to modify
----@param path string The path to the file the tree was built from
-M.post_process_tree_list = function(tree, path)
- 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 9edcea2..0000000
--- a/lua/neotest-dotnet/nunit/ts-queries.lua
+++ /dev/null
@@ -1,72 +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_test_attributes = custom_attributes
- and framework_discovery.join_test_attributes(custom_attributes.nunit)
- or ""
-
- 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
-
-return M
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
+]]
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/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/types/neotest-dotnet-types.lua b/lua/neotest-dotnet/types/neotest-dotnet-types.lua
deleted file mode 100644
index 2e4c5a0..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(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 b272dd0..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, "*.csproj").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("*.csproj")(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 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 39e6991..0000000
--- a/lua/neotest-dotnet/utils/neotest-node-tree-utils.lua
+++ /dev/null
@@ -1,38 +0,0 @@
-local M = {}
-
---- 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, ".cs::")
- 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
- node:data().full_name = node:data().name
- else
- local full_name = M.get_qualified_test_name_from_id(node:data().id)
- node:data().full_name = full_name
- end
- end
-
- return test_nodes
-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/vstest_wrapper.lua b/lua/neotest-dotnet/vstest_wrapper.lua
new file mode 100644
index 0000000..a8fbfc6
--- /dev/null
+++ b/lua/neotest-dotnet/vstest_wrapper.lua
@@ -0,0 +1,395 @@
+local nio = require("nio")
+local lib = require("neotest.lib")
+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" },
+ })
+
+ 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("neotest-dotnet: failed to detect sdk path. falling back to %s", M.sdk_path)
+
+ 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("neotest-dotnet: detected sdk path: %s", M.sdk_path))
+ else
+ M.sdk_path = default_sdk_path
+ local 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()
+ 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(vim.fs.joinpath("scripts", script_name), true)
+ logger.debug("neotest-dotnet: possible scripts:")
+ logger.debug(script_paths)
+ for _, path in ipairs(script_paths) do
+ if path:match("neotest%-dotnet") ~= nil then
+ return path
+ end
+ end
+end
+
+---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)
+ local proj_file = vim.fs.find(function(name, _)
+ return name:match("%.[cf]sproj$")
+ end, { upward = true, type = "file", path = vim.fs.dirname(path) })[1]
+
+ local _, res = lib.process.run({
+ "dotnet",
+ "msbuild",
+ proj_file,
+ "-getProperty:TargetPath",
+ "-getProperty:MSBuildProjectDirectory",
+ }, {
+ stderr = false,
+ stdout = true,
+ })
+
+ local info = nio.fn.json_decode(res.stdout).Properties
+
+ local proj_data = {
+ proj_file = proj_file,
+ dll_file = info.TargetPath,
+ proj_dir = info.MSBuildProjectDirectory,
+ }
+
+ return proj_data
+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 = get_vstest_path()
+
+ 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("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("neotest-dotnet: " .. data)
+ end
+ if err then
+ logger.trace("neotest-dotnet " .. err)
+ end
+ end,
+ }, function(obj)
+ 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("neotest-dotnet: spawned vstest process with pid: %s", process.pid))
+
+ test_runner = function(content)
+ process:write(content .. "\n")
+ end
+ end)
+
+ return test_runner(command)
+end
+
+local spin_lock = nio.control.semaphore(1)
+
+---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 milliseconds.
+---@return string?
+function M.spin_lock_wait_file(file_path, max_wait)
+ local content
+
+ 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 lib.files.exists(file_path) then
+ spin_lock.with(function()
+ file_exists = true
+ content = lib.files.read(file_path)
+ end)
+ else
+ tries = tries + 1
+ nio.sleep(sleep_time)
+ end
+ end
+
+ if not content then
+ logger.warn(string.format("neotest-dotnet: timed out reading content of file %s", file_path))
+ end
+
+ return content
+end
+
+local discovery_cache = {}
+local last_discovery = {}
+
+---@class TestCase
+---@field CodeFilePath string
+---@field DisplayName string
+---@field FullyQualifiedName string
+---@field LineNumber integer
+
+---@param path string
+---@return table | nil test_cases map from id -> test case
+function M.discover_tests(path)
+ local json
+ local proj_info = M.get_proj_info(path)
+
+ if not proj_info.proj_file then
+ logger.warn(string.format("neotest-dotnet: failed to find project file for %s", path))
+ return {}
+ 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("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("neotest-dotnet: 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)
+
+ local path_modified_time = dll_stats and dll_stats.mtime and dll_stats.mtime.sec
+
+ if
+ last_discovery[proj_info.proj_file]
+ and path_modified_time
+ and path_modified_time <= last_discovery[proj_info.proj_file]
+ then
+ logger.debug(
+ string.format(
+ "neotest-dotnet: 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(
+ "neotest-dotnet: 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("neotest-dotnet: 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("neotest-dotnet: 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("neotest-dotnet: found dlls:")
+ logger.debug(dlls)
+
+ local command = vim
+ .iter({
+ "discover",
+ output_file,
+ wait_file,
+ dlls,
+ })
+ :flatten()
+ :join(" ")
+
+ logger.debug("neotest-dotnet: Discovering tests using:")
+ logger.debug(command)
+
+ invoke_test_runner(command)
+
+ logger.debug("neotest-dotnet: Waiting for result file to populated...")
+
+ local max_wait = 60 * 1000 -- 60 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)
+
+ logger.debug("neotest-dotnet: file has been populated. Extracting test cases...")
+
+ json = (content and vim.json.decode(content, { luanil = { object = true } })) or {}
+
+ logger.debug("neotest-dotnet: 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]
+end
+
+---runs tests identified by ids.
+---@param stream_path string
+---@param output_path string
+---@param process_output_path string
+---@param ids string|string[]
+---@return string wait_file
+function M.run_tests(stream_path, output_path, process_output_path, ids)
+ lib.process.run({ "dotnet", "build" })
+
+ 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.
+---@param attached_path string
+---@param stream_path string
+---@param output_path string
+---@param ids string|string[]
+---@return string? pid
+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
+ .iter({
+ "debug-tests",
+ pid_path,
+ attached_path,
+ stream_path,
+ output_path,
+ process_output,
+ ids,
+ })
+ :flatten()
+ :join(" ")
+ logger.debug("neotest-dotnet: starting test in debug mode using:")
+ logger.debug(command)
+
+ invoke_test_runner(command)
+
+ logger.debug("neotest-dotnet: 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/lua/neotest-dotnet/xunit/init.lua b/lua/neotest-dotnet/xunit/init.lua
deleted file mode 100644
index 17466ec..0000000
--- a/lua/neotest-dotnet/xunit/init.lua
+++ /dev/null
@@ -1,329 +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(custom_attribute_args)
- return require("neotest-dotnet.xunit.ts-queries").get_queries(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("*.csproj")(path)
- local test_list_job = DotnetUtils.get_test_full_names(proj_root)
- local dotnet_tests = test_list_job.result().output
- local tree_as_list = tree:to_list()
-
- local short_name_pat = "%.([%w_]+)$"
- local short_name_only_pat = "%.([%w_]+)%(.*%)$"
- local short_name_with_param_pat = "%.([%w_]+%(.*%))$"
-
- 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 short_name = string.match(matched_name, short_name_with_param_pat)
- local sub_node = {
- id = sub_id,
- is_class = false,
- name = short_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
-
- local short_name = string.match(matched_tests[1], short_name_only_pat)
- node_tree[1] = vim.tbl_extend("force", node, {
- name = short_name,
- 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])
- local short_name = string.match(matched_tests[1], short_name_pat)
- node_tree[1] = vim.tbl_extend(
- "force",
- node,
- { name = short_name, 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 7c2acb9..0000000
--- a/lua/neotest-dotnet/xunit/ts-queries.lua
+++ /dev/null
@@ -1,79 +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.xunit)
- or ""
-
- 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
-
-return M
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/scripts/run_tests.fsx b/scripts/run_tests.fsx
new file mode 100644
index 0000000..6024ebc
--- /dev/null
+++ b/scripts/run_tests.fsx
@@ -0,0 +1,327 @@
+#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 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
+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)
+ |> Array.tail
+
+ []
+ let (|DiscoveryRequest|_|) (str: string) =
+ if str.StartsWith("discover") then
+ let args = parseArgs str
+
+ {| OutputPath = args[0]
+ WaitFile = args[1]
+ Sources = args[2..] |}
+ |> ValueOption.Some
+ else
+ ValueOption.None
+
+ []
+ let (|RunTests|_|) (str: string) =
+ if str.StartsWith("run-tests") then
+ let args = parseArgs str
+
+ {| StreamPath = args[0]
+ OutputPath = args[1]
+ ProcessOutput = args[2]
+ Ids = args[3..] |> Array.map Guid.Parse |}
+ |> ValueOption.Some
+ else
+ ValueOption.None
+
+ []
+ let (|DebugTests|_|) (str: string) =
+ if str.StartsWith("debug-tests") then
+ let args = parseArgs str
+
+ {| PidPath = args[0]
+ AttachedPath = args[1]
+ StreamPath = args[2]
+ OutputPath = args[3]
+ ProcessOutput = args[4]
+ Ids = args[5..] |> Array.map Guid.Parse |}
+ |> ValueOption.Some
+ else
+ ValueOption.None
+
+ let logHandler (level: TestMessageLevel) (message: string) =
+ if not <| String.IsNullOrWhiteSpace message then
+ if level = TestMessageLevel.Error then
+ Console.Error.WriteLine(message)
+ else
+ Console.WriteLine(message)
+
+ type TestCaseDto =
+ { CodeFilePath: string
+ DisplayName: string
+ LineNumber: int
+ FullyQualifiedName: string }
+
+ 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) =
+ discoveredTestCases
+ |> Seq.groupBy _.CodeFilePath
+ |> Seq.iter (fun (file, testCases) ->
+ discoveredTests.AddOrUpdate(file, testCases, (fun _ _ -> testCases)) |> ignore)
+
+ member _.HandleDiscoveryComplete(_, _) = ()
+
+ member __.HandleLogMessage(level, message) = logHandler level message
+
+ member __.HandleRawMessage(_) = ()
+
+ type PlaygroundTestRunHandler(streamOutputPath, outputFilePath, processOutputPath) =
+ let resultsDictionary = ConcurrentDictionary()
+ let processOutputWriter = new StreamWriter(processOutputPath, append = true)
+
+ 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) =
+ if not <| String.IsNullOrWhiteSpace message then
+ processOutputWriter.WriteLine(message)
+
+ member __.HandleRawMessage(_rawMessage) = ()
+
+ member __.HandleTestRunStatsChange(testRunChangedArgs: TestRunChangedEventArgs) : unit =
+ let toNeoTestStatus (outcome: TestOutcome) =
+ match outcome with
+ | TestOutcome.Passed -> "passed"
+ | TestOutcome.Failed -> "failed"
+ | _ -> "skipped"
+
+ 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
+
+ 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
+
+ let errors =
+ match errorMessage with
+ | Some error -> [| { message = error } |]
+ | None -> [||]
+
+ let id = result.TestCase.Id
+
+ 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)
+
+ for (id, result) in results do
+ {| id = id; result = result |}
+ |> JsonConvert.SerializeObject
+ |> streamWriter.WriteLine
+
+ 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) =
+ (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
+ ()
+
+ let attached = File.Exists(attachedFile)
+
+ Console.WriteLine($"Debugger attached: {attached}")
+
+ attached
+
+ member __.IsDebug = true
+
+
+ let main (argv: string[]) =
+ if argv.Length <> 1 then
+ invalidArg "CommandLineArgs" "Usage: fsi script.fsx "
+
+ let console = argv[0]
+
+ let sourceSettings =
+ """
+
+
+ """
+
+ 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"
+ |> Map.add "DOTNET_ROLL_FORWARD" "Major"
+ |> Dictionary
+
+ let options = TestPlatformOptions(CollectMetrics = false)
+
+ let r =
+ VsTestConsoleWrapper(console, ConsoleParameters(EnvironmentVariables = environmentVariables))
+
+ let testSession = TestSessionInfo()
+
+ r.StartSession()
+
+ let mutable loop = true
+
+ while loop do
+ match Console.ReadLine() with
+ | DiscoveryRequest args ->
+ // spawn as task to allow running discovery concurrently
+ task {
+ do! Task.Yield()
+
+ try
+ let discoveryHandler =
+ 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)
+
+ 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")
+
+ Console.WriteLine($"Wrote test results to {args.WaitFile}")
+ }
+ |> ignore
+ | RunTests args ->
+ task {
+ 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: ")
+
+ 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()
+
+ 0
+
+ let args = fsi.CommandLineArgs |> Array.tail
+
+ main args
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/tests/project_dir/dummy.test.csproj b/spec/samples/test_project/project.fsproj
similarity index 100%
rename from tests/project_dir/dummy.test.csproj
rename to spec/samples/test_project/project.fsproj
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)
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 d97df72..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",
- 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/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 cf170de..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 = "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 = "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 = "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 = "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 eec1015..0000000
--- a/tests/xunit/discover_positions/fact_attribute_spec.lua
+++ /dev/null
@@ -1,275 +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 = "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 = "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 = "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 = "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)
-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 8b46852..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 = "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 = "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 = "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 = "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 = "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