go: fix table test detection in large files

This commit is contained in:
Smit Barmase 2026-05-20 21:04:05 +05:30
parent a737361b20
commit c791f4f85b
No known key found for this signature in database
3 changed files with 388 additions and 33 deletions

View file

@ -85,24 +85,26 @@
(#eq? @_key_type "string"))
]
body: (literal_value
[
([
(literal_element
(literal_value
(keyed_element
((keyed_element
(literal_element
(identifier) @_field_name)
(literal_element
[
(interpreted_string_literal) @run @_table_test_case_name
(raw_string_literal) @run @_table_test_case_name
]))))
]))
","?)+))
(keyed_element
(literal_element
[
(interpreted_string_literal) @run @_table_test_case_name
(raw_string_literal) @run @_table_test_case_name
]))
]))))
] @_run_item
","?)+))))
(for_statement
(range_clause
left: (expression_list
@ -127,8 +129,7 @@
(selector_expression
operand: (identifier) @_tc_var
(#eq? @_tc_var @_loop_var_inner)
field: (field_identifier) @_field_check
(#eq? @_field_check @_field_name))
field: (field_identifier) @_field_check)
(identifier) @_arg_var
(#eq? @_arg_var @_loop_var_outer)
]
@ -162,24 +163,26 @@
(#eq? @_key_type "string"))
]
body: (literal_value
[
([
(literal_element
(literal_value
(keyed_element
((keyed_element
(literal_element
(identifier) @_field_name)
(literal_element
[
(interpreted_string_literal) @run @_table_test_case_name
(raw_string_literal) @run @_table_test_case_name
]))))
]))
","?)+))
(keyed_element
(literal_element
[
(interpreted_string_literal) @run @_table_test_case_name
(raw_string_literal) @run @_table_test_case_name
]))
])))
] @_run_item
","?)+)))
body: (block
(statement_list
(expression_statement
@ -194,8 +197,7 @@
(selector_expression
operand: (identifier) @_tc_var
(#eq? @_tc_var @_loop_var_inner)
field: (field_identifier) @_field_check
(#eq? @_field_check @_field_name))
field: (field_identifier) @_field_check)
(identifier) @_arg_var
(#eq? @_arg_var @_loop_var_outer)
]

View file

@ -586,6 +586,74 @@ fn adjust_runs(
pub(crate) struct GoContextProvider;
pub(crate) struct GoRunnableResolver;
impl RunnableResolver for GoRunnableResolver {
fn resolve(
&self,
local_captures: &[RunnableMatchCapture],
shared_captures: &[RunnableMatchCapture],
buffer: &BufferSnapshot,
) -> Option<ResolvedRunnable> {
const FIELD_CHECK: &str = "_field_check";
const FIELD_NAME: &str = "_field_name";
const TABLE_TEST_CASE_NAME: &str = "_table_test_case_name";
let text_for_capture = |capture: &RunnableMatchCapture| -> String {
buffer.text_for_range(capture.range()).collect()
};
// A candidate can have multiple string fields (e.g. `{ name: "x", anotherStr: "y" }`);
// each keyed_element contributes one `@_field_name` + one `@run` pair, in
// source order. The pair we want is the one whose name text equals
// `@_field_check` — that's the field `t.Run(tc.<field>, ...)` selects on.
let reference_text = shared_captures.iter().find_map(|capture| {
(capture.name() == Some(FIELD_CHECK)).then(|| text_for_capture(capture))
});
let mut name_captures: smallvec::SmallVec<[&RunnableMatchCapture; 2]> =
smallvec::SmallVec::new();
let mut run_captures: smallvec::SmallVec<[&RunnableMatchCapture; 2]> =
smallvec::SmallVec::new();
for capture in local_captures {
if capture.name() == Some(FIELD_NAME) {
name_captures.push(capture);
} else if capture.is_run() {
run_captures.push(capture);
}
}
let pair_index = match reference_text.as_deref() {
Some(reference) => name_captures.iter().position(|capture| {
let range = capture.range();
range.len() == reference.len() && buffer.text_for_range(range).equals_str(reference)
}),
None => (!run_captures.is_empty()).then_some(0),
}?;
let run_capture = *run_captures.get(pair_index)?;
let matched_name_capture = name_captures.get(pair_index).copied();
let run_range = run_capture.range();
let mut extra_captures: smallvec::SmallVec<[(String, String); 2]> =
smallvec::SmallVec::new();
if let Some(name_capture) = matched_name_capture
&& let Some(name) = name_capture.name()
{
extra_captures.push((name.to_string(), text_for_capture(name_capture)));
}
for capture in local_captures {
if capture.name() == Some(TABLE_TEST_CASE_NAME) && capture.range() == run_range {
extra_captures.push((TABLE_TEST_CASE_NAME.to_string(), text_for_capture(capture)));
}
}
Some(ResolvedRunnable {
run_range,
extra_captures,
})
}
}
const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
@ -894,10 +962,22 @@ mod tests {
use gpui::{AppContext, Hsla, TestAppContext};
use theme::SyntaxTheme;
fn go_language() -> Arc<Language> {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = Arc::try_unwrap(language)
.ok()
.expect("test language arc should be unique");
Arc::new(
language
.with_context_provider(Some(Arc::new(GoContextProvider)))
.with_runnable_resolver(Some(Arc::new(GoRunnableResolver))),
)
}
#[gpui::test]
async fn test_go_label_for_completion() {
let adapter = Arc::new(GoLspAdapter);
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let theme = SyntaxTheme::new_test([
("type", Hsla::default()),
@ -985,7 +1065,7 @@ mod tests {
#[gpui::test]
fn test_go_test_main_ignored(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let example_test = r#"
package main
@ -1019,7 +1099,7 @@ mod tests {
#[gpui::test]
fn test_testify_suite_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let testify_suite = r#"
package main
@ -1072,7 +1152,7 @@ mod tests {
#[gpui::test]
fn test_go_runnable_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let interpreted_string_subtest = r#"
package main
@ -1161,7 +1241,7 @@ mod tests {
#[gpui::test]
fn test_go_example_test_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let example_test = r#"
package main
@ -1198,7 +1278,7 @@ mod tests {
#[gpui::test]
fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let table_test = r#"
package main
@ -1272,27 +1352,54 @@ mod tests {
);
let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
// This is currently broken; see #39148
// let go_table_test_count = tag_strings
// .iter()
// .filter(|&tag| tag == "go-table-test-case")
// .count();
let go_table_test_count = tag_strings
.iter()
.filter(|&tag| tag == "go-table-test-case")
.count();
assert!(
go_test_count == 1,
"Should find exactly 1 go-test, found: {}",
go_test_count
);
// assert!(
// go_table_test_count == 3,
// "Should find exactly 3 go-table-test-case, found: {}",
// go_table_test_count
// );
assert!(
go_table_test_count == 3,
"Should find exactly 3 go-table-test-case, found: {}",
go_table_test_count
);
let Some(first_case_offset) = table_test.find("anotherStr: \"foo\"") else {
panic!("missing first table test case");
};
let first_case_offset = first_case_offset + "anotherStr".len();
let first_case_runnables: Vec<_> = buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot
.runnable_ranges(first_case_offset..first_case_offset)
.collect()
});
let table_test_case_names: Vec<_> = first_case_runnables
.iter()
.filter(|runnable| {
runnable
.runnable
.tags
.iter()
.any(|tag| tag.0 == "go-table-test-case")
})
.filter_map(|runnable| runnable.extra_captures.get("_table_test_case_name"))
.collect();
assert_eq!(
table_test_case_names,
vec!["\"test case 1\""],
"Should only return the table test case containing the requested range"
);
}
#[gpui::test]
fn test_go_table_test_slice_without_explicit_variable_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let table_test = r#"
package main
@ -1351,17 +1458,26 @@ mod tests {
);
let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
let go_table_test_count = tag_strings
.iter()
.filter(|&tag| tag == "go-table-test-case-without-explicit-variable")
.count();
assert!(
go_test_count == 1,
"Should find exactly 1 go-test, found: {}",
go_test_count
);
assert!(
go_table_test_count == 3,
"Should find exactly 3 go-table-test-case-without-explicit-variable, found: {}",
go_table_test_count
);
}
#[gpui::test]
fn test_go_table_test_map_without_explicit_variable_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let table_test = r#"
package main
@ -1435,7 +1551,7 @@ mod tests {
#[gpui::test]
fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let table_test = r#"
package main
@ -1485,7 +1601,7 @@ mod tests {
#[gpui::test]
fn test_go_table_test_map_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let table_test = r#"
package main
@ -1574,7 +1690,7 @@ mod tests {
#[gpui::test]
fn test_go_table_test_map_ignored(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let language = go_language();
let table_test = r#"
package main
@ -1622,6 +1738,241 @@ mod tests {
);
}
#[gpui::test]
fn test_go_table_test_stress(cx: &mut TestAppContext) {
let language = go_language();
let mut entries = String::new();
for i in 0..100 {
entries.push_str(&format!(
" {{ name: \"case {}\", value: {} }},\n",
i, i
));
}
let table_test = format!(
r#"
package main
import "testing"
func TestStress(t *testing.T) {{
testCases := []struct{{
name string
value int
}}{{
{entries} }}
for _, tc := range testCases {{
t.Run(tc.name, func(t *testing.T) {{
_ = tc.value
}})
}}
}}
"#,
entries = entries
);
let buffer = cx.new(|cx| {
crate::Buffer::local(table_test.clone(), 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..table_test.len()).collect()
});
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
let go_table_test_count = tag_strings
.iter()
.filter(|&tag| tag == "go-table-test-case")
.count();
assert_eq!(
go_table_test_count, 100,
"Should emit one go-table-test-case per row (got {}); tree-sitter match_limit overflow has regressed",
go_table_test_count
);
}
#[gpui::test]
fn test_go_table_test_mismatched_field(cx: &mut TestAppContext) {
let language = go_language();
let table_test = r#"
package main
import "testing"
func TestMismatchedField(t *testing.T) {
testCases := []struct{
name string
}{
{ name: "test case 1" },
{ name: "test case 2" },
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// test code here
})
}
}
"#;
let buffer =
cx.new(|cx| crate::Buffer::local(table_test, 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..table_test.len()).collect()
});
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
let go_table_test_count = tag_strings
.iter()
.filter(|&tag| tag == "go-table-test-case")
.count();
assert_eq!(
go_table_test_count, 0,
"Should not emit table-test runnables when t.Run uses a missing row field"
);
}
#[gpui::test]
fn test_go_table_test_slice_picks_correct_field_when_not_first(cx: &mut TestAppContext) {
// The subtest-name field `name` is declared AFTER `anotherStr`, but `t.Run(tc.name, ...)`
// still selects on `name`. The resolver must match `@_field_check` text to the right
// `@_field_name` regardless of source order; "first string field wins" would be a bug.
let language = go_language();
let table_test = r#"
package main
import "testing"
func TestExample(t *testing.T) {
testCases := []struct{
anotherStr string
name string
}{
{
anotherStr: "alpha",
name: "case alpha",
},
{
anotherStr: "beta",
name: "case beta",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_ = tc.anotherStr
})
}
}
"#;
let buffer =
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
cx.executor().run_until_parked();
let case_offset = table_test
.find("anotherStr: \"alpha\"")
.expect("source should contain the first case body");
let first_case_runnables: Vec<_> = buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot.runnable_ranges(case_offset..case_offset).collect()
});
let case_names: Vec<_> = first_case_runnables
.iter()
.filter(|runnable| {
runnable
.runnable
.tags
.iter()
.any(|tag| tag.0 == "go-table-test-case")
})
.filter_map(|runnable| runnable.extra_captures.get("_table_test_case_name"))
.collect();
assert_eq!(
case_names,
vec!["\"case alpha\""],
"Resolver should pick the field matching `tc.name`, not the first string field"
);
}
#[gpui::test]
fn test_go_table_test_map_extras_include_case_name(cx: &mut TestAppContext) {
// For map-based table tests, `t.Run(name, ...)` uses the loop variable (not a
// selector_expression), so the query never captures `@_field_check`. The resolver's
// `reference_text` is None and it falls back to the first `@run` in the group.
// Verify each row's `_table_test_case_name` extra is the map key for that row.
let language = go_language();
let table_test = r#"
package main
import "testing"
func TestExample(t *testing.T) {
testCases := map[string]struct {
fail bool
}{
"test failure": {fail: true},
"test success": {fail: false},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
_ = tc.fail
})
}
}
"#;
let buffer =
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
cx.executor().run_until_parked();
let all_runnables: Vec<_> = buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot.runnable_ranges(0..table_test.len()).collect()
});
let all_case_names: Vec<_> = all_runnables
.iter()
.filter(|runnable| {
runnable
.runnable
.tags
.iter()
.any(|tag| tag.0 == "go-table-test-case")
})
.filter_map(|runnable| runnable.extra_captures.get("_table_test_case_name"))
.cloned()
.collect();
assert_eq!(
all_case_names,
vec!["\"test failure\"", "\"test success\""],
"Map-based table tests should surface each row's key as `_table_test_case_name`"
);
}
#[test]
fn test_extract_subtest_name() {
// Interpreted string literal

View file

@ -62,6 +62,7 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime
let css_lsp_adapter = Arc::new(css::CssLspAdapter::new(node.clone()));
let eslint_adapter = Arc::new(eslint::EsLintLspAdapter::new(node.clone(), fs.clone()));
let go_context_provider = Arc::new(go::GoContextProvider);
let go_runnable_resolver: Arc<dyn RunnableResolver> = Arc::new(go::GoRunnableResolver);
let go_lsp_adapter = Arc::new(go::GoLspAdapter);
let json_context_provider = Arc::new(JsonTaskProvider);
let json_lsp_adapter = Arc::new(json::JsonLspAdapter::new(languages.clone(), node.clone()));
@ -117,6 +118,7 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime
name: "go",
adapters: vec![go_lsp_adapter.clone()],
context: Some(go_context_provider.clone()),
runnable_resolver: Some(go_runnable_resolver.clone()),
semantic_token_rules: Some(go::semantic_token_rules()),
..Default::default()
},