use std::fs;
use std::path::Path;
use projd_core::{DependencyEcosystem, DependencySource, render_markdown, scan_path};
use tempfile::TempDir;
#[test]
fn counts_rust_dependencies_from_cargo_manifest() {
let fixture = ProjectFixture::new();
fixture.write(
"Cargo.toml",
r#"
[package]
name = "demo"
version = "0.1.0"
[dependencies]
anyhow = "1"
serde = { version = "1", optional = true }
[dev-dependencies]
tempfile = "3"
[build-dependencies]
cc = "1"
"#,
);
fixture.write("Cargo.lock", "# lock\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
let summary = scan
.dependencies
.ecosystems
.iter()
.find(|summary| summary.ecosystem == DependencyEcosystem::Rust)
.expect("rust dependency summary exists");
assert_eq!(summary.source, DependencySource::CargoToml);
assert!(summary.manifest.ends_with("Cargo.toml"));
assert!(
summary
.lockfile
.as_ref()
.is_some_and(|path| path.ends_with("Cargo.lock"))
);
assert_eq!(summary.normal, 2);
assert_eq!(summary.development, 1);
assert_eq!(summary.build, 1);
assert_eq!(summary.optional, 1);
assert_eq!(summary.total, 4);
assert_eq!(scan.dependencies.total_manifests, 1);
assert_eq!(scan.dependencies.total_dependencies, 4);
}
#[test]
fn counts_node_dependencies_from_package_json() {
let fixture = ProjectFixture::new();
fixture.write(
"package.json",
r#"{
"name": "demo",
"version": "0.1.0",
"dependencies": {"react": "19"},
"devDependencies": {"vitest": "latest"},
"optionalDependencies": {"fsevents": "latest"}
}
"#,
);
fixture.write("pnpm-lock.yaml", "lockfileVersion: '9.0'\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
let summary = scan
.dependencies
.ecosystems
.iter()
.find(|summary| summary.ecosystem == DependencyEcosystem::Node)
.expect("node dependency summary exists");
assert_eq!(summary.source, DependencySource::PackageJson);
assert!(summary.manifest.ends_with("package.json"));
assert!(
summary
.lockfile
.as_ref()
.is_some_and(|path| path.ends_with("pnpm-lock.yaml"))
);
assert_eq!(summary.normal, 1);
assert_eq!(summary.development, 1);
assert_eq!(summary.build, 0);
assert_eq!(summary.optional, 1);
assert_eq!(summary.total, 3);
}
#[test]
fn counts_python_pyproject_dependencies() {
let fixture = ProjectFixture::new();
fixture.write(
"pyproject.toml",
r#"
[project]
name = "demo"
version = "0.1.0"
dependencies = ["requests", "pydantic"]
[project.optional-dependencies]
dev = ["pytest", "ruff"]
docs = ["mkdocs"]
"#,
);
fixture.write("uv.lock", "# lock\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
let summary = scan
.dependencies
.ecosystems
.iter()
.find(|summary| summary.source == DependencySource::PyprojectToml)
.expect("pyproject dependency summary exists");
assert_eq!(summary.ecosystem, DependencyEcosystem::Python);
assert!(summary.manifest.ends_with("pyproject.toml"));
assert!(
summary
.lockfile
.as_ref()
.is_some_and(|path| path.ends_with("uv.lock"))
);
assert_eq!(summary.normal, 2);
assert_eq!(summary.development, 2);
assert_eq!(summary.build, 0);
assert_eq!(summary.optional, 3);
assert_eq!(summary.total, 5);
}
#[test]
fn counts_requirements_txt_dependencies() {
let fixture = ProjectFixture::new();
fixture.write(
"requirements.txt",
"requests\n# comment\n\npytest>=8\n-r extra.txt\n--index-url https://example.invalid/simple\n",
);
let scan = scan_path(fixture.path()).expect("scan succeeds");
let summary = scan
.dependencies
.ecosystems
.iter()
.find(|summary| summary.source == DependencySource::RequirementsTxt)
.expect("requirements dependency summary exists");
assert_eq!(summary.ecosystem, DependencyEcosystem::Python);
assert_eq!(summary.normal, 2);
assert_eq!(summary.development, 0);
assert_eq!(summary.total, 2);
}
#[test]
fn ignores_dependency_manifests_excluded_by_ignore_files() {
let fixture = ProjectFixture::new();
fixture.write(".gitignore", "vendor/\n");
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
fixture.write(
"vendor/package.json",
"{\"dependencies\":{\"ignored\":\"1\"}}\n",
);
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(
scan.dependencies
.ecosystems
.iter()
.all(|summary| summary.ecosystem != DependencyEcosystem::Node)
);
assert_eq!(scan.dependencies.total_manifests, 1);
}
#[test]
fn renders_dependencies_in_markdown_report() {
let fixture = ProjectFixture::new();
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 report = render_markdown(&scan);
assert!(report.contains("## Dependencies"));
assert!(report.contains("- Total manifests: 1"));
assert!(report.contains("- Total dependency entries: 1"));
assert!(report.contains("Rust"));
assert!(report.contains("normal 1"));
}
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");
}
}