use std::path::Path;
use fallow_core::graph_cache::{GraphCacheManifest, GraphCacheMode};
use fallow_types::source_fingerprint::SourceFingerprint;
use super::common::{create_config_with_cache, fixture_path};
fn copy_tree(src: &Path, dst: &Path) {
std::fs::create_dir_all(dst).expect("create dest dir");
for entry in std::fs::read_dir(src).expect("read fixture dir") {
let entry = entry.expect("dir entry");
let from = entry.path();
let to = dst.join(entry.file_name());
if entry.file_type().expect("file type").is_dir() {
copy_tree(&from, &to);
} else {
std::fs::copy(&from, &to).expect("copy file");
}
}
}
fn assert_cold_warm_identical(fixture: &str) {
let temp = tempfile::tempdir().expect("create temp dir");
let root = temp.path().join("project");
copy_tree(&fixture_path(fixture), &root);
let cache_dir = temp.path().join("cache");
let config = create_config_with_cache(root, cache_dir.clone());
let cold = fallow_core::analyze(&config).expect("cold analysis succeeds");
assert!(
cache_dir.join("graph-cache.bin").exists(),
"{fixture}: cold run must persist graph-cache.bin"
);
let warm = fallow_core::analyze(&config).expect("warm analysis succeeds");
let cold_json = serde_json::to_value(&cold).expect("serialize cold results");
let warm_json = serde_json::to_value(&warm).expect("serialize warm results");
assert_eq!(
cold_json, warm_json,
"{fixture}: warm (cache hit) results must be byte-identical to cold results"
);
assert_eq!(
cold.total_issues(),
warm.total_issues(),
"{fixture}: total issue count must match cold vs warm"
);
}
#[test]
fn namespace_imports_cold_vs_warm_identical() {
assert_cold_warm_identical("namespace-imports");
}
#[test]
fn barrel_exports_cold_vs_warm_identical() {
assert_cold_warm_identical("barrel-exports");
}
#[test]
fn cross_package_members_cold_vs_warm_identical() {
assert_cold_warm_identical("cross-package-enum-class-members");
}
#[test]
fn basic_project_cold_vs_warm_identical() {
assert_cold_warm_identical("basic-project");
}
#[test]
fn source_change_misses_cache_and_reflects_change() {
let temp = tempfile::tempdir().expect("create temp dir");
let root = temp.path().join("project");
copy_tree(&fixture_path("barrel-exports"), &root);
let cache_dir = temp.path().join("cache");
let config = create_config_with_cache(root.clone(), cache_dir.clone());
let before = fallow_core::analyze(&config).expect("cold analysis");
let unused_before = before.unused_exports.len();
let target = root.join("src/module-a.ts");
let original = std::fs::read_to_string(&target).expect("read module-a");
std::thread::sleep(std::time::Duration::from_millis(10));
std::fs::write(
&target,
format!("{original}\nexport const brandNewDeadExport = 42;\n"),
)
.expect("write mutated module-a");
let files = fallow_core::discover::discover_files(&config);
let current = GraphCacheManifest::from_discovered_files(
&config.root,
&files,
GraphCacheMode::new(0, 0, 0),
|path| {
std::fs::metadata(path).map_or(SourceFingerprint::new(0, 0), |m| {
SourceFingerprint::from_metadata(&m)
})
},
);
let store = fallow_core::graph_cache::GraphCacheStore::load(&cache_dir)
.expect("persisted graph cache exists after cold run");
assert!(
!store.manifest.matches_inputs(¤t),
"a mutated source file must invalidate the persisted graph-cache manifest"
);
let after = fallow_core::analyze(&config).expect("analysis after mutation");
assert_eq!(
after.unused_exports.len(),
unused_before + 1,
"the new dead export must surface (cache must not stale-serve the old graph)"
);
}
#[test]
fn file_deletion_misses_cache_and_reflects_change() {
let temp = tempfile::tempdir().expect("create temp dir");
let root = temp.path().join("project");
copy_tree(&fixture_path("basic-project"), &root);
let cache_dir = temp.path().join("cache");
let config = create_config_with_cache(root.clone(), cache_dir.clone());
let before = fallow_core::analyze(&config).expect("cold analysis");
assert!(
before
.unused_files
.iter()
.any(|issue| issue.file.path.ends_with("src/orphan.ts")),
"fixture should expose the file that will be deleted"
);
let target = root.join("src/orphan.ts");
std::fs::remove_file(&target).expect("delete unused fixture file");
let files = fallow_core::discover::discover_files(&config);
let current = GraphCacheManifest::from_discovered_files(
&config.root,
&files,
GraphCacheMode::new(0, 0, 0),
|path| {
std::fs::metadata(path).map_or(SourceFingerprint::new(0, 0), |m| {
SourceFingerprint::from_metadata(&m)
})
},
);
let store = fallow_core::graph_cache::GraphCacheStore::load(&cache_dir)
.expect("persisted graph cache exists after cold run");
assert!(
!store.manifest.matches_inputs(¤t),
"a deleted source file must invalidate the persisted graph-cache manifest"
);
let after = fallow_core::analyze(&config).expect("analysis after deletion");
assert!(
after
.unused_files
.iter()
.all(|issue| !issue.file.path.ends_with("src/orphan.ts")),
"deleted source file must not survive through a graph-cache hit"
);
}
fn benchmark_fixture_path(name: &str) -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("benchmarks")
.join("fixtures")
.join("real-world")
.join(name)
}
fn assert_benchmark_cold_warm_total(name: &str) {
let fixture = benchmark_fixture_path(name);
if !fixture.exists() {
return;
}
let cache = tempfile::tempdir().expect("create temp cache dir");
let config = create_config_with_cache(fixture, cache.path().to_path_buf());
let cold = fallow_core::analyze(&config).expect("cold benchmark analysis");
assert!(
cache.path().join("graph-cache.bin").exists(),
"{name}: cold run must persist graph-cache.bin"
);
let warm = fallow_core::analyze(&config).expect("warm benchmark analysis");
assert_eq!(
cold.total_issues(),
warm.total_issues(),
"{name}: total_issues must be identical cold vs warm"
);
}
#[test]
fn benchmark_preact_cold_vs_warm_total_identical() {
assert_benchmark_cold_warm_total("preact");
}
#[test]
fn benchmark_zod_cold_vs_warm_total_identical() {
assert_benchmark_cold_warm_total("zod");
}
#[test]
fn manifest_matches_only_on_identical_inputs() {
let temp = tempfile::tempdir().expect("create temp dir");
let root = temp.path().join("project");
copy_tree(&fixture_path("namespace-imports"), &root);
let config = create_config_with_cache(root, temp.path().join("cache"));
let files = fallow_core::discover::discover_files(&config);
let fingerprint_provider = |path: &Path| {
std::fs::metadata(path).map_or(SourceFingerprint::new(0, 0), |m| {
SourceFingerprint::from_metadata(&m)
})
};
let manifest_a = GraphCacheManifest::from_discovered_files(
&config.root,
&files,
GraphCacheMode::new(1, 2, 3),
fingerprint_provider,
);
let manifest_same = GraphCacheManifest::from_discovered_files(
&config.root,
&files,
GraphCacheMode::new(1, 2, 3),
fingerprint_provider,
);
let manifest_other_mode = GraphCacheManifest::from_discovered_files(
&config.root,
&files,
GraphCacheMode::new(1, 99, 3),
fingerprint_provider,
);
assert!(manifest_a.matches_inputs(&manifest_same));
assert!(!manifest_a.matches_inputs(&manifest_other_mode));
}