go: Stop running ghost tests, fix broken `go test -run` for suites (#38167)

Kaikai created

Closed #33759
Closed #38166

### Summary

This PR fixes the way `go test` commands are generated for **testify
suite test methods**.
Previously, only the method name was included in the `-run` flag, which
caused Go’s test runner to fail to find suite test cases.

---

### Problem


https://github.com/user-attachments/assets/e6f80a77-bcf3-457c-8bfb-a7286d44ff71

1. **Incorrect command** was generated for suite tests:

   ```bash
   go test -run TestSomething_Success
   ```

   This results in:

   ```
   testing: warning: no tests to run
   ```

2. The correct format requires the **suite name + method name**:

   ```bash
   go test -run ^TestFooSuite$/TestSomething_Success$
   ```

Without the suite prefix (`TestFooSuite`), Go cannot locate test methods
defined on a suite struct.

---

### Changes Made

* **Updated `runnables.scm`**:

  * Added a new query rule for suite methods (`.*Suite` receiver types).
* Ensures only methods on suite structs (e.g., `FooSuite`) are matched.
  * Tagged these with `go-testify-suite` in addition to `go-test`.

* **Extended task template generation**:

  * Introduced `GO_SUITE_NAME_TASK_VARIABLE` to capture the suite name.
  * Create a `TaskTemplate` for the testify suite.

* **Improved labeling**:

* Labels now show the full path (`go test ./pkg -v -run
TestFooSuite/TestSomething_Success`) for clarity.

* **Added a test** `test_testify_suite_detection`:

* Covered testify suite cases to ensure correct detection and command
generation.

---

### Impact


https://github.com/user-attachments/assets/ef509183-534a-4aa4-9dc7-01402ac32260

* **Before**: Running a suite test method produced “no tests to run.”
* **After**: Suite test methods are runnable individually with the
correct `-run` command, and full suites can still be executed as before.

### Release Notes

* Fixed generation of `go test` commands for **testify suite test
methods**.
Suite methods now include both the suite name and the method name in the
`-run` flag (e.g., `^TestFooSuite$/TestSomething_Success$`), ensuring
they are properly detected and runnable individually.

Change summary

crates/languages/src/go.rs            | 88 ++++++++++++++++++++++++++++
crates/languages/src/go/runnables.scm | 26 +++++---
2 files changed, 102 insertions(+), 12 deletions(-)

Detailed changes

crates/languages/src/go.rs 🔗

@@ -479,6 +479,8 @@ const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
 const GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("GO_TABLE_TEST_CASE_NAME"));
+const GO_SUITE_NAME_TASK_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("GO_SUITE_NAME"));
 
 impl ContextProvider for GoContextProvider {
     fn build_context(
@@ -537,19 +539,26 @@ impl ContextProvider for GoContextProvider {
         let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
             .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
 
-        let table_test_case_name = variables.get(&VariableName::Custom(Cow::Borrowed(
+        let _table_test_case_name = variables.get(&VariableName::Custom(Cow::Borrowed(
             "_table_test_case_name",
         )));
 
-        let go_table_test_case_variable = table_test_case_name
+        let go_table_test_case_variable = _table_test_case_name
             .and_then(extract_subtest_name)
             .map(|case_name| (GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.clone(), case_name));
 
+        let _suite_name = variables.get(&VariableName::Custom(Cow::Borrowed("_suite_name")));
+
+        let go_suite_variable = _suite_name
+            .and_then(extract_subtest_name)
+            .map(|suite_name| (GO_SUITE_NAME_TASK_VARIABLE.clone(), suite_name));
+
         Task::ready(Ok(TaskVariables::from_iter(
             [
                 go_package_variable,
                 go_subtest_variable,
                 go_table_test_case_variable,
+                go_suite_variable,
                 go_module_root_variable,
             ]
             .into_iter()
@@ -566,6 +575,28 @@ impl ContextProvider for GoContextProvider {
         let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
 
         Task::ready(Some(TaskTemplates(vec![
+            TaskTemplate {
+                label: format!(
+                    "go test {} -v -run Test{}/{}",
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    GO_SUITE_NAME_TASK_VARIABLE.template_value(),
+                    VariableName::Symbol.template_value(),
+                ),
+                command: "go".into(),
+                args: vec![
+                    "test".into(),
+                    "-v".into(),
+                    "-run".into(),
+                    format!(
+                        "\\^Test{}\\$/\\^{}\\$",
+                        GO_SUITE_NAME_TASK_VARIABLE.template_value(),
+                        VariableName::Symbol.template_value(),
+                    ),
+                ],
+                cwd: package_cwd.clone(),
+                tags: vec!["go-testify-suite".to_owned()],
+                ..TaskTemplate::default()
+            },
             TaskTemplate {
                 label: format!(
                     "go test {} -v -run {}/{}",
@@ -819,6 +850,59 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    fn test_testify_suite_detection(cx: &mut TestAppContext) {
+        let language = language("go", tree_sitter_go::LANGUAGE.into());
+
+        let testify_suite = r#"
+        package main
+
+        import (
+            "testing"
+
+            "github.com/stretchr/testify/suite"
+        )
+
+        type ExampleSuite struct {
+            suite.Suite
+        }
+
+        func TestExampleSuite(t *testing.T) {
+            suite.Run(t, new(ExampleSuite))
+        }
+
+        func (s *ExampleSuite) TestSomething_Success() {
+            // test code
+        }
+        "#;
+
+        let buffer = cx
+            .new(|cx| crate::Buffer::local(testify_suite, cx).with_language(language.clone(), cx));
+        cx.executor().run_until_parked();
+
+        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
+            let snapshot = buffer.snapshot();
+            snapshot.runnable_ranges(0..testify_suite.len()).collect()
+        });
+
+        let tag_strings: Vec<String> = runnables
+            .iter()
+            .flat_map(|r| &r.runnable.tags)
+            .map(|tag| tag.0.to_string())
+            .collect();
+
+        assert!(
+            tag_strings.contains(&"go-test".to_string()),
+            "Should find go-test tag, found: {:?}",
+            tag_strings
+        );
+        assert!(
+            tag_strings.contains(&"go-testify-suite".to_string()),
+            "Should find go-testify-suite tag, found: {:?}",
+            tag_strings
+        );
+    }
+
     #[gpui::test]
     fn test_go_runnable_detection(cx: &mut TestAppContext) {
         let language = language("go", tree_sitter_go::LANGUAGE.into());

crates/languages/src/go/runnables.scm 🔗

@@ -1,22 +1,28 @@
 ; Functions names start with `Test`
 (
-  [
+  (
     (function_declaration name: (_) @run
       (#match? @run "^Test.*"))
+  ) @_
+  (#set! tag go-test)
+)
+
+; Suite test methods (testify/suite)
+(
     (method_declaration
       receiver: (parameter_list
         (parameter_declaration
-          name: (identifier) @_receiver_name
-          type: [
-            (pointer_type (type_identifier) @_receiver_type)
-            (type_identifier) @_receiver_type
-          ]
+            type: [
+                (pointer_type (type_identifier) @_suite_name)
+                (type_identifier) @_suite_name
+            ]
         )
       )
-      name: (field_identifier) @run @_method_name
-      (#match? @_method_name "^Test.*"))
-  ] @_
-  (#set! tag go-test)
+      name: (field_identifier) @run @_subtest_name
+      (#match? @_subtest_name "^Test.*")
+      (#match? @_suite_name ".*Suite")
+    ) @_
+    (#set! tag go-testify-suite)
 )
 
 ; `go:generate` comments