supercov_engine/go_test_harness.rs
1//! Binding Go's test functions to the evidence they produce.
2//!
3//! Attribution needs two things the source does not have: every test function
4//! announcing itself, and somewhere to write the evidence once the package has
5//! finished. Go gives exactly one after-all hook, `TestMain`, so this augments
6//! the package's own when it has one and synthesises it when it does not.
7//!
8//! The wrapping is deliberate rather than a deferred call. The idiomatic
9//! `TestMain` ends in `os.Exit(m.Run())`, and `os.Exit` runs no deferred
10//! function, so a `defer` there would silently produce no evidence for exactly
11//! the projects that wrote the most careful harness.
12
13use 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 /// Test functions this file declares, in source order.
20 pub tests: Vec<String>,
21 /// Functions `go test` runs that get a checkpoint rather than an
22 /// announcement, because nothing they reach can be credited to them: a
23 /// test that calls `t.Parallel()`, whose work runs alongside other tests;
24 /// and an Example or Fuzz target, which takes no `*testing.T` to be named
25 /// by. What they reach is swept into the run-wide totals all the same.
26 pub unattributed: Vec<String>,
27 /// True when this file declares `TestMain`, which the package may only
28 /// have one of.
29 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
38/// Go's own rule for what the toolchain will run as a test, from
39/// `cmd/go/internal/load/test.go`: the prefix, then either nothing or a rune
40/// that is not lowercase.
41///
42/// So a bare `Test` is a test, and `Testify` is not — the `i` makes it an
43/// ordinary function that merely starts with the word. Guessing differently
44/// from the toolchain would attribute evidence to something `go test` never
45/// runs, or miss a test that it does.
46fn 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
51/// `go test` runs more than `TestX`. An `ExampleX` with an "Output:" comment
52/// runs like any other test, and a `FuzzX` runs its seed corpus. Neither takes
53/// a `*testing.T`, so neither can be announced -- but both reach product code,
54/// and where the end-of-run write is never made, everything they reach has to
55/// be swept before they return or it is never recorded at all.
56///
57/// samber/lo has three and a half thousand lines of examples and a TestMain
58/// that ends in goleak's VerifyTestMain, which exits the process itself. Its
59/// example coverage survived or did not according to which test happened to
60/// checkpoint last, and the run reported anywhere between 74% and 95% of the
61/// same suite.
62fn 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 // An example takes nothing and returns nothing; anything else that
69 // starts with the word is an ordinary function.
70 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
88/// What the test calls its `*testing.T`, which the announcement needs in order
89/// to read the outcome from it. Almost always `t`, but nothing requires that,
90/// and a harness that assumed it would fail to compile on the files that
91/// chose otherwise.
92fn 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 // `func TestX(*testing.T)` is legal and names nothing there is to read.
96 (text != "_" && !text.is_empty()).then(|| text.to_owned())
97}
98
99/// Instrument one `_test.go` file so every test announces itself.
100///
101/// `serialised` says the run was asked for exact attribution and the command
102/// carries `-parallel=1`, so a test that calls `t.Parallel()` is the only test
103/// running while its probes fire. It can then be announced like any other, and
104/// the run credits it with what it reached. Without that, announcing it would
105/// bind whatever ran beside it to its name.
106pub fn instrument_test_file(
107 source: &str,
108 alias: &str,
109 evidence_path: &str,
110 serialised: bool,
111) -> Result<GoTestFile, GoInstrumenterError> {
112 let tree = parse(source)?;
113 let mut file = GoTestFile {
114 tests: Vec::new(),
115 unattributed: Vec::new(),
116 declares_test_main: false,
117 edits: Vec::new(),
118 };
119 let root = tree.root_node();
120 // Only the edits that name the runtime directly need its import. The
121 // per-test announcement goes through a package-local helper instead, so a
122 // file of ordinary tests must not gain an import it never mentions.
123 let mut needs_runtime = false;
124 let mut cursor = root.walk();
125 for child in root.children(&mut cursor) {
126 if child.kind() != "function_declaration" {
127 continue;
128 }
129 let Some(name) = function_name(child, source) else {
130 continue;
131 };
132 let Some(body) = child.child_by_field_name("body") else {
133 continue;
134 };
135 if name == "TestMain" {
136 file.declares_test_main = true;
137 needs_runtime = true;
138 // Whether there will be an end-of-run write decides how eagerly
139 // the runtime has to persist, so the wrapping is decided first and
140 // the destination written with the answer.
141 let mut wrapping = Vec::new();
142 let wrapped = wrap_run_calls(body, source, alias, evidence_path, &mut wrapping);
143 file.edits.push(GoEdit {
144 at: body.start_byte() + 1,
145 rank: 100,
146 text: format!(
147 "\n\t{alias}.Arm(__supercovProbeCount, __supercovDecisionWidths)\n\t{alias}.Destination(\"{evidence_path}\", {})\n",
148 !wrapped
149 ),
150 });
151 file.edits.extend(wrapping);
152 continue;
153 }
154 if !is_test_function(&name) {
155 if is_checkpoint_only_function(&name, child, source) {
156 file.unattributed.push(name.clone());
157 // A Fuzz target takes a *testing.F, which reports an outcome
158 // the same way a *testing.T does. An Example takes nothing at
159 // all, so the only honest status for it is that it ran: `go
160 // test` fails the run when an Example's output does not match,
161 // and the run's own exit code carries that.
162 let record = match parameter_name(child, source) {
163 Some(parameter)
164 if parameter_type(child, source).as_deref() == Some("*testing.F") =>
165 {
166 format!(
167 "\n\tdefer {HARNESS_UNATTRIBUTED_FUZZ}({parameter}, \"{name}\")()\n"
168 )
169 }
170 _ => format!(
171 "\n\tdefer func() {{ {alias}.Ran(\"{name}\", \"unknown\"); {alias}.Checkpoint() }}()\n"
172 ),
173 };
174 // Only a file that names the runtime needs its import, and
175 // the Fuzz form names nothing but the generated helper: an
176 // import it never mentions does not fail to measure, it fails
177 // to compile, and Supercov breaks the suite it was measuring.
178 needs_runtime |= record.contains(&format!("{alias}."));
179 file.edits.push(GoEdit {
180 at: body.start_byte() + 1,
181 rank: 100,
182 text: record,
183 });
184 }
185 continue;
186 }
187 // `func TestX(t *testing.T)` is a test; `func TestX(b *testing.B)` is
188 // not, whatever its name suggests.
189 if parameter_type(child, source).as_deref() != Some("*testing.T") {
190 continue;
191 }
192 file.tests.push(name.clone());
193 // Serialised, and the test pauses itself: the announcement waits until
194 // `t.Parallel()` has returned, which is when this test is the one
195 // running.
196 let announce_at = if serialised && calls_parallel(body, source) {
197 own_parallel_call_end(body, source).unwrap_or(body.start_byte() + 1)
198 } else {
199 body.start_byte() + 1
200 };
201 if calls_parallel(body, source) && !serialised {
202 // Announcing it would bind whatever runs next to this test, and
203 // what runs next includes the other parallel tests. Its coverage
204 // still counts run-wide; it simply belongs to no test, which is
205 // the truth rather than a guess dressed as a measurement.
206 //
207 // It still gets a checkpoint. Go resumes parallel tests after the
208 // serial ones are done, so without one there is no announcement
209 // left to sweep at and everything the parallel phase reached sits
210 // in the probe array until the process ends -- which, where the
211 // end-of-run write is never reached, means it is never recorded.
212 file.unattributed.push(name.clone());
213 // Named, with its real outcome, and credited with nothing. Leaving
214 // it out of the evidence entirely was what made a suite of
215 // parallel tests publish zero tests: `runs <id> test <name>`
216 // answered "Test not found" for a test that had just passed, and
217 // affected-test selection returned an empty set for a change those
218 // tests exercised.
219 let record = match parameter_name(child, source) {
220 Some(parameter) => {
221 format!("\n\tdefer {HARNESS_UNATTRIBUTED}({parameter}, \"{name}\")()\n")
222 }
223 // `func TestX(*testing.T)` names nothing to read an outcome
224 // from, and is reported as having passed unless the run says
225 // otherwise -- the same rule the announced path uses.
226 None => format!(
227 "\n\tdefer func() {{ {alias}.Ran(\"{name}\", \"passed\"); {alias}.Checkpoint() }}()\n"
228 ),
229 };
230 needs_runtime |= record.contains(&format!("{alias}."));
231 file.edits.push(GoEdit {
232 at: body.start_byte() + 1,
233 rank: 100,
234 text: record,
235 });
236 continue;
237 }
238 // Through the generated helper rather than the runtime directly, so
239 // the announcement can read the outcome off the test's own *testing.T
240 // without the runtime ever importing `testing`.
241 // The trailing newline matters: a body written on one line puts its
242 // first statement immediately after the brace, and an announcement
243 // with nothing after it would run into that statement and not compile.
244 let announcement = match parameter_name(child, source) {
245 Some(parameter) => {
246 format!("\n\tdefer {HARNESS_ENTER}({parameter}, \"{name}\")()\n")
247 }
248 None => format!("\n\tdefer {alias}.EnterTest(\"{name}\")()\n"),
249 };
250 if announcement.contains(&format!("{alias}.")) {
251 needs_runtime = true;
252 }
253 file.edits.push(GoEdit {
254 at: announce_at,
255 rank: 100,
256 text: announcement,
257 });
258 }
259 // A file that names the runtime needs the import that makes it resolve;
260 // one that only calls the generated helper must not get an unused import.
261 if needs_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
262 file.edits.push(import);
263 }
264 Ok(file)
265}
266
267/// Where a test's own `t.Parallel()` call ends, if it makes one directly.
268///
269/// `t.Parallel()` does not return until the serial phase is over and this test
270/// is the one being run, so a statement after it runs with the test actually
271/// running. That is the only place an announcement can go when the run is
272/// serialised: at the top of the body it would fire while every other parallel
273/// test was also entering and pausing, all of them open at once, and the
274/// runtime would rightly give up on attributing any of them.
275///
276/// Only a call the test makes itself counts. `t.Parallel()` inside a subtest
277/// closure pauses the subtest, not this function, so there is nothing to wait
278/// for here.
279fn own_parallel_call_end(body: Node, source: &str) -> Option<usize> {
280 // A block holds its statements in a `statement_list`, not directly, so the
281 // statements are one level further down than a block's own children.
282 let statements = {
283 let mut cursor = body.walk();
284 body.children(&mut cursor)
285 .find(|child| child.kind() == "statement_list")
286 .unwrap_or(body)
287 };
288 let mut cursor = statements.walk();
289 for statement in statements.children(&mut cursor).filter(Node::is_named) {
290 // The grammar wraps a call standing alone as a statement in an
291 // `expression_statement`; it is never a statement's child on its own.
292 let call = match statement.kind() {
293 "expression_statement" => match statement.named_child(0) {
294 Some(inner) if inner.kind() == "call_expression" => inner,
295 // Some other expression standing alone -- a channel receive,
296 // say -- which is not the call being looked for; the next
297 // statement might be.
298 _ => continue,
299 },
300 _ => continue,
301 };
302 if call
303 .child_by_field_name("function")
304 .is_some_and(|function| {
305 source[function.byte_range()]
306 .trim_end()
307 .ends_with(".Parallel")
308 })
309 {
310 return Some(statement.end_byte());
311 }
312 }
313 None
314}
315
316/// Whether a test hands itself to Go's parallel scheduler.
317///
318/// Detected from the source rather than at runtime, because by the time
319/// `t.Parallel()` returns the test has already been descheduled and anything
320/// observed afterwards may belong to another one.
321fn calls_parallel(node: Node, source: &str) -> bool {
322 if node.kind() == "call_expression"
323 && let Some(function) = node.child_by_field_name("function")
324 && source[function.byte_range()]
325 .trim_end()
326 .ends_with(".Parallel")
327 {
328 return true;
329 }
330 let mut cursor = node.walk();
331 node.children(&mut cursor)
332 .filter(Node::is_named)
333 .any(|child| calls_parallel(child, source))
334}
335
336fn wrap_run_calls(
337 node: Node,
338 source: &str,
339 alias: &str,
340 evidence: &str,
341 edits: &mut Vec<GoEdit>,
342) -> bool {
343 if node.kind() == "call_expression"
344 && let Some(function) = node.child_by_field_name("function")
345 && source[function.byte_range()].trim_end().ends_with(".Run")
346 {
347 edits.push(GoEdit {
348 at: node.start_byte(),
349 rank: 50,
350 text: format!("{alias}.Finish("),
351 });
352 edits.push(GoEdit {
353 at: node.end_byte(),
354 rank: 50,
355 text: format!(", \"{evidence}\")"),
356 });
357 return true;
358 }
359 let mut cursor = node.walk();
360 let mut wrapped = false;
361 for child in node.children(&mut cursor) {
362 if child.is_named() {
363 wrapped |= wrap_run_calls(child, source, alias, evidence, edits);
364 }
365 }
366 wrapped
367}
368
369/// The generated file that gives a package the array its probes store into.
370///
371/// A normal source file, not a test one: instrumented statements live in
372/// ordinary code and must compile in every build of the package, not only
373/// under `go test`.
374pub fn probe_array_file(package: &str, alias: &str, import: &str, probe_count: usize) -> String {
375 format!(
376 "// 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",
377 crate::go_instrumenter::HITS_VARIABLE
378 )
379}
380
381/// The package-local function every instrumented test defers to.
382pub const HARNESS_ENTER: &str = "__supercovTest";
383
384/// The package-local function a test that cannot be attributed defers to.
385pub const HARNESS_UNATTRIBUTED: &str = "__supercovUnattributed";
386
387/// The same for a Fuzz target, which reports its outcome through a
388/// `*testing.F` rather than a `*testing.T`.
389pub const HARNESS_UNATTRIBUTED_FUZZ: &str = "__supercovUnattributedFuzz";
390
391/// The generated `TestMain` for a package that has none, plus the probe count
392/// every instrumented file in the package refers to.
393pub fn synthesized_harness(
394 package: &str,
395 alias: &str,
396 import: &str,
397 probe_count: usize,
398 decision_widths: &[u8],
399 evidence_path: &str,
400 declares_test_main: bool,
401) -> String {
402 let mut out =
403 format!("// Code generated by Supercov. DO NOT EDIT.\n\npackage {package}\n\nimport (\n");
404 // Always, because the announcement helper below takes a *testing.T. This
405 // file is a `_test.go`, so importing `testing` here reaches only the test
406 // binary and never a build of the product.
407 out.push_str("\t\"testing\"\n\n");
408 out.push_str(&format!("\t{alias} \"{import}\"\n)\n\n"));
409 // The runtime sizes each decision's vector from this, so a width the
410 // harness got wrong would record vectors of the wrong shape.
411 let widths = decision_widths
412 .iter()
413 .map(u8::to_string)
414 .collect::<Vec<_>>()
415 .join(", ");
416 out.push_str(&format!(
417 "const __supercovProbeCount = {probe_count}\n\nvar __supercovDecisionWidths = []uint8{{{widths}}}\n\n"
418 ));
419 // Binding and outcome in one place, so the edit inside the author's test
420 // is a single deferred call. Reading *testing.T here rather than in the
421 // runtime is what keeps `testing` out of every product binary that
422 // imports the runtime.
423 out.push_str(&format!(
424 "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"
425 ));
426 // Named and credited with nothing. The outcome is read here for the same
427 // reason the announced path reads it here: so the runtime never imports
428 // `testing` and never reaches a product binary.
429 out.push_str(&format!(
430 "func {HARNESS_UNATTRIBUTED}(t *testing.T, name string) func() {{\n\treturn func() {{\n\t\tstatus := \"passed\"\n\t\tif t.Skipped() {{\n\t\t\tstatus = \"skipped\"\n\t\t}} else if t.Failed() {{\n\t\t\tstatus = \"failed\"\n\t\t}}\n\t\t{alias}.Ran(name, status)\n\t\t{alias}.Checkpoint()\n\t}}\n}}\n\n"
431 ));
432 out.push_str(&format!(
433 "func {HARNESS_UNATTRIBUTED_FUZZ}(f *testing.F, name string) func() {{\n\treturn func() {{\n\t\tstatus := \"passed\"\n\t\tif f.Skipped() {{\n\t\t\tstatus = \"skipped\"\n\t\t}} else if f.Failed() {{\n\t\t\tstatus = \"failed\"\n\t\t}}\n\t\t{alias}.Ran(name, status)\n\t\t{alias}.Checkpoint()\n\t}}\n}}\n\n"
434 ));
435 if declares_test_main {
436 // The author's TestMain arms the runtime; this keeps the widths
437 // referenced even in a package where nothing else names them.
438 out.push_str("var _ = __supercovDecisionWidths\n");
439 return out;
440 }
441 out.push_str(&format!(
442 "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"
443 ));
444 out.replace("\t\"testing\"\n", "\t\"os\"\n\t\"testing\"\n")
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::go_instrumenter::rewrite;
451
452 fn instrumented(source: &str) -> (GoTestFile, String) {
453 let file =
454 instrument_test_file(source, "__supercov", "evidence.bin", false).expect("instrument");
455 let out = rewrite(source, &file.edits);
456 parse(&out).unwrap_or_else(|error| panic!("{error}\n{out}"));
457 (file, out)
458 }
459
460 #[test]
461 fn go_s_own_rule_decides_what_counts_as_a_test() {
462 // `Test` followed by a non-lowercase rune, taking *testing.T. A
463 // benchmark named like a test is not one, and neither is a helper.
464 let (file, _) = instrumented(
465 "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",
466 );
467 // Bare `Test` counts; `Testify` does not, because the rune after the
468 // prefix is lowercase. This is the toolchain's rule, not a guess at it.
469 assert_eq!(file.tests, ["Test", "TestOne"]);
470 }
471
472 #[test]
473 fn an_existing_test_main_is_wrapped_rather_than_deferred_into() {
474 // `os.Exit(m.Run())` runs no deferred function, so a defer here would
475 // produce no evidence for exactly the projects with the most careful
476 // harness. The run call is wrapped instead.
477 let (file, out) = instrumented(
478 "package p\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n",
479 );
480 assert!(file.declares_test_main);
481 assert!(
482 out.contains("__supercov.Finish(m.Run(), \"evidence.bin\")"),
483 "{out}"
484 );
485 assert!(
486 out.contains("__supercov.Arm(__supercovProbeCount, __supercovDecisionWidths)"),
487 "{out}"
488 );
489 assert!(
490 !out.contains("defer __supercov.Write"),
491 "a defer would never run:\n{out}"
492 );
493 }
494
495 #[test]
496 fn a_package_without_test_main_gets_one_and_never_two() {
497 // Go allows a package exactly one TestMain, so the generated file must
498 // declare it only when the package has none.
499 let generated = synthesized_harness(
500 "p",
501 "__supercov",
502 "example.com/rt",
503 42,
504 &[2, 3],
505 "e.bin",
506 false,
507 );
508 assert!(
509 generated.contains("[]uint8{2, 3}"),
510 "the runtime sizes its vectors from this:\n{generated}"
511 );
512 assert!(
513 generated.contains("func TestMain(m *testing.M)"),
514 "{generated}"
515 );
516 assert!(generated.contains("const __supercovProbeCount = 42"));
517 assert!(
518 generated.contains("os.Exit"),
519 "the generated harness preserves the exit code"
520 );
521
522 let alongside =
523 synthesized_harness("p", "__supercov", "example.com/rt", 42, &[], "e.bin", true);
524 assert!(!alongside.contains("func TestMain"), "{alongside}");
525 assert!(
526 alongside.contains("func __supercovTest(t *testing.T"),
527 "every package gets the announcement helper, TestMain or not:\n{alongside}"
528 );
529 }
530
531 #[test]
532 fn a_serialised_parallel_test_is_announced_after_it_resumes() {
533 // `t.Parallel()` does not return until the serial phase is over, so an
534 // announcement above it fires while every other parallel test is also
535 // entering and pausing. All of them would be open at once and the
536 // runtime would give up attributing any of them -- which is how asking
537 // for exact attribution produced a run with no tests in it at all.
538 let file = instrument_test_file(
539 "package p\n\nimport \"testing\"\n\nfunc TestParallel(t *testing.T) {\n\tt.Parallel()\n\tdoWork()\n}\n",
540 "__supercov",
541 "evidence.bin",
542 true,
543 )
544 .expect("instrument");
545 let out = rewrite(
546 "package p\n\nimport \"testing\"\n\nfunc TestParallel(t *testing.T) {\n\tt.Parallel()\n\tdoWork()\n}\n",
547 &file.edits,
548 );
549 parse(&out).unwrap_or_else(|error| panic!("{error}\n{out}"));
550 assert!(file.unattributed.is_empty(), "{file:?}");
551 let parallel = out.find("t.Parallel()").expect("the call");
552 let announced = out
553 .find("__supercovTest(t, \"TestParallel\")")
554 .expect("announced");
555 assert!(
556 parallel < announced,
557 "the announcement has to wait for the test to resume:\n{out}"
558 );
559 }
560
561 #[test]
562 fn a_serialised_run_leaves_a_serial_test_where_it_was() {
563 // `serialised` only moves the announcement for a test that pauses
564 // itself. A serial test in the same run has no `t.Parallel()` to wait
565 // for, and putting its announcement anywhere but the top of the body
566 // would leave whatever ran first uncredited.
567 //
568 // Measured rather than guessed: the decision that reads `serialised &&
569 // calls_parallel(..)` had no witness for the second operand until this
570 // existed, so nothing showed the two apart.
571 let source =
572 "package p\n\nimport \"testing\"\n\nfunc TestSerial(t *testing.T) {\n\tdoWork()\n}\n";
573 let file =
574 instrument_test_file(source, "__supercov", "evidence.bin", true).expect("instrument");
575 let out = rewrite(source, &file.edits);
576 parse(&out).unwrap_or_else(|error| panic!("{error}\n{out}"));
577 assert!(file.unattributed.is_empty(), "{file:?}");
578 let announced = out
579 .find("__supercovTest(t, \"TestSerial\")")
580 .expect("announced");
581 let work = out.find("doWork()").expect("body");
582 assert!(
583 announced < work,
584 "a serial test is announced before anything it calls:\n{out}"
585 );
586 }
587
588 #[test]
589 fn a_test_file_is_read_past_whatever_else_it_declares() {
590 // A `_test.go` holds more than functions -- fixtures, tables, helper
591 // types -- and the scan has to walk past all of it rather than stop.
592 let (file, out) = instrumented(concat!(
593 "package p\n\nimport \"testing\"\n\n",
594 "type fixture struct{ n int }\n\n",
595 "var cases = []fixture{{1}, {2}}\n\n",
596 "const limit = 3\n\n",
597 "func TestOne(t *testing.T) {\n\tdoWork()\n}\n",
598 ));
599 assert_eq!(file.tests, ["TestOne"]);
600 assert!(out.contains("__supercovTest(t, \"TestOne\")"), "{out}");
601 }
602
603 #[test]
604 fn a_parallel_call_is_found_wherever_the_test_makes_it() {
605 // `t.Parallel()` first is the shape every other test here uses, and it
606 // is not the shape most suites are written in: a test sets something
607 // up and then hands itself to the scheduler. The scan has to walk past
608 // whatever came before, which is a path nothing exercised -- the
609 // measurement showed the statement loop only ever matching on its
610 // first look.
611 let source = concat!(
612 "package p\n\nimport \"testing\"\n\n",
613 "func TestLater(t *testing.T) {\n",
614 "\tsetUp()\n",
615 "\tname := \"x\"\n",
616 "\t<-ready\n",
617 "\tt.Parallel()\n",
618 "\tdoWork(name)\n}\n",
619 );
620 let file =
621 instrument_test_file(source, "__supercov", "evidence.bin", true).expect("instrument");
622 let out = rewrite(source, &file.edits);
623 parse(&out).unwrap_or_else(|error| panic!("{error}\n{out}"));
624 assert!(file.unattributed.is_empty(), "{file:?}");
625 let parallel = out.find("t.Parallel()").expect("the call");
626 let announced = out
627 .find("__supercovTest(t, \"TestLater\")")
628 .expect("announced");
629 assert!(
630 parallel < announced,
631 "the announcement still waits for the call, wherever it is:\n{out}"
632 );
633 // And it waits for that call rather than landing after the setup it
634 // happened to follow.
635 let work = out.find("doWork(name)").expect("body");
636 assert!(announced < work, "{out}");
637 }
638
639 #[test]
640 fn a_function_with_no_body_is_walked_past() {
641 // Go declares a function with no body when the implementation is in
642 // assembly. It is not a test, it has nothing to instrument, and the
643 // scan has to keep going rather than reach for a body that is not
644 // there.
645 let (file, out) = instrumented(concat!(
646 "package p\n\nimport \"testing\"\n\n",
647 "func fastSum(a, b int) int\n\n",
648 "func TestOne(t *testing.T) {\n\tdoWork()\n}\n",
649 ));
650 assert_eq!(file.tests, ["TestOne"]);
651 assert!(out.contains("__supercovTest(t, \"TestOne\")"), "{out}");
652 }
653
654 #[test]
655 fn a_test_file_with_nothing_in_it_is_read_without_complaint() {
656 // The loop over a file's declarations has to survive having none.
657 let file = instrument_test_file("package p\n", "__supercov", "evidence.bin", false)
658 .expect("instrument");
659 assert!(file.tests.is_empty());
660 assert!(file.edits.is_empty(), "{file:?}");
661 assert!(!file.declares_test_main);
662 }
663
664 #[test]
665 fn a_parallel_test_is_named_rather_than_attributed_by_guesswork() {
666 // Go runs these alongside each other. Binding probes to whichever was
667 // most recently announced would produce per-test numbers that look
668 // exact and are not; the coverage still counts run-wide.
669 let (file, out) = instrumented(
670 "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",
671 );
672 assert_eq!(file.tests, ["TestSerial", "TestParallel"]);
673 assert_eq!(file.unattributed, ["TestParallel"]);
674 assert!(out.contains("__supercovTest(t, \"TestSerial\")"), "{out}");
675 // It is named, and it claims nothing. Those are different edits: the
676 // announcement binds whatever runs next to the test, and what runs
677 // next includes the other parallel tests. This one records that the
678 // test ran and how it ended, and credits it with no coverage at all.
679 //
680 // Leaving the name out entirely was the other extreme, and it read
681 // downstream as a test that never ran: a package of nothing but
682 // parallel tests published zero tests, so asking for one by name
683 // answered "Test not found" for a test that had just passed.
684 assert!(
685 out.contains("__supercovUnattributed(t, \"TestParallel\")"),
686 "{out}"
687 );
688 assert!(
689 !out.contains("__supercovTest(t, \"TestParallel\")"),
690 "a parallel test must not claim what ran beside it:\n{out}"
691 );
692 }
693
694 #[test]
695 fn a_file_that_names_only_the_generated_helper_gains_no_import() {
696 // Go rejects an import nothing mentions, so an import added for a file
697 // that reaches the runtime only through a package-local helper does
698 // not fail to measure -- it fails to compile, and Supercov breaks the
699 // suite it was asked to measure. A Fuzz target is exactly that shape.
700 let (file, out) = instrumented(
701 "package p\n\nimport \"testing\"\n\nfunc FuzzWork(f *testing.F) {\n\tf.Add(1)\n\tf.Fuzz(func(t *testing.T, n int) {\n\t\tdoWork(n)\n\t})\n}\n",
702 );
703 assert_eq!(file.unattributed, ["FuzzWork"]);
704 assert!(
705 out.contains("__supercovUnattributedFuzz(f, \"FuzzWork\")"),
706 "{out}"
707 );
708 assert!(
709 !out.contains(RUNTIME_IMPORT),
710 "the file names only the generated helper:\n{out}"
711 );
712
713 // An Example has no receiver to read an outcome from, so it names the
714 // runtime directly and does need the import.
715 let (_, out) =
716 instrumented("package p\n\nfunc ExampleWork() {\n\tdoWork()\n\t// Output: 1\n}\n");
717 assert!(out.contains("__supercov.Ran(\"ExampleWork\""), "{out}");
718 assert!(out.contains(RUNTIME_IMPORT), "{out}");
719 }
720
721 #[test]
722 fn a_test_whose_subtests_run_in_parallel_cannot_claim_them_either() {
723 // `t.Run(name, func(t *testing.T) { t.Parallel() })` is how a Go suite
724 // usually reaches for parallelism, and it is the same problem one
725 // level down: Go resumes the parallel subtests after the parent
726 // returns, so what they reach arrives with no announcement standing
727 // and cannot be credited to the parent that started them.
728 //
729 // The parent is therefore checkpointed rather than announced. It costs
730 // the parent its own attribution -- the serial part of its body is
731 // swept in with the rest -- which is the honest reading: nothing can
732 // say which of the two it came from.
733 let (file, out) = instrumented(
734 "package p\n\nimport \"testing\"\n\nfunc TestGroup(t *testing.T) {\n\tsetUp()\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tdoWork(c)\n\t\t})\n\t}\n}\n",
735 );
736 assert_eq!(file.tests, ["TestGroup"]);
737 assert_eq!(file.unattributed, ["TestGroup"]);
738 assert!(
739 !out.contains("__supercovTest(t, \"TestGroup\")"),
740 "a parent cannot claim what its parallel subtests reached:\n{out}"
741 );
742 assert!(
743 out.contains("__supercovUnattributed(t, \"TestGroup\")"),
744 "{out}"
745 );
746
747 // Where the subtests are serial there is nothing to run beside them,
748 // and the parent is named for all of it as before.
749 let (file, out) = instrumented(
750 "package p\n\nimport \"testing\"\n\nfunc TestGroup(t *testing.T) {\n\tt.Run(\"one\", func(t *testing.T) {\n\t\tdoWork()\n\t})\n}\n",
751 );
752 assert!(file.unattributed.is_empty(), "{file:?}");
753 assert!(out.contains("__supercovTest(t, \"TestGroup\")"), "{out}");
754 }
755
756 #[test]
757 fn an_example_is_swept_even_though_it_cannot_be_announced() {
758 // `go test` runs an Example with an Output comment like any other
759 // test, and a Fuzz target runs its seed corpus, but neither takes a
760 // *testing.T and neither can be attributed. What they reach still has
761 // to be swept before they return: where TestMain never returns --
762 // goleak's VerifyTestMain exits the process itself -- the only writes
763 // are the ones made at a boundary, and anything reached after the last
764 // one is never recorded. samber/lo has three and a half thousand lines
765 // of examples, and reported between 74% and 95% of one unchanged suite
766 // depending on which test finished last.
767 let (file, out) = instrumented(
768 "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",
769 );
770 // Both are swept before they return, and both are named for having
771 // run. The Example sweeps inline because it has no receiver to read an
772 // outcome from; the Fuzz target goes through the generated helper,
773 // which sweeps in turn -- so counting the word here would count one.
774 assert!(
775 out.contains("Ran(\"ExampleWork\", \"unknown\"); __supercov.Checkpoint()"),
776 "{out}"
777 );
778 let generated =
779 synthesized_harness("p", "__supercov", "example.com/rt", 0, &[], "e.bin", true);
780 assert!(
781 generated.matches("__supercov.Checkpoint()").count() == 2,
782 "both generated helpers sweep before returning:\n{generated}"
783 );
784 // A Fuzz target takes a *testing.F, which reports an outcome the same
785 // way a *testing.T does, so its status is read rather than guessed.
786 assert!(
787 out.contains("__supercovUnattributedFuzz(f, \"FuzzWork\")"),
788 "{out}"
789 );
790 assert!(
791 file.unattributed.contains(&"ExampleWork".to_owned()),
792 "{file:?}"
793 );
794 assert!(
795 file.unattributed.contains(&"FuzzWork".to_owned()),
796 "{file:?}"
797 );
798 // `Examples` is an ordinary function whose name starts with the word,
799 // exactly as `Testify` is, and one taking arguments is not an example
800 // `go test` will ever run.
801 assert!(file.tests.is_empty(), "{file:?}");
802 assert!(!out.contains("\"Examples\""), "{out}");
803 assert!(!out.contains("\"ExampleHelper\""), "{out}");
804 }
805
806 #[test]
807 fn the_announcement_uses_whatever_the_test_called_its_t() {
808 // `t` is the convention, not a rule. A harness that assumed it would
809 // fail to compile on the files that chose otherwise, and a file of
810 // ordinary tests must not gain a runtime import it never names.
811 let (_, out) = instrumented(
812 "package p\n\nimport \"testing\"\n\nfunc TestOne(tt *testing.T) {\n\tdoWork()\n}\n",
813 );
814 assert!(out.contains("__supercovTest(tt, \"TestOne\")"), "{out}");
815 assert!(
816 !out.contains(RUNTIME_IMPORT),
817 "an ordinary test file names only the generated helper:\n{out}"
818 );
819 }
820
821 #[test]
822 fn a_test_that_names_no_t_still_gets_bound() {
823 // `func TestX(*testing.T)` is legal Go and there is nothing to read an
824 // outcome from, so it announces itself directly and is reported as
825 // having passed unless the run says otherwise.
826 let (file, out) = instrumented(
827 "package p\n\nimport \"testing\"\n\nfunc TestOne(*testing.T) {\n\tdoWork()\n}\n",
828 );
829 assert_eq!(file.tests, ["TestOne"]);
830 assert!(out.contains("__supercov.EnterTest(\"TestOne\")"), "{out}");
831 assert!(
832 out.contains(RUNTIME_IMPORT),
833 "naming the runtime directly requires its import:\n{out}"
834 );
835 }
836
837 #[test]
838 fn a_test_file_using_go_1_26_new_is_still_instrumented() {
839 // A source file that could not be parsed was dropped and the run went
840 // on; a test file that could not be parsed took the whole run down
841 // with it, exit 1. Both came from the same construct, and neither is
842 // the author's mistake -- `new` has taken a value since Go 1.26.
843 let (file, out) = instrumented(
844 "package p\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) {\n\twant := new(\"hello\")\n\tif *greet() != *want {\n\t\tt.Fatal(\"bad\")\n\t}\n}\n",
845 );
846 assert_eq!(file.tests, ["TestOne"]);
847 assert!(out.contains("__supercovTest(t, \"TestOne\")"), "{out}");
848 // And what is written back is the author's own file, not the stand-in
849 // the parse read.
850 assert!(out.contains("new(\"hello\")"), "{out}");
851 assert!(!out.contains("nEw"), "{out}");
852 }
853
854 #[test]
855 fn a_test_announces_itself_before_anything_it_calls() {
856 let (_, out) = instrumented(
857 "package p\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) {\n\tdoWork()\n}\n",
858 );
859 let body = out.find("TestOne").unwrap();
860 let enter = out
861 .find("__supercovTest(t, \"TestOne\")")
862 .expect("announcement");
863 let work = out.find("doWork()").unwrap();
864 assert!(body < enter && enter < work, "{out}");
865 }
866
867 #[test]
868 fn a_test_written_on_one_line_still_compiles() {
869 // A body on one line puts its first statement immediately after the
870 // brace. An announcement inserted there with nothing after it runs
871 // straight into that statement, and the file stops being Go -- so
872 // Supercov breaks the suite it was asked to measure.
873 let (file, out) = instrumented(
874 "package p\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) { if work() != 1 { t.Fatal(\"no\") } }\n",
875 );
876 assert_eq!(file.tests, ["TestOne"]);
877 // `instrumented` parses the result, so reaching here is most of the
878 // claim; this says the announcement is on a line of its own.
879 assert!(out.contains("__supercovTest(t, \"TestOne\")()\n"), "{out}");
880
881 // The same for a TestMain nobody spread over several lines.
882 let (_, out) = instrumented(
883 "package p\n\nimport \"testing\"\n\nfunc TestMain(m *testing.M) { os.Exit(m.Run()) }\n",
884 );
885 assert!(out.contains("__supercovDecisionWidths)\n"), "{out}");
886 }
887
888 #[test]
889 fn a_test_main_that_never_calls_run_makes_the_runtime_persist_eagerly() {
890 // The idiomatic TestMain ends in os.Exit(m.Run()), which the harness
891 // wraps so everything is written at the end. A TestMain that hands `m`
892 // to something else -- goleak's VerifyTestMain, testcontainers, a
893 // hand-written harness -- runs the suite and exits itself, and Go can
894 // run no code on os.Exit. Then the only evidence that survives is what
895 // was already written, so every checkpoint has to persist rather than
896 // wait out the window: samber/lo's parallel phase finishes inside one,
897 // and with a window it recorded 36% of its statements where the same
898 // run records 86% without.
899 let (_, out) = instrumented(
900 "package p\n\nimport \"testing\"\n\nfunc TestMain(m *testing.M) {\n\tgoleak.VerifyTestMain(m)\n}\n",
901 );
902 assert!(
903 out.contains(".Destination(\"evidence.bin\", true)"),
904 "{out}"
905 );
906 assert!(
907 !out.contains(".Finish("),
908 "there is no m.Run() here to wrap:\n{out}"
909 );
910
911 // Where the end-of-run write does happen, the window is safe and the
912 // per-test write is not paid.
913 let (_, out) = instrumented(
914 "package p\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n",
915 );
916 assert!(
917 out.contains(".Destination(\"evidence.bin\", false)"),
918 "{out}"
919 );
920 assert!(out.contains(".Finish(m.Run()"), "{out}");
921 }
922}