use std::fs;
use std::path::Path;
use projd_core::{ProjectHealth, RiskCode, RiskSeverity, render_markdown, scan_path};
use tempfile::TempDir;
#[test]
fn reports_missing_readme_and_license() {
let fixture = ProjectFixture::new();
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(scan.has_risk(RiskCode::MissingReadme));
assert!(scan.has_risk(RiskCode::MissingLicense));
assert_eq!(
scan.risk(RiskCode::MissingReadme).map(|risk| risk.severity),
Some(RiskSeverity::Medium)
);
assert_eq!(
scan.risk(RiskCode::MissingLicense)
.map(|risk| risk.severity),
Some(RiskSeverity::Medium)
);
assert_eq!(scan.health.grade, ProjectHealth::Risky);
}
#[test]
fn does_not_report_readme_or_license_when_present() {
let fixture = ProjectFixture::new();
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(!scan.has_risk(RiskCode::MissingReadme));
assert!(!scan.has_risk(RiskCode::MissingLicense));
}
#[test]
fn reports_unknown_license_when_license_file_is_not_recognized() {
let fixture = ProjectFixture::new();
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "custom internal license text\n");
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(!scan.has_risk(RiskCode::MissingLicense));
assert!(scan.has_risk(RiskCode::UnknownLicense));
assert_eq!(
scan.risk(RiskCode::UnknownLicense)
.map(|risk| risk.severity),
Some(RiskSeverity::Low)
);
}
#[test]
fn reports_missing_ci_and_no_tests_detected() {
let fixture = ProjectFixture::new();
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(scan.has_risk(RiskCode::MissingCi));
assert!(scan.has_risk(RiskCode::NoTestsDetected));
assert_eq!(
scan.risk(RiskCode::MissingCi).map(|risk| risk.severity),
Some(RiskSeverity::Low)
);
assert_eq!(
scan.risk(RiskCode::NoTestsDetected)
.map(|risk| risk.severity),
Some(RiskSeverity::Medium)
);
}
#[test]
fn does_not_report_no_tests_when_test_summary_exists() {
let fixture = ProjectFixture::new();
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT\n");
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
fixture.write("tests/integration.rs", "#[test]\nfn integration() {}\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(!scan.has_risk(RiskCode::NoTestsDetected));
}
#[test]
fn reports_manifest_without_lockfile() {
let fixture = ProjectFixture::new();
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT\n");
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n[dependencies]\nanyhow = \"1\"\n",
);
let scan = scan_path(fixture.path()).expect("scan succeeds");
let risk = scan
.risk(RiskCode::ManifestWithoutLockfile)
.expect("lockfile risk exists");
assert_eq!(risk.severity, RiskSeverity::Low);
assert!(
risk.path
.as_ref()
.is_some_and(|path| path.ends_with("Cargo.toml"))
);
}
#[test]
fn does_not_report_manifest_without_lockfile_when_lockfile_exists() {
let fixture = ProjectFixture::new();
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT\n");
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n[dependencies]\nanyhow = \"1\"\n",
);
fixture.write("Cargo.lock", "# lock\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(!scan.has_risk(RiskCode::ManifestWithoutLockfile));
}
#[test]
fn reports_large_project_without_ignore_rules() {
let fixture = ProjectFixture::new();
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT\n");
fixture.write(".github/workflows/ci.yml", "name: ci\n");
fixture.write("tests/integration.rs", "#[test]\nfn integration() {}\n");
for index in 0..1000 {
fixture.write(&format!("src/file_{index}.rs"), "fn example() {}\n");
}
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(scan.has_risk(RiskCode::LargeProjectWithoutIgnoreRules));
assert_eq!(
scan.risk(RiskCode::LargeProjectWithoutIgnoreRules)
.map(|risk| risk.severity),
Some(RiskSeverity::Info)
);
assert!(!scan.hygiene.has_gitignore);
assert!(!scan.hygiene.has_ignore);
}
#[test]
fn renders_risks_in_markdown_report() {
let fixture = ProjectFixture::new();
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
let scan = scan_path(fixture.path()).expect("scan succeeds");
let report = render_markdown(&scan);
assert!(report.contains("## Risks"));
assert!(report.contains("missing-license"));
}
#[test]
fn renders_no_risks_message() {
let fixture = ProjectFixture::new();
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT License\nPermission is hereby granted\n");
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n[dependencies]\nanyhow = \"1\"\n",
);
fixture.write("Cargo.lock", "# lock\n");
fixture.write(".gitignore", "target/\n");
fixture.write(".github/workflows/ci.yml", "name: ci\n");
fixture.write("tests/integration.rs", "#[test]\nfn integration() {}\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
let report = render_markdown(&scan);
assert_eq!(scan.risks.total, 0);
assert_eq!(scan.health.grade, ProjectHealth::Healthy);
assert!(report.contains("No risks detected by current rules."));
}
#[test]
fn renders_health_in_markdown_report() {
let fixture = ProjectFixture::new();
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT License\nPermission is hereby granted\n");
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
fixture.write("Cargo.lock", "# lock\n");
fixture.write(".github/workflows/ci.yml", "name: ci\n");
fixture.write("tests/integration.rs", "#[test]\nfn integration() {}\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
let report = render_markdown(&scan);
assert!(report.contains("## Health"));
assert!(report.contains("- Grade: `healthy`"));
assert!(report.contains("## Scan Observations"));
assert!(report.contains("- Files scanned:"));
}
struct ProjectFixture {
temp_dir: TempDir,
}
impl ProjectFixture {
fn new() -> Self {
Self {
temp_dir: tempfile::tempdir().expect("create temp dir"),
}
}
fn path(&self) -> &Path {
self.temp_dir.path()
}
fn write(&self, relative: &str, content: &str) {
let path = self.path().join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture parent");
}
fs::write(path, content).expect("write fixture file");
}
}