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");
62const RUNTIME_VALUES_SOURCE: &str = include_str!("../runtime-assets/go/supercov/values.go");
65
66const PROBE_FILE: &str = "supercov_probes.go";
68
69const 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 #[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 pub unattributed: usize,
103 pub source_files: usize,
104 pub unparseable_sources: usize,
107 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#[derive(Debug, Clone)]
122struct GoTestPackage {
123 directory: String,
126 evidence: PathBuf,
127 declared_in: BTreeMap<String, String>,
129 unattributed: BTreeSet<String>,
132}
133
134struct InstrumentedWorkspace {
135 project: PreparedGoProject,
136 packages: Vec<GoTestPackage>,
137 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
149fn 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 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 .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
184fn 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 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
238fn 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
248struct InstrumentedSources {
254 project: PreparedGoProject,
255 runtimes: Vec<(String, String)>,
256 probe_count: usize,
257 source_packages: BTreeMap<String, String>,
258}
259
260impl InstrumentedSources {
261 fn import_for(&self, relative: &str) -> &str {
263 runtime_for(&self.runtimes, relative)
264 }
265}
266
267fn 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 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 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 sources.probe_count = sources
328 .project
329 .probes
330 .keys()
331 .max()
332 .map_or(0, |highest| *highest as usize + 1);
333
334 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 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 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 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 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 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 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
523fn 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
537fn 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 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 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
578pub 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 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 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 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 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 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 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 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
955const SERVER_FILE: &str = "supercov_server.go";
958
959pub 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 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 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 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 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 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 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 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 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 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 assert!(declares_test_main_textually(
1160 "package p\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n"
1161 ));
1162 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 assert!(!declares_test_main_textually(
1173 "package p\n\nfunc TestMainHelper(t *testing.T) {}\n"
1174 ));
1175 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}