diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 18dea9d61b..13a4fcc9bd 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -842,7 +842,7 @@ impl ContextProvider for GoContextProvider { "-v".into(), "-run".into(), format!( - "'^{}$/^{}$'", + "\\^{}\\$/\\^{}\\$", VariableName::Symbol.template_value(), GO_SUBTEST_NAME_TASK_VARIABLE.template_value(), ), @@ -943,6 +943,7 @@ mod tests { use super::*; use crate::language; use gpui::{AppContext, Hsla, TestAppContext}; + use task::TaskContext; use theme::SyntaxTheme; fn go_language() -> Arc { @@ -1219,6 +1220,73 @@ mod tests { ); } + #[gpui::test] + async fn test_go_test_templates_run_arg_is_shell_escaped(cx: &mut TestAppContext) { + let templates = cx + .update(|cx| GoContextProvider.associated_tasks(None, cx)) + .await + .expect("Go context provider returns associated tasks"); + + // `resolve_task` returns `None` for any `ZED_` variable a template + // references but the context omits, so supply all of them. + let context = TaskContext { + cwd: None, + task_variables: TaskVariables::from_iter([ + (VariableName::Symbol, "TestFoo".to_string()), + (GO_SUBTEST_NAME_TASK_VARIABLE, "simple_subtest".to_string()), + ( + GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE, + "table_case".to_string(), + ), + (GO_SUITE_NAME_TASK_VARIABLE, "Suite".to_string()), + (GO_PACKAGE_TASK_VARIABLE, ".".to_string()), + (VariableName::Dirname, "/tmp".to_string()), + ]), + project_env: HashMap::default(), + }; + + // `go-benchmark` is excluded: its `-run='^$'` form intentionally quotes. + let escaped_run_arg_tags = [ + "go-test", + "go-example", + "go-subtest", + "go-fuzz", + "go-testify-suite", + "go-table-test-case", + ]; + + for tag in escaped_run_arg_tags { + let template = templates + .0 + .iter() + .find(|template| template.tags.iter().any(|template_tag| template_tag == tag)) + .unwrap_or_else(|| panic!("`{tag}` task template exists")); + + let resolved = template + .resolve_task("go", &context) + .unwrap_or_else(|| panic!("`{tag}` template resolves")); + + let run_index = resolved + .resolved + .args + .iter() + .position(|arg| arg == "-run") + .unwrap_or_else(|| panic!("`{tag}` resolved args contain a `-run` flag")); + let run_arg = &resolved.resolved.args[run_index + 1]; + + assert!( + !run_arg.contains('\''), + "`{tag}` -run arg must not shell-quote the regex; single quotes leak \ + literally into Delve's regex and break Debug Test (#53230), got {run_arg:?}" + ); + assert!( + run_arg.starts_with("\\^") && run_arg.ends_with("\\$"), + "`{tag}` -run arg must escape its regex anchors as \\^...\\$ so the shell \ + and GoLocator both strip them, got {run_arg:?}" + ); + } + } + #[gpui::test] fn test_go_example_test_detection(cx: &mut TestAppContext) { let language = go_language(); diff --git a/crates/project/tests/integration/debugger.rs b/crates/project/tests/integration/debugger.rs index 61bba78c74..ef0671f305 100644 --- a/crates/project/tests/integration/debugger.rs +++ b/crates/project/tests/integration/debugger.rs @@ -173,6 +173,44 @@ mod go_locator { ); } + #[gpui::test] + async fn test_go_locator_unescapes_nested_subtest_regex(_: &mut TestAppContext) { + let locator = GoLocator; + let delve = DebugAdapterName("Delve".into()); + + // Delve receives the `-run` regex with no shell, so GoLocator must strip + // the escaping itself. + let task = TaskTemplate { + label: "test subtest".into(), + command: "go".into(), + args: vec![ + "test".to_string(), + "-v".to_string(), + "-run".to_string(), + "\\^TestFoo\\$/\\^simple_subtest\\$".to_string(), + ], + ..Default::default() + }; + let result = locator.create_scenario(&task, "", &delve).await.unwrap(); + let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap(); + assert_eq!( + config, + DelveLaunchRequest { + request: "launch".to_string(), + mode: "test".to_string(), + program: ".".to_string(), + build_flags: vec![], + args: vec![ + "-test.v".to_string(), + "-test.run".to_string(), + "^TestFoo$/^simple_subtest$".to_string(), + ], + env: Default::default(), + cwd: None, + } + ); + } + #[gpui::test] async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) { let locator = GoLocator;