1use tree_sitter::Node;
14
15use crate::go_instrumenter::{GoEdit, GoInstrumenterError, RUNTIME_IMPORT, import_edit, parse};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct GoTestFile {
19 pub tests: Vec<String>,
21 pub unattributed: Vec<String>,
27 pub declares_test_main: bool,
30 pub edits: Vec<GoEdit>,
31}
32
33fn function_name(node: Node, source: &str) -> Option<String> {
34 node.child_by_field_name("name")
35 .map(|name| source[name.byte_range()].to_owned())
36}
37
38fn is_test_function(name: &str) -> bool {
47 name.strip_prefix("Test")
48 .is_some_and(|rest| rest.chars().next().is_none_or(|c| !c.is_lowercase()))
49}
50
51fn is_checkpoint_only_function(name: &str, node: Node, source: &str) -> bool {
63 let after = |prefix: &str| {
64 name.strip_prefix(prefix)
65 .is_some_and(|rest| rest.chars().next().is_none_or(|c| !c.is_lowercase()))
66 };
67 if after("Example") {
68 return first_parameter(node).is_none();
71 }
72 after("Fuzz") && parameter_type(node, source).as_deref() == Some("*testing.F")
73}
74
75fn first_parameter<'t>(node: Node<'t>) -> Option<Node<'t>> {
76 let parameters = node.child_by_field_name("parameters")?;
77 let mut cursor = parameters.walk();
78 parameters
79 .children(&mut cursor)
80 .find(|child| child.kind() == "parameter_declaration")
81}
82
83fn parameter_type(node: Node, source: &str) -> Option<String> {
84 let kind = first_parameter(node)?.child_by_field_name("type")?;
85 Some(source[kind.byte_range()].trim().to_owned())
86}
87
88fn parameter_name(node: Node, source: &str) -> Option<String> {
93 let name = first_parameter(node)?.child_by_field_name("name")?;
94 let text = source[name.byte_range()].trim();
95 (text != "_" && !text.is_empty()).then(|| text.to_owned())
97}
98
99pub fn instrument_test_file(
101 source: &str,
102 alias: &str,
103 evidence_path: &str,
104) -> Result<GoTestFile, GoInstrumenterError> {
105 let tree = parse(source)?;
106 let mut file = GoTestFile {
107 tests: Vec::new(),
108 unattributed: Vec::new(),
109 declares_test_main: false,
110 edits: Vec::new(),
111 };
112 let root = tree.root_node();
113 let mut needs_runtime = false;
117 let mut cursor = root.walk();
118 for child in root.children(&mut cursor) {
119 if child.kind() != "function_declaration" {
120 continue;
121 }
122 let Some(name) = function_name(child, source) else {
123 continue;
124 };
125 let Some(body) = child.child_by_field_name("body") else {
126 continue;
127 };
128 if name == "TestMain" {
129 file.declares_test_main = true;
130 needs_runtime = true;
131 let mut wrapping = Vec::new();
135 let wrapped = wrap_run_calls(body, source, alias, evidence_path, &mut wrapping);
136 file.edits.push(GoEdit {
137 at: body.start_byte() + 1,
138 rank: 100,
139 text: format!(
140 "\n\t{alias}.Arm(__supercovProbeCount, __supercovDecisionWidths)\n\t{alias}.Destination(\"{evidence_path}\", {})\n",
141 !wrapped
142 ),
143 });
144 file.edits.extend(wrapping);
145 continue;
146 }
147 if !is_test_function(&name) {
148 if is_checkpoint_only_function(&name, child, source) {
149 needs_runtime = true;
150 file.unattributed.push(name.clone());
151 file.edits.push(GoEdit {
152 at: body.start_byte() + 1,
153 rank: 100,
154 text: format!("\n\tdefer {alias}.Checkpoint()\n"),
155 });
156 }
157 continue;
158 }
159 if parameter_type(child, source).as_deref() != Some("*testing.T") {
162 continue;
163 }
164 file.tests.push(name.clone());
165 if calls_parallel(body, source) {
166 file.unattributed.push(name.clone());
177 needs_runtime = true;
178 file.edits.push(GoEdit {
179 at: body.start_byte() + 1,
180 rank: 100,
181 text: format!("\n\tdefer {alias}.Checkpoint()\n"),
182 });
183 continue;
184 }
185 let announcement = match parameter_name(child, source) {
192 Some(parameter) => {
193 format!("\n\tdefer {HARNESS_ENTER}({parameter}, \"{name}\")()\n")
194 }
195 None => format!("\n\tdefer {alias}.EnterTest(\"{name}\")()\n"),
196 };
197 if announcement.contains(&format!("{alias}.")) {
198 needs_runtime = true;
199 }
200 file.edits.push(GoEdit {
201 at: body.start_byte() + 1,
202 rank: 100,
203 text: announcement,
204 });
205 }
206 if needs_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
209 file.edits.push(import);
210 }
211 Ok(file)
212}
213
214fn calls_parallel(node: Node, source: &str) -> bool {
220 if node.kind() == "call_expression"
221 && let Some(function) = node.child_by_field_name("function")
222 && source[function.byte_range()]
223 .trim_end()
224 .ends_with(".Parallel")
225 {
226 return true;
227 }
228 let mut cursor = node.walk();
229 node.children(&mut cursor)
230 .filter(Node::is_named)
231 .any(|child| calls_parallel(child, source))
232}
233
234fn wrap_run_calls(
235 node: Node,
236 source: &str,
237 alias: &str,
238 evidence: &str,
239 edits: &mut Vec<GoEdit>,
240) -> bool {
241 if node.kind() == "call_expression"
242 && let Some(function) = node.child_by_field_name("function")
243 && source[function.byte_range()].trim_end().ends_with(".Run")
244 {
245 edits.push(GoEdit {
246 at: node.start_byte(),
247 rank: 50,
248 text: format!("{alias}.Finish("),
249 });
250 edits.push(GoEdit {
251 at: node.end_byte(),
252 rank: 50,
253 text: format!(", \"{evidence}\")"),
254 });
255 return true;
256 }
257 let mut cursor = node.walk();
258 let mut wrapped = false;
259 for child in node.children(&mut cursor) {
260 if child.is_named() {
261 wrapped |= wrap_run_calls(child, source, alias, evidence, edits);
262 }
263 }
264 wrapped
265}
266
267pub fn probe_array_file(package: &str, alias: &str, import: &str, probe_count: usize) -> String {
273 format!(
274 "// Code generated by Supercov. DO NOT EDIT.\n\npackage {package}\n\nimport {alias} \"{import}\"\n\n// Reserved once per package and shared across the module, so a probe is an\n// index into an array this file already holds rather than a call that has to\n// find one.\nvar {} = {alias}.Reserve({probe_count})\n",
275 crate::go_instrumenter::HITS_VARIABLE
276 )
277}
278
279pub const HARNESS_ENTER: &str = "__supercovTest";
281
282pub fn synthesized_harness(
285 package: &str,
286 alias: &str,
287 import: &str,
288 probe_count: usize,
289 decision_widths: &[u8],
290 evidence_path: &str,
291 declares_test_main: bool,
292) -> String {
293 let mut out =
294 format!("// Code generated by Supercov. DO NOT EDIT.\n\npackage {package}\n\nimport (\n");
295 out.push_str("\t\"testing\"\n\n");
299 out.push_str(&format!("\t{alias} \"{import}\"\n)\n\n"));
300 let widths = decision_widths
303 .iter()
304 .map(u8::to_string)
305 .collect::<Vec<_>>()
306 .join(", ");
307 out.push_str(&format!(
308 "const __supercovProbeCount = {probe_count}\n\nvar __supercovDecisionWidths = []uint8{{{widths}}}\n\n"
309 ));
310 out.push_str(&format!(
315 "func {HARNESS_ENTER}(t *testing.T, name string) func() {{\n\tdone := {alias}.EnterTest(name)\n\treturn func() {{\n\t\tif t.Skipped() {{\n\t\t\t{alias}.Outcome(\"skipped\")\n\t\t}} else if t.Failed() {{\n\t\t\t{alias}.Outcome(\"failed\")\n\t\t}}\n\t\tdone()\n\t}}\n}}\n\n"
316 ));
317 if declares_test_main {
318 out.push_str("var _ = __supercovDecisionWidths\n");
321 return out;
322 }
323 out.push_str(&format!(
324 "func TestMain(m *testing.M) {{\n\t{alias}.Arm(__supercovProbeCount, __supercovDecisionWidths)\n\t{alias}.Destination(\"{evidence_path}\", false)\n\tos.Exit({alias}.Finish(m.Run(), \"{evidence_path}\"))\n}}\n"
325 ));
326 out.replace("\t\"testing\"\n", "\t\"os\"\n\t\"testing\"\n")
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::go_instrumenter::rewrite;
333
334 fn instrumented(source: &str) -> (GoTestFile, String) {
335 let file = instrument_test_file(source, "__supercov", "evidence.bin").expect("instrument");
336 let out = rewrite(source, &file.edits);
337 parse(&out).unwrap_or_else(|error| panic!("{error}\n{out}"));
338 (file, out)
339 }
340
341 #[test]
342 fn go_s_own_rule_decides_what_counts_as_a_test() {
343 let (file, _) = instrumented(
346 "package p\n\nimport \"testing\"\n\nfunc Test(t *testing.T) {}\nfunc TestOne(t *testing.T) {}\nfunc Testify(t *testing.T) {}\nfunc TestBench(b *testing.B) {}\nfunc helper(t *testing.T) {}\n",
347 );
348 assert_eq!(file.tests, ["Test", "TestOne"]);
351 }
352
353 #[test]
354 fn an_existing_test_main_is_wrapped_rather_than_deferred_into() {
355 let (file, out) = instrumented(
359 "package p\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n",
360 );
361 assert!(file.declares_test_main);
362 assert!(
363 out.contains("__supercov.Finish(m.Run(), \"evidence.bin\")"),
364 "{out}"
365 );
366 assert!(
367 out.contains("__supercov.Arm(__supercovProbeCount, __supercovDecisionWidths)"),
368 "{out}"
369 );
370 assert!(
371 !out.contains("defer __supercov.Write"),
372 "a defer would never run:\n{out}"
373 );
374 }
375
376 #[test]
377 fn a_package_without_test_main_gets_one_and_never_two() {
378 let generated = synthesized_harness(
381 "p",
382 "__supercov",
383 "example.com/rt",
384 42,
385 &[2, 3],
386 "e.bin",
387 false,
388 );
389 assert!(
390 generated.contains("[]uint8{2, 3}"),
391 "the runtime sizes its vectors from this:\n{generated}"
392 );
393 assert!(
394 generated.contains("func TestMain(m *testing.M)"),
395 "{generated}"
396 );
397 assert!(generated.contains("const __supercovProbeCount = 42"));
398 assert!(
399 generated.contains("os.Exit"),
400 "the generated harness preserves the exit code"
401 );
402
403 let alongside =
404 synthesized_harness("p", "__supercov", "example.com/rt", 42, &[], "e.bin", true);
405 assert!(!alongside.contains("func TestMain"), "{alongside}");
406 assert!(
407 alongside.contains("func __supercovTest(t *testing.T"),
408 "every package gets the announcement helper, TestMain or not:\n{alongside}"
409 );
410 }
411
412 #[test]
413 fn a_parallel_test_is_named_rather_than_attributed_by_guesswork() {
414 let (file, out) = instrumented(
418 "package p\n\nimport \"testing\"\n\nfunc TestSerial(t *testing.T) {\n\tdoWork()\n}\n\nfunc TestParallel(t *testing.T) {\n\tt.Parallel()\n\tdoWork()\n}\n",
419 );
420 assert_eq!(file.tests, ["TestSerial", "TestParallel"]);
421 assert_eq!(file.unattributed, ["TestParallel"]);
422 assert!(out.contains("__supercovTest(t, \"TestSerial\")"), "{out}");
423 assert!(
424 !out.contains("\"TestParallel\""),
425 "a parallel test must not claim what ran beside it:\n{out}"
426 );
427 }
428
429 #[test]
430 fn an_example_is_swept_even_though_it_cannot_be_announced() {
431 let (file, out) = instrumented(
441 "package p\n\nimport \"testing\"\n\nfunc ExampleWork() {\n\tdoWork()\n\t// Output: 1\n}\n\nfunc FuzzWork(f *testing.F) {\n\tdoWork()\n}\n\nfunc Examples(t *testing.T) {\n\tdoWork()\n}\n\nfunc ExampleHelper(x int) {\n\tdoWork()\n}\n",
442 );
443 assert_eq!(out.matches("Checkpoint()").count(), 2, "{out}");
444 assert!(
445 file.unattributed.contains(&"ExampleWork".to_owned()),
446 "{file:?}"
447 );
448 assert!(
449 file.unattributed.contains(&"FuzzWork".to_owned()),
450 "{file:?}"
451 );
452 assert!(file.tests.is_empty(), "{file:?}");
456 assert!(!out.contains("\"Examples\""), "{out}");
457 assert!(!out.contains("\"ExampleHelper\""), "{out}");
458 }
459
460 #[test]
461 fn the_announcement_uses_whatever_the_test_called_its_t() {
462 let (_, out) = instrumented(
466 "package p\n\nimport \"testing\"\n\nfunc TestOne(tt *testing.T) {\n\tdoWork()\n}\n",
467 );
468 assert!(out.contains("__supercovTest(tt, \"TestOne\")"), "{out}");
469 assert!(
470 !out.contains(RUNTIME_IMPORT),
471 "an ordinary test file names only the generated helper:\n{out}"
472 );
473 }
474
475 #[test]
476 fn a_test_that_names_no_t_still_gets_bound() {
477 let (file, out) = instrumented(
481 "package p\n\nimport \"testing\"\n\nfunc TestOne(*testing.T) {\n\tdoWork()\n}\n",
482 );
483 assert_eq!(file.tests, ["TestOne"]);
484 assert!(out.contains("__supercov.EnterTest(\"TestOne\")"), "{out}");
485 assert!(
486 out.contains(RUNTIME_IMPORT),
487 "naming the runtime directly requires its import:\n{out}"
488 );
489 }
490
491 #[test]
492 fn a_test_announces_itself_before_anything_it_calls() {
493 let (_, out) = instrumented(
494 "package p\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) {\n\tdoWork()\n}\n",
495 );
496 let body = out.find("TestOne").unwrap();
497 let enter = out
498 .find("__supercovTest(t, \"TestOne\")")
499 .expect("announcement");
500 let work = out.find("doWork()").unwrap();
501 assert!(body < enter && enter < work, "{out}");
502 }
503
504 #[test]
505 fn a_test_written_on_one_line_still_compiles() {
506 let (file, out) = instrumented(
511 "package p\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) { if work() != 1 { t.Fatal(\"no\") } }\n",
512 );
513 assert_eq!(file.tests, ["TestOne"]);
514 assert!(out.contains("__supercovTest(t, \"TestOne\")()\n"), "{out}");
517
518 let (_, out) = instrumented(
520 "package p\n\nimport \"testing\"\n\nfunc TestMain(m *testing.M) { os.Exit(m.Run()) }\n",
521 );
522 assert!(out.contains("__supercovDecisionWidths)\n"), "{out}");
523 }
524
525 #[test]
526 fn a_test_main_that_never_calls_run_makes_the_runtime_persist_eagerly() {
527 let (_, out) = instrumented(
537 "package p\n\nimport \"testing\"\n\nfunc TestMain(m *testing.M) {\n\tgoleak.VerifyTestMain(m)\n}\n",
538 );
539 assert!(
540 out.contains(".Destination(\"evidence.bin\", true)"),
541 "{out}"
542 );
543 assert!(
544 !out.contains(".Finish("),
545 "there is no m.Run() here to wrap:\n{out}"
546 );
547
548 let (_, out) = instrumented(
551 "package p\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n",
552 );
553 assert!(
554 out.contains(".Destination(\"evidence.bin\", false)"),
555 "{out}"
556 );
557 assert!(out.contains(".Finish(m.Run()"), "{out}");
558 }
559}