use super::facts::FileFacts;
use super::seam_classification::{ClassifiedSeam, SeamGripClassCounts};
use std::path::{Path, PathBuf};
pub(crate) const CACHE_SCHEMA_VERSION: &str = "0.2";
const COUNT_CACHE_SCHEMA_VERSION: &str = "0.1";
const FILE_FACT_CACHE_SCHEMA_VERSION: &str = "0.1";
pub(crate) const CLASSIFIED_SEAM_CACHE_STORE_LIMIT: usize = 20_000;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct RepoSeamCacheKey {
pub(crate) schema_version: String,
pub(crate) analyzer_version: String,
pub(crate) workspace_root_hash: String,
pub(crate) files_content_hash: String,
pub(crate) cfg_features_hash: String,
pub(crate) config_hash: String,
pub(crate) test_intent_hash: String,
pub(crate) suppressions_hash: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct RepoFileFactCacheKey {
schema_version: String,
analyzer_version: String,
file_path: PathBuf,
content_hash: String,
}
impl RepoFileFactCacheKey {
pub(crate) fn new(file_path: &Path, content: &[u8]) -> Self {
Self {
schema_version: FILE_FACT_CACHE_SCHEMA_VERSION.to_string(),
analyzer_version: env!("CARGO_PKG_VERSION").to_string(),
file_path: file_path.to_path_buf(),
content_hash: hash_bytes(content),
}
}
fn filename(&self) -> String {
let file_path = self.file_path.to_string_lossy();
let parts = [
self.schema_version.as_str(),
self.analyzer_version.as_str(),
file_path.as_ref(),
self.content_hash.as_str(),
];
let mut buf = String::new();
for (idx, part) in parts.iter().enumerate() {
if idx > 0 {
buf.push('\0');
}
buf.push_str(part);
}
format!("{:016x}.json", fnv1a_64(buf.as_bytes()))
}
}
impl RepoSeamCacheKey {
pub(crate) fn filename(&self) -> String {
let parts: [&str; 8] = [
&self.schema_version,
&self.analyzer_version,
&self.workspace_root_hash,
&self.files_content_hash,
&self.cfg_features_hash,
&self.config_hash,
&self.test_intent_hash,
&self.suppressions_hash,
];
let mut buf = String::new();
for (i, p) in parts.iter().enumerate() {
if i > 0 {
buf.push('\0');
}
buf.push_str(p);
}
format!("{:016x}.json", fnv1a_64(buf.as_bytes()))
}
}
#[derive(Debug)]
pub(crate) enum CacheLoad<T> {
Hit(T),
Miss,
CorruptIgnored { reason: String },
}
pub(crate) struct WorkspaceState<'a> {
pub(crate) workspace_root: &'a Path,
pub(crate) files: &'a [(PathBuf, Vec<u8>)],
pub(crate) cfg_features: Option<&'a str>,
pub(crate) config_text: Option<&'a str>,
pub(crate) test_intent_text: Option<&'a str>,
pub(crate) suppressions_text: Option<&'a str>,
}
impl<'a> WorkspaceState<'a> {
pub(crate) fn cache_key(&self) -> RepoSeamCacheKey {
let workspace_root_hash = hash_str(&self.workspace_root.to_string_lossy());
let mut sorted_files: Vec<(&PathBuf, &Vec<u8>)> =
self.files.iter().map(|(p, b)| (p, b)).collect();
sorted_files.sort_by(|a, b| a.0.cmp(b.0));
let mut files_buf = String::new();
for (path, content) in sorted_files {
files_buf.push_str(&path.to_string_lossy().replace('\\', "/"));
files_buf.push('\0');
files_buf.push_str(&hash_bytes(content));
files_buf.push('\n');
}
let files_content_hash = hash_str(&files_buf);
RepoSeamCacheKey {
schema_version: CACHE_SCHEMA_VERSION.to_string(),
analyzer_version: env!("CARGO_PKG_VERSION").to_string(),
workspace_root_hash,
files_content_hash,
cfg_features_hash: hash_str(self.cfg_features.unwrap_or("")),
config_hash: hash_str(self.config_text.unwrap_or("")),
test_intent_hash: hash_str(self.test_intent_text.unwrap_or("")),
suppressions_hash: hash_str(self.suppressions_text.unwrap_or("")),
}
}
}
pub(crate) struct RepoSeamFactCache {
dir: PathBuf,
}
impl RepoSeamFactCache {
pub(crate) fn at(workspace_root: &Path) -> Self {
Self {
dir: workspace_root
.join("target")
.join("ripr")
.join("cache")
.join("repo-seam-facts")
.join(CACHE_SCHEMA_VERSION),
}
}
#[cfg(test)]
pub(crate) fn at_dir(dir: PathBuf) -> Self {
Self { dir }
}
pub(crate) fn load_classified_seams(
&self,
key: &RepoSeamCacheKey,
) -> CacheLoad<Vec<ClassifiedSeam>> {
let path = self.entry_path(key);
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return CacheLoad::Miss,
Err(err) => {
return CacheLoad::CorruptIgnored {
reason: format!("read failed: {err}"),
};
}
};
match codec::decode(&bytes) {
Ok(envelope) => {
if envelope.matches_key(key) {
CacheLoad::Hit(envelope.classified_seams)
} else {
CacheLoad::Miss
}
}
Err(reason) => CacheLoad::CorruptIgnored { reason },
}
}
pub(crate) fn store_classified_seams(
&self,
key: &RepoSeamCacheKey,
seams: &[ClassifiedSeam],
) -> Result<(), String> {
if seams.len() > CLASSIFIED_SEAM_CACHE_STORE_LIMIT {
return Err(format!(
"skipped_large_entry_seams_{}_limit_{}",
seams.len(),
CLASSIFIED_SEAM_CACHE_STORE_LIMIT
));
}
std::fs::create_dir_all(&self.dir)
.map_err(|err| format!("create cache dir failed: {err}"))?;
let envelope = CacheEnvelope::new(key.clone(), seams.to_vec());
let bytes = codec::encode(&envelope)?;
let path = self.entry_path(key);
std::fs::write(&path, &bytes).map_err(|err| format!("write cache failed: {err}"))?;
Ok(())
}
fn entry_path(&self, key: &RepoSeamCacheKey) -> PathBuf {
self.dir.join(key.filename())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct FileFactCacheStats {
pub(crate) hits: usize,
pub(crate) misses: usize,
pub(crate) corrupt_ignored: usize,
pub(crate) stores: usize,
pub(crate) store_errors: usize,
}
impl FileFactCacheStats {
pub(crate) fn status_label(&self) -> String {
format!(
"hits_{}_misses_{}_corrupt_{}_store_errors_{}",
self.hits, self.misses, self.corrupt_ignored, self.store_errors
)
}
}
pub(crate) struct RepoFileFactCache {
dir: PathBuf,
}
impl RepoFileFactCache {
pub(crate) fn at(workspace_root: &Path) -> Self {
Self {
dir: workspace_root
.join("target")
.join("ripr")
.join("cache")
.join("repo-file-facts")
.join(FILE_FACT_CACHE_SCHEMA_VERSION),
}
}
#[cfg(test)]
pub(crate) fn at_dir(dir: PathBuf) -> Self {
Self { dir }
}
pub(crate) fn load_file_facts(&self, key: &RepoFileFactCacheKey) -> CacheLoad<FileFacts> {
let path = self.entry_path(key);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return CacheLoad::Miss,
Err(err) => {
return CacheLoad::CorruptIgnored {
reason: format!("read failed: {err}"),
};
}
};
match codec::decode_file_facts(&bytes) {
Ok(envelope) => {
if envelope.matches_key(key) {
CacheLoad::Hit(envelope.file_facts)
} else {
CacheLoad::Miss
}
}
Err(reason) => CacheLoad::CorruptIgnored { reason },
}
}
pub(crate) fn store_file_facts(
&self,
key: &RepoFileFactCacheKey,
facts: &FileFacts,
) -> Result<(), String> {
std::fs::create_dir_all(&self.dir)
.map_err(|err| format!("create file fact cache dir failed: {err}"))?;
let envelope = FileFactCacheEnvelope::new(key.clone(), facts.clone());
let bytes = codec::encode_file_facts(&envelope)?;
std::fs::write(self.entry_path(key), bytes)
.map_err(|err| format!("write file fact cache failed: {err}"))?;
Ok(())
}
fn entry_path(&self, key: &RepoFileFactCacheKey) -> PathBuf {
self.dir.join(key.filename())
}
}
pub(crate) struct RepoSeamCountCache {
dir: PathBuf,
}
impl RepoSeamCountCache {
pub(crate) fn at(workspace_root: &Path) -> Self {
Self {
dir: workspace_root
.join("target")
.join("ripr")
.join("cache")
.join("repo-seam-counts")
.join(COUNT_CACHE_SCHEMA_VERSION),
}
}
pub(crate) fn load_counts(&self, key: &RepoSeamCacheKey) -> CacheLoad<SeamGripClassCounts> {
let path = self.entry_path(key);
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return CacheLoad::Miss,
Err(err) => {
return CacheLoad::CorruptIgnored {
reason: format!("read failed: {err}"),
};
}
};
match codec::decode_counts(&bytes) {
Ok(envelope) => {
if envelope.matches_key(key) {
CacheLoad::Hit(envelope.counts)
} else {
CacheLoad::Miss
}
}
Err(reason) => CacheLoad::CorruptIgnored { reason },
}
}
pub(crate) fn store_counts(
&self,
key: &RepoSeamCacheKey,
counts: &SeamGripClassCounts,
) -> Result<(), String> {
std::fs::create_dir_all(&self.dir)
.map_err(|err| format!("create count cache dir failed: {err}"))?;
let envelope = CountCacheEnvelope::new(key.clone(), counts.clone());
let bytes = codec::encode_counts(&envelope)?;
let path = self.entry_path(key);
std::fs::write(&path, &bytes).map_err(|err| format!("write count cache failed: {err}"))?;
Ok(())
}
fn entry_path(&self, key: &RepoSeamCacheKey) -> PathBuf {
self.dir.join(key.filename())
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CacheEnvelope {
schema_version: String,
analyzer_version: String,
workspace_root_hash: String,
files_content_hash: String,
cfg_features_hash: String,
config_hash: String,
test_intent_hash: String,
suppressions_hash: String,
classified_seams: Vec<ClassifiedSeam>,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CountCacheEnvelope {
count_cache_schema_version: String,
schema_version: String,
analyzer_version: String,
workspace_root_hash: String,
files_content_hash: String,
cfg_features_hash: String,
config_hash: String,
test_intent_hash: String,
suppressions_hash: String,
counts: SeamGripClassCounts,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct FileFactCacheEnvelope {
file_fact_cache_schema_version: String,
analyzer_version: String,
file_path: PathBuf,
content_hash: String,
file_facts: FileFacts,
}
impl FileFactCacheEnvelope {
fn new(key: RepoFileFactCacheKey, file_facts: FileFacts) -> Self {
Self {
file_fact_cache_schema_version: key.schema_version,
analyzer_version: key.analyzer_version,
file_path: key.file_path,
content_hash: key.content_hash,
file_facts,
}
}
fn matches_key(&self, key: &RepoFileFactCacheKey) -> bool {
self.file_fact_cache_schema_version == key.schema_version
&& self.analyzer_version == key.analyzer_version
&& self.file_path == key.file_path
&& self.content_hash == key.content_hash
}
}
impl CountCacheEnvelope {
fn new(key: RepoSeamCacheKey, counts: SeamGripClassCounts) -> Self {
Self {
count_cache_schema_version: COUNT_CACHE_SCHEMA_VERSION.to_string(),
schema_version: key.schema_version,
analyzer_version: key.analyzer_version,
workspace_root_hash: key.workspace_root_hash,
files_content_hash: key.files_content_hash,
cfg_features_hash: key.cfg_features_hash,
config_hash: key.config_hash,
test_intent_hash: key.test_intent_hash,
suppressions_hash: key.suppressions_hash,
counts,
}
}
fn matches_key(&self, key: &RepoSeamCacheKey) -> bool {
self.count_cache_schema_version == COUNT_CACHE_SCHEMA_VERSION
&& self.schema_version == key.schema_version
&& self.analyzer_version == key.analyzer_version
&& self.workspace_root_hash == key.workspace_root_hash
&& self.files_content_hash == key.files_content_hash
&& self.cfg_features_hash == key.cfg_features_hash
&& self.config_hash == key.config_hash
&& self.test_intent_hash == key.test_intent_hash
&& self.suppressions_hash == key.suppressions_hash
}
}
impl CacheEnvelope {
fn new(key: RepoSeamCacheKey, classified_seams: Vec<ClassifiedSeam>) -> Self {
Self {
schema_version: key.schema_version,
analyzer_version: key.analyzer_version,
workspace_root_hash: key.workspace_root_hash,
files_content_hash: key.files_content_hash,
cfg_features_hash: key.cfg_features_hash,
config_hash: key.config_hash,
test_intent_hash: key.test_intent_hash,
suppressions_hash: key.suppressions_hash,
classified_seams,
}
}
fn matches_key(&self, key: &RepoSeamCacheKey) -> bool {
self.schema_version == key.schema_version
&& self.analyzer_version == key.analyzer_version
&& self.workspace_root_hash == key.workspace_root_hash
&& self.files_content_hash == key.files_content_hash
&& self.cfg_features_hash == key.cfg_features_hash
&& self.config_hash == key.config_hash
&& self.test_intent_hash == key.test_intent_hash
&& self.suppressions_hash == key.suppressions_hash
}
}
mod codec {
use super::{CacheEnvelope, CountCacheEnvelope, FileFactCacheEnvelope};
pub(super) fn encode(envelope: &CacheEnvelope) -> Result<Vec<u8>, String> {
serde_json::to_vec_pretty(envelope).map_err(|err| format!("encode failed: {err}"))
}
pub(super) fn decode(bytes: &[u8]) -> Result<CacheEnvelope, String> {
serde_json::from_slice(bytes).map_err(|err| format!("decode failed: {err}"))
}
pub(super) fn encode_counts(envelope: &CountCacheEnvelope) -> Result<Vec<u8>, String> {
serde_json::to_vec_pretty(envelope).map_err(|err| format!("encode counts failed: {err}"))
}
pub(super) fn decode_counts(bytes: &[u8]) -> Result<CountCacheEnvelope, String> {
serde_json::from_slice(bytes).map_err(|err| format!("decode counts failed: {err}"))
}
pub(super) fn encode_file_facts(envelope: &FileFactCacheEnvelope) -> Result<Vec<u8>, String> {
serde_json::to_vec_pretty(envelope)
.map_err(|err| format!("encode file facts failed: {err}"))
}
pub(super) fn decode_file_facts(bytes: &[u8]) -> Result<FileFactCacheEnvelope, String> {
serde_json::from_slice(bytes).map_err(|err| format!("decode file facts failed: {err}"))
}
}
fn hash_str(s: &str) -> String {
hash_bytes(s.as_bytes())
}
fn hash_bytes(bytes: &[u8]) -> String {
format!("{:016x}", fnv1a_64(bytes))
}
fn fnv1a_64(bytes: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash: u64 = FNV_OFFSET;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::seam_classification::ClassifiedSeam;
use crate::analysis::seams::{
ExpectedSink, RepoSeam, RequiredDiscriminator, SeamGripClass, SeamKind,
};
use crate::analysis::test_grip_evidence::TestGripEvidence;
use crate::domain::{Confidence, StageEvidence, StageState};
use std::path::PathBuf;
fn sample_classified() -> ClassifiedSeam {
let seam = RepoSeam::new(
PathBuf::from("src/foo.rs"),
"src/foo.rs::foo",
SeamKind::PredicateBoundary,
42,
10,
"x > 5".to_string(),
RequiredDiscriminator::BoundaryValue {
description: "x > 5".to_string(),
},
ExpectedSink::ReturnValue,
);
let evidence = TestGripEvidence {
seam_id: seam.id().clone(),
related_tests: Vec::new(),
reach: StageEvidence::new(StageState::Yes, Confidence::High, "reach"),
activate: StageEvidence::new(StageState::Unknown, Confidence::Medium, "activate"),
propagate: StageEvidence::new(StageState::Unknown, Confidence::Medium, "propagate"),
observe: StageEvidence::new(StageState::Weak, Confidence::Low, "observe"),
discriminate: StageEvidence::new(StageState::No, Confidence::Low, "discriminate"),
observed_values: Vec::new(),
missing_discriminators: Vec::new(),
};
ClassifiedSeam {
seam,
evidence,
class: SeamGripClass::Ungripped,
}
}
fn empty_state() -> WorkspaceState<'static> {
WorkspaceState {
workspace_root: Path::new("/repo"),
files: &[],
cfg_features: None,
config_text: None,
test_intent_text: None,
suppressions_text: None,
}
}
fn isolated_dir(label: &str) -> PathBuf {
std::env::temp_dir().join(format!("ripr-cache-{label}-{}", uuid_like()))
}
#[test]
fn given_no_cache_when_load_runs_then_miss_is_returned() -> Result<(), String> {
let dir = isolated_dir("cold");
let _ = std::fs::remove_dir_all(&dir);
let cache = RepoSeamFactCache::at_dir(dir);
let key = empty_state().cache_key();
match cache.load_classified_seams(&key) {
CacheLoad::Miss => Ok(()),
other => Err(format!("expected Miss on missing cache dir, got {other:?}")),
}
}
#[test]
fn given_unchanged_inputs_when_cache_is_warm_then_classified_seams_are_reused()
-> Result<(), String> {
let dir = isolated_dir("warm");
let _ = std::fs::remove_dir_all(&dir);
let cache = RepoSeamFactCache::at_dir(dir.clone());
let key = empty_state().cache_key();
let seams = vec![sample_classified()];
cache
.store_classified_seams(&key, &seams)
.map_err(|err| format!("store should succeed: {err}"))?;
let result = match cache.load_classified_seams(&key) {
CacheLoad::Hit(loaded) => {
if loaded.len() != seams.len() {
Err(format!(
"warm path should return stored seams, got {} vs {}",
loaded.len(),
seams.len()
))
} else if loaded[0].seam.id().as_str() != seams[0].seam.id().as_str() {
Err(format!(
"round-trip should preserve seam id, got {} vs {}",
loaded[0].seam.id().as_str(),
seams[0].seam.id().as_str()
))
} else if loaded[0].class != seams[0].class {
Err(format!(
"round-trip should preserve class, got {:?} vs {:?}",
loaded[0].class, seams[0].class
))
} else {
Ok(())
}
}
other => Err(format!("expected Hit on warm cache, got {other:?}")),
};
let _ = std::fs::remove_dir_all(&dir);
result
}
#[test]
fn given_large_classified_entry_when_cache_store_runs_then_write_is_skipped()
-> Result<(), String> {
let dir = isolated_dir("large-skip");
let _ = std::fs::remove_dir_all(&dir);
let cache = RepoSeamFactCache::at_dir(dir.clone());
let key = empty_state().cache_key();
let seams = vec![sample_classified(); CLASSIFIED_SEAM_CACHE_STORE_LIMIT + 1];
let err = match cache.store_classified_seams(&key, &seams) {
Ok(()) => return Err("large classified seam cache entries should be skipped".into()),
Err(err) => err,
};
assert!(
err.contains("skipped_large_entry_seams_"),
"skip reason should be machine-readable: {err}"
);
assert!(
!cache.entry_path(&key).exists(),
"skipped cache store should not write a classified seam entry"
);
let _ = std::fs::remove_dir_all(&dir);
Ok(())
}
#[test]
fn given_changed_file_content_hash_when_cache_is_loaded_then_old_entry_is_treated_as_miss()
-> Result<(), String> {
let dir = isolated_dir("changed");
let _ = std::fs::remove_dir_all(&dir);
let cache = RepoSeamFactCache::at_dir(dir.clone());
let path = PathBuf::from("src/foo.rs");
let original_files = [(path.clone(), b"fn foo() {}\n".to_vec())];
let original_key = WorkspaceState {
workspace_root: Path::new("/repo"),
files: &original_files,
cfg_features: None,
config_text: None,
test_intent_text: None,
suppressions_text: None,
}
.cache_key();
cache
.store_classified_seams(&original_key, &[sample_classified()])
.map_err(|err| format!("store original: {err}"))?;
let new_files = [(path, b"fn foo() { let x = 1; }\n".to_vec())];
let new_key = WorkspaceState {
workspace_root: Path::new("/repo"),
files: &new_files,
cfg_features: None,
config_text: None,
test_intent_text: None,
suppressions_text: None,
}
.cache_key();
if original_key.files_content_hash == new_key.files_content_hash {
return Err("different file content must produce different files_content_hash".into());
}
let result = match cache.load_classified_seams(&new_key) {
CacheLoad::Miss => Ok(()),
other => Err(format!(
"expected Miss after file content change, got {other:?}"
)),
};
let _ = std::fs::remove_dir_all(&dir);
result
}
#[test]
fn given_test_file_content_changes_when_cache_key_is_built_then_classified_seam_cache_is_invalidated()
-> Result<(), String> {
let prod = PathBuf::from("src/foo.rs");
let prod_bytes = b"pub fn foo() -> i32 { 1 }\n".to_vec();
let test_path = PathBuf::from("tests/foo_test.rs");
let baseline_files = [
(prod.clone(), prod_bytes.clone()),
(
test_path.clone(),
b"#[test] fn smoke() { assert_eq!(1, 1); }\n".to_vec(),
),
];
let baseline = WorkspaceState {
workspace_root: Path::new("/repo"),
files: &baseline_files,
cfg_features: None,
config_text: None,
test_intent_text: None,
suppressions_text: None,
}
.cache_key();
let updated_files = [
(prod, prod_bytes),
(
test_path,
b"#[test] fn smoke() { assert_eq!(super::foo(), 1); }\n".to_vec(),
),
];
let updated = WorkspaceState {
workspace_root: Path::new("/repo"),
files: &updated_files,
cfg_features: None,
config_text: None,
test_intent_text: None,
suppressions_text: None,
}
.cache_key();
if baseline.files_content_hash == updated.files_content_hash {
return Err(
"test-only file content change must change files_content_hash so stale \
TestGripEvidence cannot survive in the cache"
.into(),
);
}
if baseline.filename() == updated.filename() {
return Err(
"test-only file content change must produce a different cache filename".into(),
);
}
Ok(())
}
#[test]
fn given_test_intent_hash_change_when_cache_is_loaded_then_classified_seam_cache_is_invalidated()
-> Result<(), String> {
let baseline = WorkspaceState {
test_intent_text: Some(""),
..empty_state()
}
.cache_key();
let updated = WorkspaceState {
test_intent_text: Some("[[test]] name = \"smoke\""),
..empty_state()
}
.cache_key();
if baseline.test_intent_hash == updated.test_intent_hash {
return Err("different test intent must produce different test_intent_hash".into());
}
if baseline.filename() == updated.filename() {
return Err(
"different test_intent_hash must produce a different cache filename".into(),
);
}
Ok(())
}
#[test]
fn given_suppression_hash_change_when_cache_is_loaded_then_classified_seam_cache_is_invalidated()
-> Result<(), String> {
let baseline = WorkspaceState {
suppressions_text: Some(""),
..empty_state()
}
.cache_key();
let updated = WorkspaceState {
suppressions_text: Some("[[suppression]] kind = \"exposure_gap\""),
..empty_state()
}
.cache_key();
if baseline.suppressions_hash == updated.suppressions_hash {
return Err(
"different suppressions text must produce different suppressions_hash".into(),
);
}
if baseline.filename() == updated.filename() {
return Err(
"different suppressions_hash must produce a different cache filename".into(),
);
}
Ok(())
}
#[test]
fn given_corrupt_cache_entry_when_loading_then_corrupt_ignored_is_reported_without_failing()
-> Result<(), String> {
let dir = isolated_dir("corrupt");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).map_err(|err| format!("mkdir: {err}"))?;
let cache = RepoSeamFactCache::at_dir(dir.clone());
let key = empty_state().cache_key();
let path = cache.entry_path(&key);
std::fs::write(&path, b"{not valid json")
.map_err(|err| format!("write corrupt entry: {err}"))?;
let result = match cache.load_classified_seams(&key) {
CacheLoad::CorruptIgnored { reason } => {
if !reason.contains("decode failed") {
Err(format!(
"corrupt reason should explain decode failure, got {reason}"
))
} else {
Ok(())
}
}
other => Err(format!(
"expected CorruptIgnored on bad json, got {other:?}"
)),
};
let _ = std::fs::remove_dir_all(&dir);
result
}
#[test]
fn given_envelope_key_mismatch_when_loading_then_miss_is_returned_without_failing()
-> Result<(), String> {
let dir = isolated_dir("keymismatch");
let _ = std::fs::remove_dir_all(&dir);
let cache = RepoSeamFactCache::at_dir(dir.clone());
let key_a = WorkspaceState {
cfg_features: Some("a"),
..empty_state()
}
.cache_key();
let key_b = WorkspaceState {
cfg_features: Some("b"),
..empty_state()
}
.cache_key();
cache
.store_classified_seams(&key_a, &[sample_classified()])
.map_err(|err| format!("store under key_a: {err}"))?;
let envelope = CacheEnvelope::new(key_a.clone(), vec![sample_classified()]);
std::fs::create_dir_all(&dir).map_err(|err| format!("mkdir: {err}"))?;
let bytes = codec::encode(&envelope)?;
std::fs::write(cache.entry_path(&key_b), bytes)
.map_err(|err| format!("write under wrong filename: {err}"))?;
let result = match cache.load_classified_seams(&key_b) {
CacheLoad::Miss => Ok(()),
other => Err(format!(
"expected Miss when envelope key mismatches request, got {other:?}"
)),
};
let _ = std::fs::remove_dir_all(&dir);
result
}
#[test]
fn given_file_facts_cached_when_loading_same_file_bytes_then_hit_is_returned()
-> Result<(), String> {
let dir = isolated_dir("file-facts-warm");
let _ = std::fs::remove_dir_all(&dir);
let cache = RepoFileFactCache::at_dir(dir.clone());
let path = PathBuf::from("src/lib.rs");
let key = RepoFileFactCacheKey::new(&path, b"pub fn cached() {}\n");
let facts = FileFacts {
path: path.clone(),
source: "pub fn cached() {}\n".to_string(),
..FileFacts::default()
};
cache
.store_file_facts(&key, &facts)
.map_err(|err| format!("store file facts should succeed: {err}"))?;
let result = match cache.load_file_facts(&key) {
CacheLoad::Hit(loaded) => {
if loaded != facts {
Err("loaded file facts should match stored facts".to_string())
} else {
Ok(())
}
}
other => Err(format!("expected file fact cache hit, got {other:?}")),
};
let _ = std::fs::remove_dir_all(&dir);
result
}
#[test]
fn given_file_content_changes_when_file_facts_load_then_miss_is_returned() -> Result<(), String>
{
let dir = isolated_dir("file-facts-invalidates");
let _ = std::fs::remove_dir_all(&dir);
let cache = RepoFileFactCache::at_dir(dir.clone());
let path = PathBuf::from("src/lib.rs");
let original_key = RepoFileFactCacheKey::new(&path, b"pub fn cached() -> i32 { 1 }\n");
let changed_key = RepoFileFactCacheKey::new(&path, b"pub fn cached() -> i32 { 2 }\n");
let facts = FileFacts {
path: path.clone(),
source: "pub fn cached() -> i32 { 1 }\n".to_string(),
..FileFacts::default()
};
cache
.store_file_facts(&original_key, &facts)
.map_err(|err| format!("store original file facts: {err}"))?;
let result = match cache.load_file_facts(&changed_key) {
CacheLoad::Miss => Ok(()),
other => Err(format!(
"expected Miss after file content change, got {other:?}"
)),
};
let _ = std::fs::remove_dir_all(&dir);
result
}
#[test]
fn file_fact_cache_stats_status_label_is_trace_safe() {
let stats = FileFactCacheStats {
hits: 2,
misses: 3,
corrupt_ignored: 1,
stores: 3,
store_errors: 0,
};
assert_eq!(
stats.status_label(),
"hits_2_misses_3_corrupt_1_store_errors_0"
);
}
fn uuid_like() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{}-{:x}", std::process::id(), nanos)
}
}