Skip to main content

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.
100pub 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    // Only the edits that name the runtime directly need its import. The
114    // per-test announcement goes through a package-local helper instead, so a
115    // file of ordinary tests must not gain an import it never mentions.
116    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            // Whether there will be an end-of-run write decides how eagerly
132            // the runtime has to persist, so the wrapping is decided first and
133            // the destination written with the answer.
134            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        // `func TestX(t *testing.T)` is a test; `func TestX(b *testing.B)` is
160        // not, whatever its name suggests.
161        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            // Announcing it would bind whatever runs next to this test, and
167            // what runs next includes the other parallel tests. Its coverage
168            // still counts run-wide; it simply belongs to no test, which is
169            // the truth rather than a guess dressed as a measurement.
170            //
171            // It still gets a checkpoint. Go resumes parallel tests after the
172            // serial ones are done, so without one there is no announcement
173            // left to sweep at and everything the parallel phase reached sits
174            // in the probe array until the process ends -- which, where the
175            // end-of-run write is never reached, means it is never recorded.
176            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        // Through the generated helper rather than the runtime directly, so
186        // the announcement can read the outcome off the test's own *testing.T
187        // without the runtime ever importing `testing`.
188        // The trailing newline matters: a body written on one line puts its
189        // first statement immediately after the brace, and an announcement
190        // with nothing after it would run into that statement and not compile.
191        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    // A file that names the runtime needs the import that makes it resolve;
207    // one that only calls the generated helper must not get an unused import.
208    if needs_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
209        file.edits.push(import);
210    }
211    Ok(file)
212}
213
214/// Whether a test hands itself to Go's parallel scheduler.
215///
216/// Detected from the source rather than at runtime, because by the time
217/// `t.Parallel()` returns the test has already been descheduled and anything
218/// observed afterwards may belong to another one.
219fn 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
267/// The generated file that gives a package the array its probes store into.
268///
269/// A normal source file, not a test one: instrumented statements live in
270/// ordinary code and must compile in every build of the package, not only
271/// under `go test`.
272pub 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
279/// The package-local function every instrumented test defers to.
280pub const HARNESS_ENTER: &str = "__supercovTest";
281
282/// The generated `TestMain` for a package that has none, plus the probe count
283/// every instrumented file in the package refers to.
284pub 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    // Always, because the announcement helper below takes a *testing.T. This
296    // file is a `_test.go`, so importing `testing` here reaches only the test
297    // binary and never a build of the product.
298    out.push_str("\t\"testing\"\n\n");
299    out.push_str(&format!("\t{alias} \"{import}\"\n)\n\n"));
300    // The runtime sizes each decision's vector from this, so a width the
301    // harness got wrong would record vectors of the wrong shape.
302    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    // Binding and outcome in one place, so the edit inside the author's test
311    // is a single deferred call. Reading *testing.T here rather than in the
312    // runtime is what keeps `testing` out of every product binary that
313    // imports the runtime.
314    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        // The author's TestMain arms the runtime; this keeps the widths
319        // referenced even in a package where nothing else names them.
320        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        // `Test` followed by a non-lowercase rune, taking *testing.T. A
344        // benchmark named like a test is not one, and neither is a helper.
345        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        // Bare `Test` counts; `Testify` does not, because the rune after the
349        // prefix is lowercase. This is the toolchain's rule, not a guess at it.
350        assert_eq!(file.tests, ["Test", "TestOne"]);
351    }
352
353    #[test]
354    fn an_existing_test_main_is_wrapped_rather_than_deferred_into() {
355        // `os.Exit(m.Run())` runs no deferred function, so a defer here would
356        // produce no evidence for exactly the projects with the most careful
357        // harness. The run call is wrapped instead.
358        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        // Go allows a package exactly one TestMain, so the generated file must
379        // declare it only when the package has none.
380        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        // Go runs these alongside each other. Binding probes to whichever was
415        // most recently announced would produce per-test numbers that look
416        // exact and are not; the coverage still counts run-wide.
417        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        // `go test` runs an Example with an Output comment like any other
432        // test, and a Fuzz target runs its seed corpus, but neither takes a
433        // *testing.T and neither can be attributed. What they reach still has
434        // to be swept before they return: where TestMain never returns --
435        // goleak's VerifyTestMain exits the process itself -- the only writes
436        // are the ones made at a boundary, and anything reached after the last
437        // one is never recorded. samber/lo has three and a half thousand lines
438        // of examples, and reported between 74% and 95% of one unchanged suite
439        // depending on which test finished last.
440        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        // `Examples` is an ordinary function whose name starts with the word,
453        // exactly as `Testify` is, and one taking arguments is not an example
454        // `go test` will ever run.
455        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        // `t` is the convention, not a rule. A harness that assumed it would
463        // fail to compile on the files that chose otherwise, and a file of
464        // ordinary tests must not gain a runtime import it never names.
465        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        // `func TestX(*testing.T)` is legal Go and there is nothing to read an
478        // outcome from, so it announces itself directly and is reported as
479        // having passed unless the run says otherwise.
480        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        // A body on one line puts its first statement immediately after the
507        // brace. An announcement inserted there with nothing after it runs
508        // straight into that statement, and the file stops being Go -- so
509        // Supercov breaks the suite it was asked to measure.
510        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        // `instrumented` parses the result, so reaching here is most of the
515        // claim; this says the announcement is on a line of its own.
516        assert!(out.contains("__supercovTest(t, \"TestOne\")()\n"), "{out}");
517
518        // The same for a TestMain nobody spread over several lines.
519        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        // The idiomatic TestMain ends in os.Exit(m.Run()), which the harness
528        // wraps so everything is written at the end. A TestMain that hands `m`
529        // to something else -- goleak's VerifyTestMain, testcontainers, a
530        // hand-written harness -- runs the suite and exits itself, and Go can
531        // run no code on os.Exit. Then the only evidence that survives is what
532        // was already written, so every checkpoint has to persist rather than
533        // wait out the window: samber/lo's parallel phase finishes inside one,
534        // and with a window it recorded 36% of its statements where the same
535        // run records 86% without.
536        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        // Where the end-of-run write does happen, the window is safe and the
549        // per-test write is not paid.
550        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}