fallow_engine/
cache_status.rs1use std::path::Path;
15
16use fallow_config::ResolvedConfig;
17use fallow_types::cache_rejection::CacheRejection;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ParseCacheStatus {
22 pub rejection: Option<CacheRejection>,
25 pub size_bytes: Option<u64>,
27}
28
29#[must_use]
43pub fn inspect_parse_cache(config: &ResolvedConfig) -> ParseCacheStatus {
44 let size_bytes = cache_file_size(&config.cache_dir);
45 let rejection = fallow_extract::cache::CacheStore::load(
46 &config.cache_dir,
47 &config.root,
48 fallow_config::cache_config_hash(&config.external_plugins, &config.flags),
49 crate::project_config::resolve_cache_max_size_bytes(config),
50 )
51 .err();
52 ParseCacheStatus {
53 rejection,
54 size_bytes,
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct GraphCacheStatus {
61 pub rejection: Option<CacheRejection>,
69 pub size_bytes: Option<u64>,
71}
72
73#[must_use]
79pub fn inspect_graph_cache(config: &ResolvedConfig) -> GraphCacheStatus {
80 let size_bytes = cache_entry_size(&config.cache_dir, fallow_graph::cache::GRAPH_CACHE_FILE);
81 let rejection = match fallow_graph::cache::GraphCacheStore::load(&config.cache_dir) {
82 Ok(store) => (store.manifest.root != config.root).then_some(CacheRejection::RootMismatch),
83 Err(rejection) => Some(rejection),
84 };
85 GraphCacheStatus {
86 rejection,
87 size_bytes,
88 }
89}
90
91fn cache_file_size(cache_dir: &Path) -> Option<u64> {
92 cache_entry_size(cache_dir, "cache.bin")
93}
94
95fn cache_entry_size(cache_dir: &Path, file_name: &str) -> Option<u64> {
96 std::fs::metadata(cache_dir.join(file_name))
97 .ok()
98 .map(|metadata| metadata.len())
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 use fallow_config::{FallowConfig, OutputFormat};
106
107 fn config_for(root: &Path, no_cache: bool) -> ResolvedConfig {
108 FallowConfig::default().resolve(
109 root.to_path_buf(),
110 OutputFormat::Json,
111 1,
112 no_cache,
113 true,
114 None,
115 )
116 }
117
118 #[test]
119 fn an_absent_cache_reports_absent_with_no_size() {
120 let root = tempfile::tempdir().expect("temp root");
121 let status = inspect_parse_cache(&config_for(root.path(), true));
122
123 assert_eq!(status.rejection, Some(CacheRejection::Absent));
124 assert_eq!(status.size_bytes, None);
125 }
126
127 #[test]
132 fn a_cache_written_by_a_run_reads_as_reusable_from_an_inspecting_config() {
133 let root = tempfile::tempdir().expect("temp root");
134 let analysis = config_for(root.path(), false);
135 let mut store = fallow_extract::cache::CacheStore::new(&analysis.root);
136 store
137 .save(
138 &analysis.cache_dir,
139 analysis.cache_config_hash,
140 fallow_extract::cache::DEFAULT_CACHE_MAX_SIZE,
141 )
142 .expect("save cache as a run would");
143
144 let status = inspect_parse_cache(&config_for(root.path(), true));
145
146 assert_eq!(status.rejection, None);
147 assert!(status.size_bytes.is_some_and(|bytes| bytes > 0));
148 }
149
150 #[test]
153 fn a_foreign_cache_blob_reports_a_decode_failure_with_its_size() {
154 let root = tempfile::tempdir().expect("temp root");
155 let config = config_for(root.path(), true);
156 std::fs::create_dir_all(&config.cache_dir).expect("cache dir");
157 std::fs::write(config.cache_dir.join("cache.bin"), b"garbage").expect("foreign cache");
158
159 let status = inspect_parse_cache(&config);
160
161 assert_eq!(status.rejection, Some(CacheRejection::Undecodable));
162 assert_eq!(status.size_bytes, Some(7));
163 }
164
165 #[test]
174 fn a_cache_from_another_build_reports_a_format_change_with_its_size() {
175 let root = tempfile::tempdir().expect("temp root");
176 let config = config_for(root.path(), true);
177 std::fs::create_dir_all(&config.cache_dir).expect("cache dir");
178 let mut blob = Vec::from(*b"FLWX");
179 blob.extend_from_slice(&u32::MAX.to_le_bytes());
180 blob.extend_from_slice(b"payload");
181 std::fs::write(config.cache_dir.join("cache.bin"), &blob)
182 .expect("cache from another build");
183
184 let status = inspect_parse_cache(&config);
185
186 assert_eq!(status.rejection, Some(CacheRejection::VersionMismatch));
187 assert_eq!(status.size_bytes, Some(15));
188 }
189
190 #[test]
191 fn an_absent_graph_cache_reports_absent_with_no_size() {
192 let root = tempfile::tempdir().expect("temp root");
193 let status = inspect_graph_cache(&config_for(root.path(), true));
194
195 assert_eq!(status.rejection, Some(CacheRejection::Absent));
196 assert_eq!(status.size_bytes, None);
197 }
198
199 #[test]
202 fn a_foreign_graph_cache_blob_reports_a_decode_failure_with_its_size() {
203 let root = tempfile::tempdir().expect("temp root");
204 let config = config_for(root.path(), true);
205 std::fs::create_dir_all(&config.cache_dir).expect("cache dir");
206 std::fs::write(
207 config.cache_dir.join(fallow_graph::cache::GRAPH_CACHE_FILE),
208 b"garbage",
209 )
210 .expect("foreign graph cache");
211
212 let status = inspect_graph_cache(&config);
213
214 assert_eq!(status.rejection, Some(CacheRejection::Undecodable));
215 assert_eq!(status.size_bytes, Some(7));
216 }
217
218 #[test]
219 fn a_graph_cache_from_another_build_reports_a_format_change_with_its_size() {
220 let root = tempfile::tempdir().expect("temp root");
221 let config = config_for(root.path(), true);
222 std::fs::create_dir_all(&config.cache_dir).expect("cache dir");
223 let mut blob = Vec::from(*b"FLWG");
224 blob.extend_from_slice(&u32::MAX.to_le_bytes());
225 blob.extend_from_slice(b"payload");
226 std::fs::write(
227 config.cache_dir.join(fallow_graph::cache::GRAPH_CACHE_FILE),
228 &blob,
229 )
230 .expect("graph cache from another build");
231
232 let status = inspect_graph_cache(&config);
233
234 assert_eq!(status.rejection, Some(CacheRejection::VersionMismatch));
235 assert_eq!(status.size_bytes, Some(15));
236 }
237
238 #[test]
239 fn a_loadable_graph_from_another_root_reports_the_known_mismatch() {
240 let original = tempfile::tempdir().expect("original root");
241 let root = original.path().canonicalize().expect("canonical root");
242 std::fs::create_dir(root.join("src")).expect("source directory");
243 std::fs::write(root.join("src/index.ts"), "export const entry = 1;\n").expect("source");
244 crate::session::AnalysisSession::load_default(&root)
245 .analyze_dead_code_with_artifacts(false, true)
246 .expect("prime graph cache");
247 let original_config = config_for(&root, true);
248 assert_eq!(inspect_graph_cache(&original_config).rejection, None);
249
250 let relocated = tempfile::tempdir().expect("relocated root");
251 let mut relocated_config = config_for(relocated.path(), true);
252 relocated_config.cache_dir = original_config.cache_dir;
253 assert_eq!(
254 inspect_graph_cache(&relocated_config).rejection,
255 Some(CacheRejection::RootMismatch)
256 );
257 }
258
259 #[test]
260 fn unreadable_cache_paths_are_not_reported_as_absent() {
261 let root = tempfile::tempdir().expect("temp root");
262 let config = config_for(root.path(), true);
263 for name in ["cache.bin", fallow_graph::cache::GRAPH_CACHE_FILE] {
264 std::fs::create_dir_all(config.cache_dir.join(name)).expect("unreadable cache path");
265 }
266 assert_eq!(
267 inspect_parse_cache(&config).rejection,
268 Some(CacheRejection::Unreadable)
269 );
270 assert_eq!(
271 inspect_graph_cache(&config).rejection,
272 Some(CacheRejection::Unreadable)
273 );
274 }
275}