1use 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
52const RUNTIME_DIRECTORY: &str = "_supercov_runtime";
60
61const RUNTIME_SOURCE: &str = include_str!("../runtime-assets/go/supercov/supercov.go");
62
63const PROBE_FILE: &str = "supercov_probes.go";
65
66const 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 #[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 pub unattributed: usize,
100 pub source_files: usize,
101 pub unparseable_sources: usize,
104 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#[derive(Debug, Clone)]
119struct GoTestPackage {
120 directory: String,
123 evidence: PathBuf,
124 declared_in: BTreeMap<String, String>,
126 unattributed: BTreeSet<String>,
129}
130
131struct InstrumentedWorkspace {
132 project: PreparedGoProject,
133 packages: Vec<GoTestPackage>,
134 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
146fn 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 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 .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
181fn 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 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
235fn 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
245struct InstrumentedSources {
251 project: PreparedGoProject,
252 runtimes: Vec<(String, String)>,
253 probe_count: usize,
254 source_packages: BTreeMap<String, String>,
255}
256
257impl InstrumentedSources {
258 fn import_for(&self, relative: &str) -> &str {
260 runtime_for(&self.runtimes, relative)
261 }
262}
263
264fn 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 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 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 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 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 let internal = source_packages.get(&directory) == Some(&name);
401 if internal || harness_package.is_none() {
402 harness_package = Some(name);
403 }
404 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 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 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 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
477fn build_ignored(source: &str) -> bool {
487 let header = if source.starts_with("package ") {
491 ""
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 .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
514fn 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
528fn 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 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 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
569pub 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 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 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 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 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 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 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 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
946const SERVER_FILE: &str = "supercov_server.go";
949
950pub 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
964#[derive(Deserialize)]
966struct ServerWindow {
967 start: i64,
968 end: i64,
969 probes: Vec<u64>,
971 vectors: Vec<u64>,
973}
974
975impl GoServer {
976 pub fn prepare(
979 root: &Path,
980 workspace: &Path,
981 run_id: &str,
982 command: &[String],
983 ) -> Result<Option<Self>, String> {
984 if !workspace.join("go.mod").is_file()
985 && crate::go_project::workspace_modules(workspace).is_empty()
986 {
987 return Ok(None);
988 }
989 let files = crate::go_project::discover_go_files(root)?;
990 if files.sources.is_empty() {
991 return Ok(None);
992 }
993 let ambient = std::env::vars().collect::<BTreeMap<_, _>>();
994 let Ok(roots) = crate::source_discovery::ExplicitSourceRoots::from_environment_in(
996 root, workspace, &ambient,
997 ) else {
998 return Ok(None);
999 };
1000 let source_roots = crate::source_discovery::configured_source_roots(&ambient);
1001 let mut integrity_inputs = go_integrity_inputs(&files, command);
1002 crate::source_discovery::fold_roots_into_configuration(
1003 &mut integrity_inputs.execution_configuration,
1004 source_roots.as_deref(),
1005 );
1006 let source_inputs =
1007 crate::source_capture::capture(root, "go", integrity_inputs.assertion_paths())?;
1008 let integrity = create_explicit_run_integrity(
1009 root,
1010 &integrity_inputs,
1011 &FrontendIntegrityInputs::embedded_go(),
1012 )
1013 .map_err(|error| error.to_string())?;
1014 let sources = instrument_sources(root, workspace, roots.as_ref())?;
1015 let widths = sources
1016 .project
1017 .decision_widths
1018 .iter()
1019 .map(u8::to_string)
1020 .collect::<Vec<_>>()
1021 .join(", ");
1022 let mut mains = 0;
1023 for (directory, package) in &sources.source_packages {
1024 if package != "main" {
1025 continue;
1026 }
1027 mains += 1;
1028 write(
1029 &workspace.join(directory).join(SERVER_FILE),
1030 &format!(
1031 "// 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",
1032 sources.import_for(&format!("{directory}/x.go")),
1033 sources.probe_count,
1034 ),
1035 )?;
1036 }
1037 if mains == 0 || sources.project.instrumented.is_empty() {
1038 return Ok(None);
1039 }
1040 let directory = root.join(".supercov/work").join(run_id).join("go");
1041 fs::create_dir_all(directory.join("evidence")).map_err(|error| error.to_string())?;
1042 Ok(Some(Self {
1043 project: sources.project,
1044 integrity,
1045 source_inputs,
1046 source_roots,
1047 directory,
1048 }))
1049 }
1050
1051 pub fn source_files(&self) -> usize {
1052 self.project.instrumented.len()
1053 }
1054
1055 pub fn environment(&self) -> BTreeMap<String, String> {
1058 BTreeMap::from([(
1059 "SUPERCOV_GO_SERVER_EVIDENCE_DIR".into(),
1060 self.directory.join("evidence").display().to_string(),
1061 )])
1062 }
1063
1064 pub fn publish(
1069 self,
1070 root: &Path,
1071 _evidence_run_id: &str,
1072 metadata: RunMetadata,
1073 served: &[crate::served_tests::ServedTest],
1074 diagnostics: &mut dyn Write,
1075 ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
1076 let evidence_directory = self.directory.join("evidence");
1077 let result = (|| {
1078 let mut windows = Vec::new();
1079 let mut files = fs::read_dir(&evidence_directory)
1080 .map(|entries| {
1081 entries
1082 .filter_map(Result::ok)
1083 .map(|entry| entry.path())
1084 .filter(|path| {
1085 path.extension()
1086 .is_some_and(|extension| extension == "windows")
1087 })
1088 .collect::<Vec<_>>()
1089 })
1090 .unwrap_or_default();
1091 files.sort();
1092 for file in &files {
1093 let text = fs::read_to_string(file)
1094 .map_err(|error| format!("{}: {error}", file.display()))?;
1095 windows.extend(
1098 text.lines()
1099 .filter_map(|line| serde_json::from_str::<ServerWindow>(line).ok()),
1100 );
1101 }
1102 if served.is_empty() || windows.is_empty() {
1103 return Ok(None);
1104 }
1105 let probe_count = self
1106 .project
1107 .probes
1108 .keys()
1109 .max()
1110 .map_or(0, |highest| *highest as usize + 1);
1111 let widths = self.project.decision_widths.clone();
1112 let mut global = vec![0_u32; probe_count];
1113 let mut decision_vectors = vec![Vec::<u64>::new(); widths.len()];
1114 let mut by_test = BTreeMap::<usize, crate::owned_evidence::OwnedTestEvidence>::new();
1115 for window in &windows {
1116 let mut running = served.iter().enumerate().filter(|(_, test)| {
1118 test.window
1119 .is_some_and(|(start, end)| start <= window.end && window.start <= end)
1120 });
1121 let owner = match (running.next(), running.next()) {
1122 (Some((index, _)), None) => Some(index),
1123 _ => None,
1124 };
1125 for pair in window.probes.chunks_exact(2) {
1126 let (index, mask) = (pair[0] as usize, pair[1] as u32);
1127 if let Some(total) = global.get_mut(index) {
1128 *total |= mask;
1129 }
1130 if let Some(owner) = owner {
1131 *by_test
1132 .entry(owner)
1133 .or_default()
1134 .probes
1135 .entry(index as u32)
1136 .or_default() |= mask;
1137 }
1138 }
1139 for pair in window.vectors.chunks_exact(2) {
1140 let (decision, key) = (pair[0] as u32, pair[1]);
1141 if let Some(seen) = decision_vectors.get_mut(decision as usize)
1142 && !seen.contains(&key)
1143 {
1144 seen.push(key);
1145 }
1146 if let Some(owner) = owner {
1147 let test = by_test.entry(owner).or_default();
1148 let vector = crate::owned_evidence::PackedVector { decision, key };
1149 if !test.vectors.contains(&vector) {
1150 test.vectors.push(vector);
1151 }
1152 }
1153 }
1154 }
1155 let credited = by_test.keys().copied().collect::<BTreeSet<_>>();
1156 let package = |test: &crate::served_tests::ServedTest| test.worker.clone();
1157 let mut tests = Vec::new();
1158 let mut outcomes = Vec::new();
1159 for (index, test) in served.iter().enumerate() {
1160 let status = match test.status.as_str() {
1161 "passed" => "passed",
1162 "skipped" => "skipped",
1163 _ => "failed",
1164 };
1165 let mut evidence = by_test.remove(&index).unwrap_or_default();
1166 evidence.name = test.name.clone();
1167 evidence.status = status.into();
1168 evidence.runner = test.provenance.runner.clone();
1169 tests.push(evidence);
1170 outcomes.push(OwnedTestOutcome {
1171 name: test.name.clone(),
1172 runner: test.provenance.runner.clone(),
1173 package: package(test),
1174 file: Some(test.file.clone()),
1175 status: status.into(),
1176 attributed: true,
1177 });
1178 }
1179 let evidence = crate::owned_evidence::OwnedEvidence {
1180 global,
1181 tests,
1182 widths,
1183 decision_vectors,
1184 };
1185 let mut declaration = go_declaration();
1186 for runner in &mut declaration.runners {
1187 runner.runner = "playwright".into();
1188 runner.execution_model =
1189 supercov_contracts::ExecutionModel::ParallelContextPropagated;
1190 }
1191 let mut run = build_frontend_run(OwnedRunInputs {
1192 declaration,
1193 environment: "go",
1194 manifest: &self.project.manifest,
1195 probes: &self.project.probes,
1196 evidence: &evidence,
1197 outcomes: &outcomes,
1198 run_id: &metadata.id,
1199 generated_at: &metadata.started_at,
1200 test_exit_code: metadata.test_exit_code.unwrap_or_default(),
1201 coverage_model: go_coverage_model(),
1202 })
1203 .map_err(|error| error.to_string())?;
1204 for (raw, (index, test)) in run
1205 .request
1206 .raw_results
1207 .iter_mut()
1208 .zip(served.iter().enumerate())
1209 {
1210 raw.title = Some(test.title.clone());
1211 raw.expected_status = Some(test.expected_status.clone());
1212 raw.status = Some(test.status.clone());
1213 raw.provenance = crate::coverage_report::TestProvenance {
1214 source: raw.provenance.source.clone(),
1215 ..test.provenance.clone()
1216 };
1217 if credited.contains(&index) {
1218 raw.attribution = crate::coverage_report::ATTRIBUTION_BY_TIME.into();
1219 }
1220 }
1221 validate_frontend_report_request(&run.declaration, &run.request)
1222 .map_err(|error| error.to_string())?;
1223 let archive_path = root
1224 .join(".supercov/work")
1225 .join(&metadata.id)
1226 .join("go-evidence.raw.gz");
1227 if let Some(parent) = archive_path.parent() {
1228 fs::create_dir_all(parent).map_err(|error| error.to_string())?;
1229 }
1230 let raw = write_archive(
1231 crate::source_capture::append(
1232 run.archive_entries().map_err(|error| error.to_string())?,
1233 &self.source_inputs,
1234 )?,
1235 &archive_path,
1236 )
1237 .map_err(|error| error.to_string())?;
1238 let metadata = RunMetadata {
1239 integrity: self.integrity.clone(),
1240 raw_evidence: RawEvidenceMetadata {
1241 schema_version: raw.schema_version,
1242 format: raw.format.into(),
1243 file: raw.file.into(),
1244 files: raw.files,
1245 uncompressed_bytes: raw.uncompressed_bytes,
1246 compressed_bytes: raw.compressed_bytes,
1247 },
1248 source_roots: self.source_roots.clone(),
1249 ..metadata
1250 };
1251 let run_directory =
1252 publish_run(root, &metadata, &archive_path).map_err(|error| error.to_string())?;
1253 writeln!(
1254 diagnostics,
1255 "[supercov] Go server coverage: {} test(s) across {} source file(s), credited by time",
1256 served.len(),
1257 self.project.instrumented.len()
1258 )
1259 .map_err(|error| error.to_string())?;
1260 Ok(Some((run_directory, metadata)))
1261 })();
1262 let result = result.map_err(|error| {
1263 note_kept_evidence(
1264 root,
1265 &evidence_directory,
1266 &format!("{}-go", metadata_id(&self.directory)),
1267 error,
1268 )
1269 });
1270 let _ = remove_stored_tree_deferred(root, &self.directory);
1271 result
1272 }
1273}
1274
1275fn metadata_id(directory: &Path) -> String {
1277 directory
1278 .parent()
1279 .and_then(Path::file_name)
1280 .map(|name| name.to_string_lossy().into_owned())
1281 .unwrap_or_default()
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286 use super::*;
1287
1288 #[test]
1289 fn the_package_clause_is_found_past_whatever_precedes_it() {
1290 assert_eq!(package_name("package main\n").as_deref(), Some("main"));
1294 assert_eq!(
1295 package_name("//go:build linux\n\n// Copyright.\npackage worker\n").as_deref(),
1296 Some("worker")
1297 );
1298 assert_eq!(
1299 package_name("/*\nA package comment\nspanning lines.\n*/\npackage doc\n").as_deref(),
1300 Some("doc")
1301 );
1302 assert_eq!(
1303 package_name("/* one line */ package inline\n").as_deref(),
1304 Some("inline")
1305 );
1306 assert_eq!(
1308 package_name("package api // the public surface\n").as_deref(),
1309 Some("api")
1310 );
1311 assert_eq!(package_name("import \"fmt\"\n"), None);
1312 }
1313
1314 #[test]
1315 fn caching_is_disabled_unless_the_author_already_chose() {
1316 let (command, forced) =
1320 command_with_fresh_results(&["go".into(), "test".into(), "./...".into()]);
1321 assert!(forced);
1322 assert_eq!(command, ["go", "test", "-count=1", "./..."]);
1323
1324 let (command, forced) =
1326 command_with_fresh_results(&["go".into(), "test".into(), "-count=3".into()]);
1327 assert!(!forced);
1328 assert_eq!(command, ["go", "test", "-count=3"]);
1329 }
1330
1331 #[test]
1332 fn each_package_gets_an_evidence_file_of_its_own() {
1333 assert_ne!(evidence_name("internal/auth"), evidence_name("internal/db"));
1336 assert_eq!(evidence_name("."), "_.bin");
1337 assert!(!evidence_name("internal/auth").contains('/'));
1338 }
1339
1340 #[test]
1341 fn only_the_constraint_nothing_ever_defines_is_read_alone() {
1342 assert!(build_ignored("//go:build ignore\n\npackage p\n"));
1348 assert!(build_ignored("// +build ignore\n\npackage p\n"));
1349 assert!(build_ignored(
1350 "// Copyright.\n\n//go:build ignore\n\npackage p\n"
1351 ));
1352 assert!(build_ignored("//go:build ignore || linux\n\npackage p\n"));
1354
1355 assert!(!build_ignored("//go:build linux\n\npackage p\n"));
1357 assert!(!build_ignored("//go:build !windows\n\npackage p\n"));
1358 assert!(!build_ignored("package p\n"));
1359 assert!(!build_ignored("//go:build ignored\n\npackage p\n"));
1361 assert!(!build_ignored("//go:build ignore_me\n\npackage p\n"));
1362 assert!(!build_ignored(
1366 "package p\n\n//go:build ignore\n\nfunc f() {}\n"
1367 ));
1368 assert!(!build_ignored(
1369 "package p\n\nvar doc = \"//go:build ignore\"\n"
1370 ));
1371 }
1372
1373 #[test]
1374 fn a_test_main_is_seen_in_a_file_nothing_could_parse() {
1375 assert!(declares_test_main_textually(
1380 "package p\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n"
1381 ));
1382 assert!(declares_test_main_textually(
1384 "package p\n\nfunc TestMain( m *testing.M ) {}\n"
1385 ));
1386 assert!(declares_test_main_textually(
1387 "package p\n\nfunc TestMain(m *testing.M) { broken( }\n"
1388 ));
1389
1390 assert!(!declares_test_main_textually(
1393 "package p\n\nfunc TestMainHelper(t *testing.T) {}\n"
1394 ));
1395 assert!(!declares_test_main_textually(
1397 "package p\n\nfunc TestMain(t *testing.T) {}\n"
1398 ));
1399 assert!(!declares_test_main_textually(
1400 "package p\n\n// func TestMain\n"
1401 ));
1402 assert!(!declares_test_main_textually("package p\n"));
1403 }
1404}