use std::fs;
use std::path::Path;
use projd_core::{DiscoverOptions, ProjectHealth, RiskCode, scan_path, scan_paths_recursive};
use tempfile::tempdir;
fn write_file(dir: &Path, name: &str, content: &str) {
let path = dir.join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, content).unwrap();
}
fn write_minimal_cargo_project(dir: &Path) {
write_file(
dir,
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
write_file(dir, "src/lib.rs", "pub fn hello() {}\n");
write_file(dir, "README.md", "# demo\n");
write_file(dir, "LICENSE", "MIT\n");
}
#[test]
fn recursive_scan_collects_each_root() {
let tmp = tempdir().unwrap();
let root = tmp.path();
for name in ["alpha", "beta"] {
write_minimal_cargo_project(&root.join(name));
}
let multi =
scan_paths_recursive(root, &DiscoverOptions::default()).expect("recursive scan succeeds");
assert_eq!(multi.roots.len(), 2);
assert_eq!(multi.summary.total, 2);
assert_eq!(multi.skipped.len(), 0);
let path_tails: Vec<String> = multi
.roots
.iter()
.map(|scan| {
scan.root
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
.to_owned()
})
.collect();
assert!(path_tails.iter().any(|name| name == "alpha"));
assert!(path_tails.iter().any(|name| name == "beta"));
}
#[test]
fn recursive_summary_aggregates_grade_and_kind() {
let tmp = tempdir().unwrap();
let root = tmp.path();
write_minimal_cargo_project(&root.join("alpha"));
write_minimal_cargo_project(&root.join("beta"));
let multi = scan_paths_recursive(root, &DiscoverOptions::default()).unwrap();
let cargo_count = multi
.summary
.by_kind
.iter()
.filter(|(kind, _)| matches!(kind, projd_core::RootKind::CargoPackage))
.map(|(_, count)| *count)
.sum::<usize>();
assert!(cargo_count >= 2);
assert!(multi.summary.by_grade.values().sum::<usize>() == multi.roots.len());
assert!(multi.summary.files_scanned >= 2);
}
#[test]
fn recursive_zero_roots_returns_empty() {
let tmp = tempdir().unwrap();
let multi = scan_paths_recursive(tmp.path(), &DiscoverOptions::default()).unwrap();
assert_eq!(multi.roots.len(), 0);
assert_eq!(multi.summary.total, 0);
assert_eq!(multi.skipped.len(), 0);
}
#[test]
fn recursive_respects_include_kind_filter() {
let tmp = tempdir().unwrap();
let root = tmp.path();
write_minimal_cargo_project(&root.join("rust_one"));
let npm = root.join("node_one");
fs::create_dir_all(&npm).unwrap();
write_file(&npm, "package.json", "{\"name\":\"node\"}\n");
let mut filter = std::collections::BTreeSet::new();
filter.insert(projd_core::RootKind::NpmPackage);
let opts = DiscoverOptions {
include_kinds: Some(filter),
..DiscoverOptions::default()
};
let multi = scan_paths_recursive(root, &opts).unwrap();
assert_eq!(multi.roots.len(), 1);
assert!(multi.roots[0].root.ends_with("node_one"));
}
#[test]
fn single_scan_flags_multiple_sibling_vcs_roots() {
let tmp = tempdir().unwrap();
let root = tmp.path();
for name in ["alpha", "beta"] {
let dir = root.join(name);
fs::create_dir_all(&dir).unwrap();
fs::create_dir(dir.join(".git")).unwrap();
write_file(
&dir,
"Cargo.toml",
"[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
);
}
let scan = scan_path(root).expect("scan succeeds");
let multi_root_finding = scan
.risks
.findings
.iter()
.find(|finding| finding.code == RiskCode::MultipleVcsRootsFound);
assert!(
multi_root_finding.is_some(),
"expected MultipleVcsRootsFound risk, got: {:#?}",
scan.risks.findings
);
}
#[test]
fn single_scan_flags_nested_vcs_root() {
let tmp = tempdir().unwrap();
let outer = tmp.path();
fs::create_dir(outer.join(".git")).unwrap();
write_file(
outer,
"Cargo.toml",
"[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
);
let inner = outer.join("third_party/dep");
fs::create_dir_all(&inner).unwrap();
fs::create_dir(inner.join(".git")).unwrap();
write_file(
&inner,
"Cargo.toml",
"[package]\nname=\"d\"\nversion=\"0.1.0\"\n",
);
let scan = scan_path(outer).expect("scan succeeds");
let nested_finding = scan
.risks
.findings
.iter()
.find(|finding| finding.code == RiskCode::NestedVcsRoot);
assert!(
nested_finding.is_some(),
"expected NestedVcsRoot risk, got: {:#?}",
scan.risks.findings
);
}
#[test]
fn single_project_does_not_trigger_multi_root_risk() {
let tmp = tempdir().unwrap();
write_minimal_cargo_project(tmp.path());
let scan = scan_path(tmp.path()).expect("scan succeeds");
assert!(
!scan.has_risk(RiskCode::MultipleVcsRootsFound),
"single project should not trigger MultipleVcsRootsFound"
);
}
#[test]
fn recursive_scan_health_grade_matches_individual_scans() {
let tmp = tempdir().unwrap();
let root = tmp.path();
write_minimal_cargo_project(&root.join("alpha"));
let multi = scan_paths_recursive(root, &DiscoverOptions::default()).unwrap();
assert_eq!(multi.roots.len(), 1);
let single = scan_path(&multi.roots[0].root).unwrap();
assert_eq!(multi.roots[0].health.grade, single.health.grade);
assert!(matches!(
multi.roots[0].health.grade,
ProjectHealth::Healthy | ProjectHealth::NeedsAttention | ProjectHealth::Risky
));
}