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, publish_run, recover_abandoned_runs,
40 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";
58
59const RUNTIME_SOURCE: &str = include_str!("../runtime-assets/go/supercov/supercov.go");
60
61const PROBE_FILE: &str = "supercov_probes.go";
63
64const HARNESS_FILE: &str = "supercov_generated_test.go";
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct DirectGoRunRequest {
70 pub root: PathBuf,
71 pub command: Vec<String>,
72 pub run_id: String,
73 pub started_at: String,
74 #[serde(default)]
83 pub exact_attribution: bool,
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct DirectGoRunResult {
88 pub run_id: String,
89 pub run_directory: PathBuf,
90 pub exit_code: i32,
91 pub tests: usize,
92 pub unattributed: usize,
98 pub source_files: usize,
99 pub unparseable_sources: usize,
102 pub unparseable_tests: usize,
106 pub packages: usize,
107 pub recovered_runs: Vec<String>,
108 pub metadata: RunMetadata,
109}
110
111fn elapsed_ms(started: Instant) -> f64 {
112 (started.elapsed().as_secs_f64() * 10_000.0).round() / 10.0
113}
114
115#[derive(Debug, Clone)]
117struct GoTestPackage {
118 directory: String,
121 evidence: PathBuf,
122 declared_in: BTreeMap<String, String>,
124 unattributed: BTreeSet<String>,
127}
128
129struct InstrumentedWorkspace {
130 project: PreparedGoProject,
131 packages: Vec<GoTestPackage>,
132 unparseable_tests: Vec<(String, String)>,
135}
136
137fn directory_of(relative: &str) -> String {
138 match relative.rsplit_once('/') {
139 Some((directory, _)) => directory.to_owned(),
140 None => ".".to_owned(),
141 }
142}
143
144fn package_name(source: &str) -> Option<String> {
151 let bytes = source.as_bytes();
152 let mut at = 0;
153 while at < bytes.len() {
154 let rest = &source[at..];
155 if rest.starts_with("//") {
156 at += rest.find('\n').map_or(rest.len(), |end| end + 1);
157 } else if rest.starts_with("/*") {
158 at += rest.find("*/").map_or(rest.len(), |end| end + 2);
160 } else if bytes[at].is_ascii_whitespace() {
161 at += 1;
162 } else {
163 return rest
164 .strip_prefix("package")
165 .and_then(|rest| rest.strip_prefix(|c: char| c.is_ascii_whitespace()))
166 .and_then(|rest| rest.split_whitespace().next())
167 .map(str::to_owned);
168 }
169 }
170 None
171}
172
173fn read(path: &Path) -> Result<String, String> {
174 fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))
175}
176
177fn write(path: &Path, contents: &str) -> Result<(), String> {
178 if let Some(parent) = path.parent() {
179 fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?;
180 }
181 fs::write(path, contents).map_err(|error| format!("{}: {error}", path.display()))
182}
183
184fn evidence_name(directory: &str) -> String {
187 let slug = directory
188 .chars()
189 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
190 .collect::<String>();
191 format!("{slug}.bin")
192}
193
194fn instrument_workspace(
197 workspace: &Path,
198 evidence_directory: &Path,
199 serialised: bool,
200) -> Result<InstrumentedWorkspace, String> {
201 let mut project = prepare_go_project(workspace)?;
202 let mut unparseable_tests: Vec<(String, String)> = Vec::new();
203
204 let members = crate::go_project::workspace_modules(workspace);
208 let directories = if members.is_empty() {
209 vec![".".to_owned()]
210 } else {
211 members.into_iter().collect::<Vec<_>>()
212 };
213 let mut runtimes: Vec<(String, String)> = Vec::new();
214 for directory in &directories {
215 let at = workspace.join(directory);
216 let module = module_path(&at).ok_or_else(|| {
217 format!(
218 "{}: no module path in go.mod, so Supercov cannot name the package its runtime is imported from",
219 at.display()
220 )
221 })?;
222 write(
223 &at.join(RUNTIME_DIRECTORY).join("supercov.go"),
224 RUNTIME_SOURCE,
225 )?;
226 runtimes.push((directory.clone(), format!("{module}/{RUNTIME_DIRECTORY}")));
227 }
228 runtimes.sort_by_key(|(directory, _)| std::cmp::Reverse(directory.len()));
230 let import_for = |relative: &str| -> &str {
231 runtimes
232 .iter()
233 .find(|(directory, _)| {
234 directory == "."
235 || relative == directory
236 || relative.starts_with(&format!("{directory}/"))
237 })
238 .map(|(_, import)| import.as_str())
239 .unwrap_or_default()
240 };
241
242 let probe_count = project
246 .probes
247 .keys()
248 .max()
249 .map_or(0, |highest| *highest as usize + 1);
250
251 let mut source_packages: BTreeMap<String, String> = BTreeMap::new();
252 for (relative, instrumented) in &project.instrumented {
253 let instrumented = instrumented.replace(RUNTIME_IMPORT, import_for(relative));
254 let path = workspace.join(relative);
255 if let Some(name) = package_name(&instrumented) {
256 source_packages
257 .entry(directory_of(relative))
258 .or_insert(name);
259 }
260 write(&path, &instrumented)?;
261 }
262 for (directory, package) in &source_packages {
263 write(
264 &workspace.join(directory).join(PROBE_FILE),
265 &probe_array_file(
266 package,
267 RUNTIME_ALIAS,
268 import_for(&format!("{directory}/x.go")),
269 probe_count,
270 ),
271 )?;
272 }
273
274 let mut by_directory: BTreeMap<String, Vec<&String>> = BTreeMap::new();
275 for relative in &project.files.tests {
276 by_directory
277 .entry(directory_of(relative))
278 .or_default()
279 .push(relative);
280 }
281
282 let mut packages = Vec::new();
283 for (directory, tests) in by_directory {
284 let evidence = evidence_directory.join(evidence_name(&directory));
285 let evidence_literal = evidence.to_string_lossy().replace('\\', "\\\\");
286 let mut declared_in = BTreeMap::new();
287 let mut unattributed = BTreeSet::new();
288 let mut declares_test_main = false;
289 let mut harness_package: Option<String> = None;
293 for relative in tests {
294 let path = workspace.join(relative);
295 let source = read(&path)?;
296 let Some(name) = package_name(&source) else {
297 continue;
298 };
299 let internal = source_packages.get(&directory) == Some(&name);
303 if internal || harness_package.is_none() {
304 harness_package = Some(name);
305 }
306 let built = !build_ignored(&source);
311 let file =
312 match instrument_test_file(&source, RUNTIME_ALIAS, &evidence_literal, serialised) {
313 Ok(file) => file,
314 Err(error) => {
315 project.manifest.limitations.push(
323 crate::go_project::unparseable_limitation(relative, &error.to_string()),
324 );
325 unparseable_tests.push((relative.clone(), error.to_string()));
326 declares_test_main |= built && declares_test_main_textually(&source);
332 continue;
333 }
334 };
335 declares_test_main |= built && file.declares_test_main;
336 for test in &file.tests {
337 declared_in.insert(test.clone(), relative.clone());
338 }
339 unattributed.extend(file.unattributed.iter().cloned());
340 if !file.edits.is_empty() {
341 write(
342 &path,
343 &rewrite(&source, &file.edits).replace(RUNTIME_IMPORT, import_for(relative)),
344 )?;
345 }
346 }
347 let Some(package) = harness_package else {
348 continue;
349 };
350 write(
351 &workspace.join(&directory).join(HARNESS_FILE),
352 &synthesized_harness(
353 &package,
354 RUNTIME_ALIAS,
355 import_for(&format!("{directory}/x.go")),
356 probe_count,
357 &project.decision_widths,
358 &evidence_literal,
359 declares_test_main,
360 ),
361 )?;
362 packages.push(GoTestPackage {
363 directory,
364 evidence,
365 declared_in,
366 unattributed,
367 });
368 }
369 Ok(InstrumentedWorkspace {
370 project,
371 packages,
372 unparseable_tests,
373 })
374}
375
376fn build_ignored(source: &str) -> bool {
386 let header = if source.starts_with("package ") {
390 ""
393 } else {
394 source
395 .find("\npackage ")
396 .map_or(source, |clause| &source[..clause])
397 };
398 header
399 .lines()
400 .map(str::trim)
401 .filter_map(|line| {
402 line.strip_prefix("//go:build")
403 .or_else(|| line.strip_prefix("// +build"))
405 })
406 .any(|constraint| {
407 constraint
408 .split(|c: char| !(c.is_alphanumeric() || c == '_'))
409 .any(|term| term == "ignore")
410 })
411}
412
413fn declares_test_main_textually(source: &str) -> bool {
420 source.split("func TestMain").skip(1).any(|rest| {
421 rest.trim_start()
422 .strip_prefix('(')
423 .is_some_and(|arguments| arguments.contains("testing.M"))
424 })
425}
426
427fn command_run_in_order(command: &[String]) -> (Vec<String>, bool) {
434 if command
435 .iter()
436 .any(|argument| argument == "-parallel" || argument.starts_with("-parallel="))
437 {
438 return (command.to_vec(), false);
441 }
442 let mut updated = command.to_vec();
443 let position = updated
444 .iter()
445 .position(|argument| argument == "test")
446 .map_or(updated.len(), |index| index + 1);
447 updated.insert(position, "-parallel=1".into());
448 (updated, true)
449}
450
451fn command_with_fresh_results(command: &[String]) -> (Vec<String>, bool) {
452 if command
453 .iter()
454 .any(|argument| argument == "-count" || argument.starts_with("-count="))
455 {
456 return (command.to_vec(), false);
457 }
458 let mut updated = command.to_vec();
459 let position = updated
461 .iter()
462 .position(|argument| argument == "test")
463 .map_or(updated.len(), |index| index + 1);
464 updated.insert(position, "-count=1".into());
465 (updated, true)
466}
467
468pub fn current_go_integrity(
470 root: &Path,
471 command: &[String],
472) -> Result<crate::run_store::RunIntegrity, String> {
473 let root = canonicalize_simplified(root).map_err(|error| error.to_string())?;
474 let files = crate::go_project::discover_go_files(&root)?;
475 create_explicit_run_integrity(
476 &root,
477 &go_integrity_inputs(&files, command),
478 &FrontendIntegrityInputs::embedded_go(),
479 )
480 .map_err(|error| error.to_string())
481}
482
483pub fn run_direct_go(
484 request: &DirectGoRunRequest,
485 diagnostics: &mut dyn Write,
486) -> Result<DirectGoRunResult, String> {
487 if request.command.is_empty() {
488 return Err("test command must not be empty".into());
489 }
490 let total_started = Instant::now();
491 let initialization_started = Instant::now();
492 let root = canonicalize_simplified(&request.root)
493 .map_err(|error| format!("{}: {error}", request.root.display()))?;
494 let mut lock = ProjectLock::acquire(&root, &request.run_id, &request.started_at)
495 .map_err(|error| error.to_string())?;
496 let initialization_ms = elapsed_ms(initialization_started);
497 let work_directory = root.join(".supercov/work").join(&request.run_id);
498 let result = (|| {
499 let recovered_runs = recover_abandoned_runs(&root, &request.started_at)
500 .map_err(|error| error.to_string())?;
501 if !recovered_runs.is_empty() {
502 writeln!(
503 diagnostics,
504 "[supercov] recovered abandoned run(s): {}",
505 recovered_runs.join(", ")
506 )
507 .map_err(|error| error.to_string())?;
508 }
509
510 let adapter_started = Instant::now();
511 let files = crate::go_project::discover_go_files(&root)?;
512 let integrity_inputs = go_integrity_inputs(&files, &request.command);
513 let assertion_inputs =
514 crate::assertion_inputs::capture(&root, "go", integrity_inputs.assertion_paths())?;
515 let integrity = create_explicit_run_integrity(
516 &root,
517 &integrity_inputs,
518 &FrontendIntegrityInputs::embedded_go(),
519 )
520 .map_err(|error| error.to_string())?;
521
522 let workspace_started = Instant::now();
523 recover_cached_workspace(&root, &lock).map_err(|error| error.to_string())?;
524 let workspace =
525 prepare_cached_workspace(&root, &lock, &[]).map_err(|error| error.to_string())?;
526 let evidence_directory = work_directory.join("go/evidence");
527 fs::create_dir_all(&evidence_directory).map_err(|error| error.to_string())?;
528 let (command, serialised) = if request.exact_attribution {
531 command_run_in_order(&request.command)
532 } else {
533 (request.command.clone(), false)
534 };
535 let instrumented = instrument_workspace(&workspace, &evidence_directory, serialised)?;
536 let workspace_preparation_ms = elapsed_ms(workspace_started);
537 let adapter_setup_ms = (elapsed_ms(adapter_started) - workspace_preparation_ms).max(0.0);
538 writeln!(
539 diagnostics,
540 "[supercov] detected Go; instrumenting {} source file(s) across {} test package(s) in isolated workspace {}",
541 instrumented.project.instrumented.len(),
542 instrumented.packages.len(),
543 workspace.display()
544 )
545 .map_err(|error| error.to_string())?;
546 for (file, reason) in &instrumented.project.unparseable {
547 writeln!(
548 diagnostics,
549 "[supercov] could not parse {file}: {reason}; it carries no obligations"
550 )
551 .map_err(|error| error.to_string())?;
552 }
553 for (file, reason) in &instrumented.unparseable_tests {
554 writeln!(
555 diagnostics,
556 "[supercov] could not parse {file}: {reason}; the tests it declares run unattributed and what they reach counts run-wide"
557 )
558 .map_err(|error| error.to_string())?;
559 }
560 if instrumented.packages.is_empty() {
561 return Err(
562 "no Go test packages were found, so a run would measure nothing: Supercov needs at least one _test.go file"
563 .into(),
564 );
565 }
566 if instrumented.project.instrumented.is_empty()
573 && !instrumented.project.files.sources.is_empty()
574 {
575 return Err(format!(
576 "none of the {} Go source file(s) could be parsed, so the run would measure a denominator of nothing: {}",
577 instrumented.project.files.sources.len(),
578 instrumented
579 .project
580 .unparseable
581 .iter()
582 .map(|(file, reason)| format!("{file}: {reason}"))
583 .collect::<Vec<_>>()
584 .join("; ")
585 ));
586 }
587
588 if serialised {
589 writeln!(
590 diagnostics,
591 "[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."
592 )
593 .map_err(|error| error.to_string())?;
594 }
595 let (command, forced_fresh) = command_with_fresh_results(&command);
596 if forced_fresh {
597 writeln!(
598 diagnostics,
599 "[supercov] added -count=1: `go test` caches passing packages, and a cached package does not run, so it would record no coverage"
600 )
601 .map_err(|error| error.to_string())?;
602 }
603 let test_started = Instant::now();
604 let plan = ExecutionPlan {
605 preparation: Vec::new(),
606 test: ExecutionPhase {
607 name: "test".into(),
608 kind: PhaseKind::Test,
609 command: CommandSpec {
610 program: command[0].clone().into(),
611 arguments: command[1..].iter().map(OsString::from).collect(),
612 cwd: workspace.clone(),
613 environment: None,
614 captured_output: None,
615 },
616 },
617 };
618 let options = SupervisionOptions::from_environment().map_err(|error| error.to_string())?;
619 let execution = execute_plan(&plan, options, diagnostics, |_, _| Ok(()))
620 .map_err(|error| error.to_string())?;
621 let test_command_ms = elapsed_ms(test_started);
622 if let Some(signal) = execution.interrupted_signal {
623 return Err(format!(
624 "the test command was interrupted by {signal:?}; no run was published"
625 ));
626 }
627 let exit_code = execution.exit_code;
628
629 let publication_started = Instant::now();
630 let mut parts = Vec::new();
631 let mut outcomes = Vec::new();
632 let mut silent = Vec::new();
633 for package in &instrumented.packages {
634 let Ok(bytes) = fs::read(&package.evidence) else {
635 silent.push(package.directory.clone());
636 continue;
637 };
638 let evidence = read_evidence(&bytes)
639 .map_err(|error| format!("{}: {error}", package.evidence.display()))?;
640 for test in &evidence.tests {
641 outcomes.push(OwnedTestOutcome {
642 name: test.name.clone(),
643 runner: test.runner.clone(),
644 package: package.directory.clone(),
645 file: package.declared_in.get(&test.name).cloned(),
646 status: test.status.clone(),
647 attributed: !test.unattributed,
648 });
649 }
650 parts.push(evidence);
651 }
652 if !silent.is_empty() {
653 writeln!(
654 diagnostics,
655 "[supercov] {} test package(s) wrote no evidence and are absent from this run: {}",
656 silent.len(),
657 silent.join(", ")
658 )
659 .map_err(|error| error.to_string())?;
660 }
661 let evidence = merge_evidence(parts);
662 let unattributed = instrumented
663 .packages
664 .iter()
665 .map(|package| package.unattributed.len())
666 .sum::<usize>();
667 let reached_something = evidence.global.iter().any(|total| *total != 0);
674 if outcomes.is_empty() && !reached_something {
675 let because = if unattributed > 0 {
676 format!(
677 "; {unattributed} test(s) ran without being able to announce themselves (t.Parallel, Example or Fuzz) and reached nothing measured either"
678 )
679 } else {
680 String::new()
681 };
682 return Err(format!(
683 "no Go test recorded evidence (the command exited {exit_code}); a run that measured nothing is not published{because}"
684 ));
685 }
686 let run = build_frontend_run(OwnedRunInputs {
687 declaration: go_declaration(),
688 environment: "go",
689 manifest: &instrumented.project.manifest,
690 probes: &instrumented.project.probes,
691 evidence: &evidence,
692 outcomes: &outcomes,
693 run_id: &request.run_id,
694 generated_at: &request.started_at,
695 test_exit_code: exit_code,
696 coverage_model: go_coverage_model(),
697 })
698 .map_err(|error| error.to_string())?;
699 validate_frontend_report_request(&run.declaration, &run.request)
703 .map_err(|error| error.to_string())?;
704 let archive_path = work_directory.join("evidence.raw.gz");
705 let raw = write_archive(
706 crate::assertion_inputs::append(
707 run.archive_entries().map_err(|error| error.to_string())?,
708 &assertion_inputs,
709 )?,
710 &archive_path,
711 )
712 .map_err(|error| error.to_string())?;
713 let evidence_publication_ms = elapsed_ms(publication_started);
714
715 let timings = RunTimings {
716 initialization_ms,
717 workspace_preparation_ms,
718 adapter_setup_ms,
719 instrumented_build_ms: 0.0,
720 test_command_ms,
721 evidence_publication_ms,
722 };
723 let metadata = RunMetadata {
724 id: request.run_id.clone(),
725 started_at: request.started_at.clone(),
726 duration_ms: elapsed_ms(total_started),
727 command: request.command.clone(),
728 test_exit_code: Some(exit_code),
729 integrity,
730 raw_evidence: RawEvidenceMetadata {
731 schema_version: raw.schema_version,
732 format: raw.format.into(),
733 file: raw.file.into(),
734 files: raw.files,
735 uncompressed_bytes: raw.uncompressed_bytes,
736 compressed_bytes: raw.compressed_bytes,
737 },
738 isolated_build: Some(true),
739 instrumented_build_cache: None,
740 timings: Some(timings),
741 merged: None,
742 parents: None,
743 };
744 let run_directory =
745 publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
746 finalize_published_run(&root, &request.run_id).map_err(|error| error.to_string())?;
747 Ok(DirectGoRunResult {
748 run_id: request.run_id.clone(),
749 run_directory,
750 exit_code,
751 tests: outcomes
752 .iter()
753 .map(|outcome| outcome.name.as_str())
754 .collect::<BTreeSet<_>>()
755 .len(),
756 unattributed,
757 source_files: instrumented.project.instrumented.len(),
758 unparseable_sources: instrumented.project.unparseable.len(),
759 unparseable_tests: instrumented.unparseable_tests.len(),
760 packages: instrumented.packages.len(),
761 recovered_runs,
762 metadata,
763 })
764 })();
765 if result.is_err() {
766 let _ = remove_stored_tree_deferred(&root, &work_directory);
767 }
768 let release = lock.release().map_err(|error| error.to_string());
769 match (result, release) {
770 (Ok(result), Ok(())) => Ok(result),
771 (Err(error), _) => Err(error),
772 (Ok(_), Err(error)) => Err(error),
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779
780 #[test]
781 fn the_package_clause_is_found_past_whatever_precedes_it() {
782 assert_eq!(package_name("package main\n").as_deref(), Some("main"));
786 assert_eq!(
787 package_name("//go:build linux\n\n// Copyright.\npackage worker\n").as_deref(),
788 Some("worker")
789 );
790 assert_eq!(
791 package_name("/*\nA package comment\nspanning lines.\n*/\npackage doc\n").as_deref(),
792 Some("doc")
793 );
794 assert_eq!(
795 package_name("/* one line */ package inline\n").as_deref(),
796 Some("inline")
797 );
798 assert_eq!(
800 package_name("package api // the public surface\n").as_deref(),
801 Some("api")
802 );
803 assert_eq!(package_name("import \"fmt\"\n"), None);
804 }
805
806 #[test]
807 fn caching_is_disabled_unless_the_author_already_chose() {
808 let (command, forced) =
812 command_with_fresh_results(&["go".into(), "test".into(), "./...".into()]);
813 assert!(forced);
814 assert_eq!(command, ["go", "test", "-count=1", "./..."]);
815
816 let (command, forced) =
818 command_with_fresh_results(&["go".into(), "test".into(), "-count=3".into()]);
819 assert!(!forced);
820 assert_eq!(command, ["go", "test", "-count=3"]);
821 }
822
823 #[test]
824 fn each_package_gets_an_evidence_file_of_its_own() {
825 assert_ne!(evidence_name("internal/auth"), evidence_name("internal/db"));
828 assert_eq!(evidence_name("."), "_.bin");
829 assert!(!evidence_name("internal/auth").contains('/'));
830 }
831
832 #[test]
833 fn only_the_constraint_nothing_ever_defines_is_read_alone() {
834 assert!(build_ignored("//go:build ignore\n\npackage p\n"));
840 assert!(build_ignored("// +build ignore\n\npackage p\n"));
841 assert!(build_ignored(
842 "// Copyright.\n\n//go:build ignore\n\npackage p\n"
843 ));
844 assert!(build_ignored("//go:build ignore || linux\n\npackage p\n"));
846
847 assert!(!build_ignored("//go:build linux\n\npackage p\n"));
849 assert!(!build_ignored("//go:build !windows\n\npackage p\n"));
850 assert!(!build_ignored("package p\n"));
851 assert!(!build_ignored("//go:build ignored\n\npackage p\n"));
853 assert!(!build_ignored("//go:build ignore_me\n\npackage p\n"));
854 assert!(!build_ignored(
858 "package p\n\n//go:build ignore\n\nfunc f() {}\n"
859 ));
860 assert!(!build_ignored(
861 "package p\n\nvar doc = \"//go:build ignore\"\n"
862 ));
863 }
864
865 #[test]
866 fn a_test_main_is_seen_in_a_file_nothing_could_parse() {
867 assert!(declares_test_main_textually(
872 "package p\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n"
873 ));
874 assert!(declares_test_main_textually(
876 "package p\n\nfunc TestMain( m *testing.M ) {}\n"
877 ));
878 assert!(declares_test_main_textually(
879 "package p\n\nfunc TestMain(m *testing.M) { broken( }\n"
880 ));
881
882 assert!(!declares_test_main_textually(
885 "package p\n\nfunc TestMainHelper(t *testing.T) {}\n"
886 ));
887 assert!(!declares_test_main_textually(
889 "package p\n\nfunc TestMain(t *testing.T) {}\n"
890 ));
891 assert!(!declares_test_main_textually(
892 "package p\n\n// func TestMain\n"
893 ));
894 assert!(!declares_test_main_textually("package p\n"));
895 }
896}