Skip to main content

skipper_core/
cache.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use crate::error::Result;
6use crate::logger::{log, warn};
7
8const CACHE_FILE_NAME: &str = "cache.json";
9
10/// Temp directory shared between the orchestrating process (`cargo skipper`)
11/// and the test binaries: it holds the resolver handoff (`cache.json`) and one
12/// JSON array of discovered test IDs per writer.
13pub struct CacheManager;
14
15impl CacheManager {
16    /// Create `skipper-*` in the temp dir and write `cache.json` into it.
17    /// Point `SKIPPER_WORKER_CACHE_FILE` at the returned file and
18    /// `SKIPPER_DISCOVERED_DIR` at its parent.
19    pub fn write_resolver_cache(data: &[u8]) -> Result<PathBuf> {
20        let dir = std::env::temp_dir().join(format!("skipper-{}", unique()));
21        std::fs::create_dir_all(&dir)?;
22        let file = dir.join(CACHE_FILE_NAME);
23        std::fs::write(&file, data)?;
24        log(format!("wrote resolver cache to {}", file.display()));
25        Ok(file)
26    }
27
28    /// Write `ids` to a uniquely named file in `dir`.
29    pub fn write_discovered_ids(dir: &Path, ids: &[String]) -> Result<()> {
30        let file = dir.join(format!("{}.json", unique()));
31        std::fs::write(file, serde_json::to_vec(ids)?)?;
32        Ok(())
33    }
34
35    /// Merge every discovered-ID file (sorted by name), deduplicated on the exact string.
36    pub fn merge_discovered_ids(dir: &Path) -> Vec<String> {
37        let mut files: Vec<PathBuf> = std::fs::read_dir(dir)
38            .map(|rd| rd.filter_map(|e| e.ok().map(|e| e.path())).collect())
39            .unwrap_or_default();
40        files.retain(|p| {
41            p.extension().is_some_and(|e| e == "json")
42                && p.file_name().is_some_and(|n| n != CACHE_FILE_NAME)
43        });
44        files.sort();
45
46        let mut seen = HashSet::new();
47        let mut out = Vec::new();
48        for f in files {
49            match std::fs::read(&f)
50                .map_err(crate::Error::from)
51                .and_then(|b| serde_json::from_slice::<Vec<String>>(&b).map_err(Into::into))
52            {
53                Ok(ids) => out.extend(ids.into_iter().filter(|id| seen.insert(id.clone()))),
54                Err(e) => warn(format!("cannot read discovered file {}: {e}", f.display())),
55            }
56        }
57        out
58    }
59
60    pub fn cleanup(dir: &Path) {
61        let _ = std::fs::remove_dir_all(dir);
62    }
63}
64
65/// `{pid}-{nanos}-{counter}`: unique across processes and threads.
66fn unique() -> String {
67    use std::sync::atomic::{AtomicU64, Ordering};
68    static COUNTER: AtomicU64 = AtomicU64::new(0);
69    let nanos = SystemTime::now()
70        .duration_since(UNIX_EPOCH)
71        .map(|d| d.as_nanos())
72        .unwrap_or_default();
73    format!(
74        "{}-{nanos}-{:08x}",
75        std::process::id(),
76        COUNTER.fetch_add(1, Ordering::Relaxed)
77    )
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn roundtrip_and_merge() {
86        let file = CacheManager::write_resolver_cache(b"{}").unwrap();
87        let dir = file.parent().unwrap();
88        CacheManager::write_discovered_ids(dir, &["a".into(), "b".into()]).unwrap();
89        CacheManager::write_discovered_ids(dir, &["b".into(), "c".into()]).unwrap();
90        std::fs::write(dir.join("broken.json"), "nope").unwrap();
91        let mut ids = CacheManager::merge_discovered_ids(dir);
92        ids.sort();
93        assert_eq!(ids, ["a", "b", "c"]);
94        CacheManager::cleanup(dir);
95        assert!(!dir.exists());
96    }
97}