Skip to main content

supercov_engine/
go_run.rs

1//! Public, isolated Go coverage run lifecycle.
2//!
3//! Supercov owns the probes here, which means it rewrites the module's own
4//! sources. That cannot happen in the user's tree, so the run copies the
5//! project into an isolated workspace, instruments it there, and runs the
6//! user's command against the copy. Nothing in the tree the author edits is
7//! touched.
8//!
9//! Two facts about `go test` shape the rest:
10//!
11//! - It builds and runs **one test binary per package**, each its own process
12//!   with its own probe array. So each test package writes its own evidence
13//!   file and the run merges them, rather than every process racing to write
14//!   one path.
15//! - It **caches** successful results. A cached package does not run, so it
16//!   records nothing, and a run that silently measured half a suite is worse
17//!   than one that took longer. The run adds `-count=1` when the command does
18//!   not already say otherwise, and says so.
19
20use std::{
21    collections::{BTreeMap, BTreeSet},
22    ffi::OsString,
23    fs,
24    io::Write,
25    path::{Path, PathBuf},
26    time::Instant,
27};
28
29use serde::{Deserialize, Serialize};
30
31use crate::{
32    evidence_archive::write_archive,
33    frontend_protocol::validate_frontend_report_request,
34    go_instrumenter::{RUNTIME_ALIAS, RUNTIME_IMPORT, rewrite},
35    go_project::{PreparedGoProject, go_integrity_inputs, module_path, prepare_go_project},
36    go_test_harness::{instrument_test_file, probe_array_file, synthesized_harness},
37    integrity::{FrontendIntegrityInputs, create_explicit_run_integrity},
38    lifecycle::{
39        ProjectLock, finalize_published_run, note_kept_evidence, publish_run,
40        recover_abandoned_runs, remove_stored_tree_deferred,
41    },
42    orchestration::{ExecutionPhase, ExecutionPlan, PhaseKind, execute_plan},
43    owned_evidence::{
44        OwnedRunInputs, OwnedTestOutcome, build_frontend_run, go_coverage_model, go_declaration,
45        merge_evidence, read_evidence,
46    },
47    process_supervision::{CommandSpec, SupervisionOptions},
48    run_store::{RawEvidenceMetadata, RunMetadata, RunTimings},
49    workspace::{canonicalize_simplified, prepare_cached_workspace, recover_cached_workspace},
50};
51
52/// Where the runtime package lands inside the workspace.
53///
54/// A plain directory rather than something hidden: the Go tool ignores any
55/// directory whose name begins with `.` or `_`, so a runtime placed out of
56/// sight would also be out of the build.
57const RUNTIME_DIRECTORY: &str = "supercov_runtime";
58
59const RUNTIME_SOURCE: &str = include_str!("../runtime-assets/go/supercov/supercov.go");
60
61/// The generated file that declares a package's probe array.
62const PROBE_FILE: &str = "supercov_probes.go";
63
64/// The generated file that arms the runtime and binds each test.
65const HARNESS_FILE: &str = "supercov_generated_test.go";
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct DirectGoRunRequest {
70    pub root: PathBuf,
71    pub command: Vec<String>,
72    pub run_id: String,
73    pub started_at: String,
74    /// Credit every test with what it reached, by running the package's tests
75    /// one at a time.
76    ///
77    /// Probes are a store into one array the whole process shares, so a test
78    /// that called `t.Parallel()` can only be credited with its own work if
79    /// nothing else is running while it does it. `-parallel 1` is what buys
80    /// that, and what it costs is the suite's own parallelism -- which is why
81    /// it is asked for rather than assumed.
82    #[serde(default)]
83    pub exact_attribution: bool,
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct DirectGoRunResult {
88    pub run_id: String,
89    pub run_directory: PathBuf,
90    pub exit_code: i32,
91    pub tests: usize,
92    /// Test functions `go test` ran that no evidence can be credited to: one
93    /// that called `t.Parallel()`, and every Example or Fuzz target. What they
94    /// reached counts run-wide, so a run of nothing but these is a measured
95    /// run with no per-test attribution -- which is a different thing from an
96    /// empty suite, and the summary has to be able to say which it was.
97    pub unattributed: usize,
98    pub source_files: usize,
99    /// Source files that did not parse, and so carry no obligations. A hole in
100    /// the denominator travels with the number it was taken out of.
101    pub unparseable_sources: usize,
102    /// Test files that did not parse. Their tests still run and still reach
103    /// what they reach; what is lost is the announcement that would name them
104    /// for it.
105    pub unparseable_tests: usize,
106    pub packages: usize,
107    pub recovered_runs: Vec<String>,
108    pub metadata: RunMetadata,
109}
110
111fn elapsed_ms(started: Instant) -> f64 {
112    (started.elapsed().as_secs_f64() * 10_000.0).round() / 10.0
113}
114
115/// One package whose tests will run, and where its evidence lands.
116#[derive(Debug, Clone)]
117struct GoTestPackage {
118    /// Directory relative to the workspace root, `/`-separated, `.` for the
119    /// module root.
120    directory: String,
121    evidence: PathBuf,
122    /// Which file declared each test, so a result can point at its source.
123    declared_in: BTreeMap<String, String>,
124    /// What ran here and announced nothing: parallel tests, Examples, Fuzz
125    /// targets.
126    unattributed: BTreeSet<String>,
127}
128
129struct InstrumentedWorkspace {
130    project: PreparedGoProject,
131    packages: Vec<GoTestPackage>,
132    /// Test files that did not parse, with the reason. They cost the tests
133    /// they declare their attribution and nothing else.
134    unparseable_tests: Vec<(String, String)>,
135}
136
137fn directory_of(relative: &str) -> String {
138    match relative.rsplit_once('/') {
139        Some((directory, _)) => directory.to_owned(),
140        None => ".".to_owned(),
141    }
142}
143
144/// The `package` a Go file declares.
145///
146/// Scanned rather than parsed: the clause is the first thing in a Go file that
147/// is not a comment, so stripping comments and reading the next word gets the
148/// same answer as a parse for a fraction of the cost — and preparing a project
149/// already parses every file once, which is enough.
150fn package_name(source: &str) -> Option<String> {
151    let bytes = source.as_bytes();
152    let mut at = 0;
153    while at < bytes.len() {
154        let rest = &source[at..];
155        if rest.starts_with("//") {
156            at += rest.find('\n').map_or(rest.len(), |end| end + 1);
157        } else if rest.starts_with("/*") {
158            // An unterminated comment is not Go; there is no clause to find.
159            at += rest.find("*/").map_or(rest.len(), |end| end + 2);
160        } else if bytes[at].is_ascii_whitespace() {
161            at += 1;
162        } else {
163            return rest
164                .strip_prefix("package")
165                .and_then(|rest| rest.strip_prefix(|c: char| c.is_ascii_whitespace()))
166                .and_then(|rest| rest.split_whitespace().next())
167                .map(str::to_owned);
168        }
169    }
170    None
171}
172
173fn read(path: &Path) -> Result<String, String> {
174    fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))
175}
176
177fn write(path: &Path, contents: &str) -> Result<(), String> {
178    if let Some(parent) = path.parent() {
179        fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?;
180    }
181    fs::write(path, contents).map_err(|error| format!("{}: {error}", path.display()))
182}
183
184/// A file-system-safe name for a package's evidence, so two packages cannot
185/// collide on one path.
186fn evidence_name(directory: &str) -> String {
187    let slug = directory
188        .chars()
189        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
190        .collect::<String>();
191    format!("{slug}.bin")
192}
193
194/// Rewrite the workspace in place: instrumented sources, the runtime package
195/// they import, a probe array per package, and a harness per test package.
196fn instrument_workspace(
197    workspace: &Path,
198    evidence_directory: &Path,
199    serialised: bool,
200    roots: Option<&crate::source_discovery::ExplicitSourceRoots>,
201) -> Result<InstrumentedWorkspace, String> {
202    let mut project = prepare_go_project(workspace, roots)?;
203    let mut unparseable_tests: Vec<(String, String)> = Vec::new();
204
205    // A go.work repository has several modules and no module at its root, so
206    // each gets a runtime of its own: an import is resolved against the module
207    // that names it, and one module cannot import a package inside another.
208    let members = crate::go_project::workspace_modules(workspace);
209    let directories = if members.is_empty() {
210        vec![".".to_owned()]
211    } else {
212        members.into_iter().collect::<Vec<_>>()
213    };
214    let mut runtimes: Vec<(String, String)> = Vec::new();
215    for directory in &directories {
216        let at = workspace.join(directory);
217        let module = module_path(&at).ok_or_else(|| {
218            format!(
219                "{}: no module path in go.mod, so Supercov cannot name the package its runtime is imported from",
220                at.display()
221            )
222        })?;
223        write(
224            &at.join(RUNTIME_DIRECTORY).join("supercov.go"),
225            RUNTIME_SOURCE,
226        )?;
227        runtimes.push((directory.clone(), format!("{module}/{RUNTIME_DIRECTORY}")));
228    }
229    // Longest first, so a nested module wins over the root it sits under.
230    runtimes.sort_by_key(|(directory, _)| std::cmp::Reverse(directory.len()));
231    let import_for = |relative: &str| -> &str {
232        runtimes
233            .iter()
234            .find(|(directory, _)| {
235                directory == "."
236                    || relative == directory
237                    || relative.starts_with(&format!("{directory}/"))
238            })
239            .map(|(_, import)| import.as_str())
240            .unwrap_or_default()
241    };
242
243    // Probe ids are handed out across the whole module, so every package
244    // reserves the same total: a probe from one package is the same index in
245    // the binary that links it as a dependency.
246    let probe_count = project
247        .probes
248        .keys()
249        .max()
250        .map_or(0, |highest| *highest as usize + 1);
251
252    let mut source_packages: BTreeMap<String, String> = BTreeMap::new();
253    for (relative, instrumented) in &project.instrumented {
254        let instrumented = instrumented.replace(RUNTIME_IMPORT, import_for(relative));
255        let path = workspace.join(relative);
256        if let Some(name) = package_name(&instrumented) {
257            source_packages
258                .entry(directory_of(relative))
259                .or_insert(name);
260        }
261        write(&path, &instrumented)?;
262    }
263    for (directory, package) in &source_packages {
264        write(
265            &workspace.join(directory).join(PROBE_FILE),
266            &probe_array_file(
267                package,
268                RUNTIME_ALIAS,
269                import_for(&format!("{directory}/x.go")),
270                probe_count,
271            ),
272        )?;
273    }
274
275    let mut by_directory: BTreeMap<String, Vec<&String>> = BTreeMap::new();
276    for relative in &project.files.tests {
277        by_directory
278            .entry(directory_of(relative))
279            .or_default()
280            .push(relative);
281    }
282
283    let mut packages = Vec::new();
284    for (directory, tests) in by_directory {
285        let evidence = evidence_directory.join(evidence_name(&directory));
286        let evidence_literal = evidence.to_string_lossy().replace('\\', "\\\\");
287        let mut declared_in = BTreeMap::new();
288        let mut unattributed = BTreeSet::new();
289        let mut declares_test_main = false;
290        // The harness joins whichever package the tests are in. A directory
291        // can hold both `foo` and `foo_test` files; the internal one wins,
292        // because that is where the instrumented sources are.
293        let mut harness_package: Option<String> = None;
294        for relative in tests {
295            let path = workspace.join(relative);
296            let source = read(&path)?;
297            let Some(name) = package_name(&source) else {
298                continue;
299            };
300            // The package a harness joins is read from the text, so it is
301            // known whether or not the file parses -- and a package whose only
302            // test file is unreadable still needs one.
303            let internal = source_packages.get(&directory) == Some(&name);
304            if internal || harness_package.is_none() {
305                harness_package = Some(name);
306            }
307            // A file the toolchain never builds declares nothing Supercov has
308            // to work around, and reading its `TestMain` as the package's own
309            // stood the harness down for a package that had none -- so the
310            // runtime was never armed and a passing suite published nothing.
311            let built = !build_ignored(&source);
312            let file =
313                match instrument_test_file(&source, RUNTIME_ALIAS, &evidence_literal, serialised) {
314                    Ok(file) => file,
315                    Err(error) => {
316                        // Taking the run down with it is the reflex a source file
317                        // was corrected for, and it is worse here: a test file
318                        // need not even be in the build for Supercov to refuse the
319                        // suite the toolchain just passed. What it costs is the
320                        // attribution of the tests it declares, because there is
321                        // nowhere to put the announcement. What they reach still
322                        // counts run-wide, like any execution no test could claim.
323                        project.manifest.limitations.push(
324                            crate::go_project::unparseable_limitation(relative, &error.to_string()),
325                        );
326                        unparseable_tests.push((relative.clone(), error.to_string()));
327                        // A TestMain nobody could read is still a TestMain, and Go
328                        // permits one per package. Synthesising a second does not
329                        // fail to measure, it fails to build -- so this is scanned
330                        // for rather than parsed for, which is the one question
331                        // that cannot go unanswered.
332                        declares_test_main |= built && declares_test_main_textually(&source);
333                        continue;
334                    }
335                };
336            declares_test_main |= built && file.declares_test_main;
337            for test in &file.tests {
338                declared_in.insert(test.clone(), relative.clone());
339            }
340            unattributed.extend(file.unattributed.iter().cloned());
341            if !file.edits.is_empty() {
342                write(
343                    &path,
344                    &rewrite(&source, &file.edits).replace(RUNTIME_IMPORT, import_for(relative)),
345                )?;
346            }
347        }
348        let Some(package) = harness_package else {
349            continue;
350        };
351        write(
352            &workspace.join(&directory).join(HARNESS_FILE),
353            &synthesized_harness(
354                &package,
355                RUNTIME_ALIAS,
356                import_for(&format!("{directory}/x.go")),
357                probe_count,
358                &project.decision_widths,
359                &evidence_literal,
360                declares_test_main,
361            ),
362        )?;
363        packages.push(GoTestPackage {
364            directory,
365            evidence,
366            declared_in,
367            unattributed,
368        });
369    }
370    Ok(InstrumentedWorkspace {
371        project,
372        packages,
373        unparseable_tests,
374    })
375}
376
377/// Whether the toolchain will refuse to build this file whatever it is asked.
378///
379/// Build constraints in general cannot be answered without knowing the GOOS,
380/// GOARCH and tags a build will use, and guessing at them would drop files a
381/// run does measure. `ignore` needs none of that: it is not a GOOS, not a
382/// GOARCH, and nothing defines it, which is exactly why it is the convention
383/// for Go kept beside Go without being compiled -- a reference harness, a
384/// generator's input. So it is the one constraint that can be read alone, and
385/// the only one that is.
386fn build_ignored(source: &str) -> bool {
387    // Constraints live above the package clause and nowhere else: the
388    // toolchain stops reading them there, so a `//go:build ignore` in a
389    // comment further down is text about Go rather than a constraint on it.
390    let header = if source.starts_with("package ") {
391        // Nothing precedes a clause on the first line, and looking for a
392        // newline before it would find none and read the whole file.
393        ""
394    } else {
395        source
396            .find("\npackage ")
397            .map_or(source, |clause| &source[..clause])
398    };
399    header
400        .lines()
401        .map(str::trim)
402        .filter_map(|line| {
403            line.strip_prefix("//go:build")
404                // The spelling Go used before 1.17, still in trees today.
405                .or_else(|| line.strip_prefix("// +build"))
406        })
407        .any(|constraint| {
408            constraint
409                .split(|c: char| !(c.is_alphanumeric() || c == '_'))
410                .any(|term| term == "ignore")
411        })
412}
413
414/// Whether a file declares `TestMain`, read from the text rather than a tree.
415///
416/// Only ever asked of a file that did not parse, which is exactly when there
417/// is no tree to ask. It errs towards seeing one: believing in a `TestMain`
418/// that is not there costs the package Supercov's own harness, while missing
419/// one that is there declares it twice and the package stops compiling.
420fn declares_test_main_textually(source: &str) -> bool {
421    source.split("func TestMain").skip(1).any(|rest| {
422        rest.trim_start()
423            .strip_prefix('(')
424            .is_some_and(|arguments| arguments.contains("testing.M"))
425    })
426}
427
428/// `go test` caches a package that passed, and a cached package does not run.
429/// The command with Go's test parallelism turned down to one.
430///
431/// `-parallel` bounds how many tests calling `t.Parallel()` run at once. At 1
432/// they resume one at a time, so each is the only test running when its probes
433/// fire and the harness can announce it like any serial test.
434fn command_run_in_order(command: &[String]) -> (Vec<String>, bool) {
435    if command
436        .iter()
437        .any(|argument| argument == "-parallel" || argument.starts_with("-parallel="))
438    {
439        // The author already said what they wanted; saying it louder is not
440        // Supercov's call.
441        return (command.to_vec(), false);
442    }
443    let mut updated = command.to_vec();
444    let position = updated
445        .iter()
446        .position(|argument| argument == "test")
447        .map_or(updated.len(), |index| index + 1);
448    updated.insert(position, "-parallel=1".into());
449    (updated, true)
450}
451
452fn command_with_fresh_results(command: &[String]) -> (Vec<String>, bool) {
453    if command
454        .iter()
455        .any(|argument| argument == "-count" || argument.starts_with("-count="))
456    {
457        return (command.to_vec(), false);
458    }
459    let mut updated = command.to_vec();
460    // After the subcommand, so `go -count=1 test` is never produced.
461    let position = updated
462        .iter()
463        .position(|argument| argument == "test")
464        .map_or(updated.len(), |index| index + 1);
465    updated.insert(position, "-count=1".into());
466    (updated, true)
467}
468
469/// The fingerprint a later query compares against the stored run.
470pub fn current_go_integrity(
471    root: &Path,
472    command: &[String],
473    source_roots: Option<&[String]>,
474) -> Result<crate::run_store::RunIntegrity, String> {
475    let root = canonicalize_simplified(root).map_err(|error| error.to_string())?;
476    let files = crate::go_project::discover_go_files(&root)?;
477    let mut inputs = go_integrity_inputs(&files, command);
478    crate::source_discovery::fold_roots_into_configuration(
479        &mut inputs.execution_configuration,
480        source_roots,
481    );
482    create_explicit_run_integrity(&root, &inputs, &FrontendIntegrityInputs::embedded_go())
483        .map_err(|error| error.to_string())
484}
485
486pub fn run_direct_go(
487    request: &DirectGoRunRequest,
488    diagnostics: &mut dyn Write,
489) -> Result<DirectGoRunResult, String> {
490    if request.command.is_empty() {
491        return Err("test command must not be empty".into());
492    }
493    let total_started = Instant::now();
494    let initialization_started = Instant::now();
495    let root = canonicalize_simplified(&request.root)
496        .map_err(|error| format!("{}: {error}", request.root.display()))?;
497    let mut lock = ProjectLock::acquire(&root, &request.run_id, &request.started_at)
498        .map_err(|error| error.to_string())?;
499    let initialization_ms = elapsed_ms(initialization_started);
500    let work_directory = root.join(".supercov/work").join(&request.run_id);
501    // Whether the tests were measured. A run that failed before that has no
502    // evidence worth keeping; one that failed after has evidence that cost the
503    // suite its wall clock.
504    let measured = std::cell::Cell::new(false);
505    let result = (|| {
506        let recovered_runs = recover_abandoned_runs(&root, &request.started_at)
507            .map_err(|error| error.to_string())?;
508        if !recovered_runs.is_empty() {
509            writeln!(
510                diagnostics,
511                "[supercov] recovered abandoned run(s): {}",
512                recovered_runs.join(", ")
513            )
514            .map_err(|error| error.to_string())?;
515        }
516
517        let adapter_started = Instant::now();
518        let files = crate::go_project::discover_go_files(&root)?;
519        let source_roots = crate::source_discovery::configured_source_roots(
520            &std::env::vars().collect::<std::collections::BTreeMap<_, _>>(),
521        );
522        let mut integrity_inputs = go_integrity_inputs(&files, &request.command);
523        crate::source_discovery::fold_roots_into_configuration(
524            &mut integrity_inputs.execution_configuration,
525            source_roots.as_deref(),
526        );
527        let source_inputs =
528            crate::source_capture::capture(&root, "go", integrity_inputs.assertion_paths())?;
529        let integrity = create_explicit_run_integrity(
530            &root,
531            &integrity_inputs,
532            &FrontendIntegrityInputs::embedded_go(),
533        )
534        .map_err(|error| error.to_string())?;
535
536        let workspace_started = Instant::now();
537        recover_cached_workspace(&root, &lock).map_err(|error| error.to_string())?;
538        let workspace =
539            prepare_cached_workspace(&root, &lock, &[]).map_err(|error| error.to_string())?;
540        let evidence_directory = work_directory.join("go/evidence");
541        fs::create_dir_all(&evidence_directory).map_err(|error| error.to_string())?;
542        // Decided before instrumenting, because whether a parallel test can be
543        // announced depends on whether anything will be running beside it.
544        let (command, serialised) = if request.exact_attribution {
545            command_run_in_order(&request.command)
546        } else {
547            (request.command.clone(), false)
548        };
549        let ambient = std::env::vars().collect::<std::collections::BTreeMap<_, _>>();
550        let roots = crate::source_discovery::ExplicitSourceRoots::from_environment_in(
551            &root, &workspace, &ambient,
552        )
553        .map_err(|error| error.to_string())?;
554        let instrumented =
555            instrument_workspace(&workspace, &evidence_directory, serialised, roots.as_ref())?;
556        let workspace_preparation_ms = elapsed_ms(workspace_started);
557        let adapter_setup_ms = (elapsed_ms(adapter_started) - workspace_preparation_ms).max(0.0);
558        writeln!(
559            diagnostics,
560            "[supercov] detected Go; instrumenting {} source file(s) across {} test package(s) in isolated workspace {}",
561            instrumented.project.instrumented.len(),
562            instrumented.packages.len(),
563            workspace.display()
564        )
565        .map_err(|error| error.to_string())?;
566        for (file, reason) in &instrumented.project.unparseable {
567            writeln!(
568                diagnostics,
569                "[supercov] could not parse {file}: {reason}; it carries no obligations"
570            )
571            .map_err(|error| error.to_string())?;
572        }
573        for (file, reason) in &instrumented.unparseable_tests {
574            writeln!(
575                diagnostics,
576                "[supercov] could not parse {file}: {reason}; the tests it declares run unattributed and what they reach counts run-wide"
577            )
578            .map_err(|error| error.to_string())?;
579        }
580        if instrumented.packages.is_empty() {
581            return Err(
582                "no Go test packages were found, so a run would measure nothing: Supercov needs at least one _test.go file"
583                    .into(),
584            );
585        }
586        // A file that does not parse is a hole in the denominator, and one
587        // hole is reported and measured around. Every file being a hole is not
588        // a hole: there is no denominator left, and a run of nothing satisfies
589        // every floor there is. Reporting that as success with exit 0 is the
590        // failure mode most likely to go unnoticed, so it is refused here the
591        // same way a project with no test package is.
592        if instrumented.project.instrumented.is_empty()
593            && !instrumented.project.files.sources.is_empty()
594        {
595            return Err(format!(
596                "none of the {} Go source file(s) could be parsed, so the run would measure a denominator of nothing: {}",
597                instrumented.project.files.sources.len(),
598                instrumented
599                    .project
600                    .unparseable
601                    .iter()
602                    .map(|(file, reason)| format!("{file}: {reason}"))
603                    .collect::<Vec<_>>()
604                    .join("; ")
605            ));
606        }
607
608        if serialised {
609            writeln!(
610                diagnostics,
611                "[supercov] added -parallel=1 for --exact-attribution: a test that calls t.Parallel() can only be credited with what it reached when nothing else is running. Your suite runs in order, which is slower."
612            )
613            .map_err(|error| error.to_string())?;
614        }
615        let (command, forced_fresh) = command_with_fresh_results(&command);
616        if forced_fresh {
617            writeln!(
618                diagnostics,
619                "[supercov] added -count=1: `go test` caches passing packages, and a cached package does not run, so it would record no coverage"
620            )
621            .map_err(|error| error.to_string())?;
622        }
623        let test_started = Instant::now();
624        let plan = ExecutionPlan {
625            preparation: Vec::new(),
626            test: ExecutionPhase {
627                name: "test".into(),
628                kind: PhaseKind::Test,
629                command: CommandSpec {
630                    program: command[0].clone().into(),
631                    arguments: command[1..].iter().map(OsString::from).collect(),
632                    cwd: workspace.clone(),
633                    environment: None,
634                    captured_output: None,
635                },
636            },
637        };
638        let options = SupervisionOptions::from_environment().map_err(|error| error.to_string())?;
639        let execution = execute_plan(&plan, options, diagnostics, |_, _| Ok(()))
640            .map_err(|error| error.to_string())?;
641        let test_command_ms = elapsed_ms(test_started);
642        if let Some(signal) = execution.interrupted_signal {
643            return Err(format!(
644                "the test command was interrupted by {signal:?}; no run was published"
645            ));
646        }
647        let exit_code = execution.exit_code;
648
649        let publication_started = Instant::now();
650        let mut parts = Vec::new();
651        let mut outcomes = Vec::new();
652        let mut silent = Vec::new();
653        for package in &instrumented.packages {
654            let Ok(bytes) = fs::read(&package.evidence) else {
655                silent.push(package.directory.clone());
656                continue;
657            };
658            let evidence = read_evidence(&bytes)
659                .map_err(|error| format!("{}: {error}", package.evidence.display()))?;
660            for test in &evidence.tests {
661                outcomes.push(OwnedTestOutcome {
662                    name: test.name.clone(),
663                    runner: test.runner.clone(),
664                    package: package.directory.clone(),
665                    file: package.declared_in.get(&test.name).cloned(),
666                    status: test.status.clone(),
667                    attributed: !test.unattributed,
668                });
669            }
670            parts.push(evidence);
671        }
672        if !silent.is_empty() {
673            writeln!(
674                diagnostics,
675                "[supercov] {} test package(s) wrote no evidence and are absent from this run: {}",
676                silent.len(),
677                silent.join(", ")
678            )
679            .map_err(|error| error.to_string())?;
680        }
681        let evidence = merge_evidence(parts);
682        let unattributed = instrumented
683            .packages
684            .iter()
685            .map(|package| package.unattributed.len())
686            .sum::<usize>();
687        // What the run reached, whether or not a test can be named for it. A
688        // package whose every test calls `t.Parallel()` announces nothing by
689        // design -- the coverage is real and simply has no owner -- and
690        // discarding the run there threw away the lines, branches and MC/DC of
691        // a fully measured suite. `t.Parallel()` is idiomatic Go: one reported
692        // codebase calls it in 935 of its 1144 test files.
693        let reached_something = evidence.global.iter().any(|total| *total != 0);
694        if outcomes.is_empty() && !reached_something {
695            let because = if unattributed > 0 {
696                format!(
697                    "; {unattributed} test(s) ran without being able to announce themselves (t.Parallel, Example or Fuzz) and reached nothing measured either"
698                )
699            } else {
700                String::new()
701            };
702            return Err(format!(
703                "no Go test recorded evidence (the command exited {exit_code}); a run that measured nothing is not published{because}"
704            ));
705        }
706        measured.set(true);
707        let run = build_frontend_run(OwnedRunInputs {
708            declaration: go_declaration(),
709            environment: "go",
710            manifest: &instrumented.project.manifest,
711            probes: &instrumented.project.probes,
712            evidence: &evidence,
713            outcomes: &outcomes,
714            run_id: &request.run_id,
715            generated_at: &request.started_at,
716            test_exit_code: exit_code,
717            coverage_model: go_coverage_model(),
718        })
719        .map_err(|error| error.to_string())?;
720        // Before anything is written: a run that cannot be read back is not a
721        // run, and finding that out at publication is far better than finding
722        // it out when someone asks for the report.
723        validate_frontend_report_request(&run.declaration, &run.request)
724            .map_err(|error| error.to_string())?;
725        let archive_path = work_directory.join("evidence.raw.gz");
726        let raw = write_archive(
727            crate::source_capture::append(
728                run.archive_entries().map_err(|error| error.to_string())?,
729                &source_inputs,
730            )?,
731            &archive_path,
732        )
733        .map_err(|error| error.to_string())?;
734        let evidence_publication_ms = elapsed_ms(publication_started);
735
736        let timings = RunTimings {
737            initialization_ms,
738            workspace_preparation_ms,
739            adapter_setup_ms,
740            instrumented_build_ms: 0.0,
741            test_command_ms,
742            evidence_publication_ms,
743        };
744        let metadata = RunMetadata {
745            id: request.run_id.clone(),
746            started_at: request.started_at.clone(),
747            duration_ms: elapsed_ms(total_started),
748            command: request.command.clone(),
749            test_exit_code: Some(exit_code),
750            integrity,
751            raw_evidence: RawEvidenceMetadata {
752                schema_version: raw.schema_version,
753                format: raw.format.into(),
754                file: raw.file.into(),
755                files: raw.files,
756                uncompressed_bytes: raw.uncompressed_bytes,
757                compressed_bytes: raw.compressed_bytes,
758            },
759            isolated_build: Some(true),
760            instrumented_build_cache: None,
761            timings: Some(timings),
762            merged: None,
763            parents: None,
764            source_roots: source_roots.clone(),
765        };
766        let run_directory =
767            publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
768        finalize_published_run(&root, &request.run_id).map_err(|error| error.to_string())?;
769        Ok(DirectGoRunResult {
770            run_id: request.run_id.clone(),
771            run_directory,
772            exit_code,
773            tests: outcomes
774                .iter()
775                .map(|outcome| outcome.name.as_str())
776                .collect::<BTreeSet<_>>()
777                .len(),
778            unattributed,
779            source_files: instrumented.project.instrumented.len(),
780            unparseable_sources: instrumented.project.unparseable.len(),
781            unparseable_tests: instrumented.unparseable_tests.len(),
782            packages: instrumented.packages.len(),
783            recovered_runs,
784            metadata,
785        })
786    })();
787    let result = result.map_err(|error| {
788        if !measured.get() {
789            return error;
790        }
791        note_kept_evidence(
792            &root,
793            &work_directory.join("go/evidence"),
794            &request.run_id,
795            error,
796        )
797    });
798    if result.is_err() {
799        let _ = remove_stored_tree_deferred(&root, &work_directory);
800    }
801    let release = lock.release().map_err(|error| error.to_string());
802    match (result, release) {
803        (Ok(result), Ok(())) => Ok(result),
804        (Err(error), _) => Err(error),
805        (Ok(_), Err(error)) => Err(error),
806    }
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812
813    #[test]
814    fn the_package_clause_is_found_past_whatever_precedes_it() {
815        // A build tag, a licence header, a block comment: all of them sit
816        // above the clause in real Go, and a reader that took the first line
817        // would find none of them.
818        assert_eq!(package_name("package main\n").as_deref(), Some("main"));
819        assert_eq!(
820            package_name("//go:build linux\n\n// Copyright.\npackage worker\n").as_deref(),
821            Some("worker")
822        );
823        assert_eq!(
824            package_name("/*\nA package comment\nspanning lines.\n*/\npackage doc\n").as_deref(),
825            Some("doc")
826        );
827        assert_eq!(
828            package_name("/* one line */ package inline\n").as_deref(),
829            Some("inline")
830        );
831        // A trailing comment is not part of the name.
832        assert_eq!(
833            package_name("package api // the public surface\n").as_deref(),
834            Some("api")
835        );
836        assert_eq!(package_name("import \"fmt\"\n"), None);
837    }
838
839    #[test]
840    fn caching_is_disabled_unless_the_author_already_chose() {
841        // `go test` skips a package whose result it has cached, and a package
842        // that does not run records nothing. A run that measured half a suite
843        // without saying so would be worse than a slow one.
844        let (command, forced) =
845            command_with_fresh_results(&["go".into(), "test".into(), "./...".into()]);
846        assert!(forced);
847        assert_eq!(command, ["go", "test", "-count=1", "./..."]);
848
849        // An author who already said how many times to run means it.
850        let (command, forced) =
851            command_with_fresh_results(&["go".into(), "test".into(), "-count=3".into()]);
852        assert!(!forced);
853        assert_eq!(command, ["go", "test", "-count=3"]);
854    }
855
856    #[test]
857    fn each_package_gets_an_evidence_file_of_its_own() {
858        // One test binary per package, each its own process: sharing a path
859        // would mean the last one to finish overwrote everyone else.
860        assert_ne!(evidence_name("internal/auth"), evidence_name("internal/db"));
861        assert_eq!(evidence_name("."), "_.bin");
862        assert!(!evidence_name("internal/auth").contains('/'));
863    }
864
865    #[test]
866    fn only_the_constraint_nothing_ever_defines_is_read_alone() {
867        // `ignore` is not a GOOS, not a GOARCH, and nothing sets it, which is
868        // why it is the convention for Go kept beside Go without being
869        // compiled. Every other constraint needs a GOOS, a GOARCH and a tag
870        // set to answer, and guessing at those would drop files a run does
871        // measure.
872        assert!(build_ignored("//go:build ignore\n\npackage p\n"));
873        assert!(build_ignored("// +build ignore\n\npackage p\n"));
874        assert!(build_ignored(
875            "// Copyright.\n\n//go:build ignore\n\npackage p\n"
876        ));
877        // Constraints combine, and one of the terms is still the term.
878        assert!(build_ignored("//go:build ignore || linux\n\npackage p\n"));
879
880        // Everything else is a question this cannot answer, so it does not.
881        assert!(!build_ignored("//go:build linux\n\npackage p\n"));
882        assert!(!build_ignored("//go:build !windows\n\npackage p\n"));
883        assert!(!build_ignored("package p\n"));
884        // A tag that merely starts with the word is a different tag.
885        assert!(!build_ignored("//go:build ignored\n\npackage p\n"));
886        assert!(!build_ignored("//go:build ignore_me\n\npackage p\n"));
887        // And a constraint has to be above the package clause to be one. The
888        // toolchain ignores the text below it, so reading it there would drop
889        // a file the build takes.
890        assert!(!build_ignored(
891            "package p\n\n//go:build ignore\n\nfunc f() {}\n"
892        ));
893        assert!(!build_ignored(
894            "package p\n\nvar doc = \"//go:build ignore\"\n"
895        ));
896    }
897
898    #[test]
899    fn a_test_main_is_seen_in_a_file_nothing_could_parse() {
900        // Only ever asked of a file that did not parse, which is exactly when
901        // there is no tree to ask. Missing a real one declares a second
902        // TestMain and the package stops compiling, so this errs towards
903        // seeing one -- but not so far that any mention of the word counts.
904        assert!(declares_test_main_textually(
905            "package p\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n"
906        ));
907        // Spacing is the author's business.
908        assert!(declares_test_main_textually(
909            "package p\n\nfunc TestMain( m  *testing.M ) {}\n"
910        ));
911        assert!(declares_test_main_textually(
912            "package p\n\nfunc TestMain(m *testing.M) { broken( }\n"
913        ));
914
915        // `TestMainHelper` is an ordinary function whose name starts with the
916        // word, exactly as `Testify` is not a test.
917        assert!(!declares_test_main_textually(
918            "package p\n\nfunc TestMainHelper(t *testing.T) {}\n"
919        ));
920        // A test that merely takes a *testing.T is not the package's harness.
921        assert!(!declares_test_main_textually(
922            "package p\n\nfunc TestMain(t *testing.T) {}\n"
923        ));
924        assert!(!declares_test_main_textually(
925            "package p\n\n// func TestMain\n"
926        ));
927        assert!(!declares_test_main_textually("package p\n"));
928    }
929}