From 13b590f2c7a8307d747d9b94857cd7c9c0cee272 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:08:51 +0800 Subject: [PATCH] feat(plugins): let test frameworks select tests to rerun (#2228) * feat(plugins): let test frameworks select tests to rerun * test(smoke): await Rerun Failed Tests --- crates/ide-host/src/plugins/execution.rs | 49 ++++- crates/ide-host/src/plugins/remote_host.rs | 10 + crates/ide-host/tests/plugins.rs | 48 ++++- crates/ide-plugin/src/runtime.rs | 16 ++ crates/ide-plugin/tests/api_compat.rs | 2 + .../tests/fixtures/api14/dist/plugin.wasm | Bin 234726 -> 246745 bytes .../fixtures/api14/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/api14/src/lib.rs | 10 +- .../fixtures/commit/dist/plugin.wasm.sources | 2 +- .../data-tools/dist/plugin.wasm.sources | 2 +- .../detail-views/dist/plugin.wasm.sources | 2 +- .../diagnostics/dist/plugin.wasm.sources | 2 +- .../endpoints/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/execution/dist/plugin.wasm | Bin 199189 -> 204242 bytes .../execution/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/execution/src/lib.rs | 11 + .../file-templates/dist/plugin.wasm.sources | 2 +- .../files-events/dist/plugin.wasm.sources | 2 +- .../grouped-views/dist/plugin.wasm.sources | 2 +- .../fixtures/hello/dist/plugin.wasm.sources | 2 +- .../fixtures/hover/dist/plugin.wasm.sources | 2 +- .../fixtures/indexer/dist/plugin.wasm.sources | 2 +- .../inlay-hints/dist/plugin.wasm.sources | 2 +- .../fixtures/jobs/dist/plugin.wasm.sources | 2 +- .../dist/plugin.wasm.sources | 2 +- .../misbehave/dist/plugin.wasm.sources | 2 +- .../path-events/dist/plugin.wasm.sources | 2 +- .../private-dir/dist/plugin.wasm.sources | 2 +- .../semantic-tokens/dist/plugin.wasm.sources | 2 +- .../settings-pages/dist/plugin.wasm.sources | 2 +- .../fixtures/storage/dist/plugin.wasm.sources | 2 +- .../fixtures/symbols/dist/plugin.wasm.sources | 2 +- .../fixtures/tables/dist/plugin.wasm.sources | 2 +- .../web-content/dist/plugin.wasm.sources | 2 +- .../fixtures/web-log/dist/plugin.wasm.sources | 2 +- .../fixtures/web-pty/dist/plugin.wasm.sources | 2 +- plugins/README.md | 26 ++- .../ai-completion/dist/plugin.wasm.sources | 2 +- .../dist/plugin.wasm.sources | 2 +- .../checkstyle/dist/plugin.wasm.sources | 2 +- .../container-runner/dist/plugin.wasm.sources | 2 +- .../csv-editor/dist/plugin.wasm.sources | 2 +- .../git-toolbox/dist/plugin.wasm.sources | 2 +- .../graphviz/dist/plugin.wasm.sources | 2 +- .../key-promoter/dist/plugin.wasm.sources | 2 +- .../license-header/dist/plugin.wasm.sources | 2 +- .../official/mybatis/dist/plugin.wasm.sources | 2 +- .../regex-tester/dist/plugin.wasm.sources | 2 +- .../solidity/dist/plugin.wasm.sources | 2 +- .../string-tools/dist/plugin.wasm.sources | 2 +- plugins/official/test-runner/README.md | 6 +- plugins/official/test-runner/dist/plugin.wasm | Bin 194786 -> 197749 bytes .../test-runner/dist/plugin.wasm.sources | 2 +- .../test-runner/openidea-example-tests | 8 +- plugins/official/test-runner/src/lib.rs | 12 ++ .../todo-finder/dist/plugin.wasm.sources | 2 +- sdk/rust/openidea-plugin/src/execution.rs | 4 + sdk/rust/openidea-plugin/src/lib.rs | 9 + .../openidea-plugin/tests/api14_stage3.rs | 11 + sdk/rust/openidea-plugin/wit/run.wit | 3 + smoke/run.test.ts | 2 +- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/plugin.rs | 16 ++ src/bindings.ts | 2 + src/features/plugins/backend.ts | 3 + src/features/plugins/runtimeUi.tsx | 3 +- src/features/run/RunView.tsx | 36 +++- src/features/run/TestTreeView.test.tsx | 32 ++- src/features/run/TestTreeView.tsx | 73 +++++-- src/features/run/index.ts | 42 +++- src/features/run/session.test.tsx | 59 +++++- src/features/run/store.ts | 40 +++- src/features/run/testing/rerun.test.ts | 191 ++++++++++++------ src/features/run/testing/rerun.ts | 34 +++- src/workbench/contributions.ts | 13 ++ wit/run.wit | 3 + 76 files changed, 693 insertions(+), 166 deletions(-) diff --git a/crates/ide-host/src/plugins/execution.rs b/crates/ide-host/src/plugins/execution.rs index 95328befe..0bd499d00 100644 --- a/crates/ide-host/src/plugins/execution.rs +++ b/crates/ide-host/src/plugins/execution.rs @@ -276,16 +276,12 @@ impl PluginHost { self.shared.run_configurations.write().insert((plugin, project), items); } } - /// Parses a bounded batch under the editor-provider soft/hard budgets. - pub fn parse_test_results( + /// Resolves only an owned plugin test configuration in the client's trusted project. + fn test_configuration( &self, client: &str, config: &str, - chunk: String, - ) -> IdeResult>> { - if chunk.len() > 64 << 10 { - return Err(IdeError::invalid("test output batch too large")); - } + ) -> IdeResult> { let root = self.shared.project_of(client).ok_or_else(|| IdeError::not_found("no project"))?; if !self.shared.workspace().is_some_and(|w| w.trusted(&root)) { return Err(IdeError::invalid("project is not trusted")); @@ -302,7 +298,44 @@ impl PluginHost { { return Ok(None); } - let plugin = self.get(owner)?; + Ok(Some((root, owner.to_owned(), id.to_owned()))) + } + + /// Derives replacement argv under the parser's soft/hard call budgets. + pub fn select_tests(&self, client: &str, config: &str, tests: Vec>) -> IdeResult>> { + if tests.is_empty() + || tests.len() > 10000 + || tests.iter().any(|p| p.is_empty() || p.len() > 128) + || serde_json::to_vec(&tests).is_ok_and(|b| b.len() > 4 << 20) + { + return Ok(None); + } + let Some((root, owner, id)) = self.test_configuration(client, config)? else { return Ok(None) }; + let plugin = self.get(&owner)?; + let (actor, activated) = self.ensure_active(&plugin, None)?; + if activated { + self.changed(); + } + let invocation = ide_plugin::Invocation { workspace_root: Some(root), ..Default::default() }; + let hard = self.limits.edit_hard; + let started = Instant::now(); + let result = actor.call(Some(self.limits.edit_soft), move |rt| rt.select_tests(id, tests, invocation, hard)); + let args = self.finish_provider_call(&plugin, &actor, started, result).flatten(); + Ok(args.filter(|args| args.len() <= 10000 && args.iter().map(String::len).sum::() <= 4 << 20)) + } + + /// Parses a bounded batch under the editor-provider soft/hard budgets. + pub fn parse_test_results( + &self, + client: &str, + config: &str, + chunk: String, + ) -> IdeResult>> { + if chunk.len() > 64 << 10 { + return Err(IdeError::invalid("test output batch too large")); + } + let Some((root, owner, id)) = self.test_configuration(client, config)? else { return Ok(None) }; + let plugin = self.get(&owner)?; let (actor, activated) = self.ensure_active(&plugin, None)?; if activated { self.changed(); diff --git a/crates/ide-host/src/plugins/remote_host.rs b/crates/ide-host/src/plugins/remote_host.rs index 524b387b9..7f86c7012 100644 --- a/crates/ide-host/src/plugins/remote_host.rs +++ b/crates/ide-host/src/plugins/remote_host.rs @@ -468,6 +468,16 @@ impl RemotePlugins { } }); let h = host.clone(); + d.add("plugin_select_tests", move |c| { + let h = h.clone(); + async move { + let host = alive(&h)?; + let config = c.arg::("config")?; + let tests = c.arg("tests")?; + blocking(move || host.select_tests(&c.window, &config, tests)).await + } + }); + let h = host.clone(); d.add("plugin_parse_test_results", move |c| { let h = h.clone(); async move { diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index e2b0cb996..f29f872c6 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -76,11 +76,11 @@ impl TestWorkspace { impl PluginWorkspace for TestWorkspace { fn run_configuration(&self, _client: &str, plugin: &str, id: &str) -> Result { - if plugin != "execution" || !matches!(id, "detected" | "dynamic") { + if !(plugin == "execution" && matches!(id, "detected" | "dynamic") || plugin == "api14" && id == "tests") { return Err(HostError::NotFound(id.into())); } Ok(ide_run::RunConfig { - id: format!("plugin:execution:{id}"), + id: format!("plugin:{plugin}:{id}"), name: "Tests".into(), kind: "plugin".into(), command: "execution-tool".into(), @@ -6018,6 +6018,50 @@ fn api14_stage3_configurations_test_results_and_content_actions_use_real_exports assert!(f.host.parse_test_results("main", "plugin:execution:dynamic", "trap".into()).unwrap().is_none()); } +#[test] +fn api14_fixture_exports_test_selection() { + let f = fixture(Limits::default()); + f.install(&sample("api14")); + assert_eq!( + f.host.select_tests("main", "plugin:api14:tests", vec![vec!["Suite".into(), "Test".into()]]).unwrap(), + Some(vec!["Suite::Test".into()]) + ); +} + +#[cfg(unix)] +#[test] +fn api14_test_selection_uses_owned_config_ids_and_name_paths() { + let f = execution_fixture(); + f.host.refresh_run_configurations(&f.project); + for id in ["dynamic", "detected"] { + assert_eq!( + f.host + .select_tests( + "main", + &format!("plugin:execution:{id}"), + vec![vec!["Suite".into(), "Test".into()], vec!["Other".into()]] + ) + .unwrap(), + Some(vec![id.into(), "Suite::Test".into(), "Other".into()]) + ); + } + assert_eq!(f.host.select_tests("main", "plugin:execution:dynamic", vec![vec!["none".into()]]).unwrap(), None); + assert_eq!(f.host.select_tests("main", "cargo:test", vec![vec!["Test".into()]]).unwrap(), None); + assert_eq!(f.host.select_tests("main", "plugin:execution:dynamic", vec![]).unwrap(), None); +} + +#[cfg(unix)] +#[test] +fn api14_test_selection_traps_and_timeouts_are_unsupported() { + for failure in ["trap", "timeout"] { + let f = execution_fixture(); + f.host.refresh_run_configurations(&f.project); + let started = Instant::now(); + assert_eq!(f.host.select_tests("main", "plugin:execution:dynamic", vec![vec![failure.into()]]).unwrap(), None); + assert!(started.elapsed() < Duration::from_secs(5)); + } +} + #[cfg(unix)] #[test] fn api14_stage3_terminal_calls_are_denied_without_process_grants() { diff --git a/crates/ide-plugin/src/runtime.rs b/crates/ide-plugin/src/runtime.rs index 6c1bda1dc..c2c43eb90 100644 --- a/crates/ide-plugin/src/runtime.rs +++ b/crates/ide-plugin/src/runtime.rs @@ -1165,6 +1165,22 @@ impl Runtime { }) .collect()) } + /// Selects tests under the same budget as the test parser. + pub fn select_tests( + &mut self, + config: String, + tests: Vec>, + invocation: Invocation, + timeout: Duration, + ) -> Result>, CallError> { + if self.exports.test_results.is_none() { + return Err(CallError::missing_export("test-results")); + } + self.call(invocation, timeout, |e, store| { + e.test_results.as_ref().unwrap().call_select_tests(store, &config, &tests) + }) + } + /// One editor-budget batch of a plugin test parser. pub fn parse_test_results( &mut self, diff --git a/crates/ide-plugin/tests/api_compat.rs b/crates/ide-plugin/tests/api_compat.rs index 6e9778a63..95825fedb 100644 --- a/crates/ide-plugin/tests/api_compat.rs +++ b/crates/ide-plugin/tests/api_compat.rs @@ -34,6 +34,8 @@ fn components() -> Vec { repo().join("crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm"), repo().join("crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm"), repo().join("crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm"), + repo().join("crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm"), + repo().join("crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm"), ]; let official = repo().join("plugins/official"); let mut dirs: Vec<_> = std::fs::read_dir(&official).unwrap().map(|e| e.unwrap().path()).collect(); diff --git a/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm index 708ba22f1f488a786a614a3f1ba9758eba610f70..5bdcd679c2716e4fffd1a6eb8406ad971a5a43da 100644 GIT binary patch delta 49481 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 delta 39086 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 Vec { + vec![] + } + fn select_tests(&mut self, config: &str, tests: &[Vec]) -> Option> { + (config == "tests").then(|| tests.iter().map(|p| p.join("::")).collect()) + } +} +register_plugin!(Api14, test_results); diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources index 1dddda010..3b08c7837 100644 --- a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources @@ -1 +1 @@ -e986322ee0a7e8b09096deee3335e542823ad836cecdf14a4f44e2066fadf7b5 +f4ae5c053230e2a805b097d79392501ad4654f715d6d194937299273b67fda7c diff --git a/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources index 71bbe995a..51acc097d 100644 --- a/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -cb74e35e0b0fa2bf7d46b143190d4e72d90abfa7a95962c037495b722d8c73d3 +7631939096b867c4707f7e0f929d2b75f91ef2b76da764d6e21233a5c06e8af9 diff --git a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources index 67c9fad7a..e5b55e534 100644 --- a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources @@ -1 +1 @@ -7ef1de0b1eb9d70ed3f5ba1dfc8cec6ccf3445ce76ad128f10f3d2456a990c2c +f5a2d43df242aa0ba9f358d9abf7296fd6a56b163bb526e62ce9c8b4d17e4b9a diff --git a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources index c5936fe68..01bab8cfe 100644 --- a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources @@ -1 +1 @@ -dcca07b99ce7d6691bd501d0e7c80d6f4ce0350b32a3bd7726b13f5b315c3982 +f19481ef89309e6fc65736cb18018a4bf9b679e5a1c6c8e9f92adf39578ca4cb diff --git a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources index 270ae051b..91cd73637 100644 --- a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources @@ -1 +1 @@ -6ab6593d3d7226a1a52bcdbe827697be39e9ed5117d238ba2366ad1a6911a623 +a6ba20c5bd531d965e64041b05f1dabfb1634c3192b06cdb61578bd2bf6f1af2 diff --git a/crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm index 996f085fa605e7cb04003933c27cd1ef39bf17cc..07476e17afb0c03ad72833cd5cd6fa93b6c9817b 100644 GIT binary patch delta 38007 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]) -> Option> { + match tests.first()?.first()?.as_str() { + "none" => return None, + "trap" => panic!("selection failed"), + "timeout" => loop { + std::hint::spin_loop(); + }, + _ => {} + } + Some(std::iter::once(config.to_owned()).chain(tests.iter().map(|p| p.join("::"))).collect()) + } fn parse(&mut self, _: &str, chunk: &str) -> Vec { if chunk == "trap" { panic!("parser failed"); diff --git a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources index 154dd39da..2b7f857ab 100644 --- a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources @@ -1 +1 @@ -86238b45b09b0274d59282b35295aaee650e999aa309640d9347787e7d461ec4 +110bbe7e4c0bd02d93c0c7ccae8fcb1eccbcf355587ca271d8046b588f3db898 diff --git a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources index 326c9358d..eb8523fe1 100644 --- a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources @@ -1 +1 @@ -d433104828c588baf00dece077a087dc7f4cd78097952adca5b6049e366ca944 +2c02b56def836148a1be5ac0429394d9430d468c3d3ee3682a3c742bd8c90436 diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources index f792701c7..d0e5c3855 100644 --- a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources @@ -1 +1 @@ -4093e1d4791c3786f4b67fd4ef76928fc17048470f858824a29aafd71855c77d +6681327fc078ecad751627998c4b6299b6176484a61760d6f265b41bd3f42c97 diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources index 02efff3b3..368e33562 100644 --- a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources @@ -1 +1 @@ -aeb6d2f0a589731a1b7f71c9c71232b72d2535f0f555c4fbdb7101861b906d77 +a6a21264dffff392ea5e998c4545cdbe21a08bae429d2765e6bfeac9f2c9f863 diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources index 28d8e8fb7..98d867b4c 100644 --- a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources @@ -1 +1 @@ -b723a8826d256fb57f04bbe8aa4b5300c9777f79d889bed2770f6f2bd7a65aa3 +f456deae0bbdee79c22d844ac8a9db5e4b867eaed245dc9332c1708e32e01ae0 diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources index 97ffa84d9..b1df655aa 100644 --- a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources @@ -1 +1 @@ -31f6c0067f7668235ce7f28cb14f75950549c690a8f191aa63a71fe25f8397c6 +50e77e306f196dd43f7d95789cc1da94085a2863f1ccbec83701c34916ffff41 diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources index c5e877c0e..f8b5ce5a8 100644 --- a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources @@ -1 +1 @@ -f03ff9487293925027c5681800633d2e0a71ef908db9ea037dff5e4e0a48458e +b9342ba2bf0107bb65074aecf726991d326a45e50ac12100a97c7a6051b01161 diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources index 44671ae8b..2f602f48c 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources @@ -1 +1 @@ -98a083b972c0a887de07b720c606ffcf13f99d835cfa64d553a1243ffc19e4b1 +b4300035602303343f8b1c15326f86a696bfe814a3f2c7e1e186f5eed5a92321 diff --git a/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources index dea43dfa8..68dd9dedb 100644 --- a/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources @@ -1 +1 @@ -639a25cbc3aaab3080bf2894bc4710f45a1043a8a0f0f7f8c41ac20253f8bc98 +91d6f1f11a26effda7a21fd423f8ea6f42e707168ac5080033e9116c89d11d10 diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources index 36c651b29..557220608 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources @@ -1 +1 @@ -741d2909f021ec4c2f572edba0d83bcee4843a1aae4d8a79ea0367419a667640 +3edc6122e6cbba033d37f44d7855deff8473f097205846e06768888ac416742c diff --git a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources index 57e81184a..f42466716 100644 --- a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources @@ -1 +1 @@ -1da271855c8e63d9b6873521392bb2e14cb85f9de4fddcc7bce0c80916298caf +83ce23d58d809e9a724dfcb8bec2cccc1f73c77324d39d1c6ee2a7c855d763fd diff --git a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources index de92d6fac..8993d4acb 100644 --- a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources @@ -1 +1 @@ -e14bfd9a7f61a7ff44f656a148ed133e973c66edfb0859bbff4d4cda9663eb3a +557e263c737bc5cdf15f5bc4ed4fa5d7a4f2e252b06c2eb873e936a968519c08 diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources index 9a638a03a..de9a6fc20 100644 --- a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources @@ -1 +1 @@ -1c2f975248675b935adac4ac096d23d3f45ca44a46e9e4a2ee578bb69bb35356 +884e60babc875a5971a9ee42d0949c22712b9af749a6995d6273efa191aa65b5 diff --git a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources index 655dad638..e970f3e1d 100644 --- a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources @@ -1 +1 @@ -2d566981f7f3aa78fbd6188e2bbdb17e33c01f6863e9b9a0b656f541371374bc +5f2f03bbc397dde8ad7c9b1cf2a61e9a4ae5881b34d71ec2e5748fc474754d59 diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources index e37a46194..9d0cc6c35 100644 --- a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources @@ -1 +1 @@ -69b349e65d32c5b22f7b6d312944e0e7387d1f7b8e46a1e88fcfcd87262ea407 +253a7ab7bb1827dee5434b47c732e484976237ca5a5d637536ccc36905d84be0 diff --git a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources index 01822bf2f..ab1331dc2 100644 --- a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources @@ -1 +1 @@ -e62e75f6e21f4d3c3797554581b6a78fc75a5362ce32f627c9858dd5f1d4edc5 +93467ab4d2a02ec72820f17df873510707dfdabd02d70c19164b8192d967bf7d diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources index 8eed9241d..8421a84e7 100644 --- a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources @@ -1 +1 @@ -c4740acdb3c805e13bf9a0c2e2c2a830336a1085285c39de3dce73a301f70150 +62372338f5c0129177936ad34c7f0fc5d8cd2f14c3f895c026ca7f79f1e31391 diff --git a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources index a3dd95e7c..8244b0f6f 100644 --- a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources @@ -1 +1 @@ -315d592adb776916170a6c2293379e7a7a086b9ae513dcac4aefad925d91df5d +4d1952be87c87f44e23b2420f966e23912405ba842d9928f1f9dec409dfa670e diff --git a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources index ddc072cc4..4e2868158 100644 --- a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources @@ -1 +1 @@ -7a88345173c4a77f6f94a047af53a43852adcf7e0377199f670374f296b98b19 +3992885be293901280938f5dc6498a39a310e1a3c7e34473394116a2327bb13b diff --git a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources index 22ad70b65..260ca0f32 100644 --- a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources @@ -1 +1 @@ -c98b94057e62a575d6c4b6c4c92e1e88e101bcef9138683e47b0bebf262e3f63 +70926629540ab1277641aef53419ce3014f291e424502619378511735bdbb8d5 diff --git a/plugins/README.md b/plugins/README.md index af88cd494..eafb63087 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -553,6 +553,7 @@ then run `openidea-plugin check`. | `host::reply_content_tab(request_id, reply)` | `views.reply-content-tab` | API 1.4: reply to an owned action in the originating window. No capability; commands/events only. | | `RunConfigurations::configurations(root)` | export `run-configurations.configurations` | API 1.4: optional `plugin-run-configurations` world; SDK registration `run_configurations`. Full project list, refreshed on configuration listing. | | `TestResults::parse(config, chunk)` | export `test-results.parse` | API 1.4: optional `plugin-test-results` world; SDK registration `test_results`. Bounded output batches for `framework = "plugin"`; failures use built-in parsers. | +| `TestResults::select_tests(config, tests)` | export `test-results.select-tests` | API 1.4: full replacement argv for test-tree name paths, or `None` when unsupported (the SDK default). Enables rerunning failed tests and running tree selections. | | `host::open_location(path, line, column)` | `views.open-location` | Opens a file inside the project in an editor of the invoking window, the caret at `line`/`column` (0-based, UTF-16): a view's links to code. Needs `project.read`. | | `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. | @@ -1628,9 +1629,10 @@ test's parent must already exist. Locations use absolute project-contained paths zero-based lines and UTF-16 columns. Passed/failed durations are milliseconds. The host uses the editor-provider soft/hard budget per batch and bounds returned events to 10,000 / 4 MiB. A missing, failed or invalid parser switches that run to -the built-in parsers; common framework commands retain their existing rerun-failed -filters. Arbitrary frameworks have no failed-test selection protocol in this -export and therefore cannot supply a custom rerun-failed filter. +the built-in parsers. Implement `select_tests(config, tests)` to derive the full +replacement argv for failed tests or test-tree selections; its default returns +`None`. See [Selecting plugin tests](#selecting-plugin-tests-api-14-unreleased) +for the name-path contract and unsupported cases. `host::open_terminal_session(key, title, &Command::new("tool"))` opens a normal Terminal tab in this window's project root. Reopening the same key focuses its @@ -2357,6 +2359,24 @@ its normal editor and project context. No capability is needed. Invalid or undeclared actions reject the whole notification. Labels are limited to 256 bytes and arguments to 64 KiB. Plain `host::notify` behaves as before. +### Selecting plugin tests (API 1.4, unreleased) + +A plugin using `framework = "plugin"` can implement `TestResults::select_tests` +(`test-results.select-tests`) alongside `parse`. It receives the same plugin-local +configuration id as `parse` and a list of test-tree paths: the names from the +plugin's `test-start` events, from parent suite to test, exactly as shown in the +tree. Return the **full replacement argument list**, including any required base +arguments, to run only those tests. `None` (the Rust SDK default), errors and +call timeouts leave test selection unsupported. An empty argument list is a +supported selection. The IDE preserves the configuration's command, environment, +working directory and run target. + +This enables **Rerun Failed Tests** and the test tree's **Run** / **Debug** entries. +Calls use the test parser's bounded background budget and run on the plugin's +host, including remote projects. See `official/test-runner` for an example. +This export is added to the existing interface while 1.4 is unreleased; released +export interfaces remain frozen under the rules above. + ### Plugin-provided run targets (API 1.4) `host::register_run_target(project, target)` registers or replaces a read-only, diff --git a/plugins/official/ai-completion/dist/plugin.wasm.sources b/plugins/official/ai-completion/dist/plugin.wasm.sources index c715f2339..efc9a0eb8 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -acb5f0a97061616d5bdcf7193d8fbe45c27818663dc0a9eab2e8203589492919 +c4900df449a6a323bab1f4fe5fe76ee42e2839c9197636b7c1cef733dffc683d diff --git a/plugins/official/blame-annotations/dist/plugin.wasm.sources b/plugins/official/blame-annotations/dist/plugin.wasm.sources index f3af33fc5..140444600 100644 --- a/plugins/official/blame-annotations/dist/plugin.wasm.sources +++ b/plugins/official/blame-annotations/dist/plugin.wasm.sources @@ -1 +1 @@ -aaa3cdd1595ba8aeabae6472f7f1ef635bd3115662d64d9284152747edfb6b16 +0ea139d4a0d47072971c0b37b93085159e412554ffb45d19590bae4dc10f48b5 diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources index ef8ba4ca4..287e7bd70 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -7d09b4e0e3edfbed92be8efaba47d468b7707d6164abf45b0bec495ac454f4dd +4e4bc361395f6364e17211a9d3c32232a370ea1ee72e5197eab5f706ea465dc6 diff --git a/plugins/official/container-runner/dist/plugin.wasm.sources b/plugins/official/container-runner/dist/plugin.wasm.sources index 88f53d631..590301e25 100644 --- a/plugins/official/container-runner/dist/plugin.wasm.sources +++ b/plugins/official/container-runner/dist/plugin.wasm.sources @@ -1 +1 @@ -929b309e182c078790940244429b9872d8c7723c0bc25a07b152c83300905c47 +c39211d416f45dff0265216509070ffdecc4e0f4e738c88176cad5faaca01b65 diff --git a/plugins/official/csv-editor/dist/plugin.wasm.sources b/plugins/official/csv-editor/dist/plugin.wasm.sources index 6d851be5b..e416f9c2f 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -5099f6ceba7936a9b54340a4ce0c9b50a49962dd96053c850c010ea5c803c6a2 +2bbb840e0bead664ccb467dbb299b269fc9a03ed4995c8d890ae195efe6e47a6 diff --git a/plugins/official/git-toolbox/dist/plugin.wasm.sources b/plugins/official/git-toolbox/dist/plugin.wasm.sources index 6b3a47871..502c85232 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -6deb9876934b6e21bd99c791f2a86167f1d67e808fbb1cc514c7e621514b654c +b1fdf25b8307803b0e5e189bbe041cdb23b1e7b73abb4b76fd7a1b9ee48d87b0 diff --git a/plugins/official/graphviz/dist/plugin.wasm.sources b/plugins/official/graphviz/dist/plugin.wasm.sources index 6f56a0252..1a52decfc 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -5c2f5a254b25ba018369366df87707b423e77a573fcb73cc3c74b1f321f5aa61 +849d83a77d80e8d8a4f2c2801f4728300dd4504da411689834217b9973e582f4 diff --git a/plugins/official/key-promoter/dist/plugin.wasm.sources b/plugins/official/key-promoter/dist/plugin.wasm.sources index 61effb082..3fb581b42 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -cb4ed7e9d793df4219e92edd5d64616db65411cb77e111df862ca8237f647efb +ff42121519f6796db56a71a504484b149592e9e28ed33c783d2e6141cfe84d6c diff --git a/plugins/official/license-header/dist/plugin.wasm.sources b/plugins/official/license-header/dist/plugin.wasm.sources index c7b41d834..563266688 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -f43f1e52813a5f992c3d06cbe03aa26092dd56db45322b090906dfbb8d08b1ab +0d5ed3d81ab5496b92e7b012ae6e1e765fb45f4f6b0613a03f2bc79029ed9397 diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index ecf858484..95a0ea2d0 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -656b51f57467a75b5416575871a5722e4f44be98407f6bfd0e6fa1e44f0bfa80 +dd34327c65747b1b57eef0854161837bd4e2843a59efec7730b81d8a2866a411 diff --git a/plugins/official/regex-tester/dist/plugin.wasm.sources b/plugins/official/regex-tester/dist/plugin.wasm.sources index 5044baccb..b4be800a5 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -bafe2cfc88d8c1272eea289d3d804287d56159b1c5893147b73a6be88e00370f +2141b215b141d82cc9a5cffa63c11e2b53e7bebcd31b93fb8d2db0ff5c2acd5b diff --git a/plugins/official/solidity/dist/plugin.wasm.sources b/plugins/official/solidity/dist/plugin.wasm.sources index 49b5d5e91..60009d455 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -ee5d474028c7068b06771efa5e8e2fa710cd0108d53737a36d8bf9f0f3490c08 +604a289139233c13108ff8aab448e4278604e5260d7b2485e3ace3bad9e855db diff --git a/plugins/official/string-tools/dist/plugin.wasm.sources b/plugins/official/string-tools/dist/plugin.wasm.sources index 1ddc444e0..bea16abcf 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -867fc7bba0e795155e96c39f5a05cc3a08a3b6809c4e3922437e393d65f6d829 +9efa3a6a41c05e81a50893a2cff79da6e8e5f97219ddfd54f7ed201270934bb5 diff --git a/plugins/official/test-runner/README.md b/plugins/official/test-runner/README.md index 92e9902da..f14434ef7 100644 --- a/plugins/official/test-runner/README.md +++ b/plugins/official/test-runner/README.md @@ -10,7 +10,7 @@ The gutter command appends the test name as a filter to `run.start`. The fake executable prints a deliberately tiny `start NAME` / `pass NAME` protocol; `test-results.parse` retains split lines and emits stable test ids. Replace that protocol/parser and executable to integrate a real framework. This is an -API example, not a general test framework or a custom rerun-failed contract. +API example for integrating a test framework. Build from the repository root: @@ -23,3 +23,7 @@ cargo run -p ide-host --bin openidea-plugin -- check plugins/official/test-runne Install the built package folder through Settings › Plugins › Install from Disk. See the [plugin guide](../../README.md#api-14) and [official/community policy](../../README.md#81-official-and-community-plugins). + +`test-results.select-tests` returns the selected names as the complete argv for +`openidea-example-tests`, enabling Rerun Failed Tests and Run from the test tree. +It returns `None` for unknown configurations or paths this example cannot filter. diff --git a/plugins/official/test-runner/dist/plugin.wasm b/plugins/official/test-runner/dist/plugin.wasm index 57025d99e2a1c824e629520fe5406aae2fdd59fb..52238d6778ccf79a6645ad492978c85e82e3deea 100644 GIT binary patch delta 27827 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 LclNLBWuN~C1vK}k delta 25301 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 MX1JdG%_e&MKN1@5q5uE@ diff --git a/plugins/official/test-runner/dist/plugin.wasm.sources b/plugins/official/test-runner/dist/plugin.wasm.sources index 273f94c58..114cf23f1 100644 --- a/plugins/official/test-runner/dist/plugin.wasm.sources +++ b/plugins/official/test-runner/dist/plugin.wasm.sources @@ -1 +1 @@ -b7aaac21c6c34d9d722aa23c13c66e61ef11028f038fa877fe67d03b03c18483 +5f05e3696671258af5474b10abafd3cab48385493d5336061a9a8679307b56b4 diff --git a/plugins/official/test-runner/openidea-example-tests b/plugins/official/test-runner/openidea-example-tests index 0c675c486..4be1dfd0e 100755 --- a/plugins/official/test-runner/openidea-example-tests +++ b/plugins/official/test-runner/openidea-example-tests @@ -5,6 +5,12 @@ while IFS= read -r line; do 'test '*) name=${line#test } ;; *) continue ;; esac - [ -z "$1" ] || [ "$1" = "$name" ] || continue + if [ "$#" -gt 0 ]; then + selected=0 + for test in "$@"; do + [ "$test" != "$name" ] || selected=1 + done + [ "$selected" -eq 1 ] || continue + fi printf 'start %s\npass %s\n' "$name" "$name" done < example.tests diff --git a/plugins/official/test-runner/src/lib.rs b/plugins/official/test-runner/src/lib.rs index 4156d15e7..a5c5728d0 100644 --- a/plugins/official/test-runner/src/lib.rs +++ b/plugins/official/test-runner/src/lib.rs @@ -48,6 +48,12 @@ impl LanguageFeatures for TestRunner { } } impl TestResults for TestRunner { + fn select_tests(&mut self, config: &str, tests: &[Vec]) -> Option> { + if config != "tests" || tests.is_empty() || tests.iter().any(|p| p.len() != 1) { + return None; + } + Some(tests.iter().map(|p| p[0].clone()).collect()) + } fn parse(&mut self, _config: &str, chunk: &str) -> Vec { self.pending.push_str(chunk); let mut events = vec![]; @@ -92,6 +98,12 @@ mod tests { mock.set_run_configurations(plugin.configurations("/project")); plugin.run_command("test", &mock.invocation(), Some("adds")).unwrap(); assert_eq!(mock.configuration_runs()[0].2, vec!["adds"]); + assert_eq!( + plugin.select_tests("tests", &[vec!["adds".into()], vec!["subs".into()]]), + Some(vec!["adds".into(), "subs".into()]) + ); + assert_eq!(plugin.select_tests("unknown", &[vec!["adds".into()]]), None); + assert_eq!(plugin.select_tests("tests", &[vec!["suite".into(), "adds".into()]]), None); assert!(plugin.parse("tests", "sta").is_empty()); let events = plugin.parse("tests", "rt adds\npass adds\n"); assert!(matches!(&events[..], [PluginTestEvent::TestStarted(_), PluginTestEvent::Passed(_)])); diff --git a/plugins/official/todo-finder/dist/plugin.wasm.sources b/plugins/official/todo-finder/dist/plugin.wasm.sources index ef43b99b8..99cf55807 100644 --- a/plugins/official/todo-finder/dist/plugin.wasm.sources +++ b/plugins/official/todo-finder/dist/plugin.wasm.sources @@ -1 +1 @@ -66e92c09c0050dbf4c302793eea5048e21d1c91b9b44811b5f7edbae6eaa9075 +677ec8005d29d4727afbc4b8fba6afcbc26996f0db461d1e98a6e9e52b5f7fac diff --git a/sdk/rust/openidea-plugin/src/execution.rs b/sdk/rust/openidea-plugin/src/execution.rs index e8866312d..da7baa83e 100644 --- a/sdk/rust/openidea-plugin/src/execution.rs +++ b/sdk/rust/openidea-plugin/src/execution.rs @@ -158,6 +158,10 @@ pub trait RunConfigurations { /// Parses bounded test output batches; register with test_results. pub trait TestResults { fn parse(&mut self, config: &str, chunk: &str) -> Vec; + /// Full replacement argv for test-tree name paths, or None when filtering is unsupported. + fn select_tests(&mut self, _config: &str, _tests: &[Vec]) -> Option> { + None + } } impl crate::Event { diff --git a/sdk/rust/openidea-plugin/src/lib.rs b/sdk/rust/openidea-plugin/src/lib.rs index faa56f39c..1c8025ad9 100644 --- a/sdk/rust/openidea-plugin/src/lib.rs +++ b/sdk/rust/openidea-plugin/src/lib.rs @@ -1101,6 +1101,12 @@ pub mod __rt { }) .collect() } + pub fn select_tests( + config: &str, + tests: &[Vec], + ) -> Option> { + with::(|p| p.select_tests(config, tests)) + } pub fn test_results( config: &str, chunk: &str, @@ -1321,6 +1327,9 @@ macro_rules! __test_results_exports { fn parse(config: String, chunk: String) -> Vec<$crate::test_results_bindings::exports::openidea::plugin::test_results::TestEvent> { $crate::__rt::test_results::<$plugin>(&config, &chunk) } + fn select_tests(config: String, tests: Vec>) -> Option> { + $crate::__rt::select_tests::<$plugin>(&config, &tests) + } } $crate::test_results_bindings::__export_tests_world!($component with_types_in $crate::test_results_bindings); }; diff --git a/sdk/rust/openidea-plugin/tests/api14_stage3.rs b/sdk/rust/openidea-plugin/tests/api14_stage3.rs index 86f011b73..2ecd5feb3 100644 --- a/sdk/rust/openidea-plugin/tests/api14_stage3.rs +++ b/sdk/rust/openidea-plugin/tests/api14_stage3.rs @@ -53,3 +53,14 @@ fn run_targets_require_a_program_grant_and_are_project_scoped() { host::unregister_run_target(&project, "ddev"); assert!(mock.run_targets().is_empty()); } + +#[test] +fn parsers_can_keep_the_default_unsupported_test_selection() { + struct Parser; + impl TestResults for Parser { + fn parse(&mut self, _: &str, _: &str) -> Vec { + vec![] + } + } + assert_eq!(Parser.select_tests("tests", &[vec!["Suite".into(), "Test".into()]]), None); +} diff --git a/sdk/rust/openidea-plugin/wit/run.wit b/sdk/rust/openidea-plugin/wit/run.wit index c1bc79cf9..6c62534e2 100644 --- a/sdk/rust/openidea-plugin/wit/run.wit +++ b/sdk/rust/openidea-plugin/wit/run.wit @@ -47,4 +47,7 @@ interface test-results { } /// Parses one bounded output batch; failures fall back to built-in parsers. parse: func(config: string, chunk: string) -> list; + /// Selects test-tree name paths (parents then test), using the same config id as parse. + /// Returns the full replacement argument list, or none when filtering is unsupported. + select-tests: func(config: string, tests: list>) -> option>; } diff --git a/smoke/run.test.ts b/smoke/run.test.ts index 7a46ec00e..55a1945b0 100644 --- a/smoke/run.test.ts +++ b/smoke/run.test.ts @@ -220,7 +220,7 @@ async function smoke(language: string) { // Rerun Failed Tests runs that one test only. const rerunOutput: string[] = []; const before = useRun.getState().sessions.find((s) => s.id === id)!.startedAt; - expect(rerunFailed(id), "Rerun Failed Tests is offered").toBe(true); + expect(await rerunFailed(id), "Rerun Failed Tests is offered").toBe(true); const ended = () => { const s = useRun.getState().sessions.find((s) => s.id === id)!; return s.startedAt > before && s.endedAt !== null && s.endedAt >= s.startedAt; diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index bcfc8a1e4..468567274 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -514,6 +514,7 @@ pub fn builder() -> tauri_specta::Builder { plugin::plugin_terminal_session_spawn, plugin::plugin_terminal_session_close, plugin::plugin_parse_test_results, + plugin::plugin_select_tests, plugin::plugin_content_tab_action, plugin::plugin_choose_program, plugin::plugin_program, diff --git a/src-tauri/src/commands/plugin.rs b/src-tauri/src/commands/plugin.rs index 6e288d8f2..6c14a3f44 100644 --- a/src-tauri/src/commands/plugin.rs +++ b/src-tauri/src/commands/plugin.rs @@ -963,6 +963,22 @@ pub fn plugin_content_tab_action( state.plugins.content_tab_action(window.label(), &plugin_id, action) } +/// Selects test-tree paths in an owned plugin test configuration. +#[tauri::command] +#[specta::specta] +pub async fn plugin_select_tests( + window: Window, + state: State<'_, AppState>, + config: String, + tests: Vec>, +) -> IdeResult>> { + let plugins = state.plugins.clone(); + let client = window.label().to_owned(); + tokio::task::spawn_blocking(move || plugins.select_tests(&client, &config, tests)) + .await + .map_err(|e| ide_core::IdeError::internal(e.to_string()))? +} + /// Parses one batch of an owned test configuration's output. #[tauri::command] #[specta::specta] diff --git a/src/bindings.ts b/src/bindings.ts index 9e5459d76..496ebca77 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1145,6 +1145,8 @@ export const commands = { pluginTerminalSessionClose: (operation: string) => typedError(__TAURI_INVOKE("plugin_terminal_session_close", { operation })), /** Parses one batch of an owned test configuration's output. */ pluginParseTestResults: (config: string, chunk: string) => typedError(__TAURI_INVOKE("plugin_parse_test_results", { config, chunk })), + /** Selects test-tree paths in an owned plugin test configuration. */ + pluginSelectTests: (config: string, tests: string[][]) => typedError(__TAURI_INVOKE("plugin_select_tests", { config, tests })), /** Dispatches a wasm content editor action through the owning plugin's event handler. */ pluginContentTabAction: (pluginId: string, action: ContentTabAction) => typedError(__TAURI_INVOKE("plugin_content_tab_action", { pluginId, action })), /** Re-approves a plugin's executable, globally or for this window's project. */ diff --git a/src/features/plugins/backend.ts b/src/features/plugins/backend.ts index c83ed774b..12824a7a7 100644 --- a/src/features/plugins/backend.ts +++ b/src/features/plugins/backend.ts @@ -88,6 +88,7 @@ export interface PluginsBackend { answerPrompt(promptId: string, answer: string[] | null): Promise; /** This window's answer to a plugin's `commitRequested`. */ answerCommand(requestId: string, answer: CommandAnswer): Promise; + selectTests(config: string, tests: string[][]): Promise; parseTestResults(config: string, chunk: string): Promise; closeTerminalSession(operation: string): Promise; terminalSessionSpawn( @@ -240,6 +241,7 @@ export const tauriPluginsBackend: PluginsBackend = { answerPrompt: async (promptId, answer) => { await commands.pluginAnswerPrompt(promptId, answer); }, + selectTests: (config, tests) => call(commands.pluginSelectTests(config, tests)), parseTestResults: (config, chunk) => call(commands.pluginParseTestResults(config, chunk)), closeTerminalSession: async (operation) => { await call(commands.pluginTerminalSessionClose(operation)); @@ -666,6 +668,7 @@ export function createMemoryPluginsBackend( throw new Error("Terminal unavailable"); }, parseTestResults: async () => null, + selectTests: async () => null, contentTabAction: async () => {}, answerCommit: async () => {}, commitChecks: async () => [], diff --git a/src/features/plugins/runtimeUi.tsx b/src/features/plugins/runtimeUi.tsx index 74c68c84a..13ee3d46c 100644 --- a/src/features/plugins/runtimeUi.tsx +++ b/src/features/plugins/runtimeUi.tsx @@ -4,7 +4,7 @@ * workbench events it subscribed to (`events.subscribe`), reported in * batches. */ -import { providePluginTestParser } from "@/workbench/contributions"; +import { providePluginTestParser, providePluginTestSelector } from "@/workbench/contributions"; import { contentTabData, contentTabKey, @@ -639,6 +639,7 @@ export function startRuntimeUi(): () => void { const disposers = [ startRunTargets(), stopParser, + providePluginTestSelector((config, tests) => pluginsBackend().selectTests(config, tests)), projectStatusItems(), trackWorkbenchEvents(), refreshOnProjectChange(), diff --git a/src/features/run/RunView.tsx b/src/features/run/RunView.tsx index 994e87c39..1f69388e7 100644 --- a/src/features/run/RunView.tsx +++ b/src/features/run/RunView.tsx @@ -1,4 +1,5 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import type { RunConfig } from "@/platform/ipc"; import type { RunTargetValid } from "@/platform/remoteSettings"; import { useWorkflowSettings } from "@/platform/workflowSettings"; import { EmptyState, IconButton, SplitGroup, SplitHandle, SplitPanel, Tabs, type TabItem, T } from "@/ui"; @@ -11,6 +12,8 @@ import { runDirectory } from "./navigate"; import { remoteLabel } from "./remote"; import { acceptsInput, + baseConfigOf, + canRunSelectedTests, clearConsole, closeSessionTab, closeStdin, @@ -29,6 +32,7 @@ import { type RunSession, isLive, } from "./store"; +import { rerunFailedConfigs } from "./testing/rerun"; import { TestTreeView, formatDuration } from "./TestTreeView"; import { StackTracePanel, closeStackTrace, useStackTraces, type StackTraceTab } from "./stackTrace"; import { showApplicationUpdate, useApplicationUpdates } from "./applicationUpdate"; @@ -77,6 +81,31 @@ export function SessionPanel({ const [findOpen, setFindOpen] = useState(false); const running = isLive(session); const cwd = runDirectory(session.config.cwd); + const baseConfig = useRun(() => baseConfigOf(session)); + // Test results mutate in place; their version refreshes the selection. + const failedSelection = useMemo( + () => (running ? [] : rerunFailedConfigs(baseConfig, failedTests(session.id))), + [running, baseConfig, session.id, version], // eslint-disable-line react-hooks/exhaustive-deps + ); + const [resolvedSelection, setResolvedSelection] = useState<{ + request: Promise; + supported: boolean; + } | null>(null); + useEffect(() => { + let current = true; + if (failedSelection instanceof Promise) { + void failedSelection.then((configs) => { + if (current) setResolvedSelection({ request: failedSelection, supported: configs.length > 0 }); + }); + } + return () => { + current = false; + }; + }, [failedSelection]); + const canRerunFailed = + failedSelection instanceof Promise + ? resolvedSelection?.request === failedSelection && resolvedSelection.supported + : failedSelection.length > 0; // Selecting a test with captured output shows just that output (like IDEA). const testModel = useMemo(() => { @@ -164,8 +193,8 @@ export function SessionPanel({ tone="danger" title="Rerun Failed Tests" shortcut={shortcutLabel("run.rerunFailed")} - disabled={running || failedTests(session.id).length === 0} - onClick={() => rerunFailed(session.id)} + disabled={!canRerunFailed} + onClick={() => void rerunFailed(session.id)} /> )} canRunSelectedTests(session.id, ids)} onRunTests={(ids, debug) => void runSelectedTests(session.id, ids, debug)} /> diff --git a/src/features/run/TestTreeView.test.tsx b/src/features/run/TestTreeView.test.tsx index 1a07f390b..dd8447065 100644 --- a/src/features/run/TestTreeView.test.tsx +++ b/src/features/run/TestTreeView.test.tsx @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { act, fireEvent, render, screen } from "@testing-library/react"; import { ContextMenuHost } from "@/ui"; import { TestResults } from "./testing"; import { TestTreeView } from "./TestTreeView"; @@ -25,7 +25,10 @@ function results() { return r; } -function Host(props: { onRunTests?: (ids: string[], debug: boolean) => void }) { +function Host(props: { + onRunTests?: (ids: string[], debug: boolean) => void; + canRunTests?: (ids: string[]) => boolean | Promise; +}) { return ( <> { expect.objectContaining({ left: { text: "1", label: "Expected" }, right: { text: "2", label: "Actual" } }), ); }); + + it.each([true, false])( + "enables Run and Debug only when the async selector returns support (%s)", + async (supported) => { + let resolve!: (supported: boolean) => void; + const selection = new Promise((done) => { + resolve = done; + }); + const canRunTests = vi.fn(() => selection); + const onRunTests = vi.fn(); + render(); + fireEvent.contextMenu(screen.getByText("subs()")); + expect(screen.getByRole("menuitem", { name: "Run 'subs()'" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("menuitem", { name: "Debug 'subs()'" })).toHaveAttribute("aria-disabled", "true"); + expect(canRunTests).toHaveBeenCalledWith(["MathTest\u0000subs()"]); + await act(async () => resolve(supported)); + for (const label of ["Run 'subs()'", "Debug 'subs()'"]) { + expect(screen.getByRole("menuitem", { name: label }).hasAttribute("aria-disabled")).toBe(!supported); + } + if (supported) { + fireEvent.click(screen.getByText("Run 'subs()'")); + expect(onRunTests).toHaveBeenCalledWith(["MathTest\u0000subs()"], false); + } + }, + ); }); diff --git a/src/features/run/TestTreeView.tsx b/src/features/run/TestTreeView.tsx index 44378442b..29f432541 100644 --- a/src/features/run/TestTreeView.tsx +++ b/src/features/run/TestTreeView.tsx @@ -1,7 +1,7 @@ import { useMemo, useState, type MouseEvent } from "react"; import clsx from "clsx"; import { executeAction } from "@/platform/actions"; -import { Icon, IconButton, showContextMenu, Spinner, Tree, type MenuItem } from "@/ui"; +import { Icon, IconButton, showContextMenu, useContextMenuStore, Spinner, Tree, type MenuItem } from "@/ui"; import { openTarget } from "./navigate"; import { nodeStatus, type TestNode, type TestResults, type TestStatus } from "./testing"; import { parseComparison } from "./testing/comparison"; @@ -106,9 +106,19 @@ export interface TestTreeViewProps { running: boolean; /** Run / Debug the selected tests (context menu); absent when the configuration can't be narrowed. */ onRunTests?: (ids: string[], debug: boolean) => void; + canRunTests?: (ids: string[]) => boolean | Promise; } -export function TestTreeView({ results, version, cwd, selected, onSelect, running, onRunTests }: TestTreeViewProps) { +export function TestTreeView({ + results, + version, + cwd, + selected, + onSelect, + running, + onRunTests, + canRunTests, +}: TestTreeViewProps) { const [filters, setFilters] = useState(loadTestFilters); const { showPassed, showIgnored } = filters; const toggle = (key: keyof TestFilters) => @@ -134,25 +144,48 @@ export function TestTreeView({ results, version, cwd, selected, onSelect, runnin const ids = sel.has(node.id) ? [...sel] : [node.id]; const name = ids.length === 1 ? (results.get(ids[0]!)?.name ?? node.name) : `${ids.length} tests`; const located = node.location ?? firstLocated(node)?.location; - const items: MenuItem[] = []; - if (onRunTests) { - items.push( - { label: tx`Run '${name}'`, verbatim: true, icon: "run", run: () => onRunTests(ids, false) }, - { label: tx`Debug '${name}'`, verbatim: true, icon: "debug", run: () => onRunTests(ids, true) }, - { kind: "separator" }, - ); - } - if (ids.length === 1 && nodeStatus(node) === "failed" && parseComparison(node.output)) - items.push({ label: "Show Diff", icon: "diff", run: () => void showTestDiff(node) }); - items.push({ - label: "Jump to Source", - icon: "external-link", - shortcut: "F4", - disabled: !located, - run: () => jumpToSource(node), - }); e.preventDefault(); - showContextMenu(e, items); + const supported = onRunTests ? (canRunTests?.(ids) ?? true) : false; + const itemsFor = (enabled: boolean) => { + const items: MenuItem[] = []; + if (onRunTests) { + items.push( + { + label: tx`Run '${name}'`, + verbatim: true, + icon: "run", + disabled: !enabled, + run: () => onRunTests(ids, false), + }, + { + label: tx`Debug '${name}'`, + verbatim: true, + icon: "debug", + disabled: !enabled, + run: () => onRunTests(ids, true), + }, + { kind: "separator" }, + ); + } + if (ids.length === 1 && nodeStatus(node) === "failed" && parseComparison(node.output)) + items.push({ label: "Show Diff", icon: "diff", run: () => void showTestDiff(node) }); + items.push({ + label: "Jump to Source", + icon: "external-link", + shortcut: "F4", + disabled: !located, + run: () => jumpToSource(node), + }); + return items; + }; + showContextMenu(e, itemsFor(supported === true)); + if (supported instanceof Promise) { + const menu = useContextMenuStore.getState().menu; + void supported.then((enabled) => { + if (useContextMenuStore.getState().menu === menu && menu) + useContextMenuStore.setState({ menu: { ...menu, items: itemsFor(enabled) } }); + }); + } }; const counts = useMemo(() => results.counts(), [results, version]); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/src/features/run/index.ts b/src/features/run/index.ts index 49ac906b8..b7c771274 100644 --- a/src/features/run/index.ts +++ b/src/features/run/index.ts @@ -67,6 +67,7 @@ import { setBeforeLaunchRunner, loadConfigs, failedTests, + baseConfigOf, onRunExited, rerunFailed, rerunSession, @@ -83,9 +84,43 @@ import { useRun, isLive, } from "./store"; +import { rerunFailedConfigs } from "./testing/rerun"; import { compoundTargets, selectedTarget } from "./sessionTargets"; import { tx } from "@/platform/i18n"; +function trackFailedTestSelection(): () => void { + let signature = ""; + let generation = 0; + const update = () => { + const state = useRun.getState(); + const id = + (state.activeSession && failedTests(state.activeSession).length ? state.activeSession : null) ?? + [...state.sessions].reverse().find((s) => failedTests(s.id).length > 0)?.id; + const session = state.sessions.find((s) => s.id === id); + const base = session && baseConfigOf(session); + const paths = id ? failedTests(id) : []; + const next = JSON.stringify([id, session?.status, base, paths]); + if (next === signature) return; + signature = next; + const request = ++generation; + setContext("run.canRerunFailed", false); + if (!session || isLive(session) || !base) return; + const selection = rerunFailedConfigs(base, paths); + if (selection instanceof Promise) { + void selection.then((configs) => { + if (request === generation) setContext("run.canRerunFailed", configs.length > 0); + }); + } else setContext("run.canRerunFailed", selection.length > 0); + }; + const stop = useRun.subscribe(update); + update(); + return () => { + generation++; + stop(); + setContext("run.canRerunFailed", false); + }; +} + function hasProject() { return !!useProject.getState().project; } @@ -383,6 +418,7 @@ export function activate(): () => void { activateStackTrace(), listenForApplicationDeactivation(), trackRecentConfigurations(), + trackFailedTestSelection(), useDebugRunSessions.subscribe(() => setContext("run.updatableRunning", updatableSessions().length > 0)), // A removed workspace member (or a closed project) takes its sessions, // temporary configurations and Services groups with it. @@ -563,14 +599,14 @@ export function activate(): () => void { title: "Rerun Failed Tests", category: "Run", icon: "rerun", - when: "projectOpen", + when: "projectOpen && run.canRerunFailed", keywords: ["failed tests", "rerun"], - run: () => { + run: async () => { const s = useRun.getState(); const id = (s.activeSession && failedTests(s.activeSession).length ? s.activeSession : null) ?? [...s.sessions].reverse().find((x) => failedTests(x.id).length > 0)?.id; - if (!id || !rerunFailed(id)) + if (!id || !(await rerunFailed(id))) notifications().notify({ group: "run", level: "info", diff --git a/src/features/run/session.test.tsx b/src/features/run/session.test.tsx index 39bd3ecf7..0b43a7dc2 100644 --- a/src/features/run/session.test.tsx +++ b/src/features/run/session.test.tsx @@ -5,6 +5,7 @@ import { setContext } from "@/platform/context"; import type { RunConfig } from "@/platform/ipc"; import { useDebugRunSessions, + providePluginTestSelector, provideNotificationService, provideToolWindowService, registerMenuGroup, @@ -15,7 +16,16 @@ import { ConsoleModel } from "./console"; import { ConsoleView, type ConsoleInput } from "./ConsoleView"; import { buildRows, mnemonics } from "./ConfigChooser"; import { activate } from "./index"; -import { closeSessionTab, resetRuns, runtime, startRun, useRun, type RunSession } from "./store"; +import { + closeSessionTab, + resetRuns, + runtime, + startRun, + useRun, + runSelectedTests, + rerunFailed, + type RunSession, +} from "./store"; const confirm = vi.hoisted(() => vi.fn(async () => true)); const confirmTerminate = vi.hoisted(() => vi.fn(async () => true)); @@ -265,3 +275,50 @@ describe("console input", () => { expect(input.onChange).toHaveBeenLastCalledWith("3"); }); }); + +describe("plugin test selection launches", () => { + it("keeps the owning configuration id, selected argv and original config when running tree tests", async () => { + const base = config("plugin:example:tests", { + kind: "plugin", + detected: true, + isTest: true, + params: { framework: "plugin" }, + args: ["all"], + }); + const session = (await startRun(base))!; + const tests = runtime(session.id)!.tests!; + tests.apply([{ type: "result", path: ["suite", "case"], status: "failed" }]); + const selector = vi.fn().mockResolvedValue(["--only", "suite::case"]); + const dispose = providePluginTestSelector(selector); + try { + expect(await runSelectedTests(session.id, ["suite"], false)).toBe(true); + const selected = useRun.getState().sessions.find((s) => s.id === session.id)!; + expect(selected.config.id).toBe(base.id); + expect(selected.config.args).toEqual(["--only", "suite::case"]); + expect(selected.baseConfig).toEqual(base); + expect(selector).toHaveBeenCalledWith(base.id, [["suite", "case"]]); + expect(useRun.getState().temporary).toEqual([]); + } finally { + dispose(); + } + }); + + it("does not launch when the plugin cannot filter the selection", async () => { + const base = config("plugin:example:tests", { + kind: "plugin", + detected: true, + isTest: true, + params: { framework: "plugin" }, + }); + const session = (await startRun(base))!; + runtime(session.id)!.tests!.apply([{ type: "result", path: ["case"], status: "failed" }]); + const dispose = providePluginTestSelector(async () => null); + try { + expect(await runSelectedTests(session.id, ["case"], false)).toBe(false); + expect(await rerunFailed(session.id)).toBe(false); + expect(useRun.getState().sessions[0]).toBe(session); + } finally { + dispose(); + } + }); +}); diff --git a/src/features/run/store.ts b/src/features/run/store.ts index c66968919..410dc9adb 100644 --- a/src/features/run/store.ts +++ b/src/features/run/store.ts @@ -894,7 +894,12 @@ export async function startRun( if ((options.launch || launch.isTest) && launch.emulateTerminal) launch = { ...launch, emulateTerminal: false }; if (launch.emulateTerminal) rt.terminal = new TerminalOutput(); patchSession(sessionId, { terminal: !!launch.emulateTerminal }); - const started = launch.isTest ? await gradleTestLaunch(withTestReporting(launch)) : await gradleBootLaunch(launch); + const started = + launch.params?.framework === "plugin" + ? launch + : launch.isTest + ? await gradleTestLaunch(withTestReporting(launch)) + : await gradleBootLaunch(launch); if (runtimes.get(sessionId)?.generation !== generation) return getSession(sessionId) ?? null; const processStarted = performance.now(); const info = await startWithRemedies(started, () => call(commands.runStart(started, channel))); @@ -1178,7 +1183,7 @@ export function rerunConfig(session: Pick): } /** The session's original configuration, as saved now (see {@link rerunConfig}). */ -function baseConfigOf(session: RunSession): RunConfig { +export function baseConfigOf(session: RunSession): RunConfig { const base = session.baseConfig ?? session.config; return useRun.getState().configs.find((c) => c.id === base.id) ?? base; } @@ -1218,11 +1223,13 @@ export function failedTests(sessionId: string): string[][] { } /** "Rerun Failed Tests": reruns only the failed tests in the same tab. Returns false when unsupported. */ -export function rerunFailed(sessionId: string): boolean { +export async function rerunFailed(sessionId: string): Promise { const session = getSession(sessionId); if (!session || isLive(session)) return false; const base = baseConfigOf(session); - const derived = rerunFailedConfigs(base, failedTests(sessionId)); + const selection = rerunFailedConfigs(base, failedTests(sessionId)); + const derived = selection instanceof Promise ? await selection : selection; + if (getSession(sessionId) !== session || isLive(session)) return false; if (!derived.length) return false; derived.forEach((config, index) => void startRun(config, index === 0 ? sessionId : undefined, base)); return true; @@ -1244,19 +1251,34 @@ export async function runSelectedTests( const paths = leafTestPaths(nodes); const base = baseConfigOf(session); const name = nodes.length === 1 ? nodes[0]!.name : `${paths.length} tests`; - const derived = testsConfigs(base, paths, name); + const selection = testsConfigs(base, paths, name); + const derived = selection instanceof Promise ? await selection : selection; + if (getSession(sessionId) !== session) return false; if (!derived.length) return false; for (const [index, next] of derived.entries()) { - const config = adoptTemporary({ - ...next, - id: `context:tests:${base.id}:${index}:${paths.map((p) => p.join("/")).join("|")}`, - }); + const config = + base.params?.framework === "plugin" + ? next + : adoptTemporary({ + ...next, + id: `context:tests:${base.id}:${index}:${paths.map((p) => p.join("/")).join("|")}`, + }); if (debug) await executeAction("run.debugConfig", config); else await startRun(config, undefined, base); } return true; } +/** Whether this exact test-tree selection can be narrowed by its framework. */ +export function canRunSelectedTests(sessionId: string, nodeIds: readonly string[]): boolean | Promise { + const session = getSession(sessionId); + const tests = runtimes.get(sessionId)?.tests; + if (!session || !tests) return false; + const nodes = nodeIds.map((id) => tests.get(id)).filter((n) => !!n); + const selection = testsConfigs(baseConfigOf(session), leafTestPaths(nodes), ""); + return selection instanceof Promise ? selection.then((configs) => configs.length > 0) : selection.length > 0; +} + /** Whether the console takes typed input: the process runs and its stdin is open. */ export function acceptsInput(session: RunSession): boolean { return session.status === "running" && session.runId !== null && !session.stdinClosed; diff --git a/src/features/run/testing/rerun.test.ts b/src/features/run/testing/rerun.test.ts index 0bedd8d88..d755a7f1b 100644 --- a/src/features/run/testing/rerun.test.ts +++ b/src/features/run/testing/rerun.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi, afterEach } from "vitest"; import type { RunConfig } from "@/platform/ipc"; import { TestResults } from "./results"; import { @@ -10,6 +10,8 @@ import { testsConfigs, } from "./rerun"; +import { providePluginTestSelector } from "@/workbench/contributions"; + const cfg = (kind: string, command: string, args: string[]): RunConfig => ({ id: `${kind}:test`, name: `${kind} test`, @@ -37,7 +39,7 @@ describe("failedTestPaths", () => { }); describe("tests selected in the test tree", () => { - it("expands suites to their tests; a suite without reported tests stands for itself", () => { + it("expands suites to their tests; a suite without reported tests stands for itself", async () => { const r = new TestResults(); r.apply([ { type: "result", path: ["com.acme.ATest", "one()"], status: "passed" }, @@ -54,7 +56,7 @@ describe("tests selected in the test tree", () => { ["com.acme.ATest", "two()"], ["com.acme.BTest"], ]); - const c = testsConfig(cfg("gradle", "./gradlew", ["test"]), leafTestPaths(nodes), "3 tests")!; + const c = (await testsConfig(cfg("gradle", "./gradlew", ["test"]), leafTestPaths(nodes), "3 tests"))!; expect(c.name).toBe("3 tests"); expect(c.args).toEqual([ "test", @@ -69,20 +71,20 @@ describe("tests selected in the test tree", () => { }); describe("rerunFailedConfig", () => { - it("cargo: exact names after --", () => { - const c = rerunFailedConfig(cfg("cargo", "cargo", ["test", "--workspace"]), [ + it("cargo: exact names after --", async () => { + const c = (await rerunFailedConfig(cfg("cargo", "cargo", ["test", "--workspace"]), [ ["src/lib.rs", "tests", "adds"], ["tests/it.rs", "smoke"], ["Doc-tests app", "src/lib.rs - foo (line 3)"], - ])!; + ]))!; expect(c.args).toEqual(["test", "--workspace", "--", "tests::adds", "smoke", "--exact"]); expect(c.name).toBe("Failed: cargo test"); }); - it("cargo: merges an existing -- section without duplicating --exact", () => { - const c = rerunFailedConfig(cfg("cargo", "cargo", ["test", "--", "--nocapture", "--exact", "old"]), [ + it("cargo: merges an existing -- section without duplicating --exact", async () => { + const c = (await rerunFailedConfig(cfg("cargo", "cargo", ["test", "--", "--nocapture", "--exact", "old"]), [ ["src/lib.rs", "a"], - ])!; + ]))!; expect(c.args).toEqual(["test", "--", "--nocapture", "old", "a", "--exact"]); }); @@ -94,11 +96,11 @@ describe("rerunFailedConfig", () => { ["yarn", ["--"]], ["bun", ["--"]], ["npx", []], - ])("jest/vitest via %s: files + -t pattern", (command, separator) => { - const c = rerunFailedConfig(cfg("npm", command, ["run", "test"]), [ + ])("jest/vitest via %s: files + -t pattern", async (command, separator) => { + const c = (await rerunFailedConfig(cfg("npm", command, ["run", "test"]), [ ["src/a.test.ts", "math", "adds (1+1)"], ["src/b.test.ts"], - ])!; + ]))!; expect(c.args).toEqual([ "run", "test", @@ -113,24 +115,24 @@ describe("rerunFailedConfig", () => { it.each([ ["run", "test", "-t", "old"], ["run", "test", "--", "--testNamePattern=old"], - ])("replaces an existing pnpm test filter in %j", (...args) => { - const c = rerunFailedConfig(cfg("npm", "pnpm", args), [["src/a.test.ts", "new"]])!; + ])("replaces an existing pnpm test filter in %j", async (...args) => { + const c = (await rerunFailedConfig(cfg("npm", "pnpm", args), [["src/a.test.ts", "new"]]))!; expect(c.args).toEqual(["run", "test", ...(args.includes("--") ? ["--"] : []), "src/a.test.ts", "-t", "^(?:new)$"]); }); - it("pytest: node ids replace explicit paths", () => { - const c = rerunFailedConfig(cfg("pytest", ".venv/bin/pytest", ["-q", "tests/", "-k", "slow"]), [ + it("pytest: node ids replace explicit paths", async () => { + const c = (await rerunFailedConfig(cfg("pytest", ".venv/bin/pytest", ["-q", "tests/", "-k", "slow"]), [ ["tests/test_x.py", "TestC", "test_y[1]"], ["tests/test_z.py", "test_a"], - ])!; + ]))!; expect(c.args).toEqual(["-q", "tests/test_x.py::TestC::test_y[1]", "tests/test_z.py::test_a"]); }); - it("django: dotted labels in place of the run's target, options kept", () => { - const c = rerunFailedConfig( + it("django: dotted labels in place of the run's target, options kept", async () => { + const c = (await rerunFailedConfig( cfg("django-test", ".venv/bin/python", ["manage.py", "test", "polls", "--keepdb", "-v", "2"]), [["polls.tests", "QuestionTests", "test_a"], ["polls.tests", "BrokenSetupTests"], ["broken.tests"]], - )!; + ))!; expect(c.args).toEqual([ "manage.py", "test", @@ -146,14 +148,14 @@ describe("rerunFailedConfig", () => { ...cfg("django-test", "uv", ["run", "python", "manage.py", "test", "polls"]), params: { command: "uv", runnerArgs: "run python", target: "polls", settings: "", options: "" }, }; - expect(rerunFailedConfig(form, [["polls.tests", "QuestionTests", "test_a"]])).toMatchObject({ + expect(await rerunFailedConfig(form, [["polls.tests", "QuestionTests", "test_a"]])).toMatchObject({ args: ["run", "python", "manage.py", "test", "polls.tests.QuestionTests.test_a"], params: { target: "polls.tests.QuestionTests.test_a" }, }); }); - it("go: keeps full subtest paths separate for each package", () => { - const configs = rerunFailedConfigs(cfg("go", "go", ["test", "-v", "./..."]), [ + it("go: keeps full subtest paths separate for each package", async () => { + const configs = await rerunFailedConfigs(cfg("go", "go", ["test", "-v", "./..."]), [ ["example.com/m/pkg", "TestAdd"], ["example.com/m/pkg", "TestSub", "case_1"], ["example.com/m/other", "TestX"], @@ -165,24 +167,22 @@ describe("rerunFailedConfig", () => { expect(new Set(configs.map((config) => config.id)).size).toBe(2); }); - it("gradle and maven filters", () => { - expect(rerunFailedConfig(cfg("gradle", "./gradlew", ["test"]), [["com.a.FooTest", "bar()"]])!.args).toEqual([ - "test", - "--tests", - "com.a.FooTest.bar", - ]); + it("gradle and maven filters", async () => { + expect((await rerunFailedConfig(cfg("gradle", "./gradlew", ["test"]), [["com.a.FooTest", "bar()"]]))!.args).toEqual( + ["test", "--tests", "com.a.FooTest.bar"], + ); expect( - rerunFailedConfig(cfg("maven", "mvn", ["test"]), [ + (await rerunFailedConfig(cfg("maven", "mvn", ["test"]), [ ["com.a.FooTest", "bar"], ["com.a.FooTest", "baz"], ["com.a.BarTest"], - ])!.args, + ]))!.args, ).toEqual(["test", "-Dtest=com.a.FooTest#bar+baz,com.a.BarTest"]); }); - it("returns null when nothing failed or the framework is unknown", () => { - expect(rerunFailedConfig(cfg("cargo", "cargo", ["test"]), [])).toBeNull(); - expect(rerunFailedConfig(cfg("make", "make", ["test"]), [["x"]])).toBeNull(); + it("returns null when nothing failed or the framework is unknown", async () => { + expect(await rerunFailedConfig(cfg("cargo", "cargo", ["test"]), [])).toBeNull(); + expect(await rerunFailedConfig(cfg("make", "make", ["test"]), [["x"]])).toBeNull(); }); }); @@ -226,16 +226,16 @@ describe("withTestReporting", () => { describe("Go rerun argument boundaries", () => { it.each(["-test.v", "-test.short", "-test.benchmem", "-test.failfast", "-test.fullpath"])( "does not retain package targets after the boolean flag %s", - (flag) => { + async (flag) => { const config = cfg("go", "go", ["test", flag, "./..."]); - const rerun = rerunFailedConfig(config, [["example.com/selected", "TestOnly"]])!; + const rerun = (await rerunFailedConfig(config, [["example.com/selected", "TestOnly"]]))!; expect(rerun.args).toEqual(["test", flag, "-run", "^TestOnly$", "example.com/selected"]); }, ); - it("replaces import-path targets without losing flag values or binary arguments", () => { + it("replaces import-path targets without losing flag values or binary arguments", async () => { const config = cfg("go", "go", ["test", "-tags", "demo", "-run", "old", "example.com/old", "-args", "-test.short"]); - const rerun = rerunFailedConfig(config, [["example.com/new", "TestChild", "case"]])!; + const rerun = (await rerunFailedConfig(config, [["example.com/new", "TestChild", "case"]]))!; expect(rerun.args).toEqual([ "test", "-tags", @@ -247,24 +247,27 @@ describe("Go rerun argument boundaries", () => { "-test.short", ]); }); - it.each(["command-line-arguments", "example.com/app"])("retains file-scoped preparation for %s", (packageName) => { - const config = { - ...cfg("go", "go", ["test", "main_test.go"]), - params: { goKind: "test", scope: "file", target: "main_test.go", module: "app", pattern: "", useTags: "true" }, - }; - const rerun = rerunFailedConfig(config, [[packageName, "TestFail"]])!; - expect(rerun.params).toMatchObject({ - goKind: "test", - scope: "file", - target: "main_test.go", - pattern: "^TestFail$", - }); - expect(rerun.args).toEqual(["test", "-run", "^TestFail$", "main_test.go"]); - }); + it.each(["command-line-arguments", "example.com/app"])( + "retains file-scoped preparation for %s", + async (packageName) => { + const config = { + ...cfg("go", "go", ["test", "main_test.go"]), + params: { goKind: "test", scope: "file", target: "main_test.go", module: "app", pattern: "", useTags: "true" }, + }; + const rerun = (await rerunFailedConfig(config, [[packageName, "TestFail"]]))!; + expect(rerun.params).toMatchObject({ + goKind: "test", + scope: "file", + target: "main_test.go", + pattern: "^TestFail$", + }); + expect(rerun.args).toEqual(["test", "-run", "^TestFail$", "main_test.go"]); + }, + ); }); describe("exact Go test and benchmark selections", () => { - it("keeps benchmark selectors and removes stale selectors from binary arguments", () => { + it("keeps benchmark selectors and removes stale selectors from binary arguments", async () => { const base = cfg("go", "go", [ "test", "-tags=demo", @@ -280,7 +283,7 @@ describe("exact Go test and benchmark selections", () => { "-custom", "keep", ]); - const [config] = testsConfigs(base, [["example.com/p", "BenchmarkNew", "case"]], "selected"); + const [config] = await testsConfigs(base, [["example.com/p", "BenchmarkNew", "case"]], "selected"); expect(config!.args).toEqual([ "test", "-tags=demo", @@ -295,8 +298,8 @@ describe("exact Go test and benchmark selections", () => { ]); }); - it("keeps independent alternatives for tests of different depths", () => { - const [config] = testsConfigs( + it("keeps independent alternatives for tests of different depths", async () => { + const [config] = await testsConfigs( cfg("go", "go", ["test", "."]), [ ["p", "TestA", "one"], @@ -315,7 +318,7 @@ describe("exact Go test and benchmark selections", () => { ]); }); - it("keeps typed file benchmark flags and clears overridden binary selectors", () => { + it("keeps typed file benchmark flags and clears overridden binary selectors", async () => { const base = { ...cfg("go", "go", ["test", "chosen_test.go", "-args", "-test.run=Old", "-custom", "keep"]), params: { @@ -326,7 +329,7 @@ describe("exact Go test and benchmark selections", () => { programArgs: "-test.run=Old -custom keep", }, }; - const [config] = testsConfigs(base, [["example.com/p", "BenchmarkNew"]], "selected"); + const [config] = await testsConfigs(base, [["example.com/p", "BenchmarkNew"]], "selected"); expect(config!.params).toMatchObject({ target: "chosen_test.go", pattern: "^$", @@ -336,17 +339,81 @@ describe("exact Go test and benchmark selections", () => { expect(config!.args).toContain("chosen_test.go"); }); - it("reruns an entire failed package with its original runner options", () => { + it("reruns an entire failed package with its original runner options", async () => { const base = cfg("go", "go", ["test", "-race", "-bench", ".", "./..."]); - const [config] = testsConfigs(base, [["example.com/p"]], "selected"); + const [config] = await testsConfigs(base, [["example.com/p"]], "selected"); expect(config!.args).toEqual(["test", "-race", "-bench", ".", "example.com/p"]); }); }); -it("keeps inherited Go build tags enabled when deriving a typed package rerun", () => { +it("keeps inherited Go build tags enabled when deriving a typed package rerun", async () => { const config = { ...cfg("go", "go", ["test", "./..."]), params: { goKind: "test", scope: "package", target: "./..." }, }; - expect(rerunFailedConfig(config, [["example.com/p", "TestChosen"]])!.params?.useTags).toBe("true"); + expect((await rerunFailedConfig(config, [["example.com/p", "TestChosen"]]))!.params?.useTags).toBe("true"); +}); + +describe("plugin test selection", () => { + let dispose = () => {}; + afterEach(() => dispose()); + const pluginConfig = (): RunConfig => ({ + ...cfg("plugin", "runner", ["test", "--old-filter"]), + id: "plugin:example:tests", + params: { framework: "plugin" }, + env: { EXAMPLE: "yes" }, + cwd: "/project", + target: "plugin:example:container", + }); + + it("passes the owning config and exact failed name paths, replacing argv and preserving launch settings", async () => { + const select = vi.fn().mockResolvedValue(["test", "--only", "suite::fails"]); + dispose = providePluginTestSelector(select); + const base = pluginConfig(); + const results = new TestResults(); + results.apply([ + { type: "result", path: ["suite", "fails"], status: "failed" }, + { type: "result", path: ["suite", "passes"], status: "passed" }, + ]); + const configs = await rerunFailedConfigs(base, failedTestPaths(results)); + expect(select).toHaveBeenCalledExactlyOnceWith(base.id, [["suite", "fails"]]); + expect(configs).toEqual([{ ...base, name: "Failed: plugin test", args: ["test", "--only", "suite::fails"] }]); + expect(configs[0]!.env).not.toBe(base.env); + expect(base.args).toEqual(["test", "--old-filter"]); + }); + + it("uses the same selection for test-tree suites and accepts empty replacement argv", async () => { + const select = vi.fn().mockResolvedValue([]); + dispose = providePluginTestSelector(select); + const base = pluginConfig(); + const paths = [ + ["suite", "one"], + ["suite", "two"], + ]; + expect(await testsConfig(base, paths, "suite")).toEqual({ ...base, args: [], name: "suite" }); + expect(await testsConfigs(base, paths, "suite")).toHaveLength(1); + expect(select).toHaveBeenCalledWith(base.id, paths); + }); + + it("returns unsupported for no provider, none, errors and empty selection", async () => { + const base = pluginConfig(); + expect(await testsConfig(base, [["test"]], "selected")).toBeNull(); + const select = vi.fn().mockResolvedValue(null); + dispose = providePluginTestSelector(select); + expect(await rerunFailedConfigs(base, [["test"]])).toEqual([]); + select.mockRejectedValue(new Error("timeout")); + expect(await testsConfigs(base, [["test"]], "selected")).toEqual([]); + select.mockClear(); + expect(testsConfigs(base, [], "selected")).toEqual([]); + expect(select).not.toHaveBeenCalled(); + }); + + it("keeps built-in selection synchronous without calling plugins", () => { + const select = vi.fn(); + dispose = providePluginTestSelector(select); + const base = cfg("cargo", "cargo", ["test"]); + expect(testsConfig(base, [["src/lib.rs", "test"]], "test")).not.toBeInstanceOf(Promise); + expect(rerunFailedConfigs(base, [["src/lib.rs", "test"]])).not.toBeInstanceOf(Promise); + expect(select).not.toHaveBeenCalled(); + }); }); diff --git a/src/features/run/testing/rerun.ts b/src/features/run/testing/rerun.ts index 94ded0b5d..83e58423f 100644 --- a/src/features/run/testing/rerun.ts +++ b/src/features/run/testing/rerun.ts @@ -2,6 +2,7 @@ * "Rerun Failed Tests": derives a run configuration that runs only the failed * tests of a previous run, using each framework's own filter syntax. */ +import { selectPluginTests } from "@/workbench/contributions"; import { phpunitArgs, phpunitFilter } from "./phpunit"; import type { RunConfig } from "@/platform/ipc"; import { formatArgs } from "../configs"; @@ -149,18 +150,25 @@ export function leafTestPaths(nodes: readonly TestNode[]): string[][] { } /** Config running only `paths` (failed test paths from the results tree), or null when unsupported. */ -export function rerunFailedConfig(config: RunConfig, paths: string[][]): RunConfig | null { +export function rerunFailedConfig(config: RunConfig, paths: string[][]): RunConfig | null | Promise { const configs = rerunFailedConfigs(config, paths); - return configs.length === 1 ? configs[0]! : null; + const single = (configs: RunConfig[]) => (configs.length === 1 ? configs[0]! : null); + return configs instanceof Promise ? configs.then(single) : single(configs); } -export function rerunFailedConfigs(config: RunConfig, paths: string[][]): RunConfig[] { +export function rerunFailedConfigs(config: RunConfig, paths: string[][]): RunConfig[] | Promise { const baseName = config.name.replace(/^Failed: /, ""); return testsConfigs(config, paths, `Failed: ${baseName}`); } /** Go filters apply to every package in an invocation, so each package keeps its own selection. */ -export function testsConfigs(config: RunConfig, paths: string[][], name: string): RunConfig[] { +export function testsConfigs(config: RunConfig, paths: string[][], name: string): RunConfig[] | Promise { + if (!paths.length) return []; + if (config.params?.framework === "plugin") { + const selected = testsConfig(config, paths, name); + const list = (config: RunConfig | null) => (config ? [config] : []); + return selected instanceof Promise ? selected.then(list) : list(selected); + } const groups = new Map(); for (const path of paths) { const key = frameworkFor(config) === "go" ? (path[0] ?? "") : ""; @@ -169,14 +177,28 @@ export function testsConfigs(config: RunConfig, paths: string[][], name: string) groups.set(key, group); } return [...groups].flatMap(([pkg, selected]) => { - const derived = testsConfig(config, selected, groups.size > 1 ? `${name} (${pkg})` : name); + const derived = builtinTestsConfig(config, selected, groups.size > 1 ? `${name} (${pkg})` : name); if (!derived) return []; return [{ ...derived, id: groups.size > 1 ? `context:tests:${config.id}:${pkg}` : derived.id }]; }); } /** Config running only the tests at `paths` (test tree paths), named `name`; null when unsupported. */ -export function testsConfig(config: RunConfig, paths: string[][], name: string): RunConfig | null { +export function testsConfig( + config: RunConfig, + paths: string[][], + name: string, +): RunConfig | null | Promise { + if (!paths.length) return null; + if (config.params?.framework === "plugin") { + return selectPluginTests(config.id, paths) + .then((args) => (args === null ? null : { ...config, args, name, detected: true, env: { ...config.env } })) + .catch(() => null); + } + return builtinTestsConfig(config, paths, name); +} + +function builtinTestsConfig(config: RunConfig, paths: string[][], name: string): RunConfig | null { if (!paths.length) return null; let args: string[] | null; switch (frameworkFor(config)) { diff --git a/src/workbench/contributions.ts b/src/workbench/contributions.ts index 50da15fb5..bd535e46c 100644 --- a/src/workbench/contributions.ts +++ b/src/workbench/contributions.ts @@ -1749,6 +1749,19 @@ export function parsePluginTests(config: string, chunk: string) { return pluginTestParser?.(config, chunk) ?? Promise.resolve(null); } +/** Optional selection support owned by the same host as the plugin test parser. */ +type PluginTestSelector = (config: string, tests: string[][]) => Promise; +let pluginTestSelector: PluginTestSelector | null = null; +export function providePluginTestSelector(selector: PluginTestSelector): () => void { + pluginTestSelector = selector; + return () => { + if (pluginTestSelector === selector) pluginTestSelector = null; + }; +} +export function selectPluginTests(config: string, tests: string[][]) { + return pluginTestSelector?.(config, tests) ?? Promise.resolve(null); +} + /** Read-only named snippets supplied by plugins. */ export interface ContributedSnippet { id: string; diff --git a/wit/run.wit b/wit/run.wit index c1bc79cf9..6c62534e2 100644 --- a/wit/run.wit +++ b/wit/run.wit @@ -47,4 +47,7 @@ interface test-results { } /// Parses one bounded output batch; failures fall back to built-in parsers. parse: func(config: string, chunk: string) -> list; + /// Selects test-tree name paths (parents then test), using the same config id as parse. + /// Returns the full replacement argument list, or none when filtering is unsupported. + select-tests: func(config: string, tests: list>) -> option>; }