pub mod cache;
pub mod condensation;
pub mod csr;
pub mod persistence;
pub mod reachability;
pub mod scc;
pub use cache::AnalysisCache;
pub use condensation::{
BudgetExceededPolicy, CondensationDag, Interval, LabelBudgetConfig, ReachabilityStrategy,
};
pub use csr::{CsrAdjacency, EdgeKindDiscriminant};
pub use persistence::{
AnalysisIdentity, compute_manifest_hash, compute_node_id_hash, load_condensation,
load_condensation_checked, load_csr, load_csr_checked, load_scc, load_scc_checked,
persist_condensation, persist_csr, persist_scc, try_load_path_analysis, try_load_scc,
try_load_scc_and_condensation,
};
pub use scc::SccData;
use crate::graph::unified::compaction::CompactionSnapshot;
use crate::graph::unified::edge::EdgeKind;
use crate::graph::unified::node::NodeId;
use anyhow::{Context, Result, bail};
use rayon::prelude::*;
use std::collections::VecDeque;
use std::path::Path;
#[derive(Debug)]
pub struct GraphAnalyses {
pub adjacency: CsrAdjacency,
pub scc_calls: SccData,
pub scc_imports: SccData,
pub scc_references: SccData,
pub scc_inherits: SccData,
pub cond_calls: CondensationDag,
pub cond_imports: CondensationDag,
pub cond_references: CondensationDag,
pub cond_inherits: CondensationDag,
}
impl GraphAnalyses {
pub fn build_all(snapshot: &CompactionSnapshot) -> Result<Self> {
Self::build_all_with_budget(snapshot, &LabelBudgetConfig::default())
}
pub fn build_all_with_budget(
snapshot: &CompactionSnapshot,
label_budget: &LabelBudgetConfig,
) -> Result<Self> {
let adjacency = CsrAdjacency::build_from_snapshot(snapshot)?;
Self::build_all_from_adjacency_with_budget(adjacency, label_budget)
}
pub fn build_all_from_adjacency_with_budget(
adjacency: CsrAdjacency,
label_budget: &LabelBudgetConfig,
) -> Result<Self> {
let edge_kinds = [
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
EdgeKind::Imports {
alias: None,
is_wildcard: false,
},
EdgeKind::References,
EdgeKind::Inherits,
];
let results: Vec<(SccData, CondensationDag)> = edge_kinds
.into_par_iter()
.map(|kind| {
let scc = SccData::compute_tarjan(&adjacency, &kind)?;
let cond = CondensationDag::build_with_budget(&scc, &adjacency, label_budget)?;
Ok((scc, cond))
})
.collect::<Result<Vec<_>>>()?;
let mut results = results.into_iter();
let (scc_calls, cond_calls) = results.next().expect("4 edge kinds");
let (scc_imports, cond_imports) = results.next().expect("4 edge kinds");
let (scc_references, cond_references) = results.next().expect("4 edge kinds");
let (scc_inherits, cond_inherits) = results.next().expect("4 edge kinds");
Ok(Self {
adjacency,
scc_calls,
scc_imports,
scc_references,
scc_inherits,
cond_calls,
cond_imports,
cond_references,
cond_inherits,
})
}
pub fn persist_all(
&self,
storage: &crate::graph::unified::persistence::GraphStorage,
identity: &AnalysisIdentity,
) -> Result<()> {
std::fs::create_dir_all(storage.analysis_dir())?;
persist_csr(&self.adjacency, identity, &storage.analysis_csr_path())?;
persist_scc(
&self.scc_calls,
identity,
&storage.analysis_scc_path("calls"),
)?;
persist_scc(
&self.scc_imports,
identity,
&storage.analysis_scc_path("imports"),
)?;
persist_scc(
&self.scc_references,
identity,
&storage.analysis_scc_path("references"),
)?;
persist_scc(
&self.scc_inherits,
identity,
&storage.analysis_scc_path("inherits"),
)?;
persist_condensation(
&self.cond_calls,
identity,
&storage.analysis_cond_path("calls"),
)?;
persist_condensation(
&self.cond_imports,
identity,
&storage.analysis_cond_path("imports"),
)?;
persist_condensation(
&self.cond_references,
identity,
&storage.analysis_cond_path("references"),
)?;
persist_condensation(
&self.cond_inherits,
identity,
&storage.analysis_cond_path("inherits"),
)?;
Ok(())
}
}
pub fn resolve_label_budget_config(
index_root: &Path,
cli_label_budget: Option<u64>,
cli_density_threshold: Option<u64>,
cli_policy: Option<&str>,
cli_no_labels: bool,
) -> Result<LabelBudgetConfig> {
let mut config = LabelBudgetConfig::default();
apply_label_budget_env_overrides(&mut config);
apply_label_budget_config_overrides(index_root, &mut config)?;
apply_label_budget_cli_overrides(
&mut config,
cli_label_budget,
cli_density_threshold,
cli_policy,
cli_no_labels,
)?;
Ok(config)
}
fn apply_label_budget_env_overrides(config: &mut LabelBudgetConfig) {
if let Some(label_budget) = parse_env_usize("SQRY_LABEL_BUDGET") {
config.budget_per_kind = label_budget;
}
if env_flag_is_true("SQRY_LABEL_BUDGET_FAIL") {
config.on_exceeded = BudgetExceededPolicy::Fail;
}
if let Some(density_threshold) = parse_env_usize("SQRY_DENSITY_GATE_THRESHOLD") {
config.density_gate_threshold = density_threshold;
}
if env_flag_is_true("SQRY_NO_LABELS") {
config.skip_labels = true;
}
}
fn apply_label_budget_config_overrides(
index_root: &Path,
config: &mut LabelBudgetConfig,
) -> Result<()> {
let Ok(store) = crate::config::GraphConfigStore::new(index_root) else {
return Ok(());
};
if !store.is_initialized() {
return Ok(());
}
let persistence = crate::config::ConfigPersistence::new(&store);
let Ok((graph_config, report)) = persistence.load() else {
return Ok(());
};
for warning in &report.warnings {
log::warn!("Config load: {warning}");
}
config.budget_per_kind =
usize::try_from(graph_config.config.limits.analysis_label_budget_per_kind)
.context("analysis_label_budget_per_kind exceeds usize range")?;
config.density_gate_threshold =
usize::try_from(graph_config.config.limits.analysis_density_gate_threshold)
.context("analysis_density_gate_threshold exceeds usize range")?;
apply_budget_policy_override(
&mut config.on_exceeded,
graph_config
.config
.limits
.analysis_budget_exceeded_policy
.as_str(),
"config",
)?;
Ok(())
}
fn apply_label_budget_cli_overrides(
config: &mut LabelBudgetConfig,
cli_label_budget: Option<u64>,
cli_density_threshold: Option<u64>,
cli_policy: Option<&str>,
cli_no_labels: bool,
) -> Result<()> {
if let Some(label_budget) = cli_label_budget {
config.budget_per_kind =
usize::try_from(label_budget).context("--label-budget value exceeds usize range")?;
}
if let Some(density_threshold) = cli_density_threshold {
config.density_gate_threshold = usize::try_from(density_threshold)
.context("--density-threshold value exceeds usize range")?;
}
if cli_no_labels {
config.skip_labels = true;
}
if let Some(policy) = cli_policy {
apply_budget_policy_override(&mut config.on_exceeded, policy, "cli")?;
}
Ok(())
}
fn parse_env_usize(key: &str) -> Option<usize> {
std::env::var(key)
.ok()
.and_then(|value| value.parse::<usize>().ok())
}
fn env_flag_is_true(key: &str) -> bool {
std::env::var(key)
.ok()
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"))
}
fn apply_budget_policy_override(
target: &mut BudgetExceededPolicy,
policy: &str,
source: &str,
) -> Result<()> {
match policy {
"fail" => *target = BudgetExceededPolicy::Fail,
"degrade" => *target = BudgetExceededPolicy::Degrade,
other if source == "config" => {
log::warn!("Unknown analysis_budget_exceeded_policy '{other}' in config, ignoring");
}
other => {
bail!("Invalid --budget-exceeded-policy: '{other}' (expected: degrade or fail)")
}
}
Ok(())
}
pub struct PathReconstructor<'a> {
csr: &'a CsrAdjacency,
scc_data: &'a SccData,
cond_dag: &'a CondensationDag,
}
impl<'a> PathReconstructor<'a> {
#[must_use]
pub fn new(
csr: &'a CsrAdjacency,
scc_data: &'a SccData,
cond_dag: &'a CondensationDag,
) -> Self {
Self {
csr,
scc_data,
cond_dag,
}
}
pub fn reconstruct_path(&self, from: NodeId, to: NodeId) -> Result<Option<Vec<NodeId>>> {
let from_idx = from.index();
let to_idx = to.index();
if from_idx >= self.csr.node_count || to_idx >= self.csr.node_count {
return Ok(None);
}
let from_scc = self
.scc_data
.scc_of(from)
.ok_or_else(|| anyhow::anyhow!("Invalid from node ID: {from:?}"))?;
let to_scc = self
.scc_data
.scc_of(to)
.ok_or_else(|| anyhow::anyhow!("Invalid to node ID: {to:?}"))?;
if !self.cond_dag.can_reach(from_scc, to_scc) {
return Ok(None);
}
if from_scc == to_scc {
return Ok(self
.reconstruct_intra_scc_path(from_idx, to_idx, from_scc)
.map(|path| path.into_iter().map(|idx| NodeId::new(idx, 0)).collect()));
}
let Some(scc_path) = self.reconstruct_scc_path(from_scc, to_scc) else {
return Ok(None);
};
let Some(node_path) = self.expand_scc_path_to_nodes(&scc_path, from_idx, to_idx, to_scc)
else {
return Ok(None);
};
Ok(Some(
node_path
.into_iter()
.map(|idx| NodeId::new(idx, 0))
.collect(),
))
}
fn reconstruct_intra_scc_path(&self, from: u32, to: u32, scc_id: u32) -> Option<Vec<u32>> {
if from == to {
return Some(vec![from]);
}
let node_count = self.csr.node_count as usize;
let mut parents = vec![None; node_count];
let mut queue = VecDeque::new();
parents[from as usize] = Some(from);
queue.push_back(from);
while let Some(current) = queue.pop_front() {
for neighbor in self
.csr
.neighbors_filtered(NodeId::new(current, 0), &self.scc_data.edge_kind)
{
let neighbor_scc = self
.scc_data
.scc_of(NodeId::new(neighbor, 0))
.unwrap_or(u32::MAX);
if neighbor_scc != scc_id {
continue;
}
if parents[neighbor as usize].is_some() {
continue;
}
parents[neighbor as usize] = Some(current);
if neighbor == to {
return reconstruct_path_from_parents(&parents, from, to);
}
queue.push_back(neighbor);
}
}
None
}
fn reconstruct_scc_path(&self, from_scc: u32, to_scc: u32) -> Option<Vec<u32>> {
if from_scc == to_scc {
return Some(vec![from_scc]);
}
let scc_count = self.cond_dag.scc_count as usize;
let mut parents = vec![None; scc_count];
let mut queue = VecDeque::new();
parents[from_scc as usize] = Some(from_scc);
queue.push_back(from_scc);
while let Some(current) = queue.pop_front() {
for &successor in self.cond_dag.successors(current) {
if parents[successor as usize].is_some() {
continue;
}
parents[successor as usize] = Some(current);
if successor == to_scc {
break;
}
queue.push_back(successor);
}
}
reconstruct_path_from_parents(&parents, from_scc, to_scc)
}
fn expand_scc_path_to_nodes(
&self,
scc_path: &[u32],
from: u32,
to: u32,
to_scc: u32,
) -> Option<Vec<u32>> {
if scc_path.is_empty() {
return None;
}
let mut full_path: Vec<u32> = Vec::new();
let mut current_node = from;
for window in scc_path.windows(2) {
let current_scc = window[0];
let next_scc = window[1];
let (segment, entry_node) = self.find_exit_path(current_node, current_scc, next_scc)?;
if full_path.is_empty() {
full_path.extend(segment);
} else {
full_path.extend(segment.into_iter().skip(1));
}
full_path.push(entry_node);
current_node = entry_node;
}
let tail = self.reconstruct_intra_scc_path(current_node, to, to_scc)?;
if full_path.is_empty() {
full_path = tail;
} else {
full_path.extend(tail.into_iter().skip(1));
}
Some(full_path)
}
fn find_exit_path(
&self,
start: u32,
current_scc: u32,
next_scc: u32,
) -> Option<(Vec<u32>, u32)> {
use std::collections::VecDeque;
let node_count = self.csr.node_count as usize;
let mut parents = vec![None; node_count];
let mut queue = VecDeque::new();
parents[start as usize] = Some(start);
queue.push_back(start);
while let Some(current) = queue.pop_front() {
for neighbor in self
.csr
.neighbors_filtered(NodeId::new(current, 0), &self.scc_data.edge_kind)
{
let neighbor_scc = self
.scc_data
.scc_of(NodeId::new(neighbor, 0))
.unwrap_or(u32::MAX);
if neighbor_scc == next_scc {
let segment = reconstruct_path_from_parents(&parents, start, current)?;
return Some((segment, neighbor));
}
if neighbor_scc != current_scc {
continue;
}
if parents[neighbor as usize].is_some() {
continue;
}
parents[neighbor as usize] = Some(current);
queue.push_back(neighbor);
}
}
None
}
}
fn reconstruct_path_from_parents(
parents: &[Option<u32>],
start: u32,
goal: u32,
) -> Option<Vec<u32>> {
if parents.get(goal as usize)?.is_none() {
return None;
}
let mut path = vec![goal];
let mut current = goal;
while current != start {
let parent = parents[current as usize]?; current = parent;
path.push(current);
}
path.reverse();
Some(path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::compaction::{CompactionSnapshot, MergedEdge};
use crate::graph::unified::edge::{DeltaEdge, DeltaOp, EdgeKind};
use crate::graph::unified::file::FileId;
use crate::graph::unified::node::NodeId;
fn create_test_snapshot() -> CompactionSnapshot {
let file = FileId::new(0);
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let edges = vec![
MergedEdge::new(NodeId::new(0, 0), NodeId::new(1, 0), kind.clone(), 1, file),
MergedEdge::new(NodeId::new(1, 0), NodeId::new(2, 0), kind.clone(), 2, file),
MergedEdge::new(NodeId::new(2, 0), NodeId::new(3, 0), kind.clone(), 3, file),
MergedEdge::new(NodeId::new(1, 0), NodeId::new(4, 0), kind.clone(), 4, file),
MergedEdge::new(NodeId::new(4, 0), NodeId::new(3, 0), kind.clone(), 5, file),
MergedEdge::new(NodeId::new(5, 0), NodeId::new(6, 0), kind.clone(), 6, file),
];
CompactionSnapshot {
csr_edges: edges,
delta_edges: Vec::new(),
node_count: 7,
csr_version: 0,
}
}
#[test]
fn test_csr_construction() {
let snapshot = create_test_snapshot();
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
assert_eq!(csr.node_count, 7);
assert_eq!(csr.edge_count, 6);
let neighbors = csr.neighbors(NodeId::new(0, 0));
assert_eq!(neighbors.len(), 1);
assert_eq!(neighbors[0], 1);
let neighbors = csr.neighbors(NodeId::new(1, 0));
assert_eq!(neighbors.len(), 2);
assert!(neighbors.contains(&2));
assert!(neighbors.contains(&4));
}
#[test]
fn test_csr_merges_lww_and_tombstones() {
let file = FileId::new(0);
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let csr_edges = vec![MergedEdge::new(
NodeId::new(0, 0),
NodeId::new(1, 0),
kind.clone(),
1,
file,
)];
let delta_edges = vec![
DeltaEdge::new(
NodeId::new(0, 0),
NodeId::new(1, 0),
kind.clone(),
2,
DeltaOp::Remove,
file,
),
DeltaEdge::new(
NodeId::new(0, 0),
NodeId::new(2, 0),
kind.clone(),
3,
DeltaOp::Add,
file,
),
];
let snapshot = CompactionSnapshot {
csr_edges,
delta_edges,
node_count: 3,
csr_version: 0,
};
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
assert_eq!(csr.edge_count, 1);
assert_eq!(csr.neighbors(NodeId::new(0, 0)), &[2]);
}
#[test]
fn test_scc_computation() {
let snapshot = create_test_snapshot();
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let scc = SccData::compute_tarjan(&csr, &kind).unwrap();
assert_eq!(scc.scc_count, 7);
assert_eq!(scc.non_trivial_count, 0);
assert_eq!(scc.max_scc_size, 1);
}
#[test]
fn test_scc_with_cycle() {
let file = FileId::new(0);
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let edges = vec![
MergedEdge::new(NodeId::new(0, 0), NodeId::new(1, 0), kind.clone(), 1, file),
MergedEdge::new(NodeId::new(1, 0), NodeId::new(2, 0), kind.clone(), 2, file),
MergedEdge::new(NodeId::new(2, 0), NodeId::new(0, 0), kind.clone(), 3, file),
];
let snapshot = CompactionSnapshot {
csr_edges: edges,
delta_edges: Vec::new(),
node_count: 3,
csr_version: 0,
};
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let scc = SccData::compute_tarjan(&csr, &kind).unwrap();
assert_eq!(scc.scc_count, 1);
assert_eq!(scc.non_trivial_count, 1);
assert_eq!(scc.max_scc_size, 3);
let scc_0 = scc.scc_of(NodeId::new(0, 0)).unwrap();
let scc_1 = scc.scc_of(NodeId::new(1, 0)).unwrap();
let scc_2 = scc.scc_of(NodeId::new(2, 0)).unwrap();
assert_eq!(scc_0, scc_1);
assert_eq!(scc_1, scc_2);
}
#[test]
fn test_condensation_dag() {
let snapshot = create_test_snapshot();
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let scc = SccData::compute_tarjan(&csr, &kind).unwrap();
let dag = CondensationDag::build(&scc, &csr).unwrap();
assert_eq!(dag.scc_count, 7);
assert_eq!(dag.edge_count, 6);
assert_eq!(dag.topo_order.len(), 7);
}
#[test]
fn test_2hop_reachability() {
let snapshot = create_test_snapshot();
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let scc = SccData::compute_tarjan(&csr, &kind).unwrap();
let dag = CondensationDag::build(&scc, &csr).unwrap();
let scc_0 = scc.scc_of(NodeId::new(0, 0)).unwrap();
let scc_3 = scc.scc_of(NodeId::new(3, 0)).unwrap();
assert!(dag.can_reach(scc_0, scc_3));
assert!(!dag.can_reach(scc_3, scc_0));
let scc_6 = scc.scc_of(NodeId::new(6, 0)).unwrap();
assert!(!dag.can_reach(scc_0, scc_6));
}
#[test]
fn test_persistence_roundtrip() {
use tempfile::TempDir;
let snapshot = create_test_snapshot();
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let scc = SccData::compute_tarjan(&csr, &kind).unwrap();
let dag = CondensationDag::build(&scc, &csr).unwrap();
let temp_dir = TempDir::new().unwrap();
let csr_path = temp_dir.path().join("test.csr");
let scc_path = temp_dir.path().join("test.scc");
let dag_path = temp_dir.path().join("test.dag");
let identity = AnalysisIdentity::new("manifest".to_string(), [42u8; 32]);
persistence::persist_csr(&csr, &identity, &csr_path).unwrap();
persistence::persist_scc(&scc, &identity, &scc_path).unwrap();
persistence::persist_condensation(&dag, &identity, &dag_path).unwrap();
let (csr_loaded, identity_loaded) = persistence::load_csr(&csr_path).unwrap();
let (scc_loaded, identity_loaded_scc) = persistence::load_scc(&scc_path).unwrap();
let (dag_loaded, identity_loaded_dag) = persistence::load_condensation(&dag_path).unwrap();
assert_eq!(csr_loaded.node_count, csr.node_count);
assert_eq!(csr_loaded.edge_count, csr.edge_count);
assert_eq!(scc_loaded.scc_count, scc.scc_count);
assert_eq!(dag_loaded.scc_count, dag.scc_count);
assert_eq!(dag_loaded.edge_count, dag.edge_count);
assert_eq!(identity_loaded, identity);
assert_eq!(identity_loaded_scc, identity);
assert_eq!(identity_loaded_dag, identity);
}
#[test]
fn test_persistence_identity_mismatch_rejected() {
use tempfile::TempDir;
let snapshot = create_test_snapshot();
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let temp_dir = TempDir::new().unwrap();
let csr_path = temp_dir.path().join("test.csr");
let identity = AnalysisIdentity::new("manifest".to_string(), [1u8; 32]);
let wrong_identity = AnalysisIdentity::new("other".to_string(), [2u8; 32]);
persistence::persist_csr(&csr, &identity, &csr_path).unwrap();
let result = persistence::load_csr_checked(&csr_path, &wrong_identity);
assert!(result.is_err());
}
#[test]
fn test_path_reconstruction_across_sccs() {
let snapshot = create_test_snapshot();
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let scc = SccData::compute_tarjan(&csr, &kind).unwrap();
let dag = CondensationDag::build(&scc, &csr).unwrap();
let recon = PathReconstructor::new(&csr, &scc, &dag);
let path = recon
.reconstruct_path(NodeId::new(0, 0), NodeId::new(3, 0))
.unwrap()
.unwrap();
assert_eq!(path.first(), Some(&NodeId::new(0, 0)));
assert_eq!(path.last(), Some(&NodeId::new(3, 0)));
}
#[test]
fn test_path_reconstruction_unreachable() {
let snapshot = create_test_snapshot();
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let scc = SccData::compute_tarjan(&csr, &kind).unwrap();
let dag = CondensationDag::build(&scc, &csr).unwrap();
let recon = PathReconstructor::new(&csr, &scc, &dag);
let path = recon
.reconstruct_path(NodeId::new(0, 0), NodeId::new(6, 0))
.unwrap();
assert!(path.is_none());
}
mod env_helpers {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn parse_env_usize_valid() {
unsafe { std::env::set_var("SQRY_TEST_PARSE_USIZE", "42") };
assert_eq!(parse_env_usize("SQRY_TEST_PARSE_USIZE"), Some(42));
unsafe { std::env::remove_var("SQRY_TEST_PARSE_USIZE") };
}
#[test]
#[serial]
fn parse_env_usize_zero() {
unsafe { std::env::set_var("SQRY_TEST_PARSE_USIZE", "0") };
assert_eq!(parse_env_usize("SQRY_TEST_PARSE_USIZE"), Some(0));
unsafe { std::env::remove_var("SQRY_TEST_PARSE_USIZE") };
}
#[test]
#[serial]
fn parse_env_usize_invalid_string() {
unsafe { std::env::set_var("SQRY_TEST_PARSE_USIZE", "not_a_number") };
assert_eq!(parse_env_usize("SQRY_TEST_PARSE_USIZE"), None);
unsafe { std::env::remove_var("SQRY_TEST_PARSE_USIZE") };
}
#[test]
#[serial]
fn parse_env_usize_negative() {
unsafe { std::env::set_var("SQRY_TEST_PARSE_USIZE", "-1") };
assert_eq!(parse_env_usize("SQRY_TEST_PARSE_USIZE"), None);
unsafe { std::env::remove_var("SQRY_TEST_PARSE_USIZE") };
}
#[test]
#[serial]
fn parse_env_usize_missing() {
unsafe { std::env::remove_var("SQRY_TEST_PARSE_USIZE") };
assert_eq!(parse_env_usize("SQRY_TEST_PARSE_USIZE"), None);
}
#[test]
#[serial]
fn parse_env_usize_empty_string() {
unsafe { std::env::set_var("SQRY_TEST_PARSE_USIZE", "") };
assert_eq!(parse_env_usize("SQRY_TEST_PARSE_USIZE"), None);
unsafe { std::env::remove_var("SQRY_TEST_PARSE_USIZE") };
}
#[test]
#[serial]
fn env_flag_is_true_with_1() {
unsafe { std::env::set_var("SQRY_TEST_FLAG", "1") };
assert!(env_flag_is_true("SQRY_TEST_FLAG"));
unsafe { std::env::remove_var("SQRY_TEST_FLAG") };
}
#[test]
#[serial]
fn env_flag_is_true_with_true_lowercase() {
unsafe { std::env::set_var("SQRY_TEST_FLAG", "true") };
assert!(env_flag_is_true("SQRY_TEST_FLAG"));
unsafe { std::env::remove_var("SQRY_TEST_FLAG") };
}
#[test]
#[serial]
fn env_flag_is_true_with_true_titlecase() {
unsafe { std::env::set_var("SQRY_TEST_FLAG", "True") };
assert!(env_flag_is_true("SQRY_TEST_FLAG"));
unsafe { std::env::remove_var("SQRY_TEST_FLAG") };
}
#[test]
#[serial]
fn env_flag_is_true_with_true_uppercase() {
unsafe { std::env::set_var("SQRY_TEST_FLAG", "TRUE") };
assert!(env_flag_is_true("SQRY_TEST_FLAG"));
unsafe { std::env::remove_var("SQRY_TEST_FLAG") };
}
#[test]
#[serial]
fn env_flag_is_false_with_0() {
unsafe { std::env::set_var("SQRY_TEST_FLAG", "0") };
assert!(!env_flag_is_true("SQRY_TEST_FLAG"));
unsafe { std::env::remove_var("SQRY_TEST_FLAG") };
}
#[test]
#[serial]
fn env_flag_is_false_with_false_string() {
unsafe { std::env::set_var("SQRY_TEST_FLAG", "false") };
assert!(!env_flag_is_true("SQRY_TEST_FLAG"));
unsafe { std::env::remove_var("SQRY_TEST_FLAG") };
}
#[test]
#[serial]
fn env_flag_is_false_when_missing() {
unsafe { std::env::remove_var("SQRY_TEST_FLAG") };
assert!(!env_flag_is_true("SQRY_TEST_FLAG"));
}
#[test]
#[serial]
fn env_flag_is_false_with_random_string() {
unsafe { std::env::set_var("SQRY_TEST_FLAG", "yes") };
assert!(!env_flag_is_true("SQRY_TEST_FLAG"));
unsafe { std::env::remove_var("SQRY_TEST_FLAG") };
}
}
mod budget_policy_override {
use super::*;
#[test]
fn apply_policy_fail() {
let mut policy = BudgetExceededPolicy::Degrade;
apply_budget_policy_override(&mut policy, "fail", "cli").unwrap();
assert_eq!(policy, BudgetExceededPolicy::Fail);
}
#[test]
fn apply_policy_degrade() {
let mut policy = BudgetExceededPolicy::Fail;
apply_budget_policy_override(&mut policy, "degrade", "cli").unwrap();
assert_eq!(policy, BudgetExceededPolicy::Degrade);
}
#[test]
fn apply_policy_invalid_cli_source_errors() {
let mut policy = BudgetExceededPolicy::Degrade;
let result = apply_budget_policy_override(&mut policy, "invalid", "cli");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("invalid"),
"Error should mention the bad value: {err}"
);
assert_eq!(policy, BudgetExceededPolicy::Degrade);
}
#[test]
fn apply_policy_invalid_config_source_warns_but_succeeds() {
let mut policy = BudgetExceededPolicy::Degrade;
let result = apply_budget_policy_override(&mut policy, "unknown_policy", "config");
assert!(result.is_ok());
assert_eq!(policy, BudgetExceededPolicy::Degrade);
}
}
mod label_budget_env_overrides {
use super::*;
use serial_test::serial;
fn cleanup_env() {
unsafe {
std::env::remove_var("SQRY_LABEL_BUDGET");
std::env::remove_var("SQRY_LABEL_BUDGET_FAIL");
std::env::remove_var("SQRY_DENSITY_GATE_THRESHOLD");
std::env::remove_var("SQRY_NO_LABELS");
}
}
#[test]
#[serial]
fn env_overrides_budget_per_kind() {
cleanup_env();
unsafe { std::env::set_var("SQRY_LABEL_BUDGET", "500") };
let mut config = LabelBudgetConfig::default();
apply_label_budget_env_overrides(&mut config);
assert_eq!(config.budget_per_kind, 500);
cleanup_env();
}
#[test]
#[serial]
fn env_overrides_fail_policy() {
cleanup_env();
unsafe { std::env::set_var("SQRY_LABEL_BUDGET_FAIL", "1") };
let mut config = LabelBudgetConfig::default();
apply_label_budget_env_overrides(&mut config);
assert_eq!(config.on_exceeded, BudgetExceededPolicy::Fail);
cleanup_env();
}
#[test]
#[serial]
fn env_overrides_density_gate_threshold() {
cleanup_env();
unsafe { std::env::set_var("SQRY_DENSITY_GATE_THRESHOLD", "128") };
let mut config = LabelBudgetConfig::default();
apply_label_budget_env_overrides(&mut config);
assert_eq!(config.density_gate_threshold, 128);
cleanup_env();
}
#[test]
#[serial]
fn env_overrides_skip_labels() {
cleanup_env();
unsafe { std::env::set_var("SQRY_NO_LABELS", "true") };
let mut config = LabelBudgetConfig::default();
apply_label_budget_env_overrides(&mut config);
assert!(config.skip_labels);
cleanup_env();
}
#[test]
#[serial]
fn env_overrides_no_change_when_vars_absent() {
cleanup_env();
let mut config = LabelBudgetConfig::default();
let default = LabelBudgetConfig::default();
apply_label_budget_env_overrides(&mut config);
assert_eq!(config.budget_per_kind, default.budget_per_kind);
assert_eq!(config.on_exceeded, default.on_exceeded);
assert_eq!(
config.density_gate_threshold,
default.density_gate_threshold
);
assert_eq!(config.skip_labels, default.skip_labels);
}
#[test]
#[serial]
fn env_overrides_multiple_vars_combined() {
cleanup_env();
unsafe {
std::env::set_var("SQRY_LABEL_BUDGET", "1000");
std::env::set_var("SQRY_LABEL_BUDGET_FAIL", "true");
std::env::set_var("SQRY_DENSITY_GATE_THRESHOLD", "256");
std::env::set_var("SQRY_NO_LABELS", "1");
}
let mut config = LabelBudgetConfig::default();
apply_label_budget_env_overrides(&mut config);
assert_eq!(config.budget_per_kind, 1000);
assert_eq!(config.on_exceeded, BudgetExceededPolicy::Fail);
assert_eq!(config.density_gate_threshold, 256);
assert!(config.skip_labels);
cleanup_env();
}
}
mod label_budget_cli_overrides {
use super::*;
#[test]
fn cli_overrides_budget() {
let mut config = LabelBudgetConfig::default();
apply_label_budget_cli_overrides(&mut config, Some(999), None, None, false).unwrap();
assert_eq!(config.budget_per_kind, 999);
}
#[test]
fn cli_overrides_density_threshold() {
let mut config = LabelBudgetConfig::default();
apply_label_budget_cli_overrides(&mut config, None, Some(200), None, false).unwrap();
assert_eq!(config.density_gate_threshold, 200);
}
#[test]
fn cli_overrides_no_labels() {
let mut config = LabelBudgetConfig::default();
apply_label_budget_cli_overrides(&mut config, None, None, None, true).unwrap();
assert!(config.skip_labels);
}
#[test]
fn cli_overrides_policy_fail() {
let mut config = LabelBudgetConfig::default();
apply_label_budget_cli_overrides(&mut config, None, None, Some("fail"), false).unwrap();
assert_eq!(config.on_exceeded, BudgetExceededPolicy::Fail);
}
#[test]
fn cli_overrides_policy_degrade() {
let mut config = LabelBudgetConfig {
on_exceeded: BudgetExceededPolicy::Fail,
..LabelBudgetConfig::default()
};
apply_label_budget_cli_overrides(&mut config, None, None, Some("degrade"), false)
.unwrap();
assert_eq!(config.on_exceeded, BudgetExceededPolicy::Degrade);
}
#[test]
fn cli_overrides_invalid_policy_errors() {
let mut config = LabelBudgetConfig::default();
let result =
apply_label_budget_cli_overrides(&mut config, None, None, Some("bad"), false);
assert!(result.is_err());
}
#[test]
fn cli_overrides_no_args_no_change() {
let mut config = LabelBudgetConfig::default();
let default = LabelBudgetConfig::default();
apply_label_budget_cli_overrides(&mut config, None, None, None, false).unwrap();
assert_eq!(config.budget_per_kind, default.budget_per_kind);
assert_eq!(config.on_exceeded, default.on_exceeded);
assert_eq!(
config.density_gate_threshold,
default.density_gate_threshold
);
assert_eq!(config.skip_labels, default.skip_labels);
}
#[test]
fn cli_overrides_all_args_combined() {
let mut config = LabelBudgetConfig::default();
apply_label_budget_cli_overrides(&mut config, Some(77), Some(33), Some("fail"), true)
.unwrap();
assert_eq!(config.budget_per_kind, 77);
assert_eq!(config.density_gate_threshold, 33);
assert_eq!(config.on_exceeded, BudgetExceededPolicy::Fail);
assert!(config.skip_labels);
}
}
mod config_file_overrides {
use super::*;
#[test]
fn config_overrides_nonexistent_dir_is_ok() {
let result = apply_label_budget_config_overrides(
Path::new("/nonexistent/path/that/does/not/exist"),
&mut LabelBudgetConfig::default(),
);
assert!(result.is_ok());
}
#[test]
fn config_overrides_uninitialized_dir_is_ok() {
let temp = tempfile::TempDir::new().unwrap();
let mut config = LabelBudgetConfig::default();
let default = LabelBudgetConfig::default();
let result = apply_label_budget_config_overrides(temp.path(), &mut config);
assert!(result.is_ok());
assert_eq!(config.budget_per_kind, default.budget_per_kind);
}
}
#[test]
fn test_path_reconstruction_intra_scc() {
let file = FileId::new(0);
let kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
let edges = vec![
MergedEdge::new(NodeId::new(0, 0), NodeId::new(1, 0), kind.clone(), 1, file),
MergedEdge::new(NodeId::new(1, 0), NodeId::new(0, 0), kind.clone(), 2, file),
];
let snapshot = CompactionSnapshot {
csr_edges: edges,
delta_edges: Vec::new(),
node_count: 2,
csr_version: 0,
};
let csr = CsrAdjacency::build_from_snapshot(&snapshot).unwrap();
let scc = SccData::compute_tarjan(&csr, &kind).unwrap();
let dag = CondensationDag::build(&scc, &csr).unwrap();
let recon = PathReconstructor::new(&csr, &scc, &dag);
let path = recon
.reconstruct_path(NodeId::new(0, 0), NodeId::new(1, 0))
.unwrap()
.unwrap();
assert_eq!(path.first(), Some(&NodeId::new(0, 0)));
assert_eq!(path.last(), Some(&NodeId::new(1, 0)));
}
}