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(str::to_owned);
170 }
171 }
172 None
173}
174
175fn read(path: &Path) -> Result<String, String> {
176 fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))
177}
178
179fn named_for_the_project(project: &Path, relative: &str, text: &str) -> String {
190 let Ok(tree) = crate::go_instrumenter::parse(text) else {
191 return text.to_owned();
192 };
193 let mut cursor = tree.root_node().walk();
194 let Some(package) = tree
195 .root_node()
196 .children(&mut cursor)
197 .find(|child| child.kind() == "package_clause")
198 else {
199 return text.to_owned();
200 };
201 let at = package.start_byte();
202 let line = text[..at].rfind('\n').map_or(0, |end| end + 1);
203 if text[line..at].starts_with(LINE_DIRECTIVE) {
204 return text.to_owned();
205 }
206 let position = package.start_position();
207 let named = relative
212 .split('/')
213 .fold(project.to_path_buf(), |path, part| path.join(part));
214 format!(
215 "{}{LINE_DIRECTIVE}{}:{}:{}*/{}",
216 &text[..at],
217 named.display(),
218 position.row + 1,
219 position.column + 1,
220 &text[at..]
221 )
222}
223
224const LINE_DIRECTIVE: &str = "/*line ";
225
226fn write(path: &Path, contents: &str) -> Result<(), String> {
227 if let Some(parent) = path.parent() {
228 fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?;
229 }
230 fs::write(path, contents).map_err(|error| format!("{}: {error}", path.display()))
231}
232
233fn evidence_name(directory: &str) -> String {
236 let slug = directory
237 .chars()
238 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
239 .collect::<String>();
240 format!("{slug}.bin")
241}
242
243fn instrument_workspace(
246 project_root: &Path,
247 workspace: &Path,
248 evidence_directory: &Path,
249 serialised: bool,
250 roots: Option<&crate::source_discovery::ExplicitSourceRoots>,
251) -> Result<InstrumentedWorkspace, String> {
252 let mut project = prepare_go_project(workspace, roots)?;
253 let mut unparseable_tests: Vec<(String, String)> = Vec::new();
254
255 let members = crate::go_project::workspace_modules(workspace);
259 let directories = if members.is_empty() {
260 vec![".".to_owned()]
261 } else {
262 members.into_iter().collect::<Vec<_>>()
263 };
264 let mut runtimes: Vec<(String, String)> = Vec::new();
265 for directory in &directories {
266 let at = workspace.join(directory);
267 let module = module_path(&at).ok_or_else(|| {
268 format!(
269 "{}: no module path in go.mod, so Supercov cannot name the package its runtime is imported from",
270 at.display()
271 )
272 })?;
273 write(
274 &at.join(RUNTIME_DIRECTORY).join("supercov.go"),
275 RUNTIME_SOURCE,
276 )?;
277 runtimes.push((directory.clone(), format!("{module}/{RUNTIME_DIRECTORY}")));
278 }
279 runtimes.sort_by_key(|(directory, _)| std::cmp::Reverse(directory.len()));
281 let import_for = |relative: &str| -> &str {
282 runtimes
283 .iter()
284 .find(|(directory, _)| {
285 directory == "."
286 || relative == directory
287 || relative.starts_with(&format!("{directory}/"))
288 })
289 .map(|(_, import)| import.as_str())
290 .unwrap_or_default()
291 };
292
293 let probe_count = project
297 .probes
298 .keys()
299 .max()
300 .map_or(0, |highest| *highest as usize + 1);
301
302 let mut source_packages: BTreeMap<String, String> = BTreeMap::new();
303 for (relative, instrumented) in &project.instrumented {
304 let instrumented = instrumented.replace(RUNTIME_IMPORT, import_for(relative));
305 let path = workspace.join(relative);
306 if let Some(name) = package_name(&instrumented) {
307 source_packages
308 .entry(directory_of(relative))
309 .or_insert(name);
310 }
311 write(
312 &path,
313 &named_for_the_project(project_root, relative, &instrumented),
314 )?;
315 }
316 for (directory, package) in &source_packages {
317 write(
318 &workspace.join(directory).join(PROBE_FILE),
319 &probe_array_file(
320 package,
321 RUNTIME_ALIAS,
322 import_for(&format!("{directory}/x.go")),
323 probe_count,
324 ),
325 )?;
326 }
327
328 let mut by_directory: BTreeMap<String, Vec<&String>> = BTreeMap::new();
329 for relative in &project.files.tests {
330 by_directory
331 .entry(directory_of(relative))
332 .or_default()
333 .push(relative);
334 }
335
336 let mut packages = Vec::new();
337 for (directory, tests) in by_directory {
338 let evidence = evidence_directory.join(evidence_name(&directory));
339 let evidence_literal = evidence.to_string_lossy().replace('\\', "\\\\");
340 let mut declared_in = BTreeMap::new();
341 let mut unattributed = BTreeSet::new();
342 let mut declares_test_main = false;
343 let mut harness_package: Option<String> = None;
347 for relative in tests {
348 let path = workspace.join(relative);
349 let source = read(&path)?;
350 let Some(name) = package_name(&source) else {
351 continue;
352 };
353 let internal = source_packages.get(&directory) == Some(&name);
357 if internal || harness_package.is_none() {
358 harness_package = Some(name);
359 }
360 let built = !build_ignored(&source);
365 let file =
366 match instrument_test_file(&source, RUNTIME_ALIAS, &evidence_literal, serialised) {
367 Ok(file) => file,
368 Err(error) => {
369 project.manifest.limitations.push(
377 crate::go_project::unparseable_limitation(relative, &error.to_string()),
378 );
379 unparseable_tests.push((relative.clone(), error.to_string()));
380 declares_test_main |= built && declares_test_main_textually(&source);
386 continue;
387 }
388 };
389 declares_test_main |= built && file.declares_test_main;
390 for test in &file.tests {
391 declared_in.insert(test.clone(), relative.clone());
392 }
393 unattributed.extend(file.unattributed.iter().cloned());
394 write(
396 &path,
397 &named_for_the_project(
398 project_root,
399 relative,
400 &rewrite(&source, &file.edits).replace(RUNTIME_IMPORT, import_for(relative)),
401 ),
402 )?;
403 }
404 let Some(package) = harness_package else {
405 continue;
406 };
407 write(
408 &workspace.join(&directory).join(HARNESS_FILE),
409 &synthesized_harness(
410 &package,
411 RUNTIME_ALIAS,
412 import_for(&format!("{directory}/x.go")),
413 probe_count,
414 &project.decision_widths,
415 &evidence_literal,
416 declares_test_main,
417 ),
418 )?;
419 packages.push(GoTestPackage {
420 directory,
421 evidence,
422 declared_in,
423 unattributed,
424 });
425 }
426 Ok(InstrumentedWorkspace {
427 project,
428 packages,
429 unparseable_tests,
430 })
431}
432
433fn build_ignored(source: &str) -> bool {
443 let header = if source.starts_with("package ") {
447 ""
450 } else {
451 source
452 .find("\npackage ")
453 .map_or(source, |clause| &source[..clause])
454 };
455 header
456 .lines()
457 .map(str::trim)
458 .filter_map(|line| {
459 line.strip_prefix("//go:build")
460 .or_else(|| line.strip_prefix("// +build"))
462 })
463 .any(|constraint| {
464 constraint
465 .split(|c: char| !(c.is_alphanumeric() || c == '_'))
466 .any(|term| term == "ignore")
467 })
468}
469
470fn declares_test_main_textually(source: &str) -> bool {
477 source.split("func TestMain").skip(1).any(|rest| {
478 rest.trim_start()
479 .strip_prefix('(')
480 .is_some_and(|arguments| arguments.contains("testing.M"))
481 })
482}
483
484fn command_run_in_order(command: &[String]) -> (Vec<String>, bool) {
491 if command
492 .iter()
493 .any(|argument| argument == "-parallel" || argument.starts_with("-parallel="))
494 {
495 return (command.to_vec(), false);
498 }
499 let mut updated = command.to_vec();
500 let position = updated
501 .iter()
502 .position(|argument| argument == "test")
503 .map_or(updated.len(), |index| index + 1);
504 updated.insert(position, "-parallel=1".into());
505 (updated, true)
506}
507
508fn command_with_fresh_results(command: &[String]) -> (Vec<String>, bool) {
509 if command
510 .iter()
511 .any(|argument| argument == "-count" || argument.starts_with("-count="))
512 {
513 return (command.to_vec(), false);
514 }
515 let mut updated = command.to_vec();
516 let position = updated
518 .iter()
519 .position(|argument| argument == "test")
520 .map_or(updated.len(), |index| index + 1);
521 updated.insert(position, "-count=1".into());
522 (updated, true)
523}
524
525pub fn current_go_integrity(
527 root: &Path,
528 command: &[String],
529 source_roots: Option<&[String]>,
530) -> Result<crate::run_store::RunIntegrity, String> {
531 let root = canonicalize_simplified(root).map_err(|error| error.to_string())?;
532 let files = crate::go_project::discover_go_files(&root)?;
533 let mut inputs = go_integrity_inputs(&files, command);
534 crate::source_discovery::fold_roots_into_configuration(
535 &mut inputs.execution_configuration,
536 source_roots,
537 );
538 create_explicit_run_integrity(&root, &inputs, &FrontendIntegrityInputs::embedded_go())
539 .map_err(|error| error.to_string())
540}
541
542pub fn run_direct_go(
543 request: &DirectGoRunRequest,
544 diagnostics: &mut dyn Write,
545) -> Result<DirectGoRunResult, String> {
546 if request.command.is_empty() {
547 return Err("test command must not be empty".into());
548 }
549 let total_started = Instant::now();
550 let initialization_started = Instant::now();
551 let root = canonicalize_simplified(&request.root)
552 .map_err(|error| format!("{}: {error}", request.root.display()))?;
553 let mut lock = ProjectLock::acquire(&root, &request.run_id, &request.started_at)
554 .map_err(|error| error.to_string())?;
555 let initialization_ms = elapsed_ms(initialization_started);
556 let work_directory = root.join(".supercov/work").join(&request.run_id);
557 let measured = std::cell::Cell::new(false);
561 let result = (|| {
562 let recovered_runs = recover_abandoned_runs(&root, &request.started_at)
563 .map_err(|error| error.to_string())?;
564 if !recovered_runs.is_empty() {
565 writeln!(
566 diagnostics,
567 "[supercov] recovered abandoned run(s): {}",
568 recovered_runs.join(", ")
569 )
570 .map_err(|error| error.to_string())?;
571 }
572
573 let adapter_started = Instant::now();
574 let files = crate::go_project::discover_go_files(&root)?;
575 let source_roots = crate::source_discovery::configured_source_roots(
576 &std::env::vars().collect::<std::collections::BTreeMap<_, _>>(),
577 );
578 let mut integrity_inputs = go_integrity_inputs(&files, &request.command);
579 crate::source_discovery::fold_roots_into_configuration(
580 &mut integrity_inputs.execution_configuration,
581 source_roots.as_deref(),
582 );
583 let source_inputs =
584 crate::source_capture::capture(&root, "go", integrity_inputs.assertion_paths())?;
585 let integrity = create_explicit_run_integrity(
586 &root,
587 &integrity_inputs,
588 &FrontendIntegrityInputs::embedded_go(),
589 )
590 .map_err(|error| error.to_string())?;
591
592 let workspace_started = Instant::now();
593 recover_cached_workspace(&root, &lock).map_err(|error| error.to_string())?;
594 let workspace =
595 prepare_cached_workspace(&root, &lock, &[]).map_err(|error| error.to_string())?;
596 let evidence_directory = work_directory.join("go/evidence");
597 fs::create_dir_all(&evidence_directory).map_err(|error| error.to_string())?;
598 let (command, serialised) = if request.exact_attribution {
601 command_run_in_order(&request.command)
602 } else {
603 (request.command.clone(), false)
604 };
605 let ambient = std::env::vars().collect::<std::collections::BTreeMap<_, _>>();
606 let roots = crate::source_discovery::ExplicitSourceRoots::from_environment_in(
607 &root, &workspace, &ambient,
608 )
609 .map_err(|error| error.to_string())?;
610 let instrumented = instrument_workspace(
611 &root,
612 &workspace,
613 &evidence_directory,
614 serialised,
615 roots.as_ref(),
616 )?;
617 let workspace_preparation_ms = elapsed_ms(workspace_started);
618 let adapter_setup_ms = (elapsed_ms(adapter_started) - workspace_preparation_ms).max(0.0);
619 writeln!(
620 diagnostics,
621 "[supercov] detected Go; instrumenting {} source file(s) across {} test package(s) in isolated workspace {}",
622 instrumented.project.instrumented.len(),
623 instrumented.packages.len(),
624 workspace.display()
625 )
626 .map_err(|error| error.to_string())?;
627 for (file, reason) in &instrumented.project.unparseable {
628 writeln!(
629 diagnostics,
630 "[supercov] could not parse {file}: {reason}; it carries no obligations"
631 )
632 .map_err(|error| error.to_string())?;
633 }
634 for (file, reason) in &instrumented.unparseable_tests {
635 writeln!(
636 diagnostics,
637 "[supercov] could not parse {file}: {reason}; the tests it declares run unattributed and what they reach counts run-wide"
638 )
639 .map_err(|error| error.to_string())?;
640 }
641 if instrumented.packages.is_empty() {
642 return Err(
643 "no Go test packages were found, so a run would measure nothing: Supercov needs at least one _test.go file"
644 .into(),
645 );
646 }
647 if instrumented.project.instrumented.is_empty()
654 && !instrumented.project.files.sources.is_empty()
655 {
656 return Err(format!(
657 "none of the {} Go source file(s) could be parsed, so the run would measure a denominator of nothing: {}",
658 instrumented.project.files.sources.len(),
659 instrumented
660 .project
661 .unparseable
662 .iter()
663 .map(|(file, reason)| format!("{file}: {reason}"))
664 .collect::<Vec<_>>()
665 .join("; ")
666 ));
667 }
668
669 if serialised {
670 writeln!(
671 diagnostics,
672 "[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."
673 )
674 .map_err(|error| error.to_string())?;
675 }
676 let (command, forced_fresh) = command_with_fresh_results(&command);
677 if forced_fresh {
678 writeln!(
679 diagnostics,
680 "[supercov] added -count=1: `go test` caches passing packages, and a cached package does not run, so it would record no coverage"
681 )
682 .map_err(|error| error.to_string())?;
683 }
684 let test_started = Instant::now();
685 let plan = ExecutionPlan {
686 preparation: Vec::new(),
687 test: ExecutionPhase {
688 name: "test".into(),
689 kind: PhaseKind::Test,
690 command: CommandSpec {
691 program: command[0].clone().into(),
692 arguments: command[1..].iter().map(OsString::from).collect(),
693 cwd: workspace.clone(),
694 environment: None,
695 captured_output: None,
696 console: Some(crate::workspace::console_names(&root, &workspace)),
697 },
698 },
699 };
700 let options = SupervisionOptions::from_environment().map_err(|error| error.to_string())?;
701 let before = crate::workspace::workspace_output_baseline(&workspace)
704 .map_err(|error| error.to_string())?;
705 let execution = execute_plan(&plan, options, diagnostics, |_, _| Ok(()))
706 .map_err(|error| error.to_string())?;
707 let test_command_ms = elapsed_ms(test_started);
708 let rewritten = instrumented
713 .project
714 .files
715 .sources
716 .iter()
717 .chain(&instrumented.project.files.tests)
718 .map(PathBuf::from)
719 .collect::<BTreeSet<_>>();
720 let outputs = crate::workspace::sync_outputs_of(
721 crate::workspace::CommandOutputs::Go,
722 &root,
723 &workspace,
724 &before,
725 &rewritten,
726 )
727 .map_err(|error| error.to_string())?;
728 for line in crate::workspace::describe_command_outputs(&outputs) {
729 writeln!(diagnostics, "{line}").map_err(|error| error.to_string())?;
730 }
731 if let Some(signal) = execution.interrupted_signal {
732 return Err(format!(
733 "the test command was interrupted by {signal:?}; no run was published"
734 ));
735 }
736 let exit_code = execution.exit_code;
737
738 let publication_started = Instant::now();
739 let mut parts = Vec::new();
740 let mut outcomes = Vec::new();
741 let mut silent = Vec::new();
742 for package in &instrumented.packages {
743 let Ok(bytes) = fs::read(&package.evidence) else {
744 silent.push(package.directory.clone());
745 continue;
746 };
747 let evidence = read_evidence(&bytes)
748 .map_err(|error| format!("{}: {error}", package.evidence.display()))?;
749 for test in &evidence.tests {
750 outcomes.push(OwnedTestOutcome {
751 name: test.name.clone(),
752 runner: test.runner.clone(),
753 package: package.directory.clone(),
754 file: package.declared_in.get(&test.name).cloned(),
755 status: test.status.clone(),
756 attributed: !test.unattributed,
757 });
758 }
759 parts.push(evidence);
760 }
761 if !silent.is_empty() {
762 writeln!(
763 diagnostics,
764 "[supercov] {} test package(s) wrote no evidence and are absent from this run: {}",
765 silent.len(),
766 silent.join(", ")
767 )
768 .map_err(|error| error.to_string())?;
769 }
770 let evidence = merge_evidence(parts);
771 let unattributed = instrumented
772 .packages
773 .iter()
774 .map(|package| package.unattributed.len())
775 .sum::<usize>();
776 let reached_something = evidence.global.iter().any(|total| *total != 0);
783 if outcomes.is_empty() && !reached_something {
784 let because = if unattributed > 0 {
785 format!(
786 "; {unattributed} test(s) ran without being able to announce themselves (t.Parallel, Example or Fuzz) and reached nothing measured either"
787 )
788 } else {
789 String::new()
790 };
791 return Err(format!(
792 "no Go test recorded evidence (the command exited {exit_code}); a run that measured nothing is not published{because}"
793 ));
794 }
795 measured.set(true);
796 let run = build_frontend_run(OwnedRunInputs {
797 declaration: go_declaration(),
798 environment: "go",
799 manifest: &instrumented.project.manifest,
800 probes: &instrumented.project.probes,
801 evidence: &evidence,
802 outcomes: &outcomes,
803 run_id: &request.run_id,
804 generated_at: &request.started_at,
805 test_exit_code: exit_code,
806 coverage_model: go_coverage_model(),
807 })
808 .map_err(|error| error.to_string())?;
809 validate_frontend_report_request(&run.declaration, &run.request)
813 .map_err(|error| error.to_string())?;
814 let archive_path = work_directory.join("evidence.raw.gz");
815 let raw = write_archive(
816 crate::source_capture::append(
817 run.archive_entries().map_err(|error| error.to_string())?,
818 &source_inputs,
819 )?,
820 &archive_path,
821 )
822 .map_err(|error| error.to_string())?;
823 let evidence_publication_ms = elapsed_ms(publication_started);
824
825 let timings = RunTimings {
826 initialization_ms,
827 workspace_preparation_ms,
828 adapter_setup_ms,
829 instrumented_build_ms: 0.0,
830 test_command_ms,
831 evidence_publication_ms,
832 };
833 let metadata = RunMetadata {
834 id: request.run_id.clone(),
835 started_at: request.started_at.clone(),
836 duration_ms: elapsed_ms(total_started),
837 command: request.command.clone(),
838 test_exit_code: Some(exit_code),
839 integrity,
840 raw_evidence: RawEvidenceMetadata {
841 schema_version: raw.schema_version,
842 format: raw.format.into(),
843 file: raw.file.into(),
844 files: raw.files,
845 uncompressed_bytes: raw.uncompressed_bytes,
846 compressed_bytes: raw.compressed_bytes,
847 },
848 isolated_build: Some(true),
849 instrumented_build_cache: None,
850 timings: Some(timings),
851 merged: None,
852 parents: None,
853 source_roots: source_roots.clone(),
854 };
855 let run_directory =
856 publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
857 finalize_published_run(&root, &request.run_id).map_err(|error| error.to_string())?;
858 Ok(DirectGoRunResult {
859 run_id: request.run_id.clone(),
860 run_directory,
861 exit_code,
862 tests: outcomes
863 .iter()
864 .map(|outcome| outcome.name.as_str())
865 .collect::<BTreeSet<_>>()
866 .len(),
867 unattributed,
868 source_files: instrumented.project.instrumented.len(),
869 unparseable_sources: instrumented.project.unparseable.len(),
870 unparseable_tests: instrumented.unparseable_tests.len(),
871 packages: instrumented.packages.len(),
872 recovered_runs,
873 metadata,
874 })
875 })();
876 let result = result.map_err(|error| {
877 if !measured.get() {
878 return error;
879 }
880 note_kept_evidence(
881 &root,
882 &work_directory.join("go/evidence"),
883 &request.run_id,
884 error,
885 )
886 });
887 if result.is_err() {
888 let _ = remove_stored_tree_deferred(&root, &work_directory);
889 }
890 let release = lock.release().map_err(|error| error.to_string());
891 match (result, release) {
892 (Ok(result), Ok(())) => Ok(result),
893 (Err(error), _) => Err(error),
894 (Ok(_), Err(error)) => Err(error),
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 #[test]
903 fn the_package_clause_is_found_past_whatever_precedes_it() {
904 assert_eq!(package_name("package main\n").as_deref(), Some("main"));
908 assert_eq!(
909 package_name("//go:build linux\n\n// Copyright.\npackage worker\n").as_deref(),
910 Some("worker")
911 );
912 assert_eq!(
913 package_name("/*\nA package comment\nspanning lines.\n*/\npackage doc\n").as_deref(),
914 Some("doc")
915 );
916 assert_eq!(
917 package_name("/* one line */ package inline\n").as_deref(),
918 Some("inline")
919 );
920 assert_eq!(
922 package_name("package api // the public surface\n").as_deref(),
923 Some("api")
924 );
925 assert_eq!(package_name("import \"fmt\"\n"), None);
926 }
927
928 #[test]
929 fn caching_is_disabled_unless_the_author_already_chose() {
930 let (command, forced) =
934 command_with_fresh_results(&["go".into(), "test".into(), "./...".into()]);
935 assert!(forced);
936 assert_eq!(command, ["go", "test", "-count=1", "./..."]);
937
938 let (command, forced) =
940 command_with_fresh_results(&["go".into(), "test".into(), "-count=3".into()]);
941 assert!(!forced);
942 assert_eq!(command, ["go", "test", "-count=3"]);
943 }
944
945 #[test]
946 fn each_package_gets_an_evidence_file_of_its_own() {
947 assert_ne!(evidence_name("internal/auth"), evidence_name("internal/db"));
950 assert_eq!(evidence_name("."), "_.bin");
951 assert!(!evidence_name("internal/auth").contains('/'));
952 }
953
954 #[test]
955 fn only_the_constraint_nothing_ever_defines_is_read_alone() {
956 assert!(build_ignored("//go:build ignore\n\npackage p\n"));
962 assert!(build_ignored("// +build ignore\n\npackage p\n"));
963 assert!(build_ignored(
964 "// Copyright.\n\n//go:build ignore\n\npackage p\n"
965 ));
966 assert!(build_ignored("//go:build ignore || linux\n\npackage p\n"));
968
969 assert!(!build_ignored("//go:build linux\n\npackage p\n"));
971 assert!(!build_ignored("//go:build !windows\n\npackage p\n"));
972 assert!(!build_ignored("package p\n"));
973 assert!(!build_ignored("//go:build ignored\n\npackage p\n"));
975 assert!(!build_ignored("//go:build ignore_me\n\npackage p\n"));
976 assert!(!build_ignored(
980 "package p\n\n//go:build ignore\n\nfunc f() {}\n"
981 ));
982 assert!(!build_ignored(
983 "package p\n\nvar doc = \"//go:build ignore\"\n"
984 ));
985 }
986
987 #[test]
988 fn a_test_main_is_seen_in_a_file_nothing_could_parse() {
989 assert!(declares_test_main_textually(
994 "package p\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n"
995 ));
996 assert!(declares_test_main_textually(
998 "package p\n\nfunc TestMain( m *testing.M ) {}\n"
999 ));
1000 assert!(declares_test_main_textually(
1001 "package p\n\nfunc TestMain(m *testing.M) { broken( }\n"
1002 ));
1003
1004 assert!(!declares_test_main_textually(
1007 "package p\n\nfunc TestMainHelper(t *testing.T) {}\n"
1008 ));
1009 assert!(!declares_test_main_textually(
1011 "package p\n\nfunc TestMain(t *testing.T) {}\n"
1012 ));
1013 assert!(!declares_test_main_textually(
1014 "package p\n\n// func TestMain\n"
1015 ));
1016 assert!(!declares_test_main_textually("package p\n"));
1017 }
1018}