use std::path::Path;
use fallow_config::ResolvedConfig;
use fallow_types::cache_rejection::CacheRejection;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseCacheStatus {
pub rejection: Option<CacheRejection>,
pub size_bytes: Option<u64>,
}
#[must_use]
pub fn inspect_parse_cache(config: &ResolvedConfig) -> ParseCacheStatus {
let size_bytes = cache_file_size(&config.cache_dir);
let rejection = fallow_extract::cache::CacheStore::load(
&config.cache_dir,
&config.root,
fallow_config::cache_config_hash(&config.external_plugins, &config.flags),
crate::project_config::resolve_cache_max_size_bytes(config),
)
.err();
ParseCacheStatus {
rejection,
size_bytes,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GraphCacheStatus {
pub rejection: Option<CacheRejection>,
pub size_bytes: Option<u64>,
}
#[must_use]
pub fn inspect_graph_cache(config: &ResolvedConfig) -> GraphCacheStatus {
let size_bytes = cache_entry_size(&config.cache_dir, fallow_graph::cache::GRAPH_CACHE_FILE);
let rejection = match fallow_graph::cache::GraphCacheStore::load(&config.cache_dir) {
Ok(store) => (store.manifest.root != config.root).then_some(CacheRejection::RootMismatch),
Err(rejection) => Some(rejection),
};
GraphCacheStatus {
rejection,
size_bytes,
}
}
fn cache_file_size(cache_dir: &Path) -> Option<u64> {
cache_entry_size(cache_dir, "cache.bin")
}
fn cache_entry_size(cache_dir: &Path, file_name: &str) -> Option<u64> {
std::fs::metadata(cache_dir.join(file_name))
.ok()
.map(|metadata| metadata.len())
}
#[cfg(test)]
mod tests {
use super::*;
use fallow_config::{FallowConfig, OutputFormat};
fn config_for(root: &Path, no_cache: bool) -> ResolvedConfig {
FallowConfig::default().resolve(
root.to_path_buf(),
OutputFormat::Json,
1,
no_cache,
true,
None,
)
}
#[test]
fn an_absent_cache_reports_absent_with_no_size() {
let root = tempfile::tempdir().expect("temp root");
let status = inspect_parse_cache(&config_for(root.path(), true));
assert_eq!(status.rejection, Some(CacheRejection::Absent));
assert_eq!(status.size_bytes, None);
}
#[test]
fn a_cache_written_by_a_run_reads_as_reusable_from_an_inspecting_config() {
let root = tempfile::tempdir().expect("temp root");
let analysis = config_for(root.path(), false);
let mut store = fallow_extract::cache::CacheStore::new(&analysis.root);
store
.save(
&analysis.cache_dir,
analysis.cache_config_hash,
fallow_extract::cache::DEFAULT_CACHE_MAX_SIZE,
)
.expect("save cache as a run would");
let status = inspect_parse_cache(&config_for(root.path(), true));
assert_eq!(status.rejection, None);
assert!(status.size_bytes.is_some_and(|bytes| bytes > 0));
}
#[test]
fn a_foreign_cache_blob_reports_a_decode_failure_with_its_size() {
let root = tempfile::tempdir().expect("temp root");
let config = config_for(root.path(), true);
std::fs::create_dir_all(&config.cache_dir).expect("cache dir");
std::fs::write(config.cache_dir.join("cache.bin"), b"garbage").expect("foreign cache");
let status = inspect_parse_cache(&config);
assert_eq!(status.rejection, Some(CacheRejection::Undecodable));
assert_eq!(status.size_bytes, Some(7));
}
#[test]
fn a_cache_from_another_build_reports_a_format_change_with_its_size() {
let root = tempfile::tempdir().expect("temp root");
let config = config_for(root.path(), true);
std::fs::create_dir_all(&config.cache_dir).expect("cache dir");
let mut blob = Vec::from(*b"FLWX");
blob.extend_from_slice(&u32::MAX.to_le_bytes());
blob.extend_from_slice(b"payload");
std::fs::write(config.cache_dir.join("cache.bin"), &blob)
.expect("cache from another build");
let status = inspect_parse_cache(&config);
assert_eq!(status.rejection, Some(CacheRejection::VersionMismatch));
assert_eq!(status.size_bytes, Some(15));
}
#[test]
fn an_absent_graph_cache_reports_absent_with_no_size() {
let root = tempfile::tempdir().expect("temp root");
let status = inspect_graph_cache(&config_for(root.path(), true));
assert_eq!(status.rejection, Some(CacheRejection::Absent));
assert_eq!(status.size_bytes, None);
}
#[test]
fn a_foreign_graph_cache_blob_reports_a_decode_failure_with_its_size() {
let root = tempfile::tempdir().expect("temp root");
let config = config_for(root.path(), true);
std::fs::create_dir_all(&config.cache_dir).expect("cache dir");
std::fs::write(
config.cache_dir.join(fallow_graph::cache::GRAPH_CACHE_FILE),
b"garbage",
)
.expect("foreign graph cache");
let status = inspect_graph_cache(&config);
assert_eq!(status.rejection, Some(CacheRejection::Undecodable));
assert_eq!(status.size_bytes, Some(7));
}
#[test]
fn a_graph_cache_from_another_build_reports_a_format_change_with_its_size() {
let root = tempfile::tempdir().expect("temp root");
let config = config_for(root.path(), true);
std::fs::create_dir_all(&config.cache_dir).expect("cache dir");
let mut blob = Vec::from(*b"FLWG");
blob.extend_from_slice(&u32::MAX.to_le_bytes());
blob.extend_from_slice(b"payload");
std::fs::write(
config.cache_dir.join(fallow_graph::cache::GRAPH_CACHE_FILE),
&blob,
)
.expect("graph cache from another build");
let status = inspect_graph_cache(&config);
assert_eq!(status.rejection, Some(CacheRejection::VersionMismatch));
assert_eq!(status.size_bytes, Some(15));
}
#[test]
fn a_loadable_graph_from_another_root_reports_the_known_mismatch() {
let original = tempfile::tempdir().expect("original root");
let root = original.path().canonicalize().expect("canonical root");
std::fs::create_dir(root.join("src")).expect("source directory");
std::fs::write(root.join("src/index.ts"), "export const entry = 1;\n").expect("source");
crate::session::AnalysisSession::load_default(&root)
.analyze_dead_code_with_artifacts(false, true)
.expect("prime graph cache");
let original_config = config_for(&root, true);
assert_eq!(inspect_graph_cache(&original_config).rejection, None);
let relocated = tempfile::tempdir().expect("relocated root");
let mut relocated_config = config_for(relocated.path(), true);
relocated_config.cache_dir = original_config.cache_dir;
assert_eq!(
inspect_graph_cache(&relocated_config).rejection,
Some(CacheRejection::RootMismatch)
);
}
#[test]
fn unreadable_cache_paths_are_not_reported_as_absent() {
let root = tempfile::tempdir().expect("temp root");
let config = config_for(root.path(), true);
for name in ["cache.bin", fallow_graph::cache::GRAPH_CACHE_FILE] {
std::fs::create_dir_all(config.cache_dir.join(name)).expect("unreadable cache path");
}
assert_eq!(
inspect_parse_cache(&config).rejection,
Some(CacheRejection::Unreadable)
);
assert_eq!(
inspect_graph_cache(&config).rejection,
Some(CacheRejection::Unreadable)
);
}
}