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