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 directory whose name begins with `_`: the Go tool leaves those out of
55/// `./...` and builds them when a file imports one, which every rewritten
56/// file does. Under a plain name the package was listed with the project's
57/// own in every run, `?  example.com/shop/supercov_runtime  [no test files]`.
58/// Go 1.22 to 1.27 import it alike.
59const RUNTIME_DIRECTORY: &str = "_supercov_runtime";
60
61const RUNTIME_SOURCE: &str = include_str!("../runtime-assets/go/supercov/supercov.go");
62
63/// The generated file that declares a package's probe array.
64const PROBE_FILE: &str = "supercov_probes.go";
65
66/// The generated file that arms the runtime and binds each test.
67const HARNESS_FILE: &str = "supercov_generated_test.go";
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase", deny_unknown_fields)]
71pub struct DirectGoRunRequest {
72    pub root: PathBuf,
73    pub command: Vec<String>,
74    pub run_id: String,
75    pub started_at: String,
76    /// Credit every test with what it reached, by running the package's tests
77    /// one at a time.
78    ///
79    /// Probes are a store into one array the whole process shares, so a test
80    /// that called `t.Parallel()` can only be credited with its own work if
81    /// nothing else is running while it does it. `-parallel 1` is what buys
82    /// that, and what it costs is the suite's own parallelism -- which is why
83    /// it is asked for rather than assumed.
84    #[serde(default)]
85    pub exact_attribution: bool,
86}
87
88#[derive(Debug, Clone, PartialEq)]
89pub struct DirectGoRunResult {
90    pub run_id: String,
91    pub run_directory: PathBuf,
92    pub exit_code: i32,
93    pub tests: usize,
94    /// Test functions `go test` ran that no evidence can be credited to: one
95    /// that called `t.Parallel()`, and every Example or Fuzz target. What they
96    /// reached counts run-wide, so a run of nothing but these is a measured
97    /// run with no per-test attribution -- which is a different thing from an
98    /// empty suite, and the summary has to be able to say which it was.
99    pub unattributed: usize,
100    pub source_files: usize,
101    /// Source files that did not parse, and so carry no obligations. A hole in
102    /// the denominator travels with the number it was taken out of.
103    pub unparseable_sources: usize,
104    /// Test files that did not parse. Their tests still run and still reach
105    /// what they reach; what is lost is the announcement that would name them
106    /// for it.
107    pub unparseable_tests: usize,
108    pub packages: usize,
109    pub recovered_runs: Vec<String>,
110    pub metadata: RunMetadata,
111}
112
113fn elapsed_ms(started: Instant) -> f64 {
114    (started.elapsed().as_secs_f64() * 10_000.0).round() / 10.0
115}
116
117/// One package whose tests will run, and where its evidence lands.
118#[derive(Debug, Clone)]
119struct GoTestPackage {
120    /// Directory relative to the workspace root, `/`-separated, `.` for the
121    /// module root.
122    directory: String,
123    evidence: PathBuf,
124    /// Which file declared each test, so a result can point at its source.
125    declared_in: BTreeMap<String, String>,
126    /// What ran here and announced nothing: parallel tests, Examples, Fuzz
127    /// targets.
128    unattributed: BTreeSet<String>,
129}
130
131struct InstrumentedWorkspace {
132    project: PreparedGoProject,
133    packages: Vec<GoTestPackage>,
134    /// Test files that did not parse, with the reason. They cost the tests
135    /// they declare their attribution and nothing else.
136    unparseable_tests: Vec<(String, String)>,
137}
138
139fn directory_of(relative: &str) -> String {
140    match relative.rsplit_once('/') {
141        Some((directory, _)) => directory.to_owned(),
142        None => ".".to_owned(),
143    }
144}
145
146/// The `package` a Go file declares.
147///
148/// Scanned rather than parsed: the clause is the first thing in a Go file that
149/// is not a comment, so stripping comments and reading the next word gets the
150/// same answer as a parse for a fraction of the cost — and preparing a project
151/// already parses every file once, which is enough.
152fn package_name(source: &str) -> Option<String> {
153    let bytes = source.as_bytes();
154    let mut at = 0;
155    while at < bytes.len() {
156        let rest = &source[at..];
157        if rest.starts_with("//") {
158            at += rest.find('\n').map_or(rest.len(), |end| end + 1);
159        } else if rest.starts_with("/*") {
160            // An unterminated comment is not Go; there is no clause to find.
161            at += rest.find("*/").map_or(rest.len(), |end| end + 2);
162        } else if bytes[at].is_ascii_whitespace() {
163            at += 1;
164        } else {
165            return rest
166                .strip_prefix("package")
167                .and_then(|rest| rest.strip_prefix(|c: char| c.is_ascii_whitespace()))
168                .and_then(|rest| rest.split_whitespace().next())
169                // The instrumented text ends the clause with `;`, as the
170                // rewriter writes an import on the same line.
171                .map(|name| name.trim_end_matches(';').to_owned());
172        }
173    }
174    None
175}
176
177fn read(path: &Path) -> Result<String, String> {
178    fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))
179}
180
181/// A copy's file under the name Go is to report it by: the project's own.
182///
183/// Go puts a file's path, as the compiler read it, into every stack trace,
184/// so a panic under Supercov named the copy: `…/.supercov/workspaces/
185/// workspace/shop/crypto.go:24`. A line directive is how Go is told that a
186/// file stands for another. It is the comment form, in front of the package
187/// clause on the clause's own line and naming that line, so the file gains
188/// no line: what reads the file as it lies, a coverage profile, counts the
189/// same lines as what reads it through the directive, a stack trace. The
190/// rewriting adds no line of its own either.
191fn named_for_the_project(project: &Path, relative: &str, text: &str) -> String {
192    let Ok(tree) = crate::go_instrumenter::parse(text) else {
193        return text.to_owned();
194    };
195    let mut cursor = tree.root_node().walk();
196    let Some(package) = tree
197        .root_node()
198        .children(&mut cursor)
199        .find(|child| child.kind() == "package_clause")
200    else {
201        return text.to_owned();
202    };
203    let at = package.start_byte();
204    let line = text[..at].rfind('\n').map_or(0, |end| end + 1);
205    if text[line..at].starts_with(LINE_DIRECTIVE) {
206        return text.to_owned();
207    }
208    let position = package.start_position();
209    // With the platform's separator all along: Go writes the file's name as
210    // it is given here, and on Windows `pkg/inner/inner.go` after the
211    // project's directory came out as `pkg/inner/inner.go` in a compile
212    // error where Go alone writes `pkg\inner\inner.go`.
213    let named = relative
214        .split('/')
215        .fold(project.to_path_buf(), |path, part| path.join(part));
216    format!(
217        "{}{LINE_DIRECTIVE}{}:{}:{}*/{}",
218        &text[..at],
219        named.display(),
220        position.row + 1,
221        position.column + 1,
222        &text[at..]
223    )
224}
225
226const LINE_DIRECTIVE: &str = "/*line ";
227
228fn write(path: &Path, contents: &str) -> Result<(), String> {
229    if let Some(parent) = path.parent() {
230        fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?;
231    }
232    fs::write(path, contents).map_err(|error| format!("{}: {error}", path.display()))
233}
234
235/// A file-system-safe name for a package's evidence, so two packages cannot
236/// collide on one path.
237fn evidence_name(directory: &str) -> String {
238    let slug = directory
239        .chars()
240        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
241        .collect::<String>();
242    format!("{slug}.bin")
243}
244
245/// Rewrite the workspace in place: instrumented sources, the runtime package
246/// they import, a probe array per package, and a harness per test package.
247/// A workspace's sources rewritten with probes: the project, each module's
248/// runtime import, the probe count every package reserves, and the package
249/// of each source directory.
250struct InstrumentedSources {
251    project: PreparedGoProject,
252    runtimes: Vec<(String, String)>,
253    probe_count: usize,
254    source_packages: BTreeMap<String, String>,
255}
256
257impl InstrumentedSources {
258    /// The runtime a file imports: its module's.
259    fn import_for(&self, relative: &str) -> &str {
260        runtime_for(&self.runtimes, relative)
261    }
262}
263
264/// The runtime a file imports, of the modules' runtimes longest first.
265fn runtime_for<'a>(runtimes: &'a [(String, String)], relative: &str) -> &'a str {
266    runtimes
267        .iter()
268        .find(|(directory, _)| {
269            directory == "."
270                || relative == directory
271                || relative.starts_with(&format!("{directory}/"))
272        })
273        .map(|(_, import)| import.as_str())
274        .unwrap_or_default()
275}
276
277fn instrument_sources(
278    project_root: &Path,
279    workspace: &Path,
280    roots: Option<&crate::source_discovery::ExplicitSourceRoots>,
281) -> Result<InstrumentedSources, String> {
282    let project = prepare_go_project(workspace, roots)?;
283
284    // A go.work repository has several modules and no module at its root, so
285    // each gets a runtime of its own: an import is resolved against the module
286    // that names it, and one module cannot import a package inside another.
287    let members = crate::go_project::workspace_modules(workspace);
288    let directories = if members.is_empty() {
289        vec![".".to_owned()]
290    } else {
291        members.into_iter().collect::<Vec<_>>()
292    };
293    let mut runtimes: Vec<(String, String)> = Vec::new();
294    for directory in &directories {
295        let at = workspace.join(directory);
296        let module = module_path(&at).ok_or_else(|| {
297            format!(
298                "{}: no module path in go.mod, so Supercov cannot name the package its runtime is imported from",
299                at.display()
300            )
301        })?;
302        write(
303            &at.join(RUNTIME_DIRECTORY).join("supercov.go"),
304            RUNTIME_SOURCE,
305        )?;
306        runtimes.push((directory.clone(), format!("{module}/{RUNTIME_DIRECTORY}")));
307    }
308    // Longest first, so a nested module wins over the root it sits under.
309    runtimes.sort_by_key(|(directory, _)| std::cmp::Reverse(directory.len()));
310    let mut sources = InstrumentedSources {
311        project,
312        runtimes,
313        probe_count: 0,
314        source_packages: BTreeMap::new(),
315    };
316
317    // Probe ids are handed out across the whole module, so every package
318    // reserves the same total: a probe from one package is the same index in
319    // the binary that links it as a dependency.
320    sources.probe_count = sources
321        .project
322        .probes
323        .keys()
324        .max()
325        .map_or(0, |highest| *highest as usize + 1);
326
327    let mut source_packages: BTreeMap<String, String> = BTreeMap::new();
328    for (relative, instrumented) in &sources.project.instrumented {
329        let instrumented = instrumented.replace(RUNTIME_IMPORT, sources.import_for(relative));
330        let path = workspace.join(relative);
331        if let Some(name) = package_name(&instrumented) {
332            source_packages
333                .entry(directory_of(relative))
334                .or_insert(name);
335        }
336        write(
337            &path,
338            &named_for_the_project(project_root, relative, &instrumented),
339        )?;
340    }
341    for (directory, package) in &source_packages {
342        write(
343            &workspace.join(directory).join(PROBE_FILE),
344            &probe_array_file(
345                package,
346                RUNTIME_ALIAS,
347                sources.import_for(&format!("{directory}/x.go")),
348                sources.probe_count,
349            ),
350        )?;
351    }
352    sources.source_packages = source_packages;
353    Ok(sources)
354}
355
356fn instrument_workspace(
357    project_root: &Path,
358    workspace: &Path,
359    evidence_directory: &Path,
360    serialised: bool,
361    roots: Option<&crate::source_discovery::ExplicitSourceRoots>,
362) -> Result<InstrumentedWorkspace, String> {
363    let InstrumentedSources {
364        mut project,
365        runtimes,
366        probe_count,
367        source_packages,
368    } = instrument_sources(project_root, workspace, roots)?;
369    let import_for = |relative: &str| runtime_for(&runtimes, relative);
370    let mut unparseable_tests: Vec<(String, String)> = Vec::new();
371
372    let mut by_directory: BTreeMap<String, Vec<&String>> = BTreeMap::new();
373    for relative in &project.files.tests {
374        by_directory
375            .entry(directory_of(relative))
376            .or_default()
377            .push(relative);
378    }
379
380    let mut packages = Vec::new();
381    for (directory, tests) in by_directory {
382        let evidence = evidence_directory.join(evidence_name(&directory));
383        let evidence_literal = evidence.to_string_lossy().replace('\\', "\\\\");
384        let mut declared_in = BTreeMap::new();
385        let mut unattributed = BTreeSet::new();
386        let mut declares_test_main = false;
387        // The harness joins whichever package the tests are in. A directory
388        // can hold both `foo` and `foo_test` files; the internal one wins,
389        // because that is where the instrumented sources are.
390        let mut harness_package: Option<String> = None;
391        for relative in tests {
392            let path = workspace.join(relative);
393            let source = read(&path)?;
394            let Some(name) = package_name(&source) else {
395                continue;
396            };
397            // The package a harness joins is read from the text, so it is
398            // known whether or not the file parses -- and a package whose only
399            // test file is unreadable still needs one.
400            let internal = source_packages.get(&directory) == Some(&name);
401            if internal || harness_package.is_none() {
402                harness_package = Some(name);
403            }
404            // A file the toolchain never builds declares nothing Supercov has
405            // to work around, and reading its `TestMain` as the package's own
406            // stood the harness down for a package that had none -- so the
407            // runtime was never armed and a passing suite published nothing.
408            let built = !build_ignored(&source);
409            let file =
410                match instrument_test_file(&source, RUNTIME_ALIAS, &evidence_literal, serialised) {
411                    Ok(file) => file,
412                    Err(error) => {
413                        // Taking the run down with it is the reflex a source file
414                        // was corrected for, and it is worse here: a test file
415                        // need not even be in the build for Supercov to refuse the
416                        // suite the toolchain just passed. What it costs is the
417                        // attribution of the tests it declares, because there is
418                        // nowhere to put the announcement. What they reach still
419                        // counts run-wide, like any execution no test could claim.
420                        project.manifest.limitations.push(
421                            crate::go_project::unparseable_limitation(relative, &error.to_string()),
422                        );
423                        unparseable_tests.push((relative.clone(), error.to_string()));
424                        // A TestMain nobody could read is still a TestMain, and Go
425                        // permits one per package. Synthesising a second does not
426                        // fail to measure, it fails to build -- so this is scanned
427                        // for rather than parsed for, which is the one question
428                        // that cannot go unanswered.
429                        declares_test_main |= built && declares_test_main_textually(&source);
430                        continue;
431                    }
432                };
433            declares_test_main |= built && file.declares_test_main;
434            for test in &file.tests {
435                declared_in.insert(test.clone(), relative.clone());
436            }
437            unattributed.extend(file.unattributed.iter().cloned());
438            // Written also when nothing in it changed, for its name.
439            write(
440                &path,
441                &named_for_the_project(
442                    project_root,
443                    relative,
444                    &rewrite(&source, &file.edits).replace(RUNTIME_IMPORT, import_for(relative)),
445                ),
446            )?;
447        }
448        let Some(package) = harness_package else {
449            continue;
450        };
451        write(
452            &workspace.join(&directory).join(HARNESS_FILE),
453            &synthesized_harness(
454                &package,
455                RUNTIME_ALIAS,
456                import_for(&format!("{directory}/x.go")),
457                probe_count,
458                &project.decision_widths,
459                &evidence_literal,
460                declares_test_main,
461            ),
462        )?;
463        packages.push(GoTestPackage {
464            directory,
465            evidence,
466            declared_in,
467            unattributed,
468        });
469    }
470    Ok(InstrumentedWorkspace {
471        project,
472        packages,
473        unparseable_tests,
474    })
475}
476
477/// Whether the toolchain will refuse to build this file whatever it is asked.
478///
479/// Build constraints in general cannot be answered without knowing the GOOS,
480/// GOARCH and tags a build will use, and guessing at them would drop files a
481/// run does measure. `ignore` needs none of that: it is not a GOOS, not a
482/// GOARCH, and nothing defines it, which is exactly why it is the convention
483/// for Go kept beside Go without being compiled -- a reference harness, a
484/// generator's input. So it is the one constraint that can be read alone, and
485/// the only one that is.
486fn build_ignored(source: &str) -> bool {
487    // Constraints live above the package clause and nowhere else: the
488    // toolchain stops reading them there, so a `//go:build ignore` in a
489    // comment further down is text about Go rather than a constraint on it.
490    let header = if source.starts_with("package ") {
491        // Nothing precedes a clause on the first line, and looking for a
492        // newline before it would find none and read the whole file.
493        ""
494    } else {
495        source
496            .find("\npackage ")
497            .map_or(source, |clause| &source[..clause])
498    };
499    header
500        .lines()
501        .map(str::trim)
502        .filter_map(|line| {
503            line.strip_prefix("//go:build")
504                // The spelling Go used before 1.17, still in trees today.
505                .or_else(|| line.strip_prefix("// +build"))
506        })
507        .any(|constraint| {
508            constraint
509                .split(|c: char| !(c.is_alphanumeric() || c == '_'))
510                .any(|term| term == "ignore")
511        })
512}
513
514/// Whether a file declares `TestMain`, read from the text rather than a tree.
515///
516/// Only ever asked of a file that did not parse, which is exactly when there
517/// is no tree to ask. It errs towards seeing one: believing in a `TestMain`
518/// that is not there costs the package Supercov's own harness, while missing
519/// one that is there declares it twice and the package stops compiling.
520fn declares_test_main_textually(source: &str) -> bool {
521    source.split("func TestMain").skip(1).any(|rest| {
522        rest.trim_start()
523            .strip_prefix('(')
524            .is_some_and(|arguments| arguments.contains("testing.M"))
525    })
526}
527
528/// `go test` caches a package that passed, and a cached package does not run.
529/// The command with Go's test parallelism turned down to one.
530///
531/// `-parallel` bounds how many tests calling `t.Parallel()` run at once. At 1
532/// they resume one at a time, so each is the only test running when its probes
533/// fire and the harness can announce it like any serial test.
534fn command_run_in_order(command: &[String]) -> (Vec<String>, bool) {
535    if command
536        .iter()
537        .any(|argument| argument == "-parallel" || argument.starts_with("-parallel="))
538    {
539        // The author already said what they wanted; saying it louder is not
540        // Supercov's call.
541        return (command.to_vec(), false);
542    }
543    let mut updated = command.to_vec();
544    let position = updated
545        .iter()
546        .position(|argument| argument == "test")
547        .map_or(updated.len(), |index| index + 1);
548    updated.insert(position, "-parallel=1".into());
549    (updated, true)
550}
551
552fn command_with_fresh_results(command: &[String]) -> (Vec<String>, bool) {
553    if command
554        .iter()
555        .any(|argument| argument == "-count" || argument.starts_with("-count="))
556    {
557        return (command.to_vec(), false);
558    }
559    let mut updated = command.to_vec();
560    // After the subcommand, so `go -count=1 test` is never produced.
561    let position = updated
562        .iter()
563        .position(|argument| argument == "test")
564        .map_or(updated.len(), |index| index + 1);
565    updated.insert(position, "-count=1".into());
566    (updated, true)
567}
568
569/// The fingerprint a later query compares against the stored run.
570pub fn current_go_integrity(
571    root: &Path,
572    command: &[String],
573    source_roots: Option<&[String]>,
574) -> Result<crate::run_store::RunIntegrity, String> {
575    let root = canonicalize_simplified(root).map_err(|error| error.to_string())?;
576    let files = crate::go_project::discover_go_files(&root)?;
577    let mut inputs = go_integrity_inputs(&files, command);
578    crate::source_discovery::fold_roots_into_configuration(
579        &mut inputs.execution_configuration,
580        source_roots,
581    );
582    create_explicit_run_integrity(&root, &inputs, &FrontendIntegrityInputs::embedded_go())
583        .map_err(|error| error.to_string())
584}
585
586pub fn run_direct_go(
587    request: &DirectGoRunRequest,
588    diagnostics: &mut dyn Write,
589) -> Result<DirectGoRunResult, String> {
590    if request.command.is_empty() {
591        return Err("test command must not be empty".into());
592    }
593    let total_started = Instant::now();
594    let initialization_started = Instant::now();
595    let root = canonicalize_simplified(&request.root)
596        .map_err(|error| format!("{}: {error}", request.root.display()))?;
597    let mut lock = ProjectLock::acquire(&root, &request.run_id, &request.started_at)
598        .map_err(|error| error.to_string())?;
599    let initialization_ms = elapsed_ms(initialization_started);
600    let work_directory = root.join(".supercov/work").join(&request.run_id);
601    // Whether the tests were measured. A run that failed before that has no
602    // evidence worth keeping; one that failed after has evidence that cost the
603    // suite its wall clock.
604    let measured = std::cell::Cell::new(false);
605    let result = (|| {
606        let recovered_runs = recover_abandoned_runs(&root, &request.started_at)
607            .map_err(|error| error.to_string())?;
608        if !recovered_runs.is_empty() {
609            writeln!(
610                diagnostics,
611                "[supercov] recovered abandoned run(s): {}",
612                recovered_runs.join(", ")
613            )
614            .map_err(|error| error.to_string())?;
615        }
616
617        let adapter_started = Instant::now();
618        let files = crate::go_project::discover_go_files(&root)?;
619        let source_roots = crate::source_discovery::configured_source_roots(
620            &std::env::vars().collect::<std::collections::BTreeMap<_, _>>(),
621        );
622        let mut integrity_inputs = go_integrity_inputs(&files, &request.command);
623        crate::source_discovery::fold_roots_into_configuration(
624            &mut integrity_inputs.execution_configuration,
625            source_roots.as_deref(),
626        );
627        let source_inputs =
628            crate::source_capture::capture(&root, "go", integrity_inputs.assertion_paths())?;
629        let integrity = create_explicit_run_integrity(
630            &root,
631            &integrity_inputs,
632            &FrontendIntegrityInputs::embedded_go(),
633        )
634        .map_err(|error| error.to_string())?;
635
636        let workspace_started = Instant::now();
637        recover_cached_workspace(&root, &lock).map_err(|error| error.to_string())?;
638        let workspace =
639            prepare_cached_workspace(&root, &lock, &[]).map_err(|error| error.to_string())?;
640        let evidence_directory = work_directory.join("go/evidence");
641        fs::create_dir_all(&evidence_directory).map_err(|error| error.to_string())?;
642        // Decided before instrumenting, because whether a parallel test can be
643        // announced depends on whether anything will be running beside it.
644        let (command, serialised) = if request.exact_attribution {
645            command_run_in_order(&request.command)
646        } else {
647            (request.command.clone(), false)
648        };
649        let ambient = std::env::vars().collect::<std::collections::BTreeMap<_, _>>();
650        let roots = crate::source_discovery::ExplicitSourceRoots::from_environment_in(
651            &root, &workspace, &ambient,
652        )
653        .map_err(|error| error.to_string())?;
654        let instrumented = instrument_workspace(
655            &root,
656            &workspace,
657            &evidence_directory,
658            serialised,
659            roots.as_ref(),
660        )?;
661        let workspace_preparation_ms = elapsed_ms(workspace_started);
662        let adapter_setup_ms = (elapsed_ms(adapter_started) - workspace_preparation_ms).max(0.0);
663        writeln!(
664            diagnostics,
665            "[supercov] detected Go; instrumenting {} source file(s) across {} test package(s) in isolated workspace {}",
666            instrumented.project.instrumented.len(),
667            instrumented.packages.len(),
668            workspace.display()
669        )
670        .map_err(|error| error.to_string())?;
671        for (file, reason) in &instrumented.project.unparseable {
672            writeln!(
673                diagnostics,
674                "[supercov] could not parse {file}: {reason}; it carries no obligations"
675            )
676            .map_err(|error| error.to_string())?;
677        }
678        for (file, reason) in &instrumented.unparseable_tests {
679            writeln!(
680                diagnostics,
681                "[supercov] could not parse {file}: {reason}; the tests it declares run unattributed and what they reach counts run-wide"
682            )
683            .map_err(|error| error.to_string())?;
684        }
685        if instrumented.packages.is_empty() {
686            return Err(
687                "no Go test packages were found, so a run would measure nothing: Supercov needs at least one _test.go file"
688                    .into(),
689            );
690        }
691        // A file that does not parse is a hole in the denominator, and one
692        // hole is reported and measured around. Every file being a hole is not
693        // a hole: there is no denominator left, and a run of nothing satisfies
694        // every floor there is. Reporting that as success with exit 0 is the
695        // failure mode most likely to go unnoticed, so it is refused here the
696        // same way a project with no test package is.
697        if instrumented.project.instrumented.is_empty()
698            && !instrumented.project.files.sources.is_empty()
699        {
700            return Err(format!(
701                "none of the {} Go source file(s) could be parsed, so the run would measure a denominator of nothing: {}",
702                instrumented.project.files.sources.len(),
703                instrumented
704                    .project
705                    .unparseable
706                    .iter()
707                    .map(|(file, reason)| format!("{file}: {reason}"))
708                    .collect::<Vec<_>>()
709                    .join("; ")
710            ));
711        }
712
713        if serialised {
714            writeln!(
715                diagnostics,
716                "[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."
717            )
718            .map_err(|error| error.to_string())?;
719        }
720        let (command, forced_fresh) = command_with_fresh_results(&command);
721        if forced_fresh {
722            writeln!(
723                diagnostics,
724                "[supercov] added -count=1: `go test` caches passing packages, and a cached package does not run, so it would record no coverage"
725            )
726            .map_err(|error| error.to_string())?;
727        }
728        let test_started = Instant::now();
729        let plan = ExecutionPlan {
730            preparation: Vec::new(),
731            test: ExecutionPhase {
732                name: "test".into(),
733                kind: PhaseKind::Test,
734                command: CommandSpec {
735                    program: command[0].clone().into(),
736                    arguments: command[1..].iter().map(OsString::from).collect(),
737                    cwd: workspace.clone(),
738                    environment: None,
739                    captured_output: None,
740                    console: Some(crate::workspace::console_names(&root, &workspace)),
741                },
742            },
743        };
744        let options = SupervisionOptions::from_environment().map_err(|error| error.to_string())?;
745        // What is in the copy now is Supercov's; what the command adds to
746        // it or changes is the command's, and belongs in the project.
747        let before = crate::workspace::workspace_output_baseline(&workspace)
748            .map_err(|error| error.to_string())?;
749        let execution = execute_plan(&plan, options, diagnostics, |_, _| Ok(()))
750            .map_err(|error| error.to_string())?;
751        let test_command_ms = elapsed_ms(test_started);
752        // `go test -coverprofile=cover.out` left no `cover.out` in the
753        // project, a golden file a test rewrote stayed in the copy, and a
754        // step after the tests that reads either found the old one or none.
755        // Every Go file in the copy is rewritten and stays there.
756        let rewritten = instrumented
757            .project
758            .files
759            .sources
760            .iter()
761            .chain(&instrumented.project.files.tests)
762            .map(PathBuf::from)
763            .collect::<BTreeSet<_>>();
764        let outputs = crate::workspace::sync_outputs_of(
765            crate::workspace::CommandOutputs::Go,
766            &root,
767            &workspace,
768            &before,
769            &rewritten,
770        )
771        .map_err(|error| error.to_string())?;
772        for line in crate::workspace::describe_command_outputs(&outputs) {
773            writeln!(diagnostics, "{line}").map_err(|error| error.to_string())?;
774        }
775        if let Some(signal) = execution.interrupted_signal {
776            return Err(format!(
777                "the test command was interrupted by {signal:?}; no run was published"
778            ));
779        }
780        let exit_code = execution.exit_code;
781
782        let publication_started = Instant::now();
783        let mut parts = Vec::new();
784        let mut outcomes = Vec::new();
785        let mut silent = Vec::new();
786        for package in &instrumented.packages {
787            let Ok(bytes) = fs::read(&package.evidence) else {
788                silent.push(package.directory.clone());
789                continue;
790            };
791            let evidence = read_evidence(&bytes)
792                .map_err(|error| format!("{}: {error}", package.evidence.display()))?;
793            for test in &evidence.tests {
794                outcomes.push(OwnedTestOutcome {
795                    name: test.name.clone(),
796                    runner: test.runner.clone(),
797                    package: package.directory.clone(),
798                    file: package.declared_in.get(&test.name).cloned(),
799                    status: test.status.clone(),
800                    attributed: !test.unattributed,
801                });
802            }
803            parts.push(evidence);
804        }
805        if !silent.is_empty() {
806            writeln!(
807                diagnostics,
808                "[supercov] {} test package(s) wrote no evidence and are absent from this run: {}",
809                silent.len(),
810                silent.join(", ")
811            )
812            .map_err(|error| error.to_string())?;
813        }
814        let evidence = merge_evidence(parts);
815        let unattributed = instrumented
816            .packages
817            .iter()
818            .map(|package| package.unattributed.len())
819            .sum::<usize>();
820        // What the run reached, whether or not a test can be named for it. A
821        // package whose every test calls `t.Parallel()` announces nothing by
822        // design -- the coverage is real and simply has no owner -- and
823        // discarding the run there threw away the lines, branches and MC/DC of
824        // a fully measured suite. `t.Parallel()` is idiomatic Go: one reported
825        // codebase calls it in 935 of its 1144 test files.
826        let reached_something = evidence.global.iter().any(|total| *total != 0);
827        if outcomes.is_empty() && !reached_something {
828            let because = if unattributed > 0 {
829                format!(
830                    "; {unattributed} test(s) ran without being able to announce themselves (t.Parallel, Example or Fuzz) and reached nothing measured either"
831                )
832            } else {
833                String::new()
834            };
835            return Err(format!(
836                "no Go test recorded evidence (the command exited {exit_code}); a run that measured nothing is not published{because}"
837            ));
838        }
839        measured.set(true);
840        let run = build_frontend_run(OwnedRunInputs {
841            declaration: go_declaration(),
842            environment: "go",
843            manifest: &instrumented.project.manifest,
844            probes: &instrumented.project.probes,
845            evidence: &evidence,
846            outcomes: &outcomes,
847            run_id: &request.run_id,
848            generated_at: &request.started_at,
849            test_exit_code: exit_code,
850            coverage_model: go_coverage_model(),
851        })
852        .map_err(|error| error.to_string())?;
853        // Before anything is written: a run that cannot be read back is not a
854        // run, and finding that out at publication is far better than finding
855        // it out when someone asks for the report.
856        validate_frontend_report_request(&run.declaration, &run.request)
857            .map_err(|error| error.to_string())?;
858        let archive_path = work_directory.join("evidence.raw.gz");
859        let raw = write_archive(
860            crate::source_capture::append(
861                run.archive_entries().map_err(|error| error.to_string())?,
862                &source_inputs,
863            )?,
864            &archive_path,
865        )
866        .map_err(|error| error.to_string())?;
867        let evidence_publication_ms = elapsed_ms(publication_started);
868
869        let timings = RunTimings {
870            initialization_ms,
871            workspace_preparation_ms,
872            adapter_setup_ms,
873            instrumented_build_ms: 0.0,
874            test_command_ms,
875            evidence_publication_ms,
876        };
877        let metadata = RunMetadata {
878            id: request.run_id.clone(),
879            started_at: request.started_at.clone(),
880            duration_ms: elapsed_ms(total_started),
881            command: request.command.clone(),
882            test_exit_code: Some(exit_code),
883            integrity,
884            raw_evidence: RawEvidenceMetadata {
885                schema_version: raw.schema_version,
886                format: raw.format.into(),
887                file: raw.file.into(),
888                files: raw.files,
889                uncompressed_bytes: raw.uncompressed_bytes,
890                compressed_bytes: raw.compressed_bytes,
891            },
892            isolated_build: Some(true),
893            instrumented_build_cache: None,
894            timings: Some(timings),
895            merged: None,
896            parents: None,
897            source_roots: source_roots.clone(),
898            copy: Some(lock.copy()),
899            merged_across: None,
900            recorded_by: None,
901            companions: None,
902        };
903        let run_directory =
904            publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
905        finalize_published_run(&root, &request.run_id).map_err(|error| error.to_string())?;
906        Ok(DirectGoRunResult {
907            run_id: request.run_id.clone(),
908            run_directory,
909            exit_code,
910            tests: outcomes
911                .iter()
912                .map(|outcome| outcome.name.as_str())
913                .collect::<BTreeSet<_>>()
914                .len(),
915            unattributed,
916            source_files: instrumented.project.instrumented.len(),
917            unparseable_sources: instrumented.project.unparseable.len(),
918            unparseable_tests: instrumented.unparseable_tests.len(),
919            packages: instrumented.packages.len(),
920            recovered_runs,
921            metadata,
922        })
923    })();
924    let result = result.map_err(|error| {
925        if !measured.get() {
926            return error;
927        }
928        note_kept_evidence(
929            &root,
930            &work_directory.join("go/evidence"),
931            &request.run_id,
932            error,
933        )
934    });
935    if result.is_err() {
936        let _ = remove_stored_tree_deferred(&root, &work_directory);
937    }
938    let release = lock.release().map_err(|error| error.to_string());
939    match (result, release) {
940        (Ok(result), Ok(())) => Ok(result),
941        (Err(error), _) => Err(error),
942        (Ok(_), Err(error)) => Err(error),
943    }
944}
945
946/// The file a server's main package gains: it arms the runtime and starts
947/// the clock its windows are harvested on.
948const SERVER_FILE: &str = "supercov_server.go";
949
950/// Go measured inside a browser suite's run: a server its tests drive, built
951/// from the copy the suite runs in by `go run` or `go build` in the command.
952/// Its sources there are rewritten with probes, and each main package arms
953/// the runtime in server mode: windows of what ran, harvested on a clock and
954/// given to the browser test running throughout each, by time. A binary
955/// built before the run is not this one and is not measured.
956pub struct GoServer {
957    project: PreparedGoProject,
958    integrity: crate::run_store::RunIntegrity,
959    source_inputs: crate::source_manifest::Inputs,
960    source_roots: Option<Vec<String>>,
961    directory: PathBuf,
962}
963
964impl GoServer {
965    /// Instrument the copy's Go sources for a server, or None when the
966    /// project has no Go module or no main package.
967    pub fn prepare(
968        root: &Path,
969        workspace: &Path,
970        run_id: &str,
971        command: &[String],
972    ) -> Result<Option<Self>, String> {
973        if !workspace.join("go.mod").is_file()
974            && crate::go_project::workspace_modules(workspace).is_empty()
975        {
976            return Ok(None);
977        }
978        let files = crate::go_project::discover_go_files(root)?;
979        if files.sources.is_empty() {
980            return Ok(None);
981        }
982        let ambient = std::env::vars().collect::<BTreeMap<_, _>>();
983        // Roots named for the JavaScript source may hold no Go at all.
984        let Ok(roots) = crate::source_discovery::ExplicitSourceRoots::from_environment_in(
985            root, workspace, &ambient,
986        ) else {
987            return Ok(None);
988        };
989        let source_roots = crate::source_discovery::configured_source_roots(&ambient);
990        let mut integrity_inputs = go_integrity_inputs(&files, command);
991        crate::source_discovery::fold_roots_into_configuration(
992            &mut integrity_inputs.execution_configuration,
993            source_roots.as_deref(),
994        );
995        let source_inputs =
996            crate::source_capture::capture(root, "go", integrity_inputs.assertion_paths())?;
997        let integrity = create_explicit_run_integrity(
998            root,
999            &integrity_inputs,
1000            &FrontendIntegrityInputs::embedded_go(),
1001        )
1002        .map_err(|error| error.to_string())?;
1003        let sources = instrument_sources(root, workspace, roots.as_ref())?;
1004        let widths = sources
1005            .project
1006            .decision_widths
1007            .iter()
1008            .map(u8::to_string)
1009            .collect::<Vec<_>>()
1010            .join(", ");
1011        let mut mains = 0;
1012        for (directory, package) in &sources.source_packages {
1013            if package != "main" {
1014                continue;
1015            }
1016            mains += 1;
1017            write(
1018                &workspace.join(directory).join(SERVER_FILE),
1019                &format!(
1020                    "// Code generated by Supercov. DO NOT EDIT.\n\npackage main\n\nimport {RUNTIME_ALIAS} \"{}\"\n\nfunc init() {{\n\t{RUNTIME_ALIAS}.Serve({}, []uint8{{{widths}}})\n}}\n",
1021                    sources.import_for(&format!("{directory}/x.go")),
1022                    sources.probe_count,
1023                ),
1024            )?;
1025        }
1026        if mains == 0 || sources.project.instrumented.is_empty() {
1027            return Ok(None);
1028        }
1029        let directory = root.join(".supercov/work").join(run_id).join("go");
1030        fs::create_dir_all(directory.join("evidence")).map_err(|error| error.to_string())?;
1031        Ok(Some(Self {
1032            project: sources.project,
1033            integrity,
1034            source_inputs,
1035            source_roots,
1036            directory,
1037        }))
1038    }
1039
1040    pub fn source_files(&self) -> usize {
1041        self.project.instrumented.len()
1042    }
1043
1044    /// What the suite's environment needs for a server it builds to write
1045    /// its windows.
1046    pub fn environment(&self) -> BTreeMap<String, String> {
1047        BTreeMap::from([(
1048            "SUPERCOV_GO_SERVER_EVIDENCE_DIR".into(),
1049            self.directory.join("evidence").display().to_string(),
1050        )])
1051    }
1052
1053    /// Publish what the server ran as a Go run of its own: each window
1054    /// credited, by time, to the one browser test whose span it falls in;
1055    /// what no single test's does is no test's. None when no instrumented
1056    /// server ran, or no window fell in a test.
1057    pub fn publish(
1058        self,
1059        root: &Path,
1060        _evidence_run_id: &str,
1061        metadata: RunMetadata,
1062        served: &[crate::served_tests::ServedTest],
1063        diagnostics: &mut dyn Write,
1064    ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
1065        crate::owned_server::publish(
1066            root,
1067            crate::owned_server::OwnedServer {
1068                language: "Go",
1069                declaration: go_declaration(),
1070                environment: "go",
1071                coverage_model: go_coverage_model(),
1072                manifest: &self.project.manifest,
1073                probes: &self.project.probes,
1074                widths: &self.project.decision_widths,
1075                integrity: &self.integrity,
1076                source_inputs: &self.source_inputs,
1077                source_roots: self.source_roots.clone(),
1078                directory: &self.directory,
1079                source_files: self.project.instrumented.len(),
1080            },
1081            metadata,
1082            served,
1083            diagnostics,
1084        )
1085    }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090    use super::*;
1091
1092    #[test]
1093    fn the_package_clause_is_found_past_whatever_precedes_it() {
1094        // A build tag, a licence header, a block comment: all of them sit
1095        // above the clause in real Go, and a reader that took the first line
1096        // would find none of them.
1097        assert_eq!(package_name("package main\n").as_deref(), Some("main"));
1098        assert_eq!(
1099            package_name("//go:build linux\n\n// Copyright.\npackage worker\n").as_deref(),
1100            Some("worker")
1101        );
1102        assert_eq!(
1103            package_name("/*\nA package comment\nspanning lines.\n*/\npackage doc\n").as_deref(),
1104            Some("doc")
1105        );
1106        assert_eq!(
1107            package_name("/* one line */ package inline\n").as_deref(),
1108            Some("inline")
1109        );
1110        // A trailing comment is not part of the name.
1111        assert_eq!(
1112            package_name("package api // the public surface\n").as_deref(),
1113            Some("api")
1114        );
1115        assert_eq!(package_name("import \"fmt\"\n"), None);
1116    }
1117
1118    #[test]
1119    fn caching_is_disabled_unless_the_author_already_chose() {
1120        // `go test` skips a package whose result it has cached, and a package
1121        // that does not run records nothing. A run that measured half a suite
1122        // without saying so would be worse than a slow one.
1123        let (command, forced) =
1124            command_with_fresh_results(&["go".into(), "test".into(), "./...".into()]);
1125        assert!(forced);
1126        assert_eq!(command, ["go", "test", "-count=1", "./..."]);
1127
1128        // An author who already said how many times to run means it.
1129        let (command, forced) =
1130            command_with_fresh_results(&["go".into(), "test".into(), "-count=3".into()]);
1131        assert!(!forced);
1132        assert_eq!(command, ["go", "test", "-count=3"]);
1133    }
1134
1135    #[test]
1136    fn each_package_gets_an_evidence_file_of_its_own() {
1137        // One test binary per package, each its own process: sharing a path
1138        // would mean the last one to finish overwrote everyone else.
1139        assert_ne!(evidence_name("internal/auth"), evidence_name("internal/db"));
1140        assert_eq!(evidence_name("."), "_.bin");
1141        assert!(!evidence_name("internal/auth").contains('/'));
1142    }
1143
1144    #[test]
1145    fn only_the_constraint_nothing_ever_defines_is_read_alone() {
1146        // `ignore` is not a GOOS, not a GOARCH, and nothing sets it, which is
1147        // why it is the convention for Go kept beside Go without being
1148        // compiled. Every other constraint needs a GOOS, a GOARCH and a tag
1149        // set to answer, and guessing at those would drop files a run does
1150        // measure.
1151        assert!(build_ignored("//go:build ignore\n\npackage p\n"));
1152        assert!(build_ignored("// +build ignore\n\npackage p\n"));
1153        assert!(build_ignored(
1154            "// Copyright.\n\n//go:build ignore\n\npackage p\n"
1155        ));
1156        // Constraints combine, and one of the terms is still the term.
1157        assert!(build_ignored("//go:build ignore || linux\n\npackage p\n"));
1158
1159        // Everything else is a question this cannot answer, so it does not.
1160        assert!(!build_ignored("//go:build linux\n\npackage p\n"));
1161        assert!(!build_ignored("//go:build !windows\n\npackage p\n"));
1162        assert!(!build_ignored("package p\n"));
1163        // A tag that merely starts with the word is a different tag.
1164        assert!(!build_ignored("//go:build ignored\n\npackage p\n"));
1165        assert!(!build_ignored("//go:build ignore_me\n\npackage p\n"));
1166        // And a constraint has to be above the package clause to be one. The
1167        // toolchain ignores the text below it, so reading it there would drop
1168        // a file the build takes.
1169        assert!(!build_ignored(
1170            "package p\n\n//go:build ignore\n\nfunc f() {}\n"
1171        ));
1172        assert!(!build_ignored(
1173            "package p\n\nvar doc = \"//go:build ignore\"\n"
1174        ));
1175    }
1176
1177    #[test]
1178    fn a_test_main_is_seen_in_a_file_nothing_could_parse() {
1179        // Only ever asked of a file that did not parse, which is exactly when
1180        // there is no tree to ask. Missing a real one declares a second
1181        // TestMain and the package stops compiling, so this errs towards
1182        // seeing one -- but not so far that any mention of the word counts.
1183        assert!(declares_test_main_textually(
1184            "package p\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n"
1185        ));
1186        // Spacing is the author's business.
1187        assert!(declares_test_main_textually(
1188            "package p\n\nfunc TestMain( m  *testing.M ) {}\n"
1189        ));
1190        assert!(declares_test_main_textually(
1191            "package p\n\nfunc TestMain(m *testing.M) { broken( }\n"
1192        ));
1193
1194        // `TestMainHelper` is an ordinary function whose name starts with the
1195        // word, exactly as `Testify` is not a test.
1196        assert!(!declares_test_main_textually(
1197            "package p\n\nfunc TestMainHelper(t *testing.T) {}\n"
1198        ));
1199        // A test that merely takes a *testing.T is not the package's harness.
1200        assert!(!declares_test_main_textually(
1201            "package p\n\nfunc TestMain(t *testing.T) {}\n"
1202        ));
1203        assert!(!declares_test_main_textually(
1204            "package p\n\n// func TestMain\n"
1205        ));
1206        assert!(!declares_test_main_textually("package p\n"));
1207    }
1208}