use super::rust_index::{
self, PROBE_SHAPE_CALL_DELETION, PROBE_SHAPE_ERROR_PATH, PROBE_SHAPE_FIELD_CONSTRUCTION,
PROBE_SHAPE_MATCH_ARM, PROBE_SHAPE_PREDICATE, PROBE_SHAPE_RETURN_VALUE,
PROBE_SHAPE_SIDE_EFFECT, ProbeShapeFact, RustIndex,
};
use super::seam_cache::{CacheLoad, RepoSeamCountCache, RepoSeamFactCache, WorkspaceState};
use super::seam_classification::{self, ClassifiedSeam, SeamGripClassCounts};
use super::seams::{ExpectedSink, RepoSeam, RequiredDiscriminator, SeamKind};
use super::test_grip_evidence;
use super::workspace;
use crate::config::RiprConfig;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
const LATENCY_TRACE_ENV: &str = "RIPR_REPO_EXPOSURE_LATENCY_TRACE";
pub(crate) fn inventory_seams_at(root: &Path) -> Result<Vec<RepoSeam>, String> {
let rust_files = workspace::discover_rust_files(root)?;
let production_files: Vec<PathBuf> = rust_files
.iter()
.filter(|p| workspace::is_production_rust_path(p))
.cloned()
.collect();
let index = rust_index::build_index(root, &rust_files)?;
Ok(inventory_seams_from_index(&production_files, &index))
}
#[cfg(test)]
pub(crate) fn inventory_classified_seams_at(root: &Path) -> Result<Vec<ClassifiedSeam>, String> {
inventory_classified_seams_at_with_config(root, &RiprConfig::default())
}
pub(crate) fn inventory_classified_seams_at_with_config(
root: &Path,
config: &RiprConfig,
) -> Result<Vec<ClassifiedSeam>, String> {
let total_started = Instant::now();
let cache = RepoSeamFactCache::at(root);
let collect_started = Instant::now();
let state = match collect_workspace_state(root, config) {
Ok(state) => {
trace_latency_phase("collect_workspace_state", "ok", collect_started.elapsed());
state
}
Err(err) => {
trace_latency_phase(
"collect_workspace_state",
"error",
collect_started.elapsed(),
);
trace_latency_phase("total", "error", total_started.elapsed());
return Err(err);
}
};
let key = state.cache_key();
let cache_started = Instant::now();
match cache.load_classified_seams(&key) {
CacheLoad::Hit(cached) => {
trace_latency_phase("cache_load", "hit", cache_started.elapsed());
trace_latency_phase("total", "cache_hit", total_started.elapsed());
return Ok(cached);
}
CacheLoad::Miss => {
trace_latency_phase("cache_load", "miss", cache_started.elapsed());
}
CacheLoad::CorruptIgnored { reason } => {
trace_latency_phase("cache_load", "corrupt_ignored", cache_started.elapsed());
eprintln!("ripr: repo seam cache entry ignored ({reason})");
}
}
let compute_started = Instant::now();
let classified = match inventory_classified_seams_from_state_with_config(&state, config) {
Ok(classified) => {
trace_latency_phase("cold_compute", "ok", compute_started.elapsed());
classified
}
Err(err) => {
trace_latency_phase("cold_compute", "error", compute_started.elapsed());
trace_latency_phase("total", "error", total_started.elapsed());
return Err(err);
}
};
let store_started = Instant::now();
let store_status = if cache.store_classified_seams(&key, &classified).is_ok() {
"ok"
} else {
"ignored_error"
};
trace_latency_phase("cache_store", store_status, store_started.elapsed());
trace_latency_phase("total", "computed", total_started.elapsed());
Ok(classified)
}
fn trace_latency_phase(phase: &str, status: &str, duration: Duration) {
if std::env::var_os(LATENCY_TRACE_ENV).is_some() {
eprintln!("{}", latency_trace_line(phase, status, duration));
}
}
fn latency_trace_line(phase: &str, status: &str, duration: Duration) -> String {
format!(
"ripr_repo_exposure_latency phase={phase} status={status} duration_ms={}",
duration.as_millis()
)
}
#[cfg(test)]
pub(crate) fn inventory_classified_seams_uncached_with_config(
root: &Path,
config: &RiprConfig,
) -> Result<Vec<ClassifiedSeam>, String> {
let discover_started = Instant::now();
let rust_files = match workspace::discover_rust_files(root) {
Ok(files) => {
trace_latency_phase("discover_rust_files", "ok", discover_started.elapsed());
files
}
Err(err) => {
trace_latency_phase("discover_rust_files", "error", discover_started.elapsed());
return Err(err);
}
};
let filter_started = Instant::now();
let production_files: Vec<PathBuf> = rust_files
.iter()
.filter(|p| workspace::is_production_rust_path(p))
.cloned()
.collect();
trace_latency_phase("filter_production_files", "ok", filter_started.elapsed());
let index_started = Instant::now();
let mut index = match rust_index::build_index(root, &rust_files) {
Ok(index) => {
trace_latency_phase("build_index", "ok", index_started.elapsed());
index
}
Err(err) => {
trace_latency_phase("build_index", "error", index_started.elapsed());
return Err(err);
}
};
let policy_started = Instant::now();
rust_index::apply_oracle_policy(&mut index, config.oracles());
trace_latency_phase("apply_oracle_policy", "ok", policy_started.elapsed());
let seams_started = Instant::now();
let seams = inventory_seams_from_index(&production_files, &index);
trace_latency_phase("inventory_seams", "ok", seams_started.elapsed());
let evidence_started = Instant::now();
let evidence = test_grip_evidence::evidence_for_seams(&seams, &index);
trace_latency_phase("evidence_for_seams", "ok", evidence_started.elapsed());
let classify_started = Instant::now();
let classified = seam_classification::classify_seams_owned(seams, evidence);
trace_latency_phase("classify_seams", "ok", classify_started.elapsed());
Ok(classified)
}
pub(crate) fn inventory_seam_grip_class_counts_at_with_config(
root: &Path,
config: &RiprConfig,
) -> Result<SeamGripClassCounts, String> {
let cache = RepoSeamCountCache::at(root);
let state = collect_workspace_state(root, config)?;
let key = state.cache_key();
match cache.load_counts(&key) {
CacheLoad::Hit(cached) => return Ok(cached),
CacheLoad::Miss => {}
CacheLoad::CorruptIgnored { reason } => {
eprintln!("ripr: repo seam count cache entry ignored ({reason})");
}
}
let counts = inventory_seam_grip_class_counts_from_state_with_config(&state, config)?;
let _ = cache.store_counts(&key, &counts);
Ok(counts)
}
#[cfg(test)]
fn inventory_seam_grip_class_counts_uncached_with_config(
root: &Path,
config: &RiprConfig,
) -> Result<SeamGripClassCounts, String> {
let rust_files = workspace::discover_rust_files(root)?;
let production_files: Vec<PathBuf> = rust_files
.iter()
.filter(|p| workspace::is_production_rust_path(p))
.cloned()
.collect();
let mut index = rust_index::build_index(root, &rust_files)?;
rust_index::apply_oracle_policy(&mut index, config.oracles());
let seams = inventory_seams_from_index(&production_files, &index);
let mut counts = SeamGripClassCounts::new(seams.len());
let context = test_grip_evidence::CompactGripContext::new(&index);
for seam in &seams {
let evidence = test_grip_evidence::compact_evidence_for_seam(seam, &context);
let class = seam_classification::classify_seam(seam, &evidence);
counts.increment(class);
}
Ok(counts)
}
fn inventory_classified_seams_from_state_with_config(
state: &OwnedWorkspaceState,
config: &RiprConfig,
) -> Result<Vec<ClassifiedSeam>, String> {
let production_files = production_files_from_state(state);
let build_started = Instant::now();
let mut cached =
rust_index::build_index_from_loaded_files_with_cache(&state.workspace_root, &state.files)?;
trace_latency_phase(
"file_fact_cache",
&cached.file_fact_cache.status_label(),
build_started.elapsed(),
);
let policy_started = Instant::now();
rust_index::apply_oracle_policy(&mut cached.index, config.oracles());
trace_latency_phase("apply_oracle_policy", "ok", policy_started.elapsed());
let seams_started = Instant::now();
let seams = inventory_seams_from_index(&production_files, &cached.index);
trace_latency_phase("inventory_seams", "ok", seams_started.elapsed());
let evidence_started = Instant::now();
let evidence = test_grip_evidence::evidence_for_seams(&seams, &cached.index);
trace_latency_phase("evidence_for_seams", "ok", evidence_started.elapsed());
let classify_started = Instant::now();
let classified = seam_classification::classify_seams_owned(seams, evidence);
trace_latency_phase("classify_seams", "ok", classify_started.elapsed());
Ok(classified)
}
fn inventory_seam_grip_class_counts_from_state_with_config(
state: &OwnedWorkspaceState,
config: &RiprConfig,
) -> Result<SeamGripClassCounts, String> {
let production_files = production_files_from_state(state);
let build_started = Instant::now();
let mut cached =
rust_index::build_index_from_loaded_files_with_cache(&state.workspace_root, &state.files)?;
trace_latency_phase(
"file_fact_cache",
&cached.file_fact_cache.status_label(),
build_started.elapsed(),
);
rust_index::apply_oracle_policy(&mut cached.index, config.oracles());
let seams = inventory_seams_from_index(&production_files, &cached.index);
let mut counts = SeamGripClassCounts::new(seams.len());
let context = test_grip_evidence::CompactGripContext::new(&cached.index);
for seam in &seams {
let evidence = test_grip_evidence::compact_evidence_for_seam(seam, &context);
let class = seam_classification::classify_seam(seam, &evidence);
counts.increment(class);
}
Ok(counts)
}
fn production_files_from_state(state: &OwnedWorkspaceState) -> Vec<PathBuf> {
state
.files
.iter()
.map(|(path, _)| path)
.filter(|path| workspace::is_production_rust_path(path))
.cloned()
.collect()
}
fn collect_workspace_state(
root: &Path,
config: &RiprConfig,
) -> Result<OwnedWorkspaceState, String> {
let rust_files = workspace::discover_rust_files(root)?;
let mut files: Vec<(PathBuf, Vec<u8>)> = Vec::with_capacity(rust_files.len());
for path in rust_files {
let bytes = std::fs::read(root.join(&path))
.map_err(|err| format!("read {} failed: {err}", path.display()))?;
files.push((path, bytes));
}
Ok(OwnedWorkspaceState {
workspace_root: root.to_path_buf(),
files,
config_text: config.source_text().map(str::to_string),
test_intent_text: read_optional(&root.join(".ripr").join("test_intent.toml")),
suppressions_text: read_optional(&root.join(config.suppressions().path())),
})
}
fn read_optional(path: &Path) -> Option<String> {
std::fs::read_to_string(path).ok()
}
struct OwnedWorkspaceState {
workspace_root: PathBuf,
files: Vec<(PathBuf, Vec<u8>)>,
config_text: Option<String>,
test_intent_text: Option<String>,
suppressions_text: Option<String>,
}
impl OwnedWorkspaceState {
fn cache_key(&self) -> super::seam_cache::RepoSeamCacheKey {
WorkspaceState {
workspace_root: &self.workspace_root,
files: &self.files,
cfg_features: None,
config_text: self.config_text.as_deref(),
test_intent_text: self.test_intent_text.as_deref(),
suppressions_text: self.suppressions_text.as_deref(),
}
.cache_key()
}
}
pub(crate) fn inventory_seams_from_index(
production_files: &[PathBuf],
index: &RustIndex,
) -> Vec<RepoSeam> {
let mut seams: Vec<RepoSeam> = Vec::new();
for path in production_files {
let Some(facts) = index.files.get(path) else {
continue;
};
for shape in &facts.probe_shapes {
let Some(seam) = build_seam_from_shape(path, shape, index) else {
continue;
};
seams.push(seam);
}
}
seams.sort_by(|a, b| {
a.file()
.cmp(b.file())
.then(a.byte_offset().cmp(&b.byte_offset()))
.then(a.kind().as_str().cmp(b.kind().as_str()))
.then(a.owner().cmp(b.owner()))
});
seams.dedup_by(|a, b| {
a.file() == b.file()
&& a.byte_offset() == b.byte_offset()
&& a.kind() == b.kind()
&& a.owner() == b.owner()
});
seams
}
fn build_seam_from_shape(
path: &Path,
shape: &ProbeShapeFact,
index: &RustIndex,
) -> Option<RepoSeam> {
let kind = seam_kind_from_probe_shape(&shape.kind)?;
let owner_fact = rust_index::find_owner_function(index, path, shape.start_line)?;
if owner_fact.is_test {
return None;
}
let owner = owner_fact.id.0.replace('\\', "/");
let expression = shape.text.clone();
let required_discriminator = required_discriminator_for(kind, &expression);
let expected_sink = expected_sink_for(kind);
Some(RepoSeam::new(
path,
owner,
kind,
shape.start_byte,
shape.start_line,
expression,
required_discriminator,
expected_sink,
))
}
fn seam_kind_from_probe_shape(kind: &str) -> Option<SeamKind> {
match kind {
PROBE_SHAPE_PREDICATE => Some(SeamKind::PredicateBoundary),
PROBE_SHAPE_RETURN_VALUE => Some(SeamKind::ReturnValue),
PROBE_SHAPE_ERROR_PATH => Some(SeamKind::ErrorVariant),
PROBE_SHAPE_FIELD_CONSTRUCTION => Some(SeamKind::FieldConstruction),
PROBE_SHAPE_SIDE_EFFECT => Some(SeamKind::SideEffect),
PROBE_SHAPE_MATCH_ARM => Some(SeamKind::MatchArm),
PROBE_SHAPE_CALL_DELETION => Some(SeamKind::CallPresence),
_ => None,
}
}
fn required_discriminator_for(kind: SeamKind, expression: &str) -> RequiredDiscriminator {
match kind {
SeamKind::PredicateBoundary => RequiredDiscriminator::BoundaryValue {
description: expression.to_string(),
},
SeamKind::ErrorVariant => RequiredDiscriminator::ErrorVariant {
variant: expression.to_string(),
},
SeamKind::ReturnValue => RequiredDiscriminator::ReturnValue {
description: expression.to_string(),
},
SeamKind::FieldConstruction => RequiredDiscriminator::FieldValue {
field: expression.to_string(),
},
SeamKind::SideEffect => RequiredDiscriminator::Effect {
sink: expression.to_string(),
},
SeamKind::MatchArm => RequiredDiscriminator::MatchArmTaken {
arm: expression.to_string(),
},
SeamKind::CallPresence => RequiredDiscriminator::CallSite {
target: expression.to_string(),
},
}
}
fn expected_sink_for(kind: SeamKind) -> ExpectedSink {
match kind {
SeamKind::PredicateBoundary | SeamKind::ReturnValue | SeamKind::MatchArm => {
ExpectedSink::ReturnValue
}
SeamKind::ErrorVariant => ExpectedSink::ErrorChannel,
SeamKind::FieldConstruction => ExpectedSink::OutputField,
SeamKind::SideEffect | SeamKind::CallPresence => ExpectedSink::SideEffect,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::rust_index::{RaRustSyntaxAdapter, RustSyntaxAdapter};
fn index_from_files(files: &[(PathBuf, &str)]) -> Result<RustIndex, String> {
let adapter = RaRustSyntaxAdapter;
let mut index = RustIndex::default();
for (path, source) in files {
let facts = adapter.summarize_file(path, source)?;
index.files.insert(path.clone(), facts);
index
.functions
.extend(index.files[path].functions.iter().cloned());
}
Ok(index)
}
#[test]
fn given_production_predicate_shape_when_repo_inventory_runs_then_predicate_boundary_seam_is_emitted()
-> Result<(), String> {
let path = PathBuf::from("src/pricing.rs");
let source = r#"
pub fn discounted_total(amount: i32, threshold: i32) -> i32 {
if amount >= threshold { amount - 10 } else { amount }
}
"#;
let index = index_from_files(&[(path.clone(), source)])?;
let seams = inventory_seams_from_index(&[path], &index);
if !seams
.iter()
.any(|s| s.kind() == SeamKind::PredicateBoundary)
{
return Err(format!(
"expected at least one PredicateBoundary seam, got {:?}",
seams.iter().map(|s| s.kind().as_str()).collect::<Vec<_>>()
));
}
let predicate_seam = seams
.iter()
.find(|s| s.kind() == SeamKind::PredicateBoundary)
.ok_or_else(|| "missing predicate seam".to_string())?;
if !predicate_seam.owner().contains("discounted_total") {
return Err(format!(
"predicate seam owner should contain discounted_total, got {}",
predicate_seam.owner()
));
}
Ok(())
}
#[test]
fn given_test_file_predicate_shape_when_repo_inventory_runs_then_no_production_seam_is_emitted()
-> Result<(), String> {
let prod = PathBuf::from("src/lib.rs");
let prod_source = "pub fn dummy() {}\n";
let test_path = PathBuf::from("tests/some_test.rs");
let test_source = r#"
#[test]
fn predicate_inside_test() {
let x = 5;
if x >= 3 {
assert!(true);
}
}
"#;
let index = index_from_files(&[
(prod.clone(), prod_source),
(test_path.clone(), test_source),
])?;
let production_files: Vec<PathBuf> = [prod, test_path.clone()]
.into_iter()
.filter(|p| workspace::is_production_rust_path(p))
.collect();
if production_files.iter().any(|p| p == &test_path) {
return Err("test file should not be in production_files".to_string());
}
let seams = inventory_seams_from_index(&production_files, &index);
for seam in &seams {
let path_str = seam.file().to_string_lossy();
if path_str.contains("tests/") || path_str.contains("tests\\") {
return Err(format!(
"seam emitted from a test file: {} (kind {})",
path_str,
seam.kind().as_str()
));
}
}
Ok(())
}
#[test]
fn given_same_files_in_different_walk_order_when_repo_inventory_runs_then_seam_ids_are_stable()
-> Result<(), String> {
let a = PathBuf::from("src/a.rs");
let a_src = r#"
pub fn check_a(x: i32) -> bool {
x > 5
}
"#;
let b = PathBuf::from("src/b.rs");
let b_src = r#"
pub fn check_b(x: i32) -> i32 {
if x < 0 { return -1; }
x
}
"#;
let index = index_from_files(&[(a.clone(), a_src), (b.clone(), b_src)])?;
let forward = inventory_seams_from_index(&[a.clone(), b.clone()], &index);
let reversed = inventory_seams_from_index(&[b.clone(), a.clone()], &index);
let forward_ids: Vec<&str> = forward.iter().map(|s| s.id().as_str()).collect();
let reversed_ids: Vec<&str> = reversed.iter().map(|s| s.id().as_str()).collect();
if forward_ids != reversed_ids {
return Err(format!(
"seam IDs depend on input order:\n forward: {forward_ids:?}\n reversed: {reversed_ids:?}"
));
}
Ok(())
}
#[test]
fn given_error_path_shape_when_repo_inventory_runs_then_error_variant_seam_is_emitted()
-> Result<(), String> {
let path = PathBuf::from("src/parse.rs");
let source = r#"
pub fn parse(value: &str) -> Result<i32, String> {
if value.is_empty() {
return Err("empty input".to_string());
}
value
.parse::<i32>()
.map_err(|err| format!("parse failed: {err}"))
}
"#;
let index = index_from_files(&[(path.clone(), source)])?;
let seams = inventory_seams_from_index(&[path], &index);
if !seams.iter().any(|s| s.kind() == SeamKind::ErrorVariant) {
return Err(format!(
"expected at least one ErrorVariant seam, got {:?}",
seams.iter().map(|s| s.kind().as_str()).collect::<Vec<_>>()
));
}
Ok(())
}
#[test]
fn given_field_construction_shape_when_repo_inventory_runs_then_field_construction_seam_is_emitted()
-> Result<(), String> {
let path = PathBuf::from("src/build.rs");
let source = r#"
pub struct Quote {
pub amount: i32,
pub fee: i32,
}
pub fn build_quote(amount: i32, fee: i32) -> Quote {
Quote {
amount: amount,
fee: fee,
}
}
"#;
let index = index_from_files(&[(path.clone(), source)])?;
let seams = inventory_seams_from_index(&[path], &index);
if !seams
.iter()
.any(|s| s.kind() == SeamKind::FieldConstruction)
{
return Err(format!(
"expected at least one FieldConstruction seam, got {:?}",
seams.iter().map(|s| s.kind().as_str()).collect::<Vec<_>>()
));
}
Ok(())
}
#[test]
fn seam_inventory_omits_seams_with_no_owner_function() -> Result<(), String> {
let path = PathBuf::from("src/orphan.rs");
let source = "pub const X: i32 = if true { 1 } else { 0 };\n";
let index = index_from_files(&[(path.clone(), source)])?;
let seams = inventory_seams_from_index(&[path], &index);
for seam in &seams {
if seam.owner().is_empty() {
return Err("seam emitted with empty owner".to_string());
}
}
Ok(())
}
#[test]
fn seam_inventory_maps_call_sites_to_call_presence_and_side_effect_sink() -> Result<(), String>
{
let path = PathBuf::from("src/service.rs");
let source = r#"
pub fn run(flag: bool) {
if flag {
notify();
}
}
fn notify() {}
"#;
let index = index_from_files(&[(path.clone(), source)])?;
let seams = inventory_seams_from_index(&[path], &index);
let kinds = seams.iter().map(|s| s.kind().as_str()).collect::<Vec<_>>();
assert!(
kinds.contains(&SeamKind::CallPresence.as_str()),
"expected a CallPresence seam, got {kinds:?}"
);
let call_presence = seams
.iter()
.find(|s| s.kind() == SeamKind::CallPresence)
.ok_or("CallPresence seam kind should have a matching seam")?;
assert!(matches!(
call_presence.required_discriminator(),
RequiredDiscriminator::CallSite { .. }
));
assert_eq!(call_presence.expected_sink(), ExpectedSink::SideEffect);
Ok(())
}
#[test]
fn seam_inventory_skips_inline_test_functions_inside_production_files() -> Result<(), String> {
let path = PathBuf::from("src/lib.rs");
let source = r#"
pub fn production_fn(x: i32) -> bool {
x > 0
}
#[cfg(test)]
mod tests {
#[test]
fn inline_test() {
assert!(2 > 1);
}
}
"#;
let index = index_from_files(&[(path.clone(), source)])?;
let seams = inventory_seams_from_index(&[path], &index);
let owners = seams.iter().map(|s| s.owner()).collect::<Vec<_>>();
assert!(
!owners.iter().any(|owner| owner.contains("inline_test")),
"expected inline #[test] owner to be filtered out, got owners {owners:?}"
);
Ok(())
}
fn unique_suffix() -> 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)
}
fn make_tempdir(label: &str) -> Result<PathBuf, String> {
let dir = std::env::temp_dir().join(format!("ripr-inv-{label}-{}", unique_suffix()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).map_err(|err| format!("create {}: {err}", dir.display()))?;
Ok(dir)
}
fn write_file(path: &Path, content: &str) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|err| format!("mkdir {}: {err}", parent.display()))?;
}
std::fs::write(path, content).map_err(|err| format!("write {}: {err}", path.display()))
}
fn cache_dir_under(root: &Path) -> PathBuf {
root.join("target")
.join("ripr")
.join("cache")
.join("repo-seam-facts")
.join(super::super::seam_cache::CACHE_SCHEMA_VERSION)
}
fn count_cache_dir_under(root: &Path) -> PathBuf {
root.join("target")
.join("ripr")
.join("cache")
.join("repo-seam-counts")
.join("0.1")
}
fn list_cache_entries(root: &Path) -> Result<Vec<PathBuf>, String> {
let dir = cache_dir_under(root);
list_entries(&dir)
}
fn list_count_cache_entries(root: &Path) -> Result<Vec<PathBuf>, String> {
let dir = count_cache_dir_under(root);
list_entries(&dir)
}
fn list_entries(dir: &Path) -> Result<Vec<PathBuf>, String> {
if !dir.exists() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for entry in
std::fs::read_dir(dir).map_err(|err| format!("read {}: {err}", dir.display()))?
{
let entry = entry.map_err(|err| format!("read entry: {err}"))?;
out.push(entry.path());
}
out.sort();
Ok(out)
}
#[test]
fn latency_trace_line_formats_phase_status_and_duration() {
let line = latency_trace_line("cache_load", "hit", Duration::from_millis(7));
assert_eq!(
line,
"ripr_repo_exposure_latency phase=cache_load status=hit duration_ms=7"
);
}
#[test]
fn classified_inventory_returns_collect_error_for_non_directory_root() -> Result<(), String> {
let root = make_tempdir("collect-error")?;
let file_root = root.join("not-a-directory");
write_file(&file_root, "not a directory")?;
let result = inventory_classified_seams_at(&file_root);
if result.is_ok() {
return Err("inventory should fail when root is not a directory".to_string());
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn compact_seam_class_counts_match_full_classification_for_small_workspace()
-> Result<(), String> {
let root = make_tempdir("compact-counts")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
write_file(
&root.join("tests/foo_test.rs"),
"#[test] fn discount_calls_owner() { assert!(x::discount(100, 100)); }\n",
)?;
let full = inventory_classified_seams_uncached_with_config(&root, &RiprConfig::default())?;
let compact =
inventory_seam_grip_class_counts_uncached_with_config(&root, &RiprConfig::default())?;
if compact.analyzed_seams() != full.len() {
return Err(format!(
"compact analyzed count {} did not match full classified count {}",
compact.analyzed_seams(),
full.len()
));
}
for class in super::super::seams::SeamGripClass::ALL {
let full_count = full.iter().filter(|entry| entry.class == class).count();
let compact_count = compact.count_for(class);
if compact_count != full_count {
return Err(format!(
"compact count for {} was {}, full count was {}",
class.as_str(),
compact_count,
full_count
));
}
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn given_cached_seam_class_counts_when_badge_count_runs_then_cached_counts_are_returned()
-> Result<(), String> {
let root = make_tempdir("count-cache")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
let cold = inventory_seam_grip_class_counts_at_with_config(&root, &RiprConfig::default())?;
if cold.analyzed_seams() == 0 {
return Err("cold count path should analyze at least one seam".into());
}
let entries = list_count_cache_entries(&root)?;
if entries.len() != 1 {
return Err(format!(
"expected exactly 1 count cache entry, got {}",
entries.len()
));
}
let cache_file = &entries[0];
let bytes = std::fs::read(cache_file)
.map_err(|err| format!("read {}: {err}", cache_file.display()))?;
let mut envelope: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|err| format!("parse count cache: {err}"))?;
envelope["counts"]["analyzed_seams"] = serde_json::json!(0);
envelope["counts"]["counts"] = serde_json::json!({});
let rewritten =
serde_json::to_vec(&envelope).map_err(|err| format!("encode count cache: {err}"))?;
std::fs::write(cache_file, rewritten)
.map_err(|err| format!("rewrite {}: {err}", cache_file.display()))?;
let warm = inventory_seam_grip_class_counts_at_with_config(&root, &RiprConfig::default())?;
if warm.analyzed_seams() != 0 {
return Err(format!(
"warm count path should return cached analyzed_seams=0, got {}",
warm.analyzed_seams()
));
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn given_corrupt_count_cache_entry_when_badge_count_runs_then_uncached_path_computes_without_failure()
-> Result<(), String> {
let root = make_tempdir("count-cache-corrupt")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
let state = collect_workspace_state(&root, &RiprConfig::default())?;
let key = state.cache_key();
let dir = count_cache_dir_under(&root);
std::fs::create_dir_all(&dir).map_err(|err| format!("mkdir {}: {err}", dir.display()))?;
let entry = dir.join(key.filename());
std::fs::write(&entry, b"{not valid json")
.map_err(|err| format!("write corrupt count entry: {err}"))?;
let result =
inventory_seam_grip_class_counts_at_with_config(&root, &RiprConfig::default())?;
if result.analyzed_seams() == 0 {
return Err("count path should compute real seams when count cache is corrupt".into());
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn given_count_cache_store_fails_when_badge_count_runs_then_analysis_result_is_still_returned()
-> Result<(), String> {
let root = make_tempdir("count-cache-storefail")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
let state = collect_workspace_state(&root, &RiprConfig::default())?;
let key = state.cache_key();
let dir = count_cache_dir_under(&root);
std::fs::create_dir_all(dir.join(key.filename()))
.map_err(|err| format!("mkdir count conflict path: {err}"))?;
let result =
inventory_seam_grip_class_counts_at_with_config(&root, &RiprConfig::default())?;
if result.analyzed_seams() == 0 {
return Err(
"count path should return real seams even when count cache write fails".into(),
);
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn given_cached_classified_seams_when_inventory_runs_then_cached_seams_are_returned()
-> Result<(), String> {
let root = make_tempdir("warm-hit")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
let cold = inventory_classified_seams_at(&root)?;
if cold.is_empty() {
return Err("cold path should classify at least one seam from foo.rs".into());
}
let entries = list_cache_entries(&root)?;
if entries.len() != 1 {
return Err(format!(
"expected exactly 1 cache entry, got {}",
entries.len()
));
}
let cache_file = &entries[0];
let bytes = std::fs::read(cache_file)
.map_err(|err| format!("read {}: {err}", cache_file.display()))?;
let mut envelope: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|err| format!("parse cache: {err}"))?;
envelope["classified_seams"] = serde_json::Value::Array(Vec::new());
let rewritten =
serde_json::to_vec(&envelope).map_err(|err| format!("encode cache: {err}"))?;
std::fs::write(cache_file, rewritten)
.map_err(|err| format!("rewrite {}: {err}", cache_file.display()))?;
let warm = inventory_classified_seams_at(&root)?;
if !warm.is_empty() {
return Err(format!(
"warm path should return cached (empty) seams, got {} seams",
warm.len()
));
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn given_corrupt_cache_entry_when_inventory_runs_then_uncached_path_computes_without_failure()
-> Result<(), String> {
let root = make_tempdir("corrupt-recover")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
let state = collect_workspace_state(&root, &RiprConfig::default())?;
let key = state.cache_key();
let dir = cache_dir_under(&root);
std::fs::create_dir_all(&dir).map_err(|err| format!("mkdir {}: {err}", dir.display()))?;
let entry = dir.join(key.filename());
std::fs::write(&entry, b"{not valid json")
.map_err(|err| format!("write corrupt entry: {err}"))?;
let result = inventory_classified_seams_at(&root)?;
if result.is_empty() {
return Err("inventory should compute real seams when cache is corrupt".into());
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn given_cache_store_fails_when_inventory_runs_then_analysis_result_is_still_returned()
-> Result<(), String> {
let root = make_tempdir("storefail")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
let state = collect_workspace_state(&root, &RiprConfig::default())?;
let key = state.cache_key();
let dir = cache_dir_under(&root);
std::fs::create_dir_all(dir.join(key.filename()))
.map_err(|err| format!("mkdir conflict path: {err}"))?;
let result = inventory_classified_seams_at(&root)?;
if result.is_empty() {
return Err("inventory should return real seams even when cache write fails".into());
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn given_cached_classified_seams_when_related_test_changes_then_inventory_recomputes()
-> Result<(), String> {
let root = make_tempdir("test-edit-invalidates")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
write_file(
&root.join("tests/foo_test.rs"),
"#[test] fn smoke() { assert_eq!(1, 1); }\n",
)?;
let cold = inventory_classified_seams_at(&root)?;
if cold.is_empty() {
return Err("cold path should classify at least one seam".into());
}
let entries = list_cache_entries(&root)?;
if entries.len() != 1 {
return Err(format!(
"expected exactly 1 cache entry after cold pass, got {}",
entries.len()
));
}
let cache_file = &entries[0];
let bytes = std::fs::read(cache_file)
.map_err(|err| format!("read {}: {err}", cache_file.display()))?;
let mut envelope: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|err| format!("parse cache: {err}"))?;
envelope["classified_seams"] = serde_json::Value::Array(Vec::new());
let rewritten =
serde_json::to_vec(&envelope).map_err(|err| format!("encode cache: {err}"))?;
std::fs::write(cache_file, rewritten)
.map_err(|err| format!("rewrite {}: {err}", cache_file.display()))?;
write_file(
&root.join("tests/foo_test.rs"),
"#[test] fn smoke() { assert!(super::discount(10, 5)); }\n",
)?;
let warm = inventory_classified_seams_at(&root)?;
if warm.is_empty() {
return Err(
"test-only edit must invalidate the classified seam cache; got the poisoned \
empty entry, meaning stale TestGripEvidence would have leaked through"
.into(),
);
}
let entries_after = list_cache_entries(&root)?;
if entries_after.len() < 2 {
return Err(format!(
"expected at least 2 cache entries after test-file edit (poisoned + recomputed), \
got {}",
entries_after.len()
));
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
#[test]
fn given_test_intent_or_suppressions_change_when_inventory_runs_then_cache_key_changes()
-> Result<(), String> {
let root = make_tempdir("intentkey")?;
write_file(
&root.join("src/foo.rs"),
"pub fn discount(amount: i32, threshold: i32) -> bool { amount >= threshold }\n",
)?;
let baseline = collect_workspace_state(&root, &RiprConfig::default())?.cache_key();
write_file(
&root.join(".ripr/test_intent.toml"),
concat!(
"[[test]]\n",
"name = \"smoke\"\n",
"owner = \"src/foo.rs\"\n",
"intent = \"smoke\"\n",
"reason = \"bar\"\n"
),
)?;
let with_intent = collect_workspace_state(&root, &RiprConfig::default())?.cache_key();
if baseline.test_intent_hash == with_intent.test_intent_hash {
return Err("adding test_intent.toml should change test_intent_hash".into());
}
if baseline.filename() == with_intent.filename() {
return Err("adding test_intent.toml should change cache filename".into());
}
write_file(
&root.join(".ripr/suppressions.toml"),
concat!(
"[[suppression]]\n",
"kind = \"exposure_gap\"\n",
"owner = \"src/foo.rs\"\n",
"reason = \"bar\"\n"
),
)?;
let with_both = collect_workspace_state(&root, &RiprConfig::default())?.cache_key();
if with_intent.suppressions_hash == with_both.suppressions_hash {
return Err("adding suppressions.toml should change suppressions_hash".into());
}
if with_intent.filename() == with_both.filename() {
return Err("adding suppressions.toml should change cache filename".into());
}
let _ = std::fs::remove_dir_all(&root);
Ok(())
}
}