1use std::collections::BTreeSet;
11use std::path::{Path, PathBuf};
12
13use crate::coverage_report::CoverageManifest;
14use crate::go_instrumenter::GoProbe;
15use crate::integrity::ExplicitIntegrityInputs;
16
17const EXCLUDED_DIRECTORIES: &[&str] = &[
22 ".git",
23 ".idea",
24 ".vscode",
25 "bin",
26 "node_modules",
27 "testdata",
28 "third_party",
29 "vendor",
30];
31
32const DEPENDENCY_FILES: &[&str] = &[
34 "go.mod",
35 "go.sum",
36 "go.work",
37 "go.work.sum",
38 "vendor/modules.txt",
39];
40
41const CONFIGURATION_FILES: &[&str] = &[".go-version", "go.env"];
43
44#[derive(Debug, Clone, PartialEq, Eq, Default)]
45pub struct GoFiles {
46 pub sources: Vec<String>,
48 pub tests: Vec<String>,
50 pub dependency_files: Vec<PathBuf>,
51 pub configuration_files: Vec<PathBuf>,
52 pub excluded: Vec<(String, &'static str)>,
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub struct PreparedGoProject {
57 pub root: PathBuf,
58 pub files: GoFiles,
59 pub manifest: CoverageManifest,
60 pub probes: std::collections::BTreeMap<u64, GoProbe>,
61 pub instrumented: Vec<(String, String)>,
67 pub decision_widths: Vec<u8>,
70 pub unparseable: Vec<(String, String)>,
74}
75
76pub fn is_test_file(relative: &str) -> bool {
79 relative
80 .rsplit('/')
81 .next()
82 .is_some_and(|name| name.ends_with("_test.go"))
83}
84
85pub fn workspace_modules(root: &Path) -> BTreeSet<String> {
91 let Ok(text) = std::fs::read_to_string(root.join("go.work")) else {
92 return BTreeSet::new();
93 };
94 let mut modules = BTreeSet::new();
95 let mut in_block = false;
96 for line in text.lines() {
97 let line = line.split("//").next().unwrap_or_default().trim();
98 if line.is_empty() {
99 continue;
100 }
101 let entry = if in_block {
103 if line == ")" {
104 in_block = false;
105 continue;
106 }
107 Some(line)
108 } else if let Some(rest) = line.strip_prefix("use ") {
109 let rest = rest.trim();
110 if rest == "(" {
111 in_block = true;
112 continue;
113 }
114 Some(rest)
115 } else {
116 if line.starts_with("use(") {
117 in_block = true;
118 }
119 None
120 };
121 if let Some(entry) = entry {
122 let entry = entry.trim_matches('"').trim();
123 let entry = entry.strip_prefix("./").unwrap_or(entry);
124 let entry = entry.trim_end_matches('/');
125 if !entry.is_empty() {
126 modules.insert(if entry == "." {
127 ".".to_owned()
128 } else {
129 entry.replace('\\', "/")
130 });
131 }
132 }
133 }
134 modules
135}
136
137fn walk(root: &Path, directory: &Path, files: &mut GoFiles) -> Result<(), String> {
138 let members = workspace_modules(root);
139 let entries = std::fs::read_dir(directory)
140 .map_err(|error| format!("could not read {}: {error}", directory.display()))?;
141 let mut sorted = entries
142 .collect::<Result<Vec<_>, _>>()
143 .map_err(|error| format!("could not read {}: {error}", directory.display()))?;
144 sorted.sort_by_key(std::fs::DirEntry::path);
145 for entry in sorted {
146 let path = entry.path();
147 let Ok(relative) = path.strip_prefix(root) else {
148 continue;
149 };
150 let relative = relative.to_string_lossy().replace('\\', "/");
151 let name = path
152 .file_name()
153 .map(|name| name.to_string_lossy().into_owned())
154 .unwrap_or_default();
155 let file_type = entry
156 .file_type()
157 .map_err(|error| format!("could not inspect {}: {error}", path.display()))?;
158 if file_type.is_dir() {
159 if EXCLUDED_DIRECTORIES.contains(&name.as_str()) || name.starts_with('.') {
160 files
161 .excluded
162 .push((relative, "tooling or vendored directory"));
163 continue;
164 }
165 if name.starts_with('_') {
171 ignored_by_the_toolchain(root, &path, files)?;
172 continue;
173 }
174 if path.join("go.mod").is_file() && !members.contains(&relative) {
182 files.excluded.push((relative, "a module of its own"));
183 continue;
184 }
185 walk(root, &path, files)?;
186 continue;
187 }
188 if !file_type.is_file() {
189 continue;
190 }
191 if DEPENDENCY_FILES.contains(&relative.as_str()) {
192 files.dependency_files.push(PathBuf::from(&relative));
193 continue;
194 }
195 if CONFIGURATION_FILES.contains(&name.as_str()) {
196 files.configuration_files.push(PathBuf::from(&relative));
197 continue;
198 }
199 if !name.ends_with(".go") {
200 continue;
201 }
202 if name.starts_with('_') || name.starts_with('.') {
204 files.excluded.push((relative, IGNORED_BY_THE_TOOLCHAIN));
205 continue;
206 }
207 if build_ignored(&read_source(&path)?) {
213 files.excluded.push((relative, IGNORED_BY_THE_TOOLCHAIN));
214 continue;
215 }
216 if is_test_file(&relative) {
217 files.tests.push(relative);
218 } else {
219 files.sources.push(relative);
220 }
221 }
222 Ok(())
223}
224
225const IGNORED_BY_THE_TOOLCHAIN: &str = "ignored by the Go toolchain";
226
227fn read_source(path: &Path) -> Result<String, String> {
229 let bytes = std::fs::read(path)
230 .map_err(|error| format!("could not read {}: {error}", path.display()))?;
231 Ok(String::from_utf8_lossy(&bytes).into_owned())
232}
233
234pub(crate) fn build_ignored(source: &str) -> bool {
244 let header = if source.starts_with("package ") {
248 ""
251 } else {
252 source
253 .find("\npackage ")
254 .map_or(source, |clause| &source[..clause])
255 };
256 let terms = |expression: &str, and: &str| -> bool {
257 expression.split(and).any(|term| term.trim() == "ignore")
258 };
259 header.lines().map(str::trim).any(|line| {
260 if let Some(expression) = line.strip_prefix("//go:build") {
261 !expression.contains("||") && !expression.contains('(') && terms(expression, "&&")
263 } else if let Some(options) = line.strip_prefix("// +build") {
264 let options = options.split_whitespace().collect::<Vec<_>>();
267 options.len() == 1 && terms(options[0], ",")
268 } else {
269 false
270 }
271 })
272}
273
274fn ignored_by_the_toolchain(
276 root: &Path,
277 directory: &Path,
278 files: &mut GoFiles,
279) -> Result<(), String> {
280 let entries = std::fs::read_dir(directory)
281 .map_err(|error| format!("could not read {}: {error}", directory.display()))?;
282 for entry in entries {
283 let entry =
284 entry.map_err(|error| format!("could not read {}: {error}", directory.display()))?;
285 let path = entry.path();
286 let file_type = entry
287 .file_type()
288 .map_err(|error| format!("could not inspect {}: {error}", path.display()))?;
289 if file_type.is_dir() {
290 ignored_by_the_toolchain(root, &path, files)?;
291 } else if file_type.is_file()
292 && path.extension().is_some_and(|extension| extension == "go")
293 && let Ok(relative) = path.strip_prefix(root)
294 {
295 files.excluded.push((
296 relative.to_string_lossy().replace('\\', "/"),
297 IGNORED_BY_THE_TOOLCHAIN,
298 ));
299 }
300 }
301 Ok(())
302}
303
304fn example_program(file: &str) -> bool {
310 file.split_once('/').is_some_and(|(first, _)| {
311 matches!(first.to_ascii_lowercase().as_str(), "examples" | "example")
312 })
313}
314
315pub fn discover_go_files(root: &Path) -> Result<GoFiles, String> {
316 let mut files = GoFiles::default();
317 walk(root, root, &mut files)?;
318 files.sources.sort();
319 files.tests.sort();
320 files.dependency_files.sort();
321 files.configuration_files.sort();
322 files.excluded.sort();
323 Ok(files)
324}
325
326const LANGUAGE: &str = "go";
327
328pub(crate) fn unparseable_limitation(file: &str, reason: &str) -> serde_json::Value {
334 serde_json::json!({
335 "id": crate::go_instrumenter::stable_obligation_id(LANGUAGE, file, "unparseable", 0, 0),
336 "kind": "file-does-not-parse",
337 "file": file,
338 "source": file,
341 "line": 1,
342 "column": 1,
343 "reason": format!(
344 "{reason}; the file carries no obligations and nothing in it counts towards this run"
345 ),
346 })
347}
348
349pub fn prepare_go_project(
350 root: &Path,
351 roots: Option<&crate::source_discovery::ExplicitSourceRoots>,
352) -> Result<PreparedGoProject, String> {
353 let mut files = discover_go_files(root)?;
354 match roots {
355 Some(roots) => {
356 roots.narrow(&mut files.sources, &mut files.excluded, String::as_str);
357 roots.refuse_if_empty(files.sources.len(), "Go")?;
358 }
359 None => {
361 let GoFiles {
362 sources, excluded, ..
363 } = &mut files;
364 sources.retain(|file| {
365 let example = example_program(file);
366 if example {
367 excluded.push((file.clone(), "example"));
368 }
369 !example
370 });
371 excluded.sort();
372 }
373 }
374 if files.sources.is_empty() && files.tests.is_empty() {
375 return Err(
376 "no Go source files were found under the project root; Supercov measures .go files outside vendor, testdata and tooling directories"
377 .into(),
378 );
379 }
380 let mut manifest = CoverageManifest {
381 decisions: Vec::new(),
382 points: Vec::new(),
383 branches: Vec::new(),
384 limitations: Vec::new(),
385 unmeasured: Vec::new(),
386 scope: None,
387 assertion_sites: Vec::new(),
388 };
389 let mut probes = std::collections::BTreeMap::new();
390 let mut instrumented = Vec::new();
391 let mut decision_widths = Vec::new();
392 let mut unparseable = Vec::new();
393 let mut next_probe = 0_u64;
394 let mut next_decision = 0_u32;
395 for relative in &files.sources {
396 let path = root.join(relative);
397 let Ok(source) = std::fs::read_to_string(&path) else {
398 let reason = "file could not be read as UTF-8";
399 manifest
400 .limitations
401 .push(unparseable_limitation(relative, reason));
402 unparseable.push((relative.clone(), reason.to_owned()));
403 continue;
404 };
405 match crate::go_instrumenter::build_go_obligations_for(
406 relative,
407 &source,
408 &mut next_probe,
409 &mut next_decision,
410 crate::go_instrumenter::RUNTIME_ALIAS,
411 has_generics(root, relative, &source),
412 ) {
413 Ok(obligations) => {
414 manifest.decisions.extend(obligations.manifest.decisions);
415 manifest.points.extend(obligations.manifest.points);
416 manifest.branches.extend(obligations.manifest.branches);
417 manifest
421 .limitations
422 .extend(obligations.manifest.limitations);
423 probes.extend(obligations.probes);
424 decision_widths.extend(obligations.decision_widths);
425 instrumented.push((
426 relative.clone(),
427 crate::go_instrumenter::rewrite(&source, &obligations.edits),
428 ));
429 }
430 Err(error) => {
431 manifest
432 .limitations
433 .push(unparseable_limitation(relative, &error.to_string()));
434 unparseable.push((relative.clone(), error.to_string()));
435 }
436 }
437 }
438 let scope = match roots {
439 Some(roots) => roots
440 .scope(&files.sources, &files.excluded)
441 .map_err(|error| error.to_string())?,
442 None => {
443 let unread = |file: &String| unparseable.iter().any(|(path, _)| path == file);
444 crate::source_discovery::automatic_scope(
445 files
446 .sources
447 .iter()
448 .filter(|file| !unread(file))
449 .map(|file| (file.clone(), "Go application source".to_owned())),
450 files
451 .sources
452 .iter()
453 .filter(|file| unread(file))
454 .map(|file| (file.clone(), "could not be parsed".to_owned()))
455 .chain(
456 files
457 .tests
458 .iter()
459 .map(|file| (file.clone(), "test file".to_owned())),
460 )
461 .chain(
462 files
464 .excluded
465 .iter()
466 .filter(|(file, _)| file.ends_with(".go"))
467 .map(|(file, reason)| (file.clone(), (*reason).to_owned())),
468 ),
469 )
470 }
471 };
472 manifest.scope = Some(serde_json::to_value(scope).map_err(|error| error.to_string())?);
473 Ok(PreparedGoProject {
474 root: root.to_owned(),
475 files,
476 manifest,
477 probes,
478 instrumented,
479 decision_widths,
480 unparseable,
481 })
482}
483
484pub fn go_integrity_inputs(files: &GoFiles, command: &[String]) -> ExplicitIntegrityInputs {
491 ExplicitIntegrityInputs {
492 source_files: files.sources.iter().map(PathBuf::from).collect(),
493 test_files: files.tests.iter().map(PathBuf::from).collect(),
494 dependency_files: files.dependency_files.clone(),
495 configuration_files: files.configuration_files.clone(),
496 execution_configuration: command.join("\0").into_bytes(),
497 }
498}
499
500fn has_generics(root: &Path, relative: &str, source: &str) -> bool {
504 let minor = |text: &str| -> Option<u32> {
505 text.strip_prefix("1.")?
506 .split(|c: char| !c.is_ascii_digit())
507 .next()?
508 .parse()
509 .ok()
510 };
511 let mut directory = root.join(relative);
512 let module = loop {
513 if !directory.pop() || !directory.starts_with(root) {
514 break None;
515 }
516 if let Ok(text) = std::fs::read_to_string(directory.join("go.mod")) {
517 break Some(text);
518 }
519 };
520 let declared = module
521 .as_deref()
522 .and_then(|text| {
523 text.lines()
524 .map(str::trim)
525 .find_map(|line| line.strip_prefix("go ").map(str::trim))
526 .and_then(minor)
527 })
528 .unwrap_or(16);
529 let header = source.split("\npackage ").next().unwrap_or("");
530 let constrained = header
531 .lines()
532 .filter_map(|line| line.trim().strip_prefix("//go:build"))
533 .flat_map(|constraint| {
534 constraint
535 .split(|c: char| !(c.is_ascii_alphanumeric() || c == '.' || c == '!'))
536 .filter_map(|term| term.strip_prefix("go").and_then(minor))
537 .collect::<Vec<_>>()
538 })
539 .min();
540 constrained.map_or(declared, |file| file.min(declared)) >= 18
541}
542
543pub fn module_path(root: &Path) -> Option<String> {
546 let text = std::fs::read_to_string(root.join("go.mod")).ok()?;
547 text.lines()
548 .map(str::trim)
549 .find_map(|line| line.strip_prefix("module "))
550 .map(|path| path.trim().to_owned())
551}
552
553pub fn test_packages(files: &GoFiles) -> Vec<String> {
556 files
557 .tests
558 .iter()
559 .map(|test| match test.rsplit_once('/') {
560 Some((directory, _)) => directory.to_owned(),
561 None => ".".to_owned(),
562 })
563 .collect::<BTreeSet<_>>()
564 .into_iter()
565 .collect()
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use std::fs;
572
573 #[test]
574 fn only_the_constraint_nothing_ever_defines_is_read_alone() {
575 assert!(build_ignored("//go:build ignore\n\npackage p\n"));
581 assert!(build_ignored("// +build ignore\n\npackage p\n"));
582 assert!(build_ignored(
583 "// Copyright.\n\n//go:build ignore\n\npackage p\n"
584 ));
585 assert!(build_ignored("//go:build ignore && linux\n\npackage p\n"));
588 assert!(build_ignored("// +build linux,ignore\n\npackage p\n"));
589 assert!(!build_ignored("//go:build ignore || linux\n\npackage p\n"));
590 assert!(!build_ignored("// +build ignore linux\n\npackage p\n"));
591 assert!(!build_ignored("//go:build !ignore\n\npackage p\n"));
592
593 assert!(!build_ignored("//go:build linux\n\npackage p\n"));
595 assert!(!build_ignored("//go:build !windows\n\npackage p\n"));
596 assert!(!build_ignored("package p\n"));
597 assert!(!build_ignored("//go:build ignored\n\npackage p\n"));
599 assert!(!build_ignored("//go:build ignore_me\n\npackage p\n"));
600 assert!(!build_ignored(
604 "package p\n\n//go:build ignore\n\nfunc f() {}\n"
605 ));
606 assert!(!build_ignored(
607 "package p\n\nvar doc = \"//go:build ignore\"\n"
608 ));
609 }
610
611 fn fixture(label: &str) -> PathBuf {
612 let root = std::env::temp_dir().join(format!(
613 "supercov-go-project-{label}-{}-{}",
614 std::process::id(),
615 std::time::SystemTime::now()
616 .duration_since(std::time::UNIX_EPOCH)
617 .unwrap()
618 .as_nanos()
619 ));
620 fs::create_dir_all(&root).unwrap();
621 root
622 }
623
624 fn write(root: &Path, relative: &str, contents: &str) {
625 let path = root.join(relative);
626 fs::create_dir_all(path.parent().unwrap()).unwrap();
627 fs::write(path, contents).unwrap();
628 }
629
630 fn explicit_roots(root: &Path, roots: &[&str]) -> crate::source_discovery::ExplicitSourceRoots {
631 let roots = roots
632 .iter()
633 .map(|root| (*root).to_owned())
634 .collect::<Vec<_>>();
635 crate::source_discovery::ExplicitSourceRoots::resolve(root, &roots).unwrap()
636 }
637
638 #[test]
639 fn explicit_roots_narrow_go_to_the_code_they_name() {
640 let root = fixture("explicit-roots");
645 write(&root, "go.mod", "module example.com/app\n");
646 write(
647 &root,
648 "app.go",
649 "package main\n\nfunc f(a int) int {\n\treturn a\n}\n",
650 );
651 write(
652 &root,
653 "extra/dep.go",
654 "package extra\n\nfunc G() int {\n\treturn 1\n}\n",
655 );
656
657 let project = prepare_go_project(&root, Some(&explicit_roots(&root, &["app.go"]))).unwrap();
658 assert_eq!(project.files.sources, ["app.go"], "{:?}", project.files);
659 assert!(
660 project
661 .files
662 .excluded
663 .contains(&("extra/dep.go".into(), "outside explicit source roots")),
664 "{:?}",
665 project.files.excluded
666 );
667 let scope: crate::source_discovery::SourceScope = serde_json::from_value(
668 project
669 .manifest
670 .scope
671 .clone()
672 .expect("explicit roots leave a scope"),
673 )
674 .unwrap();
675 assert_eq!(
676 scope.mode,
677 crate::source_discovery::SourceScopeMode::Explicit
678 );
679 assert_eq!(scope.roots, ["app.go"]);
680
681 fs::remove_dir_all(root).ok();
682 }
683
684 #[test]
685 fn a_run_lists_what_it_measures_and_what_it_left_out() {
686 let root = fixture("automatic-scope");
691 write(&root, "go.mod", "module example.com/app\n");
692 let function = "\n\nfunc F(a int) int {\n\treturn a\n}\n";
693 write(&root, "app.go", &format!("package app{function}"));
694 write(&root, "app_test.go", "package app\n");
695 write(
696 &root,
697 "_examples/hello/main.go",
698 &format!("package main{function}"),
699 );
700 write(&root, "_draft.go", &format!("package app{function}"));
701 write(
702 &root,
703 "examples/echo/main.go",
704 &format!("package main{function}"),
705 );
706 write(
707 &root,
708 "internal/examples/kept.go",
709 &format!("package examples{function}"),
710 );
711 write(&root, "doc/gen.go", &format!("package doc{function}"));
712 write(
713 &root,
714 "vendor/other/lib.go",
715 &format!("package lib{function}"),
716 );
717
718 let project = prepare_go_project(&root, None).unwrap();
719 assert_eq!(
720 project.files.sources,
721 ["app.go", "doc/gen.go", "internal/examples/kept.go"]
722 );
723 let scope: crate::source_discovery::SourceScope =
724 serde_json::from_value(project.manifest.scope.clone().expect("a scope")).unwrap();
725 assert_eq!(
726 scope.mode,
727 crate::source_discovery::SourceScopeMode::Automatic
728 );
729 let listed = scope
730 .entries
731 .iter()
732 .map(|entry| {
733 (
734 entry.file.as_str(),
735 entry.status == crate::source_discovery::SourceScopeStatus::Included,
736 entry.reason.as_str(),
737 )
738 })
739 .collect::<Vec<_>>();
740 assert_eq!(
741 listed,
742 [
743 ("_draft.go", false, "ignored by the Go toolchain"),
744 (
745 "_examples/hello/main.go",
746 false,
747 "ignored by the Go toolchain"
748 ),
749 ("app.go", true, "Go application source"),
750 ("app_test.go", false, "test file"),
751 ("doc/gen.go", true, "Go application source"),
752 ("examples/echo/main.go", false, "example"),
753 ("internal/examples/kept.go", true, "Go application source"),
754 ]
755 );
756
757 let named = prepare_go_project(&root, Some(&explicit_roots(&root, &["examples"]))).unwrap();
759 assert_eq!(named.files.sources, ["examples/echo/main.go"]);
760 fs::remove_dir_all(root).ok();
761 }
762
763 #[test]
764 fn a_test_file_is_the_one_the_toolchain_says_it_is() {
765 for test in ["main_test.go", "pkg/api/handler_test.go", "a/b/z_test.go"] {
768 assert!(is_test_file(test), "{test}");
769 }
770 for source in ["main.go", "pkg/api/handler.go", "testing.go", "pkg/test.go"] {
771 assert!(!is_test_file(source), "{source}");
772 }
773 }
774
775 #[test]
776 fn vendored_and_toolchain_directories_are_not_this_project_s_source() {
777 let root = fixture("scope");
781 write(&root, "go.mod", "module example.com/app\n\ngo 1.22\n");
782 write(&root, "go.sum", "");
783 write(&root, ".go-version", "1.22.0\n");
784 write(&root, "main.go", "package main\n\nfunc main() {}\n");
785 write(
786 &root,
787 "pkg/api/handler.go",
788 "package api\n\nfunc Handle() int {\n\treturn 1\n}\n",
789 );
790 write(
791 &root,
792 "pkg/api/handler_test.go",
793 "package api\n\nimport \"testing\"\n\nfunc TestHandle(t *testing.T) {}\n",
794 );
795 write(
796 &root,
797 "vendor/other/lib.go",
798 "package other\n\nfunc X() {}\n",
799 );
800 write(&root, "testdata/golden.go", "package testdata\n");
801 write(&root, "bin/tool.go", "package main\n");
802
803 let files = discover_go_files(&root).unwrap();
804 assert_eq!(files.sources, ["main.go", "pkg/api/handler.go"]);
805 assert_eq!(files.tests, ["pkg/api/handler_test.go"]);
806 assert_eq!(
807 files.dependency_files,
808 [PathBuf::from("go.mod"), PathBuf::from("go.sum")]
809 );
810 assert_eq!(files.configuration_files, [PathBuf::from(".go-version")]);
811 assert_eq!(module_path(&root).as_deref(), Some("example.com/app"));
812 assert_eq!(test_packages(&files), ["pkg/api"]);
813 fs::remove_dir_all(root).unwrap();
814 }
815
816 #[test]
817 fn a_file_that_does_not_parse_is_reported_rather_than_skipped() {
818 let root = fixture("unparseable");
822 write(&root, "go.mod", "module example.com/app\n");
823 write(
824 &root,
825 "good.go",
826 "package main\n\nfunc f(a int) bool {\n\tif a > 1 && a < 9 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
827 );
828 write(&root, "broken.go", "package main\n\nfunc f( {\n");
829
830 let project = prepare_go_project(&root, None).unwrap();
831 assert_eq!(project.unparseable.len(), 1);
832 assert_eq!(project.unparseable[0].0, "broken.go");
833 let declared = project
836 .manifest
837 .limitations
838 .iter()
839 .filter(|limitation| limitation["kind"] == "file-does-not-parse")
840 .collect::<Vec<_>>();
841 assert_eq!(declared.len(), 1, "{declared:?}");
842 assert!(
843 declared[0]["file"].as_str().unwrap().ends_with("broken.go"),
844 "{declared:?}"
845 );
846 assert!(
847 declared[0]["id"].as_str().is_some_and(|id| !id.is_empty()),
848 "the declaration needs an id to reference: {declared:?}"
849 );
850 assert!(!project.manifest.points.is_empty());
852 assert_eq!(project.manifest.decisions.len(), 1);
853 assert!(!project.probes.is_empty());
854 fs::remove_dir_all(root).unwrap();
855 }
856
857 #[test]
858 fn an_empty_project_is_refused_rather_than_measured_as_complete() {
859 let root = fixture("empty");
862 write(&root, "go.mod", "module example.com/app\n");
863 assert!(prepare_go_project(&root, None).is_err());
864 fs::remove_dir_all(root).unwrap();
865 }
866
867 #[test]
868 fn the_ambient_environment_is_not_part_of_run_identity() {
869 let files = GoFiles::default();
871 let inputs = go_integrity_inputs(
872 &files,
873 &["go".to_owned(), "test".to_owned(), "./...".to_owned()],
874 );
875 assert_eq!(inputs.execution_configuration, b"go\0test\0./...");
876 }
877
878 #[test]
879 fn a_nested_module_belongs_to_itself() {
880 let root = fixture("nested-module");
886 fs::write(root.join("go.mod"), "module example.com/root\n").unwrap();
887 fs::write(root.join("root.go"), "package root\n\nfunc A() {}\n").unwrap();
888 fs::create_dir_all(root.join("sub")).unwrap();
889 fs::write(root.join("sub/go.mod"), "module example.com/sub\n").unwrap();
890 fs::write(root.join("sub/sub.go"), "package sub\n\nfunc B() {}\n").unwrap();
891 fs::create_dir_all(root.join("internal")).unwrap();
893 fs::write(
894 root.join("internal/helper.go"),
895 "package internal\n\nfunc C() {}\n",
896 )
897 .unwrap();
898
899 let files = discover_go_files(&root).expect("discovery");
900 assert_eq!(files.sources, ["internal/helper.go", "root.go"]);
901 assert!(
902 files
903 .excluded
904 .iter()
905 .any(|(path, reason)| path == "sub" && *reason == "a module of its own"),
906 "{:?}",
907 files.excluded
908 );
909 fs::remove_dir_all(root).unwrap();
910 }
911
912 #[test]
913 fn a_workspace_names_the_modules_it_uses() {
914 let root = fixture("go-work");
919 fs::write(
920 root.join("go.work"),
921 "go 1.22\n\n// the services\nuse (\n\t./core\n\t\"./app\" // quoted\n\t./tools/gen\n)\n",
922 )
923 .unwrap();
924 let modules = workspace_modules(&root);
925 assert_eq!(
926 modules.iter().map(String::as_str).collect::<Vec<_>>(),
927 ["app", "core", "tools/gen"]
928 );
929
930 fs::write(root.join("go.work"), "go 1.22\n\nuse ./only\n").unwrap();
932 assert_eq!(
933 workspace_modules(&root)
934 .iter()
935 .map(String::as_str)
936 .collect::<Vec<_>>(),
937 ["only"]
938 );
939
940 fs::remove_file(root.join("go.work")).unwrap();
943 assert!(workspace_modules(&root).is_empty());
944 fs::remove_dir_all(root).unwrap();
945 }
946
947 #[test]
948 fn a_workspace_member_is_measured_where_a_nested_module_is_not() {
949 let root = fixture("go-work-members");
952 fs::write(root.join("go.work"), "go 1.22\n\nuse (\n\t./core\n)\n").unwrap();
953 for module in ["core", "vendored"] {
954 fs::create_dir_all(root.join(module)).unwrap();
955 fs::write(
956 root.join(module).join("go.mod"),
957 format!("module example.com/{module}\n"),
958 )
959 .unwrap();
960 fs::write(
961 root.join(module).join("code.go"),
962 format!("package {module}\n\nfunc A() {{}}\n"),
963 )
964 .unwrap();
965 }
966 let files = discover_go_files(&root).expect("discovery");
967 assert_eq!(files.sources, ["core/code.go"]);
968 assert!(
969 files
970 .excluded
971 .iter()
972 .any(|(path, reason)| path == "vendored" && *reason == "a module of its own"),
973 "{:?}",
974 files.excluded
975 );
976 fs::remove_dir_all(root).unwrap();
977 }
978}