1use anyhow::Result;
2use sha2::{Digest, Sha256};
3use std::path::{Path, PathBuf};
4
5pub fn expand_globs(patterns: &[String], root: &Path) -> Vec<PathBuf> {
12 let mut paths = Vec::new();
13 for pattern in patterns {
14 let full = root.join(pattern).to_string_lossy().into_owned();
15 if let Ok(entries) = glob::glob(&full) {
16 for entry in entries.flatten() {
17 if entry.is_file() {
18 paths.push(entry);
19 }
20 }
21 }
22 }
23 paths.sort();
24 paths.dedup();
25 paths
26}
27
28pub fn read_inputs(paths: &[PathBuf]) -> Vec<(PathBuf, Vec<u8>)> {
31 paths
32 .iter()
33 .filter_map(|p| std::fs::read(p).ok().map(|c| (p.clone(), c)))
34 .collect()
35}
36
37pub fn compute_key(
46 run: &str,
47 extra_keys: &[String],
48 input_files: &[(PathBuf, Vec<u8>)],
49) -> String {
50 let mut hasher = Sha256::new();
51
52 hasher.update(run.as_bytes());
53
54 for k in extra_keys {
55 hasher.update(b"\x00key\x00");
56 hasher.update(k.as_bytes());
57 }
58
59 for (path, content) in input_files {
60 hasher.update(b"\x00file\x00");
61 hasher.update(path.to_string_lossy().as_bytes());
62 hasher.update(b"\x00");
63 hasher.update(content);
64 }
65
66 format!("{:x}", hasher.finalize())
67}
68
69pub fn is_hit(key: &str, cache_dir: &Path) -> bool {
76 cache_dir.join(format!("{}.ok", key)).exists()
77}
78
79pub fn record_hit(key: &str, cache_dir: &Path) -> Result<()> {
81 std::fs::create_dir_all(cache_dir)?;
82 std::fs::write(cache_dir.join(format!("{}.ok", key)), b"")?;
84 Ok(())
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use tempfile::TempDir;
91
92 #[test]
93 fn test_compute_key_is_deterministic() {
94 let inputs = vec![(
95 std::path::PathBuf::from("src/main.rs"),
96 b"fn main() {}".to_vec(),
97 )];
98 let key1 = compute_key("cargo build", &[], &inputs);
99 let key2 = compute_key("cargo build", &[], &inputs);
100 assert_eq!(key1, key2);
101 }
102
103 #[test]
104 fn test_compute_key_differs_on_run_change() {
105 let inputs: Vec<(std::path::PathBuf, Vec<u8>)> = vec![];
106 let key1 = compute_key("cargo build", &[], &inputs);
107 let key2 = compute_key("cargo test", &[], &inputs);
108 assert_ne!(key1, key2);
109 }
110
111 #[test]
112 fn test_compute_key_differs_on_file_content_change() {
113 let inputs1 = vec![(
114 std::path::PathBuf::from("src/main.rs"),
115 b"fn main() { println!(\"v1\"); }".to_vec(),
116 )];
117 let inputs2 = vec![(
118 std::path::PathBuf::from("src/main.rs"),
119 b"fn main() { println!(\"v2\"); }".to_vec(),
120 )];
121 let key1 = compute_key("build", &[], &inputs1);
122 let key2 = compute_key("build", &[], &inputs2);
123 assert_ne!(key1, key2);
124 }
125
126 #[test]
127 fn test_compute_key_differs_on_extra_keys() {
128 let inputs: Vec<(std::path::PathBuf, Vec<u8>)> = vec![];
129 let key1 = compute_key("build", &["1.70.0".to_string()], &inputs);
130 let key2 = compute_key("build", &["1.71.0".to_string()], &inputs);
131 assert_ne!(key1, key2);
132 }
133
134 #[test]
135 fn test_compute_key_is_hex_string() {
136 let inputs: Vec<(std::path::PathBuf, Vec<u8>)> = vec![];
137 let key = compute_key("build", &[], &inputs);
138 assert_eq!(key.len(), 64); assert!(key.chars().all(|c| c.is_ascii_hexdigit()));
140 }
141
142 #[test]
143 fn test_is_hit_false_before_recording() {
144 let dir = TempDir::new().unwrap();
145 let key = "abc123";
146 assert!(!is_hit(key, dir.path()));
147 }
148
149 #[test]
150 fn test_record_hit_and_then_is_hit() {
151 let dir = TempDir::new().unwrap();
152 let key = "deadbeef";
153 assert!(!is_hit(key, dir.path()));
154 record_hit(key, dir.path()).unwrap();
155 assert!(is_hit(key, dir.path()));
156 }
157
158 #[test]
159 fn test_record_hit_creates_ok_file() {
160 let dir = TempDir::new().unwrap();
161 record_hit("myhash", dir.path()).unwrap();
162 assert!(dir.path().join("myhash.ok").exists());
163 }
164
165 #[test]
166 fn test_is_hit_different_keys_independent() {
167 let dir = TempDir::new().unwrap();
168 record_hit("key1", dir.path()).unwrap();
169 assert!(is_hit("key1", dir.path()));
170 assert!(!is_hit("key2", dir.path()));
171 }
172
173 #[test]
174 fn test_expand_globs_finds_existing_files() {
175 let dir = TempDir::new().unwrap();
176 std::fs::write(dir.path().join("a.rs"), "fn a() {}").unwrap();
177 std::fs::write(dir.path().join("b.rs"), "fn b() {}").unwrap();
178 let pattern = format!("{}/*.rs", dir.path().to_str().unwrap());
179 let found = expand_globs(&[pattern], dir.path());
180 assert_eq!(found.len(), 2);
181 }
182
183 #[test]
184 fn test_expand_globs_empty_on_no_match() {
185 let dir = TempDir::new().unwrap();
186 let pattern = format!("{}/*.nonexistent", dir.path().to_str().unwrap());
187 let found = expand_globs(&[pattern], dir.path());
188 assert!(found.is_empty());
189 }
190
191 #[test]
192 fn test_expand_globs_deduplicates() {
193 let dir = TempDir::new().unwrap();
194 std::fs::write(dir.path().join("main.rs"), "").unwrap();
195 let path_str = dir.path().to_str().unwrap();
196 let patterns = vec![
198 format!("{}/*.rs", path_str),
199 format!("{}/*.rs", path_str),
200 ];
201 let found = expand_globs(&patterns, dir.path());
202 assert_eq!(found.len(), 1);
203 }
204
205 #[test]
206 fn test_read_inputs_reads_content() {
207 let dir = TempDir::new().unwrap();
208 let path = dir.path().join("test.rs");
209 std::fs::write(&path, "hello world").unwrap();
210 let result = read_inputs(&[path.clone()]);
211 assert_eq!(result.len(), 1);
212 assert_eq!(result[0].1, b"hello world");
213 }
214
215 #[test]
216 fn test_read_inputs_skips_missing_files() {
217 let missing = std::path::PathBuf::from("/nonexistent/path/file.rs");
218 let result = read_inputs(&[missing]);
219 assert!(result.is_empty());
220 }
221}