From a227b1833a232823ac4a6472b7a88902696ec462 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Fri, 20 Oct 2023 11:36:37 +0200 Subject: [PATCH 01/32] WIP: Improve test_case detection with async functions & comments --- lua/neotest-rust/init.lua | 199 +++++++++++++++++++++++++++++++++----- 1 file changed, 174 insertions(+), 25 deletions(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 0ecc846..ddab09b 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -160,50 +160,148 @@ local function binary_name(path) return vim.split(path, "/")[1] end +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 + +local function find_parent(nodes, name) + for _, value in nodes:iter_nodes() do + local data = value:data() + if data.name == name then + return value + end + end + return nil +end + +-- See https://github.com/frondeus/test-case/blob/master/crates/test-case-core/src/utils.rs#L4 +local function escape_testcase_name(name) + name = name:gsub('"', "") -- remove any surrounding dquotes from string literal + if name == nil or name == "" then + return "_empty" + end + name = string.lower(name) -- make all letters lowercase + local ident = {} + local last_under = false + for c in name:gmatch(".") do + if c:match("%w") then + -- alphanumeric character + last_under = false + table.insert(ident, c) + elseif not last_under then + -- non alphanumeric char not yet prefixed with underscore + last_under = true + table.insert(ident, "_") + end + end + if ident[1] ~= "_" and not ident[1]:match("%a") then + table.insert(ident, 1, "_") + end + name = table.concat(ident, "") + return name +end + +-- let mut acc = String::new(); +-- for arg in &self.args { +-- acc.push_str(&fmt_syn(&arg)); +-- acc.push('_'); +-- } +-- acc.push_str("expects"); +-- if let Some(expression) = &self.expression { +-- acc.push(' '); +-- acc.push_str(&expression.to_string()) +-- } +-- acc + +-- Enrich `it.each` tests with metadata about TS node position ---Given a file path, parse all the tests within it. ---@async ---@param path string Absolute file path ---@return neotest.Tree | nil function adapter.discover_positions(path) local query = [[;; query + +;; Matches mod {} +((mod_item name: (identifier) @namespace.name) @namespace.definition) + +;; Matches `#[test]` ( - (attribute_item - [ - (attribute - (identifier) @macro_name - ) - (attribute - [ - (identifier) @macro_name - (scoped_identifier - name: (identifier) @macro_name - ) - ] - ) - ] + (attribute_item + (attribute (identifier) @macro + (#eq? @macro "test") + ) ) - [ + (function_item name: (identifier) @test.name) @test.definition +) + +;; Matches `#[test_case(...)] fn ()` +( + (attribute_item + (attribute (identifier) @macro) (#eq? @macro "test_case") + ) @parameterized + . + (line_comment)* + . + (function_item name: (identifier) @test.name) @test.definition +) + +;; Matches `#[test_case(...)] #[{tokio,async_std}::test] async fn ()` +( + (attribute_item + (attribute (identifier) @macro) (#eq? @macro "test_case") + ) @parameterized + . + (line_comment)* + . (attribute_item (attribute - (identifier) + (scoped_identifier + path: (identifier) @package + name: (identifier) + ) ) + ;; all packages which provide a #[::test] macro + (#any-of? @package "tokio" "async_std") ) - (line_comment) - ]* + . + (line_comment)* . (function_item + (function_modifiers) @modifier name: (identifier) @test.name ) @test.definition - (#any-of? @macro_name "test" "rstest" "case") - + (#eq? @modifier "async") ) -(mod_item name: (identifier) @namespace.name)? @namespace.definition ]] + local positions = lib.treesitter.parse_positions(path, query, { + require_namespaces = true, + nested_tests = true, + build_position = function(file_path, source, captured_nodes) + local match_type = get_match_type(captured_nodes) + if not match_type then + return + end - return lib.treesitter.parse_positions(path, query, { - require_namespaces = false, + local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) + local definition = captured_nodes[match_type .. ".definition"] + local range = { definition:range() } + local is_parameterized = captured_nodes["parameterized"] and true or false + + return { + type = match_type, + path = file_path, + name = name, + range = range, + is_parameterized = is_parameterized, + } + end, position_id = function(position, namespaces) - return table.concat( + local id = table.concat( vim.tbl_flatten({ path_to_test_path(path), vim.tbl_map(function(pos) @@ -213,8 +311,54 @@ function adapter.discover_positions(path) }), "::" ) + return id end, }) + local content = lib.files.read(path) + local root, lang = lib.treesitter.get_parse_root(path, content, { fast = true }) + for _, value in positions:iter_nodes() do + local data = value:data() + if data.is_parameterized then + local query = [[ +;; Matches `#[test_case(... ; "")]*fn ()` +( + (attribute_item + (attribute (identifier) @macro arguments: (token_tree (string_literal) @test.name)) (#eq? @macro "test_case") + ) @test.definition + . + [ + (line_comment) + (attribute_item) + ]* + . + (function_item name: (identifier) @parent) (#eq? @parent "]] .. data.name .. [[") +) +]] + local q = lib.treesitter.normalise_query(lang, query) + for _, match in q:iter_matches(root, content) do + local captured_nodes = {} + for i, capture in ipairs(q.captures) do + captured_nodes[capture] = match[i] + end + if captured_nodes["test.name"] and captured_nodes["test.definition"] then + local name = vim.treesitter.get_node_text(captured_nodes["test.name"], content) + local definition = captured_nodes["test.definition"] + name = escape_testcase_name(name) + + local new_data = { + type = "test", + id = data.id .. "::" .. name, + name = name, + range = { definition:range() }, + path = path, + } + local new_pos = value:new(new_data, {}, value._key, {}, {}) + value:add_child(new_data.id, new_pos) + end + end + end + end + return positions end ---@param args neotest.RunArgs @@ -271,7 +415,7 @@ function adapter.build_spec(args) if position.type == "test" then position_id = position.id -- TODO: Support rstest parametrized tests - test_filter = "-E " .. vim.fn.shellescape(package_filter .. "test(/^" .. position_id .. "$/)") + test_filter = "-E " .. vim.fn.shellescape(package_filter .. "test(/^" .. position_id .. "/)") elseif position.type == "file" then if package_name then -- A basic filter to run tests within the package that will be @@ -368,6 +512,11 @@ function adapter.results(spec, result, tree) local root = xml.parse(data) + if root.testsuites.testsuite == nil then + lib.notify("Test didn't produce any output") + return results + end + local testsuites if root.testsuites.testsuite == nil then testsuites = {} From 5b4bcae5e7a4d124c5392c7c62a830b2eb604aeb Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Fri, 20 Oct 2023 14:43:50 +0200 Subject: [PATCH 02/32] WIP: Support `rstest`s `#[case(...)] macros --- lua/neotest-rust/init.lua | 59 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index ddab09b..6559858 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -169,16 +169,6 @@ local function get_match_type(captured_nodes) end end -local function find_parent(nodes, name) - for _, value in nodes:iter_nodes() do - local data = value:data() - if data.name == name then - return value - end - end - return nil -end - -- See https://github.com/frondeus/test-case/blob/master/crates/test-case-core/src/utils.rs#L4 local function escape_testcase_name(name) name = name:gsub('"', "") -- remove any surrounding dquotes from string literal @@ -206,18 +196,6 @@ local function escape_testcase_name(name) return name end --- let mut acc = String::new(); --- for arg in &self.args { --- acc.push_str(&fmt_syn(&arg)); --- acc.push('_'); --- } --- acc.push_str("expects"); --- if let Some(expression) = &self.expression { --- acc.push(' '); --- acc.push_str(&expression.to_string()) --- } --- acc - -- Enrich `it.each` tests with metadata about TS node position ---Given a file path, parse all the tests within it. ---@async @@ -277,6 +255,22 @@ function adapter.discover_positions(path) ) @test.definition (#eq? @modifier "async") ) + +;; Matches `#[rstest] fn (#[case] ...)` +( + (attribute_item (attribute (identifier) @macro) (#eq? @macro "rstest")) + . + [ + (line_comment) + (attribute_item) + ]* + . + (function_item + name: (identifier) @test.name + parameters: (parameters (attribute_item (attribute (identifier) @parameterized))) + (#eq? @parameterized "case") + ) @test.definition +) ]] local positions = lib.treesitter.parse_positions(path, query, { require_namespaces = true, @@ -320,11 +314,13 @@ function adapter.discover_positions(path) local data = value:data() if data.is_parameterized then local query = [[ -;; Matches `#[test_case(... ; "")]*fn ()` +;; Matches `#[test_case(... ; "")]*fn ()` (test_case) +;; ... or `#[case(...)]*fn ()` (rstest) ( - (attribute_item - (attribute (identifier) @macro arguments: (token_tree (string_literal) @test.name)) (#eq? @macro "test_case") - ) @test.definition + (attribute_item + (attribute (identifier) @macro (#any-of? @macro "test_case" "case") + arguments: (token_tree ((_) (string_literal)? @test.name . )) + )) @test.definition . [ (line_comment) @@ -335,15 +331,20 @@ function adapter.discover_positions(path) ) ]] local q = lib.treesitter.normalise_query(lang, query) + local case_index = 1 for _, match in q:iter_matches(root, content) do local captured_nodes = {} for i, capture in ipairs(q.captures) do captured_nodes[capture] = match[i] end - if captured_nodes["test.name"] and captured_nodes["test.definition"] then - local name = vim.treesitter.get_node_text(captured_nodes["test.name"], content) + if captured_nodes["test.definition"] then + local name = "case_" .. tostring(case_index) + case_index = case_index + 1 + if captured_nodes["test.name"] ~= nil then + name = vim.treesitter.get_node_text(captured_nodes["test.name"], content) + name = escape_testcase_name(name) + end local definition = captured_nodes["test.definition"] - name = escape_testcase_name(name) local new_data = { type = "test", From ecc3592e5f1dd951cf007b704ab2c518a321bf65 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Fri, 20 Oct 2023 15:17:17 +0200 Subject: [PATCH 03/32] ADD: Example project with `test-case` dependency --- tests/data/testcase/Cargo.lock | 816 +++++++++++++++++++++++++++++++++ tests/data/testcase/Cargo.toml | 11 + tests/data/testcase/src/lib.rs | 30 ++ 3 files changed, 857 insertions(+) create mode 100644 tests/data/testcase/Cargo.lock create mode 100644 tests/data/testcase/Cargo.toml create mode 100644 tests/data/testcase/src/lib.rs diff --git a/tests/data/testcase/Cargo.lock b/tests/data/testcase/Cargo.lock new file mode 100644 index 0000000..cfdae16 --- /dev/null +++ b/tests/data/testcase/Cargo.lock @@ -0,0 +1,816 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blocking" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test-case" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f1e820b7f1d95a0cdbf97a5df9de10e1be731983ab943e56703ac1b8e9d425" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462" +dependencies = [ + "cfg-if", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "test-case-macros" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37cfd7bbc88a0104e304229fba519bdc45501a30b760fb72240342f1289ad257" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", + "test-case-core", +] + +[[package]] +name = "testcase" +version = "0.1.0" +dependencies = [ + "async-std", + "test-case", + "tokio", +] + +[[package]] +name = "tokio" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +dependencies = [ + "backtrace", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "value-bag" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/tests/data/testcase/Cargo.toml b/tests/data/testcase/Cargo.toml new file mode 100644 index 0000000..a2b0908 --- /dev/null +++ b/tests/data/testcase/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "testcase" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-std = { version = "1.12.0", features = ["attributes"] } +test-case = "3.2.1" +tokio = { version = "1.33.0", features = ["macros", "rt"] } diff --git a/tests/data/testcase/src/lib.rs b/tests/data/testcase/src/lib.rs new file mode 100644 index 0000000..811ca91 --- /dev/null +++ b/tests/data/testcase/src/lib.rs @@ -0,0 +1,30 @@ +#[cfg(test)] +mod tests { + use test_case::test_case; + + #[test_case(0 ; "")] + #[test_case(1 ; "one")] + #[test_case(2 ; "name with spaces")] + // random comment in between + #[test_case(3 ; "MixEd-CaSe")] + #[test_case(4 ; "sp3(|a/-(ar5")] + fn first(x: u64) { + assert!(x < 4); + } + + #[test_case(true ; "yes")] + // random comment in between + #[test_case(false ; "no")] + #[tokio::test] + async fn second(y: bool) { + assert!(y) + } + + #[test_case(true ; "yes")] + // random comment in between + #[test_case(false ; "no")] + #[async_std::test] + async fn third(y: bool) { + assert!(y) + } +} From 09fa96fc440da4ab6e0aa8f3914d248fdd3a60c9 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Fri, 20 Oct 2023 15:17:31 +0200 Subject: [PATCH 04/32] ADD: Example project with `rstest` dependency --- tests/data/rs-test/Cargo.lock | 919 ++++++++++++++++++++++++++++++++++ tests/data/rs-test/Cargo.toml | 11 + tests/data/rs-test/src/lib.rs | 42 ++ 3 files changed, 972 insertions(+) create mode 100644 tests/data/rs-test/Cargo.lock create mode 100644 tests/data/rs-test/Cargo.toml create mode 100644 tests/data/rs-test/src/lib.rs diff --git a/tests/data/rs-test/Cargo.lock b/tests/data/rs-test/Cargo.lock new file mode 100644 index 0000000..e965781 --- /dev/null +++ b/tests/data/rs-test/Cargo.lock @@ -0,0 +1,919 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blocking" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "relative-path" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" + +[[package]] +name = "rs-test" +version = "0.1.0" +dependencies = [ + "async-std", + "rstest", + "tokio", +] + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.38", + "unicode-ident", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +dependencies = [ + "backtrace", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "value-bag" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/tests/data/rs-test/Cargo.toml b/tests/data/rs-test/Cargo.toml new file mode 100644 index 0000000..3bda1db --- /dev/null +++ b/tests/data/rs-test/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rs-test" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-std = { version = "1.12.0", features = ["attributes"] } +rstest = "0.18.2" +tokio = { version = "1.33.0", features = ["rt", "macros"] } diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs new file mode 100644 index 0000000..fef0345 --- /dev/null +++ b/tests/data/rs-test/src/lib.rs @@ -0,0 +1,42 @@ +#[cfg(test)] +mod tests { + use rstest::rstest; + + #[rstest] + #[case(0)] + #[case(1)] + #[case(5)] + // random comment in between + #[case(42)] + fn parameterized(#[case] x: u64) { + assert!(x < 10) + } + + #[rstest] + #[case(0)] + // random comment in between + #[case(1)] + #[case(5)] + #[case(42)] + #[tokio::test] + async fn parameterized_tokio(#[case] x: u64) { + assert!(x < 10) + } + + #[rstest] + #[case(0)] + // random comment in between + #[case(1)] + #[case(5)] + #[case(42)] + #[async_std::test] + async fn parameterized_async_std(#[case] x: u64) { + assert!(x < 10) + } + + // Not supported right now. Too complex for a plain tree sitter query =( + // #[rstest] + // fn fifth(#[values("aaa", "bbb", "ccc")] _name: &str, #[values(1, 2)] _foo: u32) { + // assert!(true) + // } +} From cf75a8ef351daec4e2b93f453779c82245ae2c8d Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Mon, 23 Oct 2023 14:53:29 +0200 Subject: [PATCH 05/32] CHANGE: Make `parameterized_test_discovery` strategy configurable --- lua/neotest-rust/discovery.lua | 102 ++++++++++++++++++++++ lua/neotest-rust/init.lua | 155 ++++++++++----------------------- 2 files changed, 150 insertions(+), 107 deletions(-) create mode 100644 lua/neotest-rust/discovery.lua diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua new file mode 100644 index 0000000..26cb12a --- /dev/null +++ b/lua/neotest-rust/discovery.lua @@ -0,0 +1,102 @@ +local lib = require("neotest.lib") + +local M = {} + +--- Build a query to find parameterized tests given the name of a function +--- @param test string name of the test function, i.e. `foo` for `#[test_case(...)] fn foo(...) {}` +--- @return string +local function build_query(test) + return [[ +;; Matches `#[test_case(... ; "")]*fn ()` (test_case) +;; ... or `#[case(...)]*fn ()` (rstest) +( + (attribute_item + (attribute (identifier) @macro (#any-of? @macro "test_case" "case") + arguments: (token_tree ((_) (string_literal)? @test.name . )) + )) @test.definition + . + [ + (line_comment) + (attribute_item) + ]* + . + (function_item name: (identifier) @parent) (#eq? @parent "]] .. test .. [[") +) +]] +end + +-- See https://github.com/frondeus/test-case/blob/master/crates/test-case-core/src/utils.rs#L4 +local function escape_testcase_name(name) + name = name:gsub('"', "") -- remove any surrounding dquotes from string literal + if name == nil or name == "" then + return "_empty" + end + name = string.lower(name) -- make all letters lowercase + local ident = {} + local last_under = false + for c in name:gmatch(".") do + if c:match("%w") then + -- alphanumeric character + last_under = false + table.insert(ident, c) + elseif not last_under then + -- non alphanumeric char not yet prefixed with underscore + last_under = true + table.insert(ident, "_") + end + end + if ident[1] ~= "_" and not ident[1]:match("%a") then + table.insert(ident, 1, "_") + end + name = table.concat(ident, "") + return name +end + +--- Discover paramterized tests with treesitter +--- +--- @param path string path to test file +--- @param positions neotest.Tree of already parsed namespaces and tests (without parameterized tests) +--- @return neotest.Tree `positions` with additional leafs for parameterized tests +function M.treesitter(path, positions) + local content = lib.files.read(path, positions) + local root, lang = lib.treesitter.get_parse_root(path, content, { fast = true }) + for _, value in positions:iter_nodes() do + local data = value:data() + local query = build_query(data.name) + if data.is_parameterized then + local q = lib.treesitter.normalise_query(lang, query) + local case_index = 1 + for _, match in q:iter_matches(root, content) do + local captured_nodes = {} + for i, capture in ipairs(q.captures) do + captured_nodes[capture] = match[i] + end + if captured_nodes["test.definition"] then + local id = "case_" .. tostring(case_index) + local name = id + case_index = case_index + 1 + + if captured_nodes["test.name"] ~= nil then + name = vim.treesitter.get_node_text(captured_nodes["test.name"], content) + name = name:gsub('"', "") -- remove any surrounding dquotes from string literal + id = escape_testcase_name(name) + end + local definition = captured_nodes["test.definition"] + + local new_data = { + type = "test", + id = data.id .. "::" .. id, + name = name, + range = { definition:range() }, + path = path, + } + local new_pos = value:new(new_data, {}, value._key, {}, {}) + value:add_child(new_data.id, new_pos) + end + end + end + end + return positions +end + +return M diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 6559858..9738e26 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -1,7 +1,9 @@ local async = require("neotest.async") +local logger = require("neotest.logging") local context_manager = require("plenary.context_manager") local dap = require("neotest-rust.dap") local util = require("neotest-rust.util") +local discovery = require("neotest-rust.discovery") local errors = require("neotest-rust.errors") local Job = require("plenary.job") local open = context_manager.open @@ -64,6 +66,8 @@ local get_dap_adapter = function() return "codelldb" end +local param_discovery + local is_callable = function(obj) return type(obj) == "function" or (type(obj) == "table" and obj.__call) end @@ -169,41 +173,8 @@ local function get_match_type(captured_nodes) end end --- See https://github.com/frondeus/test-case/blob/master/crates/test-case-core/src/utils.rs#L4 -local function escape_testcase_name(name) - name = name:gsub('"', "") -- remove any surrounding dquotes from string literal - if name == nil or name == "" then - return "_empty" - end - name = string.lower(name) -- make all letters lowercase - local ident = {} - local last_under = false - for c in name:gmatch(".") do - if c:match("%w") then - -- alphanumeric character - last_under = false - table.insert(ident, c) - elseif not last_under then - -- non alphanumeric char not yet prefixed with underscore - last_under = true - table.insert(ident, "_") - end - end - if ident[1] ~= "_" and not ident[1]:match("%a") then - table.insert(ident, 1, "_") - end - name = table.concat(ident, "") - return name -end - --- Enrich `it.each` tests with metadata about TS node position ----Given a file path, parse all the tests within it. ----@async ----@param path string Absolute file path ----@return neotest.Tree | nil -function adapter.discover_positions(path) - local query = [[;; query - +-- Tree Sitter query to discover test structure in document +local query = [[ ;; Matches mod {} ((mod_item name: (identifier) @namespace.name) @namespace.definition) @@ -271,31 +242,45 @@ function adapter.discover_positions(path) (#eq? @parameterized "case") ) @test.definition ) - ]] +]] + +-- Given a `file_path` with its content as `source` and the `nodes` captured by tree-sitter +-- build position object containing: +---@param file_path string +---@param source string +---@param nodes +---@return table|nil +--- +-- * `type`: Either `namespace` or `test`, depending if the ts query captured `test.name` or `namespace.name` +-- * `path`: same as `file_path` +-- * `name`: text of `.name` capture +-- * `range`: start and end position (row, col) of the test or namespace according to the `.definition` capture +-- * `is_parameterized`: true if the test seems parameterized (e.g. has a #[test_case(...)] or #[rstest::case] in front of it (heueristic) +function adapter.build_position(file_path, source, nodes) + local type = get_match_type(nodes) + if not type then + return + end + return { + type = type, + path = file_path, + name = vim.treesitter.get_node_text(nodes[type .. ".name"], source), + range = { nodes[type .. ".definition"]:range() }, + is_parameterized = nodes["parameterized"] and true or false, + } +end + +---Given a file path, parse all the tests within it. +---@async +---@param path string Absolute file path +---@return neotest.Tree | nil +function adapter.discover_positions(path) local positions = lib.treesitter.parse_positions(path, query, { require_namespaces = true, nested_tests = true, - build_position = function(file_path, source, captured_nodes) - local match_type = get_match_type(captured_nodes) - if not match_type then - return - end - - local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source) - local definition = captured_nodes[match_type .. ".definition"] - local range = { definition:range() } - local is_parameterized = captured_nodes["parameterized"] and true or false - - return { - type = match_type, - path = file_path, - name = name, - range = range, - is_parameterized = is_parameterized, - } - end, + build_position = 'require("neotest-rust").build_position', position_id = function(position, namespaces) - local id = table.concat( + return table.concat( vim.tbl_flatten({ path_to_test_path(path), vim.tbl_map(function(pos) @@ -305,60 +290,15 @@ function adapter.discover_positions(path) }), "::" ) - return id end, }) - local content = lib.files.read(path) - local root, lang = lib.treesitter.get_parse_root(path, content, { fast = true }) - for _, value in positions:iter_nodes() do - local data = value:data() - if data.is_parameterized then - local query = [[ -;; Matches `#[test_case(... ; "")]*fn ()` (test_case) -;; ... or `#[case(...)]*fn ()` (rstest) -( - (attribute_item - (attribute (identifier) @macro (#any-of? @macro "test_case" "case") - arguments: (token_tree ((_) (string_literal)? @test.name . )) - )) @test.definition - . - [ - (line_comment) - (attribute_item) - ]* - . - (function_item name: (identifier) @parent) (#eq? @parent "]] .. data.name .. [[") -) -]] - local q = lib.treesitter.normalise_query(lang, query) - local case_index = 1 - for _, match in q:iter_matches(root, content) do - local captured_nodes = {} - for i, capture in ipairs(q.captures) do - captured_nodes[capture] = match[i] - end - if captured_nodes["test.definition"] then - local name = "case_" .. tostring(case_index) - case_index = case_index + 1 - if captured_nodes["test.name"] ~= nil then - name = vim.treesitter.get_node_text(captured_nodes["test.name"], content) - name = escape_testcase_name(name) - end - local definition = captured_nodes["test.definition"] - - local new_data = { - type = "test", - id = data.id .. "::" .. name, - name = name, - range = { definition:range() }, - path = path, - } - local new_pos = value:new(new_data, {}, value._key, {}, {}) - value:add_child(new_data.id, new_pos) - end - end - end + + if param_discovery == "treesitter" then + positions = discovery.treesitter(path, positions) + elseif param_discovery ~= "none" then + logger.warn("Unsupported value `" .. param_discovery .. "` for parameterized_test_discovery. Assuming `none`") end + return positions end @@ -579,6 +519,7 @@ setmetatable(adapter, { return opts.dap_adapter end end + param_discovery = opts.parameterized_test_discovery or "none" return adapter end, }) From 57e460398e11a02db05a2ceb50b56356a491f22b Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Tue, 24 Oct 2023 11:40:16 +0200 Subject: [PATCH 06/32] ADD: `parameterized_test_discovery = "cargo"` --- lua/neotest-rust/discovery.lua | 192 ++++++++++++++++++++++++++++++++- lua/neotest-rust/init.lua | 20 ++-- tests/data/rs-test/src/lib.rs | 10 +- 3 files changed, 208 insertions(+), 14 deletions(-) diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua index 26cb12a..71c724d 100644 --- a/lua/neotest-rust/discovery.lua +++ b/lua/neotest-rust/discovery.lua @@ -1,4 +1,6 @@ local lib = require("neotest.lib") +local logger = require("neotest.logging") +local Path = require("plenary.path") local M = {} @@ -63,7 +65,7 @@ function M.treesitter(path, positions) for _, value in positions:iter_nodes() do local data = value:data() local query = build_query(data.name) - if data.is_parameterized then + if data.parameterization ~= nil then local q = lib.treesitter.normalise_query(lang, query) local case_index = 1 for _, match in q:iter_matches(root, content) do @@ -99,4 +101,192 @@ function M.treesitter(path, positions) return positions end +--- Given a certain path to a rust file, guess its [test binary name](https://nexte.st/book/running.html) +-- +--- unit tests: /src/ -> +--- integration: /tests/.rs -> :: +--- binary: /src/bin/.rs -> ::bin/ +--- example: /examples/.rs -> ::example/ +--- @param path string +--- @return string|nil +local function binary_name(path) + local workspace = lib.files.match_root_pattern("Cargo.toml")(path) + local parts = Path:new(workspace):_split() + if parts == nil or #parts == 0 then + return nil + end + local package = parts[#parts] + path = Path:new(path):make_relative(workspace):gsub(".rs$", "") + if path:match("^src" .. Path.path.sep .. "bin") then + -- tests in binary + return package .. "::" .. path:gsub("^src" .. Path.path.sep) + end + if path:match("^tests") then + -- integration test + return package .. "::" .. path:gsub("^tests" .. Path.path.sep, "") + end + if path:match("^examples") then + -- tests in example + return package .. "::example" .. path:gsub("^examples", "") + end + if path:match("^src") then + -- unit test + return package + end + logger.warn("Cannot guess unit, binary, integration or example test target of " .. path) + return nil +end + +--- Heueristically guess a file name from a cargo test identifier, depending if such a file +--- exists within a given `workspace` +--- +--- Examples (assuming such a file exists beneath `workspace`): +--- * `foo` -> `foo` +--- * `foo_bar` -> `foo/bar` +--- * `foo_bar_baz` -> `foo/bar/baz` +--- * `foo_bar_baz` -> `foo/bar/baz` +--- * `foo_bar_rs` -> `foo/bar.rs` +--- * `foo_bar_rs` -> `foo_bar.rs` +--- * `foo_bar_rs` -> `foo_bar_rs` +--- +--- @param id string the cargo test identifier where `/`, `.` and `_` got replaced by underscore. Assumed the original path was relative +--- @param workspace Path the folder on which to look for files/folders for each part between underscores in `id`. +--- @return Path|nil The _relative_ path to `id` (relative to `workspace`) if it exists, otherwise `nil` +local function guess_file_path(id, workspace) + -- #[files] will always be relative + local path = nil + local stem = nil + + for _, part in pairs(vim.split(id:gsub("_UP", ".."), "_")) do + local cwd = path or workspace + local candidate = cwd:joinpath(part) + if candidate:exists() then + path = candidate + goto continue + end + if not stem then + stem = part + goto continue + end + + candidate = cwd:joinpath(stem .. "." .. part) + if candidate:exists() then + path = candidate + stem = nil + goto continue + end + + stem = stem .. "_" .. part + candidate = cwd:joinpath(stem) + if candidate:exists() then + path = candidate + stem = nil + goto continue + end + + ::continue:: + end + if not path or not path:exists() then + return nil + end + return path:make_relative(tostring(workspace)) +end + +--- Prettify test case IDs into a more human readable format. +--- +--- This is a heureristic process only and does not cover all edge cases. Users are free +--- to use a custom implementation here by overwriting `resolve_case_name` in the adapter. +--- +--- @param id string the test case identifier returned by `cargo nextest list` +--- @param macro string the (first) macro name which makes this test parameterized (e.g. `values`, `files`, `test_case`, ...) +--- @param file Path the path to the file under test +--- @return string any string which should be shown in the neotest summary panel for this case, or `nil` to not show this case +M.resolve_case_name = function(id, macro, file) + if macro == "values" then + -- Turn `foo_3___blub__::bar_10_3` -> `foo[blub] bar[3]` + return table.concat( + vim.tbl_map(function(x) + local _, _, key, value = x:find("([%w_]+)_%d+_(.*)") + return key .. "[" .. value:gsub("__", '"') .. "]" + end, vim.split(id, "::")), + " " + ) + end + if macro == "files" then + -- Turn `file_1_src_bin_main_rs` -> `file[src/bin/main.rs]` + local workspace = Path:new(lib.files.match_root_pattern("Cargo.toml")(tostring(file))) + return table.concat( + vim.tbl_map(function(x) + local _, _, key, path = x:find("([%w_]+)_%d+_(.*)") + path = guess_file_path(path, workspace) + if not path then + return x + end + return key .. "[" .. path .. "]" + end, vim.split(id, "::")), + " " + ) + end + + return id +end + +--- Discover paramterized tests with `cargo nextest list` +--- +--- @param path string path to test file +--- @param positions neotest.Tree of already parsed namespaces and tests (without parameterized tests) +--- @param name_mapper (fun(string, string, Path): string|nil)|nil a custom mapping function to map test ids, macro names and file path to humand readable case names. See `resolve_case_name` for an example +--- @return neotest.Tree `positions` with additional leafs for parameterized tests +function M.cargo(path, positions, name_mapper) + name_mapper = name_mapper or M.resolve_case_name + local command = "cargo nextest list --message-format json" + + local result = { lib.process.run(vim.split(command, "%s+"), { stdout = true, stderr = true }) } + local code = result[1] + local output = result[2] + if code ~= 0 then + logger.error("Cargo failed with exit code " .. tostring(code) .. ": " .. output.stderr) + return positions + end + local json = vim.json.decode(output.stdout) + + local tests = {} + for key, value in pairs(json["rust-suites"]) do + tests[key] = {} + for case, _ in pairs(value["testcases"]) do + table.insert(tests[key], case) + end + end + local target = binary_name(path) + if target == nil then + return positions + end + + for _, value in positions:iter_nodes() do + local data = value:data() + if data.type == "test" and data.parameterization ~= nil then + for _, case in pairs(tests[target]) do + if case:match("^" .. data.id) then + -- `case` is a parameterized version of `value`, so add it as child + local name = name_mapper(case:gsub("^" .. data.id .. "::", ""), data.parameterization, path) + if name ~= nil then + value:add_child( + case, + value:new({ + type = "test", + id = case, + name = name, + range = data.range, + path = path, + }, {}, value._key, {}, {}) + ) + end + end + end + end + end + + return positions +end + return M diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 9738e26..8c15497 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -190,9 +190,7 @@ local query = [[ ;; Matches `#[test_case(...)] fn ()` ( - (attribute_item - (attribute (identifier) @macro) (#eq? @macro "test_case") - ) @parameterized + (attribute_item (attribute (identifier) @parameterization) (#eq? @parameterization "test_case")) . (line_comment)* . @@ -203,7 +201,7 @@ local query = [[ ( (attribute_item (attribute (identifier) @macro) (#eq? @macro "test_case") - ) @parameterized + ) @parameterization . (line_comment)* . @@ -238,8 +236,8 @@ local query = [[ . (function_item name: (identifier) @test.name - parameters: (parameters (attribute_item (attribute (identifier) @parameterized))) - (#eq? @parameterized "case") + parameters: (parameters . (attribute_item (attribute (identifier) @parameterization ))) + (#any-of? @parameterization "case" "values" "files") ) @test.definition ) ]] @@ -255,18 +253,21 @@ local query = [[ -- * `path`: same as `file_path` -- * `name`: text of `.name` capture -- * `range`: start and end position (row, col) of the test or namespace according to the `.definition` capture --- * `is_parameterized`: true if the test seems parameterized (e.g. has a #[test_case(...)] or #[rstest::case] in front of it (heueristic) +-- * `parameterization`: contains the macro name (e.g. `case`, `test_case`, `values` ... ) if the test is parameterized +-- (e.g. has a #[test_case(...)] or #[rstest::case] in front of it (heueristic) or nil if unparameterized function adapter.build_position(file_path, source, nodes) local type = get_match_type(nodes) if not type then return end + return { type = type, path = file_path, name = vim.treesitter.get_node_text(nodes[type .. ".name"], source), range = { nodes[type .. ".definition"]:range() }, - is_parameterized = nodes["parameterized"] and true or false, + parameterization = nodes["parameterization"] + and vim.treesitter.get_node_text(nodes["parameterization"], source), } end @@ -295,6 +296,8 @@ function adapter.discover_positions(path) if param_discovery == "treesitter" then positions = discovery.treesitter(path, positions) + elseif param_discovery == "cargo" then + positions = discovery.cargo(path, positions, resolve_case_name) elseif param_discovery ~= "none" then logger.warn("Unsupported value `" .. param_discovery .. "` for parameterized_test_discovery. Assuming `none`") end @@ -520,6 +523,7 @@ setmetatable(adapter, { end end param_discovery = opts.parameterized_test_discovery or "none" + resolve_case_name = opts.resolve_case_name return adapter end, }) diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index fef0345..8e9f835 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -34,9 +34,9 @@ mod tests { assert!(x < 10) } - // Not supported right now. Too complex for a plain tree sitter query =( - // #[rstest] - // fn fifth(#[values("aaa", "bbb", "ccc")] _name: &str, #[values(1, 2)] _foo: u32) { - // assert!(true) - // } + // Only supported by `parameterized_test_discovery="cargo"` mode right now. Too complex for a plain tree sitter =( + #[rstest] + fn fifth(#[values("a", "bb", "ccc")] word: &str, #[values(1, 2, 3)] has_chars: usize) { + assert_eq!(word.chars().count(), has_chars) + } } From 6c9d936b42c0c41453dba7aace932251dcf17b05 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Tue, 24 Oct 2023 16:23:58 +0200 Subject: [PATCH 07/32] ADD: README updates --- README.md | 33 ++++++++++++++++++++++++++++++--- media/loc-cargo.png | Bin 0 -> 64919 bytes media/loc-treesitter.png | Bin 0 -> 67012 bytes 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 media/loc-cargo.png create mode 100644 media/loc-treesitter.png diff --git a/README.md b/README.md index e582ce7..098162d 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,14 @@ require("neotest").setup({ adapters = { require("neotest-rust") { args = { "--no-capture" }, + parameterized_test_discovery = "cargo" -- One of { "none", "treesitter", "cargo" } } } }) ``` Supports standard library tests, [`rstest`](https://github.com/la10736/rstest), -Tokio's `[#tokio::test]`, and more. Does not support `rstest`'s parametrized -tests. +Tokio's `[#tokio::test]`. Parameterized tests are also (partially) supported. ## Debugging Tests @@ -51,13 +51,40 @@ See [nvim-dap](https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-insta and [rust-tools#debugging](https://github.com/simrat39/rust-tools.nvim/wiki/Debugging) if you are using rust-tools.nvim, for more information. +## Parameterized Tests + +Parameterized tests are difficult for external tooling to detect, because there exist many different macro +engines to generate the test cases. `neotest-rust` currently supports: + +* [rstest](https://crates.io/crates/rstest) +* [test_case](https://crates.io/crates/test-case) + +To discover parameterized tests `neotest-rust` offers two discovery strategies, which you can choose by setting the `parameterized_test_discovery` option during setup (or choose `none` to disable them entirely). Both have their unique characteristics: + +| `parameterized_test_discovery` | `"treesitter"` | `"cargo"` | +|:---------|:------------|:------| +| general approach | Find special macros in the AST to determine which tests are parameterized | Call `cargo nextest list` and parse its output | +| use when |
  • you want icons placed on each testcase directly
  • test case discovery to be faster
  • you don't want to wait to recompile your tests to discover changes
|
  • you are using `#[rstest::values(...)]`
  • you are using `#[rstest::files(...)]`
  • you are using `#[test_case(...)]` _without_ test comments
  • the `treesitter` mode doesn't detect your test(s)
| +| `#[rstest]` | ✓ | ✓ | +| `#[rstest]` with `async` | ✓ | ✓ | +| `#[test_case(...)]` | ✓ | ✓ | +| `#[test_case(...)]` with `async` | ✓ | ✓ | +| `#[rstest]` with [parameters](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-specific-case-attributes) | ✓ | ✓ | +| `#[rstest]` with [values](https://docs.rs/rstest/latest/rstest/attr.rstest.html#values-lists) | ✗ | ✓ | +| `#[rstest` with [files](https://docs.rs/rstest/latest/rstest/attr.rstest.html#files-path-as-input-arguments) | ✗ | ✓ | +| [`rstest_reuse`](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-parametrize-definition-in-more-tests) | ✗ | ✗ | +| `rstest` case names | enumerated, e.g. `case_1` | enumerated, e.g `case_1` | +| `#[test_case]` case names | Proper, but requires a [case name comment](https://github.com/frondeus/test-case/wiki/Test-Names) (e.g. `#[test_case(... ; "name comment")]`) | Same as `cargo nextest list` outputs it | +| discovery done | instantly (synchronous) when tree-sitter has parsed the document| delayed (asynchronously) when cargo has compiled the project | +| case location (e.g. when you jump to test from summary panel and where result check marks are displayed) | Each case points to its corresponding macro above the test function ![_](./media/loc-treesitter.png) | All cases of a test point to test itself ![_](./media/loc-cargo.png) | + + ## Limitations The following limitations apply to both running and debugging tests. - Assumes unit tests in `main.rs`, `mod.rs`, and `lib.rs` are in a `tests` module. -- Does not support `rstest`'s `#[case]` macro. - When running tests for a `main.rs` in an integration test subdirectory (e.g. `tests/testsuite/main.rs`), all tests in that subdirectory will be run (e.g. all tests in `tests/testsuite/`). This is because Cargo lacks the capability diff --git a/media/loc-cargo.png b/media/loc-cargo.png new file mode 100644 index 0000000000000000000000000000000000000000..65ac25141b28ae554699a536ef6bc4ed0d4c9d54 GIT binary patch literal 64919 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 Yk3;u7=PB6dppwA=qOu}YLiz#!3q87EO#lD@ literal 0 HcmV?d00001 From 2cb5cc0cc996562d4d01feafd18a979ba145b80e Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 25 Oct 2023 08:23:51 +0200 Subject: [PATCH 08/32] ADD: Support for injected fixtures of `rstest` --- README.md | 1 + lua/neotest-rust/discovery.lua | 5 +++++ lua/neotest-rust/init.lua | 18 ++++++++++++++---- tests/data/rs-test/src/lib.rs | 11 ++++++++++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 098162d..e31d3a2 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ To discover parameterized tests `neotest-rust` offers two discovery strategies, | use when |
  • you want icons placed on each testcase directly
  • test case discovery to be faster
  • you don't want to wait to recompile your tests to discover changes
|
  • you are using `#[rstest::values(...)]`
  • you are using `#[rstest::files(...)]`
  • you are using `#[test_case(...)]` _without_ test comments
  • the `treesitter` mode doesn't detect your test(s)
| | `#[rstest]` | ✓ | ✓ | | `#[rstest]` with `async` | ✓ | ✓ | +| `#[rstest]` with [injected fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#injecting-fixtures) | ✓ | ✓ | | `#[test_case(...)]` | ✓ | ✓ | | `#[test_case(...)]` with `async` | ✓ | ✓ | | `#[rstest]` with [parameters](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-specific-case-attributes) | ✓ | ✓ | diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua index 71c724d..094bbb6 100644 --- a/lua/neotest-rust/discovery.lua +++ b/lua/neotest-rust/discovery.lua @@ -227,6 +227,11 @@ M.resolve_case_name = function(id, macro, file) " " ) end + if macro == "" then + -- Strip namespaces from test name `test::foo::bar` -> `bar` + local parts = vim.split(id, "::") + return parts[#parts] + end return id end diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 8c15497..82616e2 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -225,7 +225,8 @@ local query = [[ (#eq? @modifier "async") ) -;; Matches `#[rstest] fn (#[case] ...)` +;; Matches `#[rstest] fn (...)` +;; ... or `#[rstest] fn (#[case/values/files] ...)` ( (attribute_item (attribute (identifier) @macro) (#eq? @macro "rstest")) . @@ -236,7 +237,7 @@ local query = [[ . (function_item name: (identifier) @test.name - parameters: (parameters . (attribute_item (attribute (identifier) @parameterization ))) + parameters: (parameters . (attribute_item (attribute (identifier) @parameterization ))? ) (#any-of? @parameterization "case" "values" "files") ) @test.definition ) @@ -261,13 +262,22 @@ function adapter.build_position(file_path, source, nodes) return end + local parameterization + + if nodes["macro"] and vim.treesitter.get_node_text(nodes["macro"], source) == "rstest" then + -- Need this because rstest function can also contain injected fixture without any parameterization attribute + parameterization = "" + end + if nodes["parameterization"] then + parameterization = vim.treesitter.get_node_text(nodes["parameterization"], source) + end + return { type = type, path = file_path, name = vim.treesitter.get_node_text(nodes[type .. ".name"], source), range = { nodes[type .. ".definition"]:range() }, - parameterization = nodes["parameterization"] - and vim.treesitter.get_node_text(nodes["parameterization"], source), + parameterization = parameterization, } end diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index 8e9f835..ad6b8b2 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -1,6 +1,15 @@ #[cfg(test)] mod tests { - use rstest::rstest; + use rstest::{fixture, rstest}; + + #[fixture] + fn bar() -> i32 { + 42 + } + #[rstest] + fn fixture_injected(bar: i32) { + assert_eq!(42, bar) + } #[rstest] #[case(0)] From 85969d0953648b4a662c5722272f4253e7ab2224 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 25 Oct 2023 10:30:47 +0200 Subject: [PATCH 09/32] ADD: Support for rename fixtures of `rstest` --- README.md | 1 + lua/neotest-rust/discovery.lua | 2 +- lua/neotest-rust/init.lua | 4 ++-- tests/data/rs-test/src/lib.rs | 11 ++++++++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e31d3a2..0e12d8a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ To discover parameterized tests `neotest-rust` offers two discovery strategies, | `#[rstest]` | ✓ | ✓ | | `#[rstest]` with `async` | ✓ | ✓ | | `#[rstest]` with [injected fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#injecting-fixtures) | ✓ | ✓ | +| `#[rstest]` with [rename fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#injecting-fixtures) | ✓ | ✓ | | `#[test_case(...)]` | ✓ | ✓ | | `#[test_case(...)]` with `async` | ✓ | ✓ | | `#[rstest]` with [parameters](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-specific-case-attributes) | ✓ | ✓ | diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua index 094bbb6..1f06eff 100644 --- a/lua/neotest-rust/discovery.lua +++ b/lua/neotest-rust/discovery.lua @@ -227,7 +227,7 @@ M.resolve_case_name = function(id, macro, file) " " ) end - if macro == "" then + if macro == "" or "from" then -- Strip namespaces from test name `test::foo::bar` -> `bar` local parts = vim.split(id, "::") return parts[#parts] diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 82616e2..802b960 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -226,7 +226,7 @@ local query = [[ ) ;; Matches `#[rstest] fn (...)` -;; ... or `#[rstest] fn (#[case/values/files] ...)` +;; ... or `#[rstest] fn (#[from/case/values/files] ...)` ( (attribute_item (attribute (identifier) @macro) (#eq? @macro "rstest")) . @@ -238,7 +238,7 @@ local query = [[ (function_item name: (identifier) @test.name parameters: (parameters . (attribute_item (attribute (identifier) @parameterization ))? ) - (#any-of? @parameterization "case" "values" "files") + (#any-of? @parameterization "from" "case" "values" "files") ) @test.definition ) ]] diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index ad6b8b2..08f5af8 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use rstest::{fixture, rstest}; + use rstest::*; #[fixture] fn bar() -> i32 { @@ -11,6 +11,15 @@ mod tests { assert_eq!(42, bar) } + #[fixture] + fn long_and_boring_descriptive_name() -> i32 { + 42 + } + #[rstest] + fn fixture_rename(#[from(long_and_boring_descriptive_name)] short: i32) { + assert_eq!(42, short) + } + #[rstest] #[case(0)] #[case(1)] From 9b518d276954c7950638ccaecc9fbd7cb4ae8b03 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 25 Oct 2023 10:51:29 +0200 Subject: [PATCH 10/32] ADD: Support for partial injection fixtures of `rstest` --- README.md | 1 + lua/neotest-rust/init.lua | 4 ++-- tests/data/rs-test/src/lib.rs | 12 +++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0e12d8a..a53b755 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ To discover parameterized tests `neotest-rust` offers two discovery strategies, | `#[rstest]` with `async` | ✓ | ✓ | | `#[rstest]` with [injected fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#injecting-fixtures) | ✓ | ✓ | | `#[rstest]` with [rename fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#injecting-fixtures) | ✓ | ✓ | +| `#[rstest]` with [partial injection](https://docs.rs/rstest/latest/rstest/attr.fixture.html#partial-injection) | ✓ | ✓ | | `#[test_case(...)]` | ✓ | ✓ | | `#[test_case(...)]` with `async` | ✓ | ✓ | | `#[rstest]` with [parameters](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-specific-case-attributes) | ✓ | ✓ | diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 802b960..ccb5220 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -226,7 +226,7 @@ local query = [[ ) ;; Matches `#[rstest] fn (...)` -;; ... or `#[rstest] fn (#[from/case/values/files] ...)` +;; ... or `#[rstest] fn (#[from/with/case/values/files/future] ...)` ( (attribute_item (attribute (identifier) @macro) (#eq? @macro "rstest")) . @@ -238,7 +238,7 @@ local query = [[ (function_item name: (identifier) @test.name parameters: (parameters . (attribute_item (attribute (identifier) @parameterization ))? ) - (#any-of? @parameterization "from" "case" "values" "files") + (#any-of? @parameterization "from" "with" "case" "values" "files") ) @test.definition ) ]] diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index 08f5af8..d4a9639 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -20,6 +20,16 @@ mod tests { assert_eq!(42, short) } + struct User(String, u8); + #[fixture] + fn user(#[default("Alice")] name: impl AsRef, #[default(22)] age: u8) -> User { + User(name.as_ref().to_owned(), age) + } + #[rstest] + fn fixture_partial_injection(#[with("Bob")] user: User) { + assert_eq!("Bob", user.0) + } + #[rstest] #[case(0)] #[case(1)] @@ -54,7 +64,7 @@ mod tests { // Only supported by `parameterized_test_discovery="cargo"` mode right now. Too complex for a plain tree sitter =( #[rstest] - fn fifth(#[values("a", "bb", "ccc")] word: &str, #[values(1, 2, 3)] has_chars: usize) { + fn combinations(#[values("a", "bb", "ccc")] word: &str, #[values(1, 2, 3)] has_chars: usize) { assert_eq!(word.chars().count(), has_chars) } } From 63bde7b3ba748b92cf035ef6fef01b56ddda2b26 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 25 Oct 2023 12:43:36 +0200 Subject: [PATCH 11/32] FIX: Exactly match selected test with testcases Imaginge you have the following tests: ```rust #[test] fn foo_bar() {} #[test_case(1 ; "one")] #[test_case(2 ; "two")] fn foo() {} ``` Selecting `foo` in the summary and running it would also run `foo_bar` since we only match the _prefix_ of the test. To exactly match this test we have to include "end of line" ($) char in the match, but optionally also match `::.*` so we include all subtests as well --- lua/neotest-rust/init.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index ccb5220..22878ec 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -185,6 +185,9 @@ local query = [[ (#eq? @macro "test") ) ) + . + (line_comment)* + . (function_item name: (identifier) @test.name) @test.definition ) @@ -368,8 +371,7 @@ function adapter.build_spec(args) local test_filter if position.type == "test" then position_id = position.id - -- TODO: Support rstest parametrized tests - test_filter = "-E " .. vim.fn.shellescape(package_filter .. "test(/^" .. position_id .. "/)") + test_filter = "-E " .. vim.fn.shellescape(package_filter .. "test(/^" .. position_id .. "(::.*)?$/)") elseif position.type == "file" then if package_name then -- A basic filter to run tests within the package that will be From 285e26545ea1a33bc173e88a5cbefb8bc1eab44e Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 25 Oct 2023 12:51:57 +0200 Subject: [PATCH 12/32] ADD: Support case descriptions for `rstest` --- README.md | 1 + lua/neotest-rust/discovery.lua | 43 ++++++++++++++++++++++++++++------ tests/data/rs-test/src/lib.rs | 10 ++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a53b755..4b8b6aa 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ To discover parameterized tests `neotest-rust` offers two discovery strategies, | `#[test_case(...)]` | ✓ | ✓ | | `#[test_case(...)]` with `async` | ✓ | ✓ | | `#[rstest]` with [parameters](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-specific-case-attributes) | ✓ | ✓ | +| `#[rstest]` with [parameter descriptions](https://docs.rs/rstest/latest/rstest/attr.rstest.html#optional-case-description) | ✓ | ✓ | | `#[rstest]` with [values](https://docs.rs/rstest/latest/rstest/attr.rstest.html#values-lists) | ✗ | ✓ | | `#[rstest` with [files](https://docs.rs/rstest/latest/rstest/attr.rstest.html#files-path-as-input-arguments) | ✗ | ✓ | | [`rstest_reuse`](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-parametrize-definition-in-more-tests) | ✗ | ✗ | diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua index 1f06eff..9503bb3 100644 --- a/lua/neotest-rust/discovery.lua +++ b/lua/neotest-rust/discovery.lua @@ -11,11 +11,24 @@ local function build_query(test) return [[ ;; Matches `#[test_case(... ; "")]*fn ()` (test_case) ;; ... or `#[case(...)]*fn ()` (rstest) +;; ... or `#[case::]*fn ()` (rstest) ( - (attribute_item - (attribute (identifier) @macro (#any-of? @macro "test_case" "case") - arguments: (token_tree ((_) (string_literal)? @test.name . )) - )) @test.definition + [ + (attribute_item + (attribute + (identifier) @macro (#any-of? @macro "test_case" "case") + arguments: (token_tree ((_) (string_literal)? @test.name . )) + ) + ) + (attribute_item + (attribute + (scoped_identifier + path: (identifier) @macro + name: (identifier) @test.name + ) (#eq? @macro "case") + ) + ) + ] @test.definition . [ (line_comment) @@ -76,13 +89,24 @@ function M.treesitter(path, positions) if captured_nodes["test.definition"] then local id = "case_" .. tostring(case_index) local name = id + local macro = vim.treesitter.get_node_text(captured_nodes["macro"], content) case_index = case_index + 1 if captured_nodes["test.name"] ~= nil then name = vim.treesitter.get_node_text(captured_nodes["test.name"], content) - name = name:gsub('"', "") -- remove any surrounding dquotes from string literal - id = escape_testcase_name(name) + if macro == "test_case" then + -- #[test_case(arg1, arg2, ... ; "foo bar")] -> test.name = "foo bar" + name = name:gsub('"', "") -- remove any surrounding dquotes from string literal + id = escape_testcase_name(name) + end + if macro == "case" then + -- #[case::foo_bar(arg1, arg2, ...)] -> test.name = "foo_bar" + local description = vim.treesitter.get_node_text(captured_nodes["test.name"], content) + id = id .. "_" .. description + name = description + end end + local definition = captured_nodes["test.definition"] local new_data = { @@ -202,6 +226,11 @@ end --- @param file Path the path to the file under test --- @return string any string which should be shown in the neotest summary panel for this case, or `nil` to not show this case M.resolve_case_name = function(id, macro, file) + if macro == "case" then + -- rstest with description, turn `case_3_foo_bar` -> `foo_bar` + id = id:gsub("^case_%d+_", "") + return id + end if macro == "values" then -- Turn `foo_3___blub__::bar_10_3` -> `foo[blub] bar[3]` return table.concat( @@ -271,7 +300,7 @@ function M.cargo(path, positions, name_mapper) local data = value:data() if data.type == "test" and data.parameterization ~= nil then for _, case in pairs(tests[target]) do - if case:match("^" .. data.id) then + if case:match("^" .. data.id .. "::") then -- `case` is a parameterized version of `value`, so add it as child local name = name_mapper(case:gsub("^" .. data.id .. "::", ""), data.parameterization, path) if name ~= nil then diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index d4a9639..b5ca3a7 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -40,6 +40,16 @@ mod tests { assert!(x < 10) } + #[rstest] + #[case::one(1)] + #[case::two(2)] + #[case::three(3)] + #[case(4)] + #[case::ten(10)] + fn parameterized_with_descriptions(#[case] x: u64) { + assert!(x < 10) + } + #[rstest] #[case(0)] // random comment in between From 25d7e663e3c6e5aed0a42a78fd65d750075c5263 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 25 Oct 2023 15:24:43 +0200 Subject: [PATCH 13/32] ADD: Support for async fixture parameters of `rstest` --- README.md | 1 + lua/neotest-rust/init.lua | 2 +- tests/data/rs-test/src/lib.rs | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b8b6aa..0d32093 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ To discover parameterized tests `neotest-rust` offers two discovery strategies, | `#[rstest]` with [injected fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#injecting-fixtures) | ✓ | ✓ | | `#[rstest]` with [rename fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#injecting-fixtures) | ✓ | ✓ | | `#[rstest]` with [partial injection](https://docs.rs/rstest/latest/rstest/attr.fixture.html#partial-injection) | ✓ | ✓ | +| `#[rstest]` with [async fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#async) | ✓ | ✓ | | `#[test_case(...)]` | ✓ | ✓ | | `#[test_case(...)]` with `async` | ✓ | ✓ | | `#[rstest]` with [parameters](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-specific-case-attributes) | ✓ | ✓ | diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 22878ec..78acf12 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -241,7 +241,7 @@ local query = [[ (function_item name: (identifier) @test.name parameters: (parameters . (attribute_item (attribute (identifier) @parameterization ))? ) - (#any-of? @parameterization "from" "with" "case" "values" "files") + (#any-of? @parameterization "from" "with" "case" "values" "files" "future") ) @test.definition ) ]] diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index b5ca3a7..ad6c083 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -30,6 +30,16 @@ mod tests { assert_eq!("Bob", user.0) } + #[fixture] + async fn magic() -> i32 { + 42 + } + #[rstest] + #[tokio::test] + async fn fixture_async(#[future] magic: i32) { + assert_eq!(magic.await, 42) + } + #[rstest] #[case(0)] #[case(1)] @@ -72,6 +82,20 @@ mod tests { assert!(x < 10) } + #[rstest] + #[case::even(async { 2 })] + // random comment in between + #[case::odd(async { 3 })] + #[tokio::test] + async fn parameterized_async_parameter( + #[future] + #[case] + n: u32, + ) { + let n = n.await; + assert!(n % 2 == 0, "{n} not even"); + } + // Only supported by `parameterized_test_discovery="cargo"` mode right now. Too complex for a plain tree sitter =( #[rstest] fn combinations(#[values("a", "bb", "ccc")] word: &str, #[values(1, 2, 3)] has_chars: usize) { From f52d2db3a4d23b6fc8f6b8bfa18be04138536640 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 25 Oct 2023 15:35:30 +0200 Subject: [PATCH 14/32] ADD: Support timeouts of `rstest` --- README.md | 1 + tests/data/rs-test/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/README.md b/README.md index 0d32093..1f78db6 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ To discover parameterized tests `neotest-rust` offers two discovery strategies, | `#[rstest]` with [rename fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#injecting-fixtures) | ✓ | ✓ | | `#[rstest]` with [partial injection](https://docs.rs/rstest/latest/rstest/attr.fixture.html#partial-injection) | ✓ | ✓ | | `#[rstest]` with [async fixtures](https://docs.rs/rstest/latest/rstest/attr.rstest.html#async) | ✓ | ✓ | +| `#[rstest]` with [timeouts](https://docs.rs/rstest/latest/rstest/attr.rstest.html#test-timeout) | ✓ | ✓ | | `#[test_case(...)]` | ✓ | ✓ | | `#[test_case(...)]` with `async` | ✓ | ✓ | | `#[rstest]` with [parameters](https://docs.rs/rstest/latest/rstest/attr.rstest.html#use-specific-case-attributes) | ✓ | ✓ | diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index ad6c083..ca09e64 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -1,6 +1,14 @@ #[cfg(test)] mod tests { use rstest::*; + use std::time::Duration; + + #[rstest] + #[timeout(Duration::from_millis(10))] + fn timeout() { + std::thread::sleep(Duration::from_millis(15)); + assert!(true) + } #[fixture] fn bar() -> i32 { @@ -96,6 +104,30 @@ mod tests { assert!(n % 2 == 0, "{n} not even"); } + #[rstest] + #[case::pass(Duration::from_millis(1))] + #[timeout(Duration::from_millis(10))] + #[case::fail(Duration::from_millis(25))] + #[timeout(Duration::from_millis(20))] + fn parameterized_timeout(#[case] sleepy: Duration) { + std::thread::sleep(sleepy); + assert!(true) + } + + #[rstest] + #[case::pass(Duration::from_millis(1), 4)] + #[timeout(Duration::from_millis(10))] + #[case::fail_timeout(Duration::from_millis(60), 4)] + #[case::fail_value(Duration::from_millis(1), 5)] + #[timeout(Duration::from_millis(100))] + async fn parameterized_async_timeout(#[case] delay: Duration, #[case] expected: u32) { + async fn delayed_sum(a: u32, b: u32, delay: Duration) -> u32 { + async_std::task::sleep(delay).await; + a + b + } + assert_eq!(expected, delayed_sum(2, 2, delay).await); + } + // Only supported by `parameterized_test_discovery="cargo"` mode right now. Too complex for a plain tree sitter =( #[rstest] fn combinations(#[values("a", "bb", "ccc")] word: &str, #[values(1, 2, 3)] has_chars: usize) { From 6dfe72d3ef27925d816edd9e2e06d338e365ce7a Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 25 Oct 2023 15:48:49 +0200 Subject: [PATCH 15/32] FIX: In discovery mode `cargo` keep the order of test cases ...by iterating keys in the same order as `cargo nextest` spits them out --- lua/neotest-rust/discovery.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua index 9503bb3..96edadf 100644 --- a/lua/neotest-rust/discovery.lua +++ b/lua/neotest-rust/discovery.lua @@ -287,9 +287,10 @@ function M.cargo(path, positions, name_mapper) local tests = {} for key, value in pairs(json["rust-suites"]) do tests[key] = {} - for case, _ in pairs(value["testcases"]) do + for case in pairs(value["testcases"]) do table.insert(tests[key], case) end + table.sort(tests[key]) end local target = binary_name(path) if target == nil then @@ -299,7 +300,7 @@ function M.cargo(path, positions, name_mapper) for _, value in positions:iter_nodes() do local data = value:data() if data.type == "test" and data.parameterization ~= nil then - for _, case in pairs(tests[target]) do + for _, case in ipairs(tests[target]) do if case:match("^" .. data.id .. "::") then -- `case` is a parameterized version of `value`, so add it as child local name = name_mapper(case:gsub("^" .. data.id .. "::", ""), data.parameterization, path) From 68f31cf432091f16103100915c7f05827fa7b51c Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 26 Oct 2023 09:42:47 +0200 Subject: [PATCH 16/32] FIX: Discovery tests without namespaces --- lua/neotest-rust/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 78acf12..e270afc 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -290,7 +290,7 @@ end ---@return neotest.Tree | nil function adapter.discover_positions(path) local positions = lib.treesitter.parse_positions(path, query, { - require_namespaces = true, + require_namespaces = false, nested_tests = true, build_position = 'require("neotest-rust").build_position', position_id = function(position, namespaces) From 88583687288da6b0dc4542ac93a9012e13e5bf1e Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 26 Oct 2023 09:44:38 +0200 Subject: [PATCH 17/32] FIX: Crash in tests because `param_discovery` cannot be printed --- lua/neotest-rust/init.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index e270afc..4341fe9 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -312,7 +312,9 @@ function adapter.discover_positions(path) elseif param_discovery == "cargo" then positions = discovery.cargo(path, positions, resolve_case_name) elseif param_discovery ~= "none" then - logger.warn("Unsupported value `" .. param_discovery .. "` for parameterized_test_discovery. Assuming `none`") + logger.warn( + "Unsupported value `" .. tostring(param_discovery) .. "` for parameterized_test_discovery. Assuming `none`" + ) end return positions From 9dabc2fd8cce4a097881a1280976842e4b9960a4 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 26 Oct 2023 09:45:25 +0200 Subject: [PATCH 18/32] CHANGE: Support `#[should_panic]` & `#[ignore]` in tests --- lua/neotest-rust/init.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 4341fe9..63303c1 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -186,7 +186,12 @@ local query = [[ ) ) . - (line_comment)* + [ + (line_comment) + ;; Don't match any attribute here but deliberately only "#[should_panic]" & "#[ignore]" + ;; macros here for regular tests to not interfere with parameterized tests + (attribute_item (attribute (identifier) @othermacro) (#any-of? @othermacro "should_panic" "ignore")) + ]* . (function_item name: (identifier) @test.name) @test.definition ) From 52c10c8a29c6861b9cda1bc28628a4327240d58b Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 26 Oct 2023 09:46:53 +0200 Subject: [PATCH 19/32] FIX: Make `simple-package/src/mymod/multiple_macros.rs` actually executable Without modules being included in the module tree `cargo` is not able to compile them nor execute their tests --- tests/data/simple-package/src/mymod/mod.rs | 1 + tests/init_spec.lua | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/data/simple-package/src/mymod/mod.rs b/tests/data/simple-package/src/mymod/mod.rs index 00cf2de..a343227 100644 --- a/tests/data/simple-package/src/mymod/mod.rs +++ b/tests/data/simple-package/src/mymod/mod.rs @@ -1,4 +1,5 @@ mod foo; +mod multiple_macros; #[cfg(test)] mod tests { diff --git a/tests/init_spec.lua b/tests/init_spec.lua index 73e33fc..adaecf2 100644 --- a/tests/init_spec.lua +++ b/tests/init_spec.lua @@ -166,7 +166,7 @@ describe("discover_positions", function() id = vim.loop.cwd() .. "/tests/data/simple-package/src/mymod/mod.rs", name = "mod.rs", path = vim.loop.cwd() .. "/tests/data/simple-package/src/mymod/mod.rs", - range = { 0, 0, 9, 0 }, + range = { 0, 0, 10, 0 }, type = "file", }, { @@ -174,7 +174,7 @@ describe("discover_positions", function() id = "mymod::tests", name = "tests", path = vim.loop.cwd() .. "/tests/data/simple-package/src/mymod/mod.rs", - range = { 3, 0, 8, 1 }, + range = { 4, 0, 9, 1 }, type = "namespace", }, { @@ -182,7 +182,7 @@ describe("discover_positions", function() id = "mymod::tests::math", name = "math", path = vim.loop.cwd() .. "/tests/data/simple-package/src/mymod/mod.rs", - range = { 5, 4, 7, 5 }, + range = { 6, 4, 8, 5 }, type = "test", }, }, From e987f91ec49b98c90a25ac1b6fe2238cde093ff0 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 26 Oct 2023 09:52:07 +0200 Subject: [PATCH 20/32] FIX: Tests with new filter expression The cargo nextest -E ... filter now also optionally matches subtests (with "(::.*)?") and was missing in some tests --- tests/init_spec.lua | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/init_spec.lua b/tests/init_spec.lua index adaecf2..1e28d51 100644 --- a/tests/init_spec.lua +++ b/tests/init_spec.lua @@ -390,7 +390,10 @@ describe("build_spec", function() end, {}) local spec = plugin.build_spec({ tree = tree }) - assert.equal(spec.context.test_filter, "-E " .. vim.fn.shellescape("test(/^mymod::foo::tests::math$/)")) + assert.equal( + spec.context.test_filter, + "-E " .. vim.fn.shellescape("test(/^mymod::foo::tests::math(::.*)?$/)") + ) assert.equal(spec.cwd, vim.loop.cwd() .. "/tests/data/simple-package") end) @@ -475,7 +478,7 @@ describe("build_spec", function() end, {}) local spec = plugin.build_spec({ tree = tree }) - assert.equal(spec.context.test_filter, "-E " .. vim.fn.shellescape("test(/^top_level_math$/)")) + assert.equal(spec.context.test_filter, "-E " .. vim.fn.shellescape("test(/^top_level_math(::.*)?$/)")) assert.equal(spec.cwd, vim.loop.cwd() .. "/tests/data/simple-package") assert.matches(".+ %-%-test test_it", spec.command) end) @@ -505,7 +508,10 @@ describe("build_spec", function() end, {}) local spec = plugin.build_spec({ tree = tree }) - assert.equal(spec.context.test_filter, "-E " .. vim.fn.shellescape("test(/^testsuite_top_level_math$/)")) + assert.equal( + spec.context.test_filter, + "-E " .. vim.fn.shellescape("test(/^testsuite_top_level_math(::.*)?$/)") + ) assert.equal(spec.cwd, vim.loop.cwd() .. "/tests/data/simple-package") assert.matches(".+ %-%-test testsuite ", spec.command) end) @@ -535,7 +541,10 @@ describe("build_spec", function() end, {}) local spec = plugin.build_spec({ tree = tree }) - assert.equal(spec.context.test_filter, "-E " .. vim.fn.shellescape("test(/^it::testsuite_it_math$/)")) + assert.equal( + spec.context.test_filter, + "-E " .. vim.fn.shellescape("test(/^it::testsuite_it_math(::.*)?$/)") + ) assert.equal(spec.cwd, vim.loop.cwd() .. "/tests/data/simple-package") assert.matches(".+ %-%-test testsuite ", spec.command) end) @@ -592,7 +601,7 @@ describe("build_spec", function() local spec = plugin.build_spec({ tree = tree }) assert.equal( spec.context.test_filter, - "-E " .. vim.fn.shellescape("package(with_integration_tests) & test(/^it_works$/)") + "-E " .. vim.fn.shellescape("package(with_integration_tests) & test(/^it_works(::.*)?$/)") ) assert.equal(spec.cwd, vim.loop.cwd() .. "/tests/data/workspace") assert.matches(".+ %-%-test it", spec.command) @@ -626,7 +635,7 @@ describe("build_spec", function() local spec = plugin.build_spec({ tree = tree }) assert.equal( spec.context.test_filter, - "-E " .. vim.fn.shellescape("package(with_unit_tests) & test(/^test_it$/)") + "-E " .. vim.fn.shellescape("package(with_unit_tests) & test(/^test_it(::.*)?$/)") ) assert.equal(spec.cwd, vim.loop.cwd() .. "/tests/data/workspace") end) @@ -662,7 +671,7 @@ describe("build_spec", function() local spec = plugin.build_spec({ tree = tree }) assert.equal( spec.context.test_filter, - "-E " .. vim.fn.shellescape("package(some_other_name) & test(/^test_it$/)") + "-E " .. vim.fn.shellescape("package(some_other_name) & test(/^test_it(::.*)?$/)") ) assert.equal(spec.cwd, vim.loop.cwd() .. "/tests/data/workspace") end) From 34333680854ed1f0a869279789f18e9401e4dacc Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 26 Oct 2023 10:19:52 +0200 Subject: [PATCH 21/32] ADD: Limitations in README --- README.md | 18 ++++++++++++++++++ media/rstest-files-naming.png | Bin 0 -> 14140 bytes tests/data/rs-test/foo-bar.txt | 0 tests/data/rs-test/src/bar/b_a_z.txt | 0 tests/data/rs-test/src/foo.txt | 0 tests/data/rs-test/src/lib.rs | 9 +++++++-- 6 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 media/rstest-files-naming.png create mode 100644 tests/data/rs-test/foo-bar.txt create mode 100644 tests/data/rs-test/src/bar/b_a_z.txt create mode 100644 tests/data/rs-test/src/foo.txt diff --git a/README.md b/README.md index 1f78db6..025a263 100644 --- a/README.md +++ b/README.md @@ -95,5 +95,23 @@ The following limitations apply to both running and debugging tests. `tests/testsuite/main.rs`), all tests in that subdirectory will be run (e.g. all tests in `tests/testsuite/`). This is because Cargo lacks the capability to specify a test file. +- When using `#[test_case(...)]` for parameterized tests and `treesitter` as + discovery mode, make sure to _always_ use a test comment (e.g. + `#[test_case(arg1, arg2, ... ; "test_comment")]`). Otherwise the test runner + might not find this case. With `cargo` as strategy this is not strictly + necessary. +- When using for example `#[rstest::files("**/*.txt")]` in a test the discovery + tries to heuristically "guess" each file and render it in the summary view. + This only works, though, if the file exists relative to the project root + and contains no special characters except underscores (and `.` before the file + extension). Otherwise the name resolution is just skipped and the `cargo` + id is displayed. Has no influence on the running of each test case, only for + the name in the summary. ![_](./media/rstest-files-naming.png) +- When using a combination of `rstest` features the _first_ one in the argument + list always decides on the heuristic name resolution. For example if you have + a test like `fn foo(#[case] x: i32, #[files("*")] file: PathBuf) {}` the name + resolution will try to resolve the test ID based on the `case` attribute, which + is not consistent with the `files` rendering. Try swapping arguments if necessary. + Has no influence on the running of each test case, only for the name in the summary Additionally, when debugging tests, no output from failed tests will be captured in the results provided to Neotest. diff --git a/media/rstest-files-naming.png b/media/rstest-files-naming.png new file mode 100644 index 0000000000000000000000000000000000000000..9755a7379f39d29acde65c73bbf3ffb99f29f27e GIT binary patch literal 14140 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 literal 0 HcmV?d00001 diff --git a/tests/data/rs-test/foo-bar.txt b/tests/data/rs-test/foo-bar.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/rs-test/src/bar/b_a_z.txt b/tests/data/rs-test/src/bar/b_a_z.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/rs-test/src/foo.txt b/tests/data/rs-test/src/foo.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index ca09e64..88913ea 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use rstest::*; - use std::time::Duration; + use std::{ffi::OsStr, path::PathBuf, time::Duration}; #[rstest] #[timeout(Duration::from_millis(10))] @@ -128,9 +128,14 @@ mod tests { assert_eq!(expected, delayed_sum(2, 2, delay).await); } - // Only supported by `parameterized_test_discovery="cargo"` mode right now. Too complex for a plain tree sitter =( + // The following are only supported by `parameterized_test_discovery="cargo"` mode right now. Too complex for a plain tree sitter =( #[rstest] fn combinations(#[values("a", "bb", "ccc")] word: &str, #[values(1, 2, 3)] has_chars: usize) { assert_eq!(word.chars().count(), has_chars) } + + #[rstest] + fn files(#[files("**/*.txt")] file: PathBuf) { + assert_eq!(file.extension(), Some(OsStr::new("txt"))) + } } From b65bef33419ddd8b34124a0fd3174cc0c84a5bad Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 26 Oct 2023 10:51:53 +0200 Subject: [PATCH 22/32] ADD: Chapter about Name Heureristic --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 025a263..b358f53 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,42 @@ To discover parameterized tests `neotest-rust` offers two discovery strategies, | discovery done | instantly (synchronous) when tree-sitter has parsed the document| delayed (asynchronously) when cargo has compiled the project | | case location (e.g. when you jump to test from summary panel and where result check marks are displayed) | Each case points to its corresponding macro above the test function ![_](./media/loc-treesitter.png) | All cases of a test point to test itself ![_](./media/loc-cargo.png) | +### Name Heuristic + +When using the `cargo` discovery strategy `neotest-rust` tries to "guess" test case names based on their test IDs. +A test ID is the one which `cargo nextest` uses to identify each test. Depending on the type of test case +this heuristic does the following: + +| Macro | ID | Name | +|:--------------------------------------------|:--------------------|:--------------------------------------| +| `#[rstest::case(...)]` | `case_1` | `case_1` | +| `#[rstest::case::foo_bar(...)]` | `case_1_foo_bar` | `foo_bar` | +| `#[rstest::values("foo", ...)] param: &str` | `param_1___foo__` | `param["foo"]` | +| `#[rstest::values(42, ...)] param: i32` | `param_1_42` | `param[42]` | +| `#[rstest::files("**/*.rs")] file: PathBuf` | `file_1_src_lib_rs` | `file[src/lib.rs]` _(if this exists)_ | +| otherwise | any | same as ID | + +You can overwrite this behaviour by providing a custom mapping function during setup: + +```lua +require("neotest").setup({ + adapters = { + require("neotest-rust") { + parameterized_test_discovery = "cargo" + resolve_case_name = function(id, macro, file) + -- id: string the test case identifier returned by `cargo nextest list` + -- macro: string the (first) macro name which makes this test parameterized (e.g. `values`, `files`, `test_case`, ...) + -- file: path the path to the file under test + local name = ... + return name + end, + } + } +}) + +``` + + ## Limitations From 903ba8f532d62f7fed2b11b4fb935d2c08aed514 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Fri, 27 Oct 2023 08:24:32 +0200 Subject: [PATCH 23/32] ADD: `set_param_discovery()` function to `adapter` --- README.md | 8 +++++++- lua/neotest-rust/init.lua | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b358f53..51bd4c0 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,13 @@ engines to generate the test cases. `neotest-rust` currently supports: * [rstest](https://crates.io/crates/rstest) * [test_case](https://crates.io/crates/test-case) -To discover parameterized tests `neotest-rust` offers two discovery strategies, which you can choose by setting the `parameterized_test_discovery` option during setup (or choose `none` to disable them entirely). Both have their unique characteristics: +To discover parameterized tests `neotest-rust` offers two discovery strategies, which you can choose by setting the `parameterized_test_discovery` option during setup (or choose `none` to disable them entirely). Alternatively you can call this lua function to set the discovery mode: + +```lua +require("neotest-rust").set_param_discovery("treesitter") +``` + +Both strategies have their unique characteristics: | `parameterized_test_discovery` | `"treesitter"` | `"cargo"` | |:---------|:------------|:------| diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 63303c1..2b3f6d4 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -68,6 +68,18 @@ end local param_discovery +function adapter.set_param_discovery(strategy) + if strategy ~= "treesitter" and strategy ~= "cargo" and strategy ~= "none" then + lib.notify( + "Unsupported value `" + .. tostring(strategy) + .. "` for parameterized_test_discovery. Provide one of {`treesitter`, `cargo`, `none`}" + ) + return + end + param_discovery = strategy +end + local is_callable = function(obj) return type(obj) == "function" or (type(obj) == "table" and obj.__call) end From 95b5a132881d2d201b292631fc26234aba0429a2 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Fri, 27 Oct 2023 08:26:08 +0200 Subject: [PATCH 24/32] FIX: Discovery not recognizing `#[test_case]` anymore Somehow the alternative [(...) (...)] syntax of treesitter does not work with our query to match both scoped and unscoped identifiers. Quick solution is just to duplicate both parts of the entire query and provide them as separate patterns --- lua/neotest-rust/discovery.lua | 45 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua index 96edadf..85afca3 100644 --- a/lua/neotest-rust/discovery.lua +++ b/lua/neotest-rust/discovery.lua @@ -9,26 +9,34 @@ local M = {} --- @return string local function build_query(test) return [[ +;; Matches `#[case::]*fn ()` (rstest) +( + (attribute_item + (attribute + (scoped_identifier + path: (identifier) @macro + name: (identifier) @test.name + ) (#eq? @macro "case") + ) + ) @test.definition + . + [ + (line_comment) + (attribute_item) + ]* + . + (function_item name: (identifier) @parent) (#eq? @parent "]] .. test .. [[") +) + ;; Matches `#[test_case(... ; "")]*fn ()` (test_case) ;; ... or `#[case(...)]*fn ()` (rstest) -;; ... or `#[case::]*fn ()` (rstest) ( - [ - (attribute_item - (attribute - (identifier) @macro (#any-of? @macro "test_case" "case") - arguments: (token_tree ((_) (string_literal)? @test.name . )) - ) - ) - (attribute_item - (attribute - (scoped_identifier - path: (identifier) @macro - name: (identifier) @test.name - ) (#eq? @macro "case") - ) - ) - ] @test.definition + (attribute_item + (attribute + (identifier) @macro (#any-of? @macro "test_case" "case") + arguments: (token_tree ((_) (string_literal)? @test.name . )) + ) + ) @test.definition . [ (line_comment) @@ -73,7 +81,7 @@ end --- @param positions neotest.Tree of already parsed namespaces and tests (without parameterized tests) --- @return neotest.Tree `positions` with additional leafs for parameterized tests function M.treesitter(path, positions) - local content = lib.files.read(path, positions) + local content = lib.files.read(path) local root, lang = lib.treesitter.get_parse_root(path, content, { fast = true }) for _, value in positions:iter_nodes() do local data = value:data() @@ -81,6 +89,7 @@ function M.treesitter(path, positions) if data.parameterization ~= nil then local q = lib.treesitter.normalise_query(lang, query) local case_index = 1 + for _, match in q:iter_matches(root, content) do local captured_nodes = {} for i, capture in ipairs(q.captures) do From 103e3fa2bb9119a09f7f5ef0d04c760a2b6722f7 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 26 Oct 2023 14:12:41 +0200 Subject: [PATCH 25/32] ADD: Tests for new parameterized test discovery module --- lua/neotest-rust/discovery.lua | 21 +- tests/data/rs-test/src/lib.rs | 2 - tests/discovery_spec.lua | 1235 ++++++++++++++++++++++++++++++++ 3 files changed, 1248 insertions(+), 10 deletions(-) create mode 100644 tests/discovery_spec.lua diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua index 85afca3..8b3c29d 100644 --- a/lua/neotest-rust/discovery.lua +++ b/lua/neotest-rust/discovery.lua @@ -49,7 +49,7 @@ local function build_query(test) end -- See https://github.com/frondeus/test-case/blob/master/crates/test-case-core/src/utils.rs#L4 -local function escape_testcase_name(name) +function M._escape_testcase_name(name) name = name:gsub('"', "") -- remove any surrounding dquotes from string literal if name == nil or name == "" then return "_empty" @@ -85,8 +85,8 @@ function M.treesitter(path, positions) local root, lang = lib.treesitter.get_parse_root(path, content, { fast = true }) for _, value in positions:iter_nodes() do local data = value:data() - local query = build_query(data.name) if data.parameterization ~= nil then + local query = build_query(data.name) local q = lib.treesitter.normalise_query(lang, query) local case_index = 1 @@ -106,7 +106,7 @@ function M.treesitter(path, positions) if macro == "test_case" then -- #[test_case(arg1, arg2, ... ; "foo bar")] -> test.name = "foo bar" name = name:gsub('"', "") -- remove any surrounding dquotes from string literal - id = escape_testcase_name(name) + id = M._escape_testcase_name(name) end if macro == "case" then -- #[case::foo_bar(arg1, arg2, ...)] -> test.name = "foo_bar" @@ -141,9 +141,9 @@ end --- binary: /src/bin/.rs -> ::bin/ --- example: /examples/.rs -> ::example/ --- @param path string +--- @param workspace string|nil root of the project (containing Cargo.toml) --- @return string|nil -local function binary_name(path) - local workspace = lib.files.match_root_pattern("Cargo.toml")(path) +function M._binary_name(path, workspace) local parts = Path:new(workspace):_split() if parts == nil or #parts == 0 then return nil @@ -152,7 +152,7 @@ local function binary_name(path) path = Path:new(path):make_relative(workspace):gsub(".rs$", "") if path:match("^src" .. Path.path.sep .. "bin") then -- tests in binary - return package .. "::" .. path:gsub("^src" .. Path.path.sep) + return package .. "::" .. path:gsub("^src" .. Path.path.sep, "") end if path:match("^tests") then -- integration test @@ -282,7 +282,11 @@ end --- @return neotest.Tree `positions` with additional leafs for parameterized tests function M.cargo(path, positions, name_mapper) name_mapper = name_mapper or M.resolve_case_name - local command = "cargo nextest list --message-format json" + local workspace = lib.files.match_root_pattern("Cargo.toml")(path) + local command = "cargo nextest list --message-format json --manifest-path " + .. workspace + .. Path.path.sep + .. "Cargo.toml" local result = { lib.process.run(vim.split(command, "%s+"), { stdout = true, stderr = true }) } local code = result[1] @@ -301,7 +305,8 @@ function M.cargo(path, positions, name_mapper) end table.sort(tests[key]) end - local target = binary_name(path) + + local target = M._binary_name(path, workspace) if target == nil then return positions end diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index 88913ea..740319f 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -61,8 +61,6 @@ mod tests { #[rstest] #[case::one(1)] #[case::two(2)] - #[case::three(3)] - #[case(4)] #[case::ten(10)] fn parameterized_with_descriptions(#[case] x: u64) { assert!(x < 10) diff --git a/tests/discovery_spec.lua b/tests/discovery_spec.lua new file mode 100644 index 0000000..2445727 --- /dev/null +++ b/tests/discovery_spec.lua @@ -0,0 +1,1235 @@ +local async = require("nio.tests") +local discovery = require("neotest-rust.discovery") +local plugin = require("neotest-rust") +local lib = require("neotest.lib") +local Tree = require("neotest.types.tree") +local Path = require("plenary.path") +local say = require("say") + +describe("escape_testcase_name", function() + describe("converts", function() + it("single word", function() + assert.equals("word", discovery._escape_testcase_name("word")) + end) + it("simple sentence", function() + assert.equals("a_simple_sentence", discovery._escape_testcase_name("a simple sentence")) + end) + it("converts extra spaces inbetween", function() + assert.equals("extra_spaces_inbetween", discovery._escape_testcase_name("extra spaces inbetween")) + end) + it("extra end and start spaces", function() + assert.equals( + "_extra_end_and_start_spaces_", + discovery._escape_testcase_name(" extra end and start spaces ") + ) + end) + it("alphabet", function() + assert.equals( + "abcdefghijklmnoqprstuwvxyz1234567890", + discovery._escape_testcase_name("abcdefghijklmnoqprstuwvxyz1234567890") + ) + end) + end) + describe("converts to lowercase", function() + it("ALL UPPER", function() + assert.equals("all_upper", discovery._escape_testcase_name("ALL_UPPER")) + end) + it("MiXeD CaSe", function() + assert.equals("mixed_case", discovery._escape_testcase_name("MiXeD CaSe")) + end) + end) + it("handles numeric first char", function() + assert.equals("_1test", discovery._escape_testcase_name("1test")) + end) + it("omits unicode", function() + assert.equals("from_to", discovery._escape_testcase_name("from⟶to")) + end) + it("handles empty input", function() + assert.equals("_empty", discovery._escape_testcase_name("")) + end) +end) + +describe("binary_path", function() + local function workspace() + return vim.fn.expand("%:p:h") .. "/package" + end + + it("checks module file", function() + assert.equals("package", discovery._binary_name(workspace() .. "/src/foo.rs", workspace())) + end) + + it("checks integration test file", function() + assert.equals("package::foo", discovery._binary_name(workspace() .. "/tests/foo.rs", workspace())) + end) + + it("checks binary file", function() + assert.equals("package::bin/foo", discovery._binary_name(workspace() .. "/src/bin/foo.rs", workspace())) + end) + + it("checks example file", function() + assert.equals("package::example/foo", discovery._binary_name(workspace() .. "/examples/foo.rs", workspace())) + end) + + it("returns nil for unknown path", function() + assert.equals(nil, discovery._binary_name(workspace() .. "foo/bar")) + end) +end) + +describe("resolve_case_name", function() + local function p() + return Path:new("") + end + describe("#[values]", function() + it("resolves numeric parameter", function() + assert.equals("foo[42]", discovery.resolve_case_name("foo_1_42", "values", p())) + assert.equals("foo[43]", discovery.resolve_case_name("foo_2_43", "values", p())) + assert.equals("foo[44]", discovery.resolve_case_name("foo_10_44", "values", p())) + end) + it("resolves string parameter", function() + assert.equals('blub["foo"]', discovery.resolve_case_name("blub_1___foo__", "values", p())) + assert.equals('blub["bar"]', discovery.resolve_case_name("blub_2___bar__", "values", p())) + assert.equals('blub["baz"]', discovery.resolve_case_name("blub_10___baz__", "values", p())) + end) + + it("resolves multiple value parameter sets", function() + assert.equals('foo["a"] bar[7]', discovery.resolve_case_name("foo_1___a__::bar_1_7", "values", p())) + assert.equals('foo["b"] bar[8]', discovery.resolve_case_name("foo_2___b__::bar_2_8", "values", p())) + assert.equals('foo["Z"] bar[9]', discovery.resolve_case_name("foo_20___Z__::bar_20_9", "values", p())) + end) + end) + + describe("#[case]", function() + it("strips 'case_X' prefix", function() + assert.equals("foo_bar", discovery.resolve_case_name("case_1_foo_bar", "case", p())) + assert.equals("foo_bar", discovery.resolve_case_name("case_10_foo_bar", "case", p())) + end) + it("keeps 'case_X' prefix if no description provided", function() + assert.equals("case_1", discovery.resolve_case_name("case_1", "case", p())) + assert.equals("case_42", discovery.resolve_case_name("case_42", "case", p())) + end) + end) + + describe("#[files]", function() + local function root() + -- path to `rs-test/` crate + return Path:new(vim.loop.cwd() .. "/tests/data/rs-test/") + end + it("resolves file", function() + assert.equals("file[Cargo.toml]", discovery.resolve_case_name("file_1_Cargo_toml", "files", root())) + assert.equals("file[Cargo.lock]", discovery.resolve_case_name("file_22_Cargo_lock", "files", root())) + end) + it("resolves folder", function() + assert.equals("file[src]", discovery.resolve_case_name("file_2_src", "files", root())) + end) + it("resolves nested file", function() + assert.equals("file[src/lib.rs]", discovery.resolve_case_name("file_1_src/lib.rs", "files", root())) + assert.equals("file[src/foo.txt]", discovery.resolve_case_name("file_22_src_foo_txt", "files", root())) + assert.equals( + "file[src/bar/b_a_z.txt]", + discovery.resolve_case_name("file_202_src_bar_b_a_z_txt", "files", root()) + ) + end) + it("resolves nested folder", function() + assert.equals("file[src/bar]", discovery.resolve_case_name("file_3_src_bar", "files", root())) + end) + it("isn't capable to resolve paths with special chars other than '_'", function() + -- To bad =( + assert.not_equals("file[foo-bar.txt]", discovery.resolve_case_name("file_3_foo_bar_txt", "files", root())) + end) + end) +end) + +describe("discovery", function() + -- Helper functions + local function relative(file) + return vim.loop.cwd() .. "/" .. file + end + + local function discover(strategy, file, pred) + plugin.set_param_discovery(strategy) + local tree = plugin.discover_positions(file) + local tests = {} + for _, node in tree:iter_nodes() do + local data = node:data() + if pred(data) then + table.insert(tests, data) + end + end + return tests + end + + local function with_type(t) + return function(n) + return n.type == t + end + end + local function with_id(pattern) + return function(n) + return string.match(n.id, pattern) ~= nil + end + end + + describe("testcase/src/lib.rs", function() + local file = relative("tests/data/testcase/src/lib.rs") + describe("strategy=`treesitter`", function() + local strategy = "treesitter" + async.it("has namespace `tests`", function() + local namespaces = discover(strategy, file, with_type("namespace")) + assert.are.same(namespaces, { + { + path = file, + id = "tests", + name = "tests", + type = "namespace", + range = { 1, 0, 29, 1 }, + parameterization = nil, + }, + }) + end) + + async.it("has tests named `first`", function() + assert.is.same(discover(strategy, file, with_id("^tests::first$")), { + { + id = "tests::first", + name = "first", + path = file, + type = "test", + range = { 10, 4, 12, 5 }, + parameterization = "test_case", + }, + }) + end) + + async.it("has tests named `second`", function() + assert.is.same(discover(strategy, file, with_id("^tests::second$")), { + { + id = "tests::second", + name = "second", + path = file, + type = "test", + range = { 18, 4, 20, 5 }, + parameterization = '#[test_case(false ; "no")]', + }, + }) + end) + + async.it("has tests named `third`", function() + assert.is.same(discover(strategy, file, with_id("^tests::third$")), { + { + id = "tests::third", + name = "third", + path = file, + type = "test", + range = { 26, 4, 28, 5 }, + parameterization = '#[test_case(false ; "no")]', + }, + }) + end) + + async.it("`first` test has five cases", function() + local tests = discover(strategy, file, with_id("^tests::first::.*$")) + assert.are.same(tests, { + { id = "tests::first::_empty", name = "", path = file, type = "test", range = { 4, 4, 4, 24 } }, + { id = "tests::first::one", name = "one", path = file, type = "test", range = { 5, 4, 5, 27 } }, + { + id = "tests::first::name_with_spaces", + name = "name with spaces", + path = file, + type = "test", + range = { 6, 4, 6, 40 }, + }, + { + id = "tests::first::mixed_case", + name = "MixEd-CaSe", + path = file, + type = "test", + range = { 8, 4, 8, 34 }, + }, + { + id = "tests::first::sp3_a_ar5", + name = "sp3(|a/-(ar5", + path = file, + type = "test", + range = { 9, 4, 9, 36 }, + }, + }) + end) + + async.it("`second` test has two cases", function() + local tests = discover(strategy, file, with_id("^tests::second::.*$")) + assert.are.same(tests, { + { id = "tests::second::yes", name = "yes", path = file, type = "test", range = { 14, 4, 14, 30 } }, + { id = "tests::second::no", name = "no", path = file, type = "test", range = { 16, 4, 16, 30 } }, + }) + end) + + async.it("`third` test has three cases", function() + local tests = discover(strategy, file, with_id("^tests::third::.*$")) + assert.are.same(tests, { + { id = "tests::third::yes", name = "yes", path = file, type = "test", range = { 22, 4, 22, 30 } }, + { id = "tests::third::no", name = "no", path = file, type = "test", range = { 24, 4, 24, 30 } }, + }) + end) + end) + + describe("strategy=`cargo`", function() + local strategy = "cargo" + + async.it("has namespace `tests`", function() + local namespaces = discover(strategy, file, with_type("namespace")) + assert.are.same(namespaces, { + { + path = file, + id = "tests", + name = "tests", + type = "namespace", + range = { 1, 0, 29, 1 }, + parameterization = nil, + }, + }) + end) + + async.it("has tests named `first`", function() + assert.is.same(discover(strategy, file, with_id("^tests::first$")), { + { + id = "tests::first", + name = "first", + path = file, + type = "test", + range = { 10, 4, 12, 5 }, + parameterization = "test_case", + }, + }) + -- + end) + + async.it("named `second`", function() + assert.is.same(discover(strategy, file, with_id("^tests::second$")), { + { + id = "tests::second", + name = "second", + path = file, + type = "test", + range = { 18, 4, 20, 5 }, + parameterization = '#[test_case(false ; "no")]', + }, + }) + end) + async.it("named `third`", function() + assert.is.same(discover(strategy, file, with_id("^tests::third$")), { + { + id = "tests::third", + name = "third", + path = file, + type = "test", + range = { 26, 4, 28, 5 }, + parameterization = '#[test_case(false ; "no")]', + }, + }) + end) + + async.it("`first` test has five cases", function() + local tests = discover(strategy, file, with_id("^tests::first::.*$")) + assert.are.same(tests, { + { + id = "tests::first::_empty", + name = "_empty", + path = file, + type = "test", + range = { 10, 4, 12, 5 }, + }, + { + id = "tests::first::mixed_case", + name = "mixed_case", + path = file, + type = "test", + range = { 10, 4, 12, 5 }, + }, + { + id = "tests::first::name_with_spaces", + name = "name_with_spaces", + path = file, + type = "test", + range = { 10, 4, 12, 5 }, + }, + { id = "tests::first::one", name = "one", path = file, type = "test", range = { 10, 4, 12, 5 } }, + { + id = "tests::first::sp3_a_ar5", + name = "sp3_a_ar5", + path = file, + type = "test", + range = { 10, 4, 12, 5 }, + }, + }) + end) + + async.it("`second` test has two cases", function() + local tests = discover(strategy, file, with_id("^tests::second::.*$")) + assert.are.same(tests, { + { id = "tests::second::no", name = "no", path = file, type = "test", range = { 18, 4, 20, 5 } }, + { id = "tests::second::yes", name = "yes", path = file, type = "test", range = { 18, 4, 20, 5 } }, + }) + end) + + async.it("`third` test has three cases", function() + local tests = discover(strategy, file, with_id("^tests::third::.*$")) + assert.are.same(tests, { + { id = "tests::third::no", name = "no", path = file, type = "test", range = { 26, 4, 28, 5 } }, + { id = "tests::third::yes", name = "yes", path = file, type = "test", range = { 26, 4, 28, 5 } }, + }) + end) + end) + end) + + describe("rs-test/src/lib.rs", function() + local file = relative("tests/data/rs-test/src/lib.rs") + describe("strategy=`treesitter`", function() + local strategy = "treesitter" + async.it("has namespace `tests`", function() + assert.is.same(discover(strategy, file, with_type("namespace")), { + { + path = file, + id = "tests", + name = "tests", + type = "namespace", + range = { 1, 0, 138, 1 }, + parameterization = nil, + }, + }) + end) + + describe("contains test...", function() + async.it("`timeout`", function() + assert.is.same(discover(strategy, file, with_id("^tests::timeout$")), { + { + id = "tests::timeout", + name = "timeout", + path = file, + type = "test", + range = { 7, 4, 10, 5 }, + parameterization = "", + }, + }) + end) + + async.it("`fixture_injected`", function() + assert.is.same(discover(strategy, file, with_id("^tests::fixture_injected$")), { + { + id = "tests::fixture_injected", + name = "fixture_injected", + path = file, + type = "test", + range = { 17, 4, 19, 5 }, + parameterization = "", + }, + }) + end) + + async.it("`fixture_rename`", function() + assert.is.same(discover(strategy, file, with_id("^tests::fixture_rename$")), { + { + id = "tests::fixture_rename", + name = "fixture_rename", + path = file, + type = "test", + range = { 26, 4, 28, 5 }, + parameterization = "from", + }, + }) + end) + + async.it("`fixture_partial_injection`", function() + assert.is.same(discover(strategy, file, with_id("^tests::fixture_partial_injection$")), { + { + id = "tests::fixture_partial_injection", + name = "fixture_partial_injection", + path = file, + type = "test", + range = { 36, 4, 38, 5 }, + parameterization = "with", + }, + }) + end) + + async.it("`fixture_async`", function() + assert.is.same(discover(strategy, file, with_id("^tests::fixture_async$")), { + { + id = "tests::fixture_async", + name = "fixture_async", + path = file, + type = "test", + range = { 46, 4, 48, 5 }, + parameterization = "future", + }, + }) + end) + + async.it("`parameterized`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized$")), { + { + id = "tests::parameterized", + name = "parameterized", + path = file, + type = "test", + range = { 56, 4, 58, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_timeout`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_timeout$")), { + { + id = "tests::parameterized_timeout", + name = "parameterized_timeout", + path = file, + type = "test", + range = { 109, 4, 112, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_with_descriptions`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_with_descriptions$")), { + { + id = "tests::parameterized_with_descriptions", + name = "parameterized_with_descriptions", + path = file, + type = "test", + range = { 64, 4, 66, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_tokio`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_tokio$")), { + { + id = "tests::parameterized_tokio", + name = "parameterized_tokio", + path = file, + type = "test", + range = { 75, 4, 77, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_async_std`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_async_std$")), { + { + id = "tests::parameterized_async_std", + name = "parameterized_async_std", + path = file, + type = "test", + range = { 86, 4, 88, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_async_parameter`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_async_parameter$")), { + { + id = "tests::parameterized_async_parameter", + name = "parameterized_async_parameter", + path = file, + type = "test", + range = { 95, 4, 102, 5 }, + parameterization = "", + }, + }) + end) + + async.it("`parameterized_async_timeout`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_async_timeout$")), { + { + id = "tests::parameterized_async_timeout", + name = "parameterized_async_timeout", + path = file, + type = "test", + range = { 120, 4, 126, 5 }, + parameterization = "case", + }, + }) + end) + end) + + describe("contains cases...", function() + async.it("4 x `parameterized`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized::.*$")), { + { + id = "tests::parameterized::case_1", + name = "case_1", + path = file, + type = "test", + range = { 51, 4, 51, 14 }, + }, + { + id = "tests::parameterized::case_2", + name = "case_2", + path = file, + type = "test", + range = { 52, 4, 52, 14 }, + }, + { + id = "tests::parameterized::case_3", + name = "case_3", + path = file, + type = "test", + range = { 53, 4, 53, 14 }, + }, + { + id = "tests::parameterized::case_4", + name = "case_4", + path = file, + type = "test", + range = { 55, 4, 55, 15 }, + }, + }) + end) + + async.it("3 x `parameterized_with_descriptions`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_with_descriptions::.*$")), { + { + id = "tests::parameterized_with_descriptions::case_1_one", + name = "one", + path = file, + type = "test", + range = { 61, 4, 61, 19 }, + }, + { + id = "tests::parameterized_with_descriptions::case_2_two", + name = "two", + path = file, + type = "test", + range = { 62, 4, 62, 19 }, + }, + { + id = "tests::parameterized_with_descriptions::case_3_ten", + name = "ten", + path = file, + type = "test", + range = { 63, 4, 63, 20 }, + }, + }) + end) + async.it("4 x `parameterized_tokio`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_tokio::.*$")), { + { + id = "tests::parameterized_tokio::case_1", + name = "case_1", + path = file, + type = "test", + range = { 69, 4, 69, 14 }, + }, + { + id = "tests::parameterized_tokio::case_2", + name = "case_2", + path = file, + type = "test", + range = { 71, 4, 71, 14 }, + }, + { + id = "tests::parameterized_tokio::case_3", + name = "case_3", + path = file, + type = "test", + range = { 72, 4, 72, 14 }, + }, + { + id = "tests::parameterized_tokio::case_4", + name = "case_4", + path = file, + type = "test", + range = { 73, 4, 73, 15 }, + }, + }) + end) + + async.it("4 x `parameterized_async_std`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_async_std::.*$")), { + { + id = "tests::parameterized_async_std::case_1", + name = "case_1", + path = file, + type = "test", + range = { 80, 4, 80, 14 }, + }, + { + id = "tests::parameterized_async_std::case_2", + name = "case_2", + path = file, + type = "test", + range = { 82, 4, 82, 14 }, + }, + { + id = "tests::parameterized_async_std::case_3", + name = "case_3", + path = file, + type = "test", + range = { 83, 4, 83, 14 }, + }, + { + id = "tests::parameterized_async_std::case_4", + name = "case_4", + path = file, + type = "test", + range = { 84, 4, 84, 15 }, + }, + }) + end) + + async.it("2 x `parameterized_async_parameter`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_async_parameter::.*$")), { + { + id = "tests::parameterized_async_parameter::case_1_even", + name = "even", + path = file, + type = "test", + range = { 91, 4, 91, 30 }, + }, + { + id = "tests::parameterized_async_parameter::case_2_odd", + name = "odd", + path = file, + type = "test", + range = { 93, 4, 93, 29 }, + }, + }) + end) + + async.it("2 x `parameterized_timeout`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_timeout::.*$")), { + { + id = "tests::parameterized_timeout::case_1_pass", + name = "pass", + path = file, + type = "test", + range = { 105, 4, 105, 43 }, + }, + { + id = "tests::parameterized_timeout::case_2_fail", + name = "fail", + path = file, + type = "test", + range = { 107, 4, 107, 44 }, + }, + }) + end) + + async.it("3 x `parameterized_async_timeout`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_async_timeout::.*$")), { + { + id = "tests::parameterized_async_timeout::case_1_pass", + name = "pass", + path = file, + type = "test", + range = { 115, 4, 115, 46 }, + }, + { + id = "tests::parameterized_async_timeout::case_2_fail_timeout", + name = "fail_timeout", + path = file, + type = "test", + range = { 117, 4, 117, 55 }, + }, + { + id = "tests::parameterized_async_timeout::case_3_fail_value", + name = "fail_value", + path = file, + type = "test", + range = { 118, 4, 118, 52 }, + }, + }) + end) + end) + end) + + describe("strategy=`cargo`", function() + local strategy = "cargo" + async.it("has namespace `tests`", function() + assert.are.same(discover(strategy, file, with_type("namespace")), { + { + path = file, + id = "tests", + name = "tests", + type = "namespace", + range = { 1, 0, 138, 1 }, + parameterization = nil, + }, + }) + end) + + describe("contains test...", function() + async.it("`timeout`", function() + assert.is.same(discover(strategy, file, with_id("^tests::timeout$")), { + { + id = "tests::timeout", + name = "timeout", + path = file, + type = "test", + range = { 7, 4, 10, 5 }, + parameterization = "", + }, + }) + end) + + async.it("`fixture_injected`", function() + assert.is.same(discover(strategy, file, with_id("^tests::fixture_injected$")), { + { + id = "tests::fixture_injected", + name = "fixture_injected", + path = file, + type = "test", + range = { 17, 4, 19, 5 }, + parameterization = "", + }, + }) + end) + + async.it("`fixture_rename`", function() + assert.is.same(discover(strategy, file, with_id("^tests::fixture_rename$")), { + { + id = "tests::fixture_rename", + name = "fixture_rename", + path = file, + type = "test", + range = { 26, 4, 28, 5 }, + parameterization = "from", + }, + }) + end) + + async.it("`fixture_partial_injection`", function() + assert.is.same(discover(strategy, file, with_id("^tests::fixture_partial_injection$")), { + { + id = "tests::fixture_partial_injection", + name = "fixture_partial_injection", + path = file, + type = "test", + range = { 36, 4, 38, 5 }, + parameterization = "with", + }, + }) + end) + + async.it("`fixture_async`", function() + assert.is.same(discover(strategy, file, with_id("^tests::fixture_async$")), { + { + id = "tests::fixture_async", + name = "fixture_async", + path = file, + type = "test", + range = { 46, 4, 48, 5 }, + parameterization = "future", + }, + }) + end) + + async.it("`parameterized`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized$")), { + { + id = "tests::parameterized", + name = "parameterized", + path = file, + type = "test", + range = { 56, 4, 58, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_with_descriptions`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_with_descriptions$")), { + { + id = "tests::parameterized_with_descriptions", + name = "parameterized_with_descriptions", + path = file, + type = "test", + range = { 64, 4, 66, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_tokio`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_tokio$")), { + { + id = "tests::parameterized_tokio", + name = "parameterized_tokio", + path = file, + type = "test", + range = { 75, 4, 77, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_async_std`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_async_std$")), { + { + id = "tests::parameterized_async_std", + name = "parameterized_async_std", + path = file, + type = "test", + range = { 86, 4, 88, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_async_parameter`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_async_parameter$")), { + { + id = "tests::parameterized_async_parameter", + name = "parameterized_async_parameter", + path = file, + type = "test", + range = { 95, 4, 102, 5 }, + parameterization = "", + }, + }) + end) + + async.it("`parameterized_timeout`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_timeout$")), { + { + id = "tests::parameterized_timeout", + name = "parameterized_timeout", + path = file, + type = "test", + range = { 109, 4, 112, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`parameterized_async_timeout`", function() + assert.is.same(discover(strategy, file, with_id("^tests::parameterized_async_timeout$")), { + { + id = "tests::parameterized_async_timeout", + name = "parameterized_async_timeout", + path = file, + type = "test", + range = { 120, 4, 126, 5 }, + parameterization = "case", + }, + }) + end) + + async.it("`combinations`", function() + assert.is.same(discover(strategy, file, with_id("^tests::combinations$")), { + { + id = "tests::combinations", + name = "combinations", + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + parameterization = "values", + }, + }) + end) + + async.it("`files`", function() + assert.is.same(discover(strategy, file, with_id("^tests::files$")), { + { + id = "tests::files", + name = "files", + path = file, + type = "test", + range = { 135, 4, 137, 5 }, + parameterization = "files", + }, + }) + end) + end) + + describe("contains cases...", function() + async.it("4 x `parameterized`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized::.*$")), { + { + id = "tests::parameterized::case_1", + name = "case_1", + path = file, + type = "test", + range = { 56, 4, 58, 5 }, + }, + { + id = "tests::parameterized::case_2", + name = "case_2", + path = file, + type = "test", + range = { 56, 4, 58, 5 }, + }, + { + id = "tests::parameterized::case_3", + name = "case_3", + path = file, + type = "test", + range = { 56, 4, 58, 5 }, + }, + { + id = "tests::parameterized::case_4", + name = "case_4", + path = file, + type = "test", + range = { 56, 4, 58, 5 }, + }, + }) + end) + + async.it("3 x `parameterized_with_descriptions`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_with_descriptions::.*$")), { + { + id = "tests::parameterized_with_descriptions::case_1_one", + name = "one", + path = file, + type = "test", + range = { 64, 4, 66, 5 }, + }, + { + id = "tests::parameterized_with_descriptions::case_2_two", + name = "two", + path = file, + type = "test", + range = { 64, 4, 66, 5 }, + }, + { + id = "tests::parameterized_with_descriptions::case_3_ten", + name = "ten", + path = file, + type = "test", + range = { 64, 4, 66, 5 }, + }, + }) + end) + + async.it("4 x `parameterized_tokio`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_tokio::.*$")), { + { + id = "tests::parameterized_tokio::case_1", + name = "case_1", + path = file, + type = "test", + range = { 75, 4, 77, 5 }, + }, + { + id = "tests::parameterized_tokio::case_2", + name = "case_2", + path = file, + type = "test", + range = { 75, 4, 77, 5 }, + }, + { + id = "tests::parameterized_tokio::case_3", + name = "case_3", + path = file, + type = "test", + range = { 75, 4, 77, 5 }, + }, + { + id = "tests::parameterized_tokio::case_4", + name = "case_4", + path = file, + type = "test", + range = { 75, 4, 77, 5 }, + }, + }) + end) + + async.it("4 x `parameterized_async_std`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_async_std::.*$")), { + { + id = "tests::parameterized_async_std::case_1", + name = "case_1", + path = file, + type = "test", + range = { 86, 4, 88, 5 }, + }, + { + id = "tests::parameterized_async_std::case_2", + name = "case_2", + path = file, + type = "test", + range = { 86, 4, 88, 5 }, + }, + { + id = "tests::parameterized_async_std::case_3", + name = "case_3", + path = file, + type = "test", + range = { 86, 4, 88, 5 }, + }, + { + id = "tests::parameterized_async_std::case_4", + name = "case_4", + path = file, + type = "test", + range = { 86, 4, 88, 5 }, + }, + }) + end) + + async.it("2 x `parameterized_async_parameter`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_async_parameter::.*$")), { + { + id = "tests::parameterized_async_parameter::case_1_even", + name = "case_1_even", + path = file, + type = "test", + range = { 95, 4, 102, 5 }, + }, + { + id = "tests::parameterized_async_parameter::case_2_odd", + name = "case_2_odd", + path = file, + type = "test", + range = { 95, 4, 102, 5 }, + }, + }) + end) + + async.it("2 x `parameterized_timeout`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_timeout::.*$")), { + { + id = "tests::parameterized_timeout::case_1_pass", + name = "pass", + path = file, + type = "test", + range = { 109, 4, 112, 5 }, + }, + { + id = "tests::parameterized_timeout::case_2_fail", + name = "fail", + path = file, + type = "test", + range = { 109, 4, 112, 5 }, + }, + }) + end) + + async.it("3 x `parameterized_async_timeout`", function() + assert.are.same(discover(strategy, file, with_id("^tests::parameterized_async_timeout::.*$")), { + { + id = "tests::parameterized_async_timeout::case_1_pass", + name = "pass", + path = file, + type = "test", + range = { 120, 4, 126, 5 }, + }, + { + id = "tests::parameterized_async_timeout::case_2_fail_timeout", + name = "fail_timeout", + path = file, + type = "test", + range = { 120, 4, 126, 5 }, + }, + { + id = "tests::parameterized_async_timeout::case_3_fail_value", + name = "fail_value", + path = file, + type = "test", + range = { 120, 4, 126, 5 }, + }, + }) + end) + + async.it("9 x `combinations`", function() + assert.are.same(discover(strategy, file, with_id("^tests::combinations::.*$")), { + { + id = "tests::combinations::word_1___a__::has_chars_1_1", + name = 'word["a"] has_chars[1]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + { + id = "tests::combinations::word_1___a__::has_chars_2_2", + name = 'word["a"] has_chars[2]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + { + id = "tests::combinations::word_1___a__::has_chars_3_3", + name = 'word["a"] has_chars[3]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + { + id = "tests::combinations::word_2___bb__::has_chars_1_1", + name = 'word["bb"] has_chars[1]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + { + id = "tests::combinations::word_2___bb__::has_chars_2_2", + name = 'word["bb"] has_chars[2]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + { + id = "tests::combinations::word_2___bb__::has_chars_3_3", + name = 'word["bb"] has_chars[3]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + { + id = "tests::combinations::word_3___ccc__::has_chars_1_1", + name = 'word["ccc"] has_chars[1]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + { + id = "tests::combinations::word_3___ccc__::has_chars_2_2", + name = 'word["ccc"] has_chars[2]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + { + id = "tests::combinations::word_3___ccc__::has_chars_3_3", + name = 'word["ccc"] has_chars[3]', + path = file, + type = "test", + range = { 130, 4, 132, 5 }, + }, + }) + end) + + async.it("3 x `files`", function() + assert.is.same(discover(strategy, file, with_id("^tests::files::.*$")), { + { + id = "tests::files::file_1_foo_bar_txt", + name = "file_1_foo_bar_txt", + path = file, + type = "test", + range = { 135, 4, 137, 5 }, + }, + { + id = "tests::files::file_2_src_bar_b_a_z_txt", + name = "file[src/bar/b_a_z.txt]", + path = file, + type = "test", + range = { 135, 4, 137, 5 }, + }, + { + id = "tests::files::file_3_src_foo_txt", + name = "file[src/foo.txt]", + path = file, + type = "test", + range = { 135, 4, 137, 5 }, + }, + }) + end) + end) + end) + end) +end) From 462f6d78a0f5b8392dc2ff4fb2dc9d986ea79972 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Sun, 29 Oct 2023 10:54:52 +0100 Subject: [PATCH 26/32] CI: Install `cargo nextest` This is needed for the new `param_discovery_mode="cargo"` in the tests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69ec9b6..9d9d75b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,11 @@ jobs: run: | mkdir -p _neovim curl -sL https://github.com/neovim/neovim/releases/download/v0.9.0/nvim-linux64.tar.gz | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + cargo install cargo-nextest --locked - name: Run tests run: | export PATH="${PWD}/_neovim/bin:${PATH}" nvim --version + cargo --version + cargo nextest --version make test From ff4cb9eeaa90f97a8f3f4ec1f714183fb791ffb2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 10:35:47 +0000 Subject: [PATCH 27/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lua/neotest-rust/init.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 2b3f6d4..3c1c385 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -192,7 +192,7 @@ local query = [[ ;; Matches `#[test]` ( - (attribute_item + (attribute_item (attribute (identifier) @macro (#eq? @macro "test") ) @@ -206,7 +206,7 @@ local query = [[ ]* . (function_item name: (identifier) @test.name) @test.definition -) +) ;; Matches `#[test_case(...)] fn ()` ( @@ -255,7 +255,7 @@ local query = [[ (attribute_item) ]* . - (function_item + (function_item name: (identifier) @test.name parameters: (parameters . (attribute_item (attribute (identifier) @parameterization ))? ) (#any-of? @parameterization "from" "with" "case" "values" "files" "future") From 785b4b3baa2c6cb3295e231e12f81ac211ef89f5 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 24 Jan 2024 14:43:30 +0100 Subject: [PATCH 28/32] FIX: Support plain async tests without `rstest` or `test_case` attr --- lua/neotest-rust/init.lua | 40 ++++++++++++++++++++++++++++++---- tests/data/rs-test/src/lib.rs | 6 ++--- tests/data/testcase/src/lib.rs | 10 +++++++++ tests/discovery_spec.lua | 40 +++++++++++++++++++++++++++------- 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 3c1c385..e26f6ec 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -190,7 +190,7 @@ local query = [[ ;; Matches mod {} ((mod_item name: (identifier) @namespace.name) @namespace.definition) -;; Matches `#[test]` +;; Matches `#[test] fn ()` ( (attribute_item (attribute (identifier) @macro @@ -205,16 +205,47 @@ local query = [[ (attribute_item (attribute (identifier) @othermacro) (#any-of? @othermacro "should_panic" "ignore")) ]* . - (function_item name: (identifier) @test.name) @test.definition + (function_item + name: (identifier) @test.name + parameters: (parameters) @params + ) @test.definition + (#eq? @params "()") ) -;; Matches `#[test_case(...)] fn ()` +;; Matches `#[{tokio,async_std}::test] async fn ()` +( + (attribute_item + (attribute + (scoped_identifier + path: (identifier) @package + name: (identifier) + ) + ) + ;; all packages which provide a #[::test] macro + (#any-of? @package "tokio" "async_std") + ) + . + (line_comment)* + . + (function_item + (function_modifiers) @modifier + name: (identifier) @test.name + parameters: (parameters) @params + ) @test.definition + (#eq? @modifier "async") + (#eq? @params "()") +) + +;; Matches `#[test_case(...)] fn (...)` ( (attribute_item (attribute (identifier) @parameterization) (#eq? @parameterization "test_case")) . (line_comment)* . - (function_item name: (identifier) @test.name) @test.definition + (function_item + name: (identifier) @test.name + parameters: (parameters (parameter)) + ) @test.definition ) ;; Matches `#[test_case(...)] #[{tokio,async_std}::test] async fn ()` @@ -241,6 +272,7 @@ local query = [[ (function_item (function_modifiers) @modifier name: (identifier) @test.name + parameters: (parameters (parameter)) ) @test.definition (#eq? @modifier "async") ) diff --git a/tests/data/rs-test/src/lib.rs b/tests/data/rs-test/src/lib.rs index 740319f..647d7df 100644 --- a/tests/data/rs-test/src/lib.rs +++ b/tests/data/rs-test/src/lib.rs @@ -42,8 +42,8 @@ mod tests { async fn magic() -> i32 { 42 } + #[rstest] - #[tokio::test] async fn fixture_async(#[future] magic: i32) { assert_eq!(magic.await, 42) } @@ -78,21 +78,21 @@ mod tests { } #[rstest] + #[async_std::test] #[case(0)] // random comment in between #[case(1)] #[case(5)] #[case(42)] - #[async_std::test] async fn parameterized_async_std(#[case] x: u64) { assert!(x < 10) } #[rstest] + #[tokio::test] #[case::even(async { 2 })] // random comment in between #[case::odd(async { 3 })] - #[tokio::test] async fn parameterized_async_parameter( #[future] #[case] diff --git a/tests/data/testcase/src/lib.rs b/tests/data/testcase/src/lib.rs index 811ca91..35c054c 100644 --- a/tests/data/testcase/src/lib.rs +++ b/tests/data/testcase/src/lib.rs @@ -27,4 +27,14 @@ mod tests { async fn third(y: bool) { assert!(y) } + + #[tokio::test] + async fn plain_tokio() { + assert!(true) + } + + #[async_std::test] + async fn plain_async_std() { + assert!(true) + } } diff --git a/tests/discovery_spec.lua b/tests/discovery_spec.lua index 2445727..35ddf3b 100644 --- a/tests/discovery_spec.lua +++ b/tests/discovery_spec.lua @@ -181,7 +181,7 @@ describe("discovery", function() id = "tests", name = "tests", type = "namespace", - range = { 1, 0, 29, 1 }, + range = { 1, 0, 39, 1 }, parameterization = nil, }, }) @@ -226,6 +226,30 @@ describe("discovery", function() }) end) + async.it("has test named `plain_tokio`", function() + assert.is.same(discover(strategy, file, with_id("^tests::plain_tokio$")), { + { + id = "tests::plain_tokio", + name = "plain_tokio", + path = file, + type = "test", + range = { 31, 4, 33, 5 }, + }, + }) + end) + + async.it("has test named `plain_async_std`", function() + assert.is.same(discover(strategy, file, with_id("^tests::plain_async_std")), { + { + id = "tests::plain_async_std", + name = "plain_async_std", + path = file, + type = "test", + range = { 36, 4, 38, 5 }, + }, + }) + end) + async.it("`first` test has five cases", function() local tests = discover(strategy, file, with_id("^tests::first::.*$")) assert.are.same(tests, { @@ -283,7 +307,7 @@ describe("discovery", function() id = "tests", name = "tests", type = "namespace", - range = { 1, 0, 29, 1 }, + range = { 1, 0, 39, 1 }, parameterization = nil, }, }) @@ -655,28 +679,28 @@ describe("discovery", function() name = "case_1", path = file, type = "test", - range = { 80, 4, 80, 14 }, + range = { 81, 4, 81, 14 }, }, { id = "tests::parameterized_async_std::case_2", name = "case_2", path = file, type = "test", - range = { 82, 4, 82, 14 }, + range = { 83, 4, 83, 14 }, }, { id = "tests::parameterized_async_std::case_3", name = "case_3", path = file, type = "test", - range = { 83, 4, 83, 14 }, + range = { 84, 4, 84, 14 }, }, { id = "tests::parameterized_async_std::case_4", name = "case_4", path = file, type = "test", - range = { 84, 4, 84, 15 }, + range = { 85, 4, 85, 15 }, }, }) end) @@ -688,14 +712,14 @@ describe("discovery", function() name = "even", path = file, type = "test", - range = { 91, 4, 91, 30 }, + range = { 92, 4, 92, 30 }, }, { id = "tests::parameterized_async_parameter::case_2_odd", name = "odd", path = file, type = "test", - range = { 93, 4, 93, 29 }, + range = { 94, 4, 94, 29 }, }, }) end) From 3b91983c47d0f92ea90b1ec5be0ddb2271337ffc Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Wed, 24 Jan 2024 14:44:39 +0100 Subject: [PATCH 29/32] FIX: Discovery unit tests in `src/main.rs` correctly Cargo nextest will identify the tests of translation unit `src/main.rs` as `::bin/`, not as unit tests. --- lua/neotest-rust/discovery.lua | 10 ++++++++-- tests/discovery_spec.lua | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lua/neotest-rust/discovery.lua b/lua/neotest-rust/discovery.lua index 8b3c29d..751c3ed 100644 --- a/lua/neotest-rust/discovery.lua +++ b/lua/neotest-rust/discovery.lua @@ -134,11 +134,13 @@ function M.treesitter(path, positions) return positions end ---- Given a certain path to a rust file, guess its [test binary name](https://nexte.st/book/running.html) --- +--- Given a certain `path` to a rust file, the path to its package `workspace` and +--- the contents of the package's `cargo_toml`, guess its [test binary name](https://nexte.st/book/running.html) +--- --- unit tests: /src/ -> --- integration: /tests/.rs -> :: --- binary: /src/bin/.rs -> ::bin/ +--- binary: /src/main.rs -> ::bin/ --- example: /examples/.rs -> ::example/ --- @param path string --- @param workspace string|nil root of the project (containing Cargo.toml) @@ -162,6 +164,10 @@ function M._binary_name(path, workspace) -- tests in example return package .. "::example" .. path:gsub("^examples", "") end + if path:match("^src/main$") then + -- tests in main executable + return package .. "::bin/" .. package + end if path:match("^src") then -- unit test return package diff --git a/tests/discovery_spec.lua b/tests/discovery_spec.lua index 35ddf3b..98d683a 100644 --- a/tests/discovery_spec.lua +++ b/tests/discovery_spec.lua @@ -70,6 +70,10 @@ describe("binary_path", function() assert.equals("package::example/foo", discovery._binary_name(workspace() .. "/examples/foo.rs", workspace())) end) + it("checks main.rs", function() + assert.equals("package::bin/package", discovery._binary_name(workspace() .. "/src/main.rs", workspace())) + end) + it("returns nil for unknown path", function() assert.equals(nil, discovery._binary_name(workspace() .. "foo/bar")) end) From 0c2107f8e468689036267c4a9ddaa4540f6352f2 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Thu, 25 Jan 2024 09:58:20 +0100 Subject: [PATCH 30/32] FIX: Guard against error from `cargo metadata` --- lua/neotest-rust/init.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index e26f6ec..94cf57a 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -25,7 +25,9 @@ local cargo_metadata = setmetatable({}, { args = { "metadata", "--no-deps" }, cwd = cwd, on_exit = function(j, return_val) - metadata = vim.json.decode(j:result()[1]) + if return_val == 0 then + metadata = vim.json.decode(j:result()[1]) + end end, }):sync() self[cwd] = metadata From 8cc3af597da161b59e1f596c467c5af8443eca7b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 09:21:06 +0000 Subject: [PATCH 31/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lua/neotest-rust/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/neotest-rust/init.lua b/lua/neotest-rust/init.lua index 94cf57a..c88362e 100644 --- a/lua/neotest-rust/init.lua +++ b/lua/neotest-rust/init.lua @@ -207,8 +207,8 @@ local query = [[ (attribute_item (attribute (identifier) @othermacro) (#any-of? @othermacro "should_panic" "ignore")) ]* . - (function_item - name: (identifier) @test.name + (function_item + name: (identifier) @test.name parameters: (parameters) @params ) @test.definition (#eq? @params "()") From d6fbf401f4bc7fa0246d43621d8364ae077801f2 Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Mon, 23 Dec 2024 20:57:57 -0800 Subject: [PATCH 32/32] Bump test timeout This is necessary for tests using the `cargo` discovery strategy since they have to build the project. --- scripts/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test b/scripts/test index 31039fd..5fb6ac6 100755 --- a/scripts/test +++ b/scripts/test @@ -3,4 +3,4 @@ set -eu unset CARGO_TARGET_DIR -nvim --noplugin -l tests/busted.lua --output TAP tests/ +PLENARY_TEST_TIMEOUT=20000 nvim --noplugin -l tests/busted.lua --output TAP tests/