From 2abdac4fdbdf6c54a7611d9efcf1fc8eeb4af61d Mon Sep 17 00:00:00 2001 From: GuoYu Date: Fri, 13 Nov 2026 13:04:35 +0800 Subject: [PATCH] fix(mcp): a run point in a Cargo workspace member runs that member's tests (#3059) execute_run_configuration with a run point in a Cargo workspace member ran the first member's tests: every member's `cargo test -p ` configuration runs from the workspace root, so all were equally close to the file and the first won. A Cargo configuration now covers its member's directory (extras.module) when the closest configuration is picked; its working directory stays the workspace root. Co-authored-by: guoyucode <29471644+guoyucode@users.noreply.github.com> --- crates/ide-host/src/mcp/run/targets.rs | 44 ++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/ide-host/src/mcp/run/targets.rs b/crates/ide-host/src/mcp/run/targets.rs index 4a1b8e4ff..6ef50e7ff 100644 --- a/crates/ide-host/src/mcp/run/targets.rs +++ b/crates/ide-host/src/mcp/run/targets.rs @@ -434,10 +434,23 @@ fn cwd_of(root: &Path, c: &RunConfig) -> String { cwd.trim_end_matches('/').to_owned() } -/// Configs whose working directory most specifically contains `rel` (ties in their original order). +/// The directory a config covers: a Cargo workspace member's own directory (its `cargo test -p` +/// runs from the workspace root), else its working directory. +fn scope_of(root: &Path, c: &RunConfig) -> String { + match c.extras.module.as_deref() { + Some(module) if c.kind == "cargo" => { + let module = module.replace('\\', "/"); + let module = module.strip_prefix("./").unwrap_or(&module); + if module == "." { String::new() } else { module.trim_end_matches('/').to_owned() } + } + _ => cwd_of(root, c), + } +} + +/// Configs whose scope ([`scope_of`]) most specifically contains `rel` (ties in their original order). fn all_closest<'a>(root: &Path, configs: &[&'a RunConfig], rel: &str) -> Vec<&'a RunConfig> { let score = |c: &RunConfig| -> Option { - let cwd = cwd_of(root, c); + let cwd = scope_of(root, c); if cwd.is_empty() { Some(0) } else { (rel == cwd || rel.starts_with(&format!("{cwd}/"))).then_some(cwd.len()) } }; let scored: Vec<(&RunConfig, usize)> = configs.iter().filter_map(|c| score(c).map(|s| (*c, s))).collect(); @@ -922,6 +935,33 @@ mod tests { assert_eq!(c.args, ["test", "--", "math::tests::"]); } + #[test] + fn a_workspace_member_runs_its_own_tests() { + let root = Path::new("/p"); + let member = |pkg: &str, dir: &str| { + let mut c = blank( + format!("cargo:test:{pkg}"), + "test", + "cargo", + "cargo".into(), + vec!["test".into(), "-p".into(), pkg.into()], + ); + c.is_test = true; + c.extras.module = Some(dir.into()); + c + }; + let mut workspace = blank("cargo:test".into(), "test", "cargo", "cargo".into(), vec!["test".into()]); + workspace.is_test = true; + let configs = vec![member("ide-agent", "crates/ide-agent"), member("ide-mcp", "crates/ide-mcp"), workspace]; + let src = "#[cfg(test)]\nmod tests {\n #[test]\n fn adds() {}\n}\n"; + let rel = "crates/ide-mcp/src/stdio.rs"; + let ts = detect(src, rel); + let c = target_config(root, &ts[1], rel, &configs, src).unwrap(); + assert_eq!(c.args, ["test", "-p", "ide-mcp", "--", "stdio::tests::adds", "--exact"]); + let c = target_config(root, &ts[1], "src/lib.rs", &configs, src).unwrap(); + assert!(!c.args.iter().any(|a| a == "-p"), "outside every member: the workspace's tests, {:?}", c.args); + } + #[test] fn cargo_test_args_get_one_separator() { let root = Path::new("/p");