use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::OnceLock;
use dashmap::DashMap;
use tldr_core::ssa::SsaFunction;
use tldr_core::{CfgInfo, ChangeImpactReport, DfgInfo, Language, ProjectCallGraph};
use super::daemon_client::{DaemonClient, NoDaemon};
use super::types::FunctionId;
use crate::commands::contracts::types::ContractsReport;
use crate::commands::remaining::types::ASTChange;
#[derive(Debug, Clone)]
pub struct FunctionChange {
pub id: FunctionId,
pub name: String,
pub old_source: String,
pub new_source: String,
}
#[derive(Debug, Clone)]
pub struct InsertedFunction {
pub id: FunctionId,
pub name: String,
pub source: String,
}
#[derive(Debug, Clone)]
pub struct DeletedFunction {
pub id: FunctionId,
pub name: String,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum ContractVersion {
Baseline,
Current,
}
#[derive(Debug, Clone)]
pub struct FunctionDiff {
pub changed: Vec<FunctionChange>,
pub inserted: Vec<InsertedFunction>,
pub deleted: Vec<DeletedFunction>,
}
pub struct L2Context {
pub project: PathBuf,
pub language: Language,
pub changed_files: Vec<PathBuf>,
pub function_diff: FunctionDiff,
pub baseline_contents: HashMap<PathBuf, String>,
pub current_contents: HashMap<PathBuf, String>,
pub ast_changes: HashMap<PathBuf, Vec<ASTChange>>,
cfg_cache: DashMap<FunctionId, CfgInfo>,
dfg_cache: DashMap<FunctionId, DfgInfo>,
ssa_cache: DashMap<FunctionId, SsaFunction>,
contracts_cache: DashMap<(FunctionId, ContractVersion), ContractsReport>,
call_graph: OnceLock<ProjectCallGraph>,
change_impact: OnceLock<ChangeImpactReport>,
pub is_first_run: bool,
pub base_ref: String,
daemon: Box<dyn DaemonClient>,
}
impl L2Context {
pub fn new(
project: PathBuf,
language: Language,
changed_files: Vec<PathBuf>,
function_diff: FunctionDiff,
baseline_contents: HashMap<PathBuf, String>,
current_contents: HashMap<PathBuf, String>,
ast_changes: HashMap<PathBuf, Vec<ASTChange>>,
) -> Self {
Self {
project,
language,
changed_files,
function_diff,
baseline_contents,
current_contents,
ast_changes,
cfg_cache: DashMap::new(),
dfg_cache: DashMap::new(),
ssa_cache: DashMap::new(),
contracts_cache: DashMap::new(),
call_graph: OnceLock::new(),
change_impact: OnceLock::new(),
is_first_run: false,
base_ref: String::from("HEAD"),
daemon: Box::new(NoDaemon),
}
}
pub fn with_first_run(mut self, is_first_run: bool) -> Self {
self.is_first_run = is_first_run;
self
}
pub fn with_base_ref(mut self, base_ref: String) -> Self {
self.base_ref = base_ref;
self
}
pub fn with_daemon(mut self, daemon: Box<dyn DaemonClient>) -> Self {
daemon.notify_changed_files(&self.changed_files);
self.daemon = daemon;
self
}
pub fn daemon_available(&self) -> bool {
self.daemon.is_available()
}
pub fn daemon(&self) -> &dyn DaemonClient {
self.daemon.as_ref()
}
pub fn changed_functions(&self) -> &[FunctionChange] {
&self.function_diff.changed
}
pub fn inserted_functions(&self) -> &[InsertedFunction] {
&self.function_diff.inserted
}
pub fn deleted_functions(&self) -> &[DeletedFunction] {
&self.function_diff.deleted
}
pub fn cfg_for(
&self,
file_contents: &str,
function_id: &FunctionId,
language: Language,
) -> anyhow::Result<dashmap::mapref::one::Ref<'_, FunctionId, CfgInfo>> {
if let Some(entry) = self.cfg_cache.get(function_id) {
return Ok(entry);
}
if let Some(cached) = self.daemon.query_cfg(function_id) {
self.cfg_cache.insert(function_id.clone(), cached);
return Ok(self.cfg_cache.get(function_id).unwrap());
}
let cfg = super::ir::build_cfg_for_function(file_contents, function_id, language)?;
self.cfg_cache.insert(function_id.clone(), cfg);
Ok(self.cfg_cache.get(function_id).unwrap())
}
pub fn dfg_for(
&self,
file_contents: &str,
function_id: &FunctionId,
language: Language,
) -> anyhow::Result<dashmap::mapref::one::Ref<'_, FunctionId, DfgInfo>> {
if let Some(entry) = self.dfg_cache.get(function_id) {
return Ok(entry);
}
if let Some(cached) = self.daemon.query_dfg(function_id) {
self.dfg_cache.insert(function_id.clone(), cached);
return Ok(self.dfg_cache.get(function_id).unwrap());
}
let dfg = super::ir::build_dfg_for_function(file_contents, function_id, language)?;
self.dfg_cache.insert(function_id.clone(), dfg);
Ok(self.dfg_cache.get(function_id).unwrap())
}
pub fn ssa_for(
&self,
file_contents: &str,
function_id: &FunctionId,
language: Language,
) -> anyhow::Result<dashmap::mapref::one::Ref<'_, FunctionId, SsaFunction>> {
if let Some(entry) = self.ssa_cache.get(function_id) {
return Ok(entry);
}
if let Some(cached) = self.daemon.query_ssa(function_id) {
self.ssa_cache.insert(function_id.clone(), cached);
return Ok(self.ssa_cache.get(function_id).unwrap());
}
let ssa = super::ir::build_ssa_for_function(file_contents, function_id, language)?;
self.ssa_cache.insert(function_id.clone(), ssa);
Ok(self.ssa_cache.get(function_id).unwrap())
}
pub fn contracts_for(
&self,
function_id: &FunctionId,
version: ContractVersion,
build_fn: impl FnOnce() -> anyhow::Result<ContractsReport>,
) -> anyhow::Result<
dashmap::mapref::one::Ref<'_, (FunctionId, ContractVersion), ContractsReport>,
> {
let key = (function_id.clone(), version);
if let Some(entry) = self.contracts_cache.get(&key) {
return Ok(entry);
}
let report = build_fn()?;
self.contracts_cache.insert(key.clone(), report);
Ok(self.contracts_cache.get(&key).unwrap())
}
pub fn call_graph(&self) -> Option<&ProjectCallGraph> {
if let Some(cg) = self.call_graph.get() {
return Some(cg);
}
if let Some(cached) = self.daemon.query_call_graph() {
let _ = self.call_graph.set(cached);
return self.call_graph.get();
}
None
}
pub fn set_call_graph(&self, cg: ProjectCallGraph) -> Result<(), ProjectCallGraph> {
self.call_graph.set(cg)
}
pub fn change_impact(&self) -> Option<&ChangeImpactReport> {
self.change_impact.get()
}
pub fn set_change_impact(
&self,
report: ChangeImpactReport,
) -> Result<(), Box<ChangeImpactReport>> {
self.change_impact.set(report).map_err(Box::new)
}
#[cfg(test)]
pub fn test_fixture() -> Self {
Self::new(
PathBuf::from("/tmp/test-project"),
Language::Rust,
vec![],
FunctionDiff {
changed: vec![],
inserted: vec![],
deleted: vec![],
},
HashMap::new(),
HashMap::new(),
HashMap::new(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_l2_context_new() {
let ctx = L2Context::new(
PathBuf::from("/projects/myapp"),
Language::Python,
vec![PathBuf::from("src/main.py")],
FunctionDiff {
changed: vec![],
inserted: vec![],
deleted: vec![],
},
HashMap::new(),
HashMap::new(),
HashMap::new(),
);
assert_eq!(ctx.project, PathBuf::from("/projects/myapp"));
assert_eq!(ctx.language, Language::Python);
assert_eq!(ctx.changed_files.len(), 1);
assert_eq!(ctx.changed_files[0], PathBuf::from("src/main.py"));
assert!(ctx.changed_functions().is_empty());
assert!(ctx.inserted_functions().is_empty());
assert!(ctx.deleted_functions().is_empty());
assert!(ctx.baseline_contents.is_empty());
assert!(ctx.current_contents.is_empty());
assert!(ctx.ast_changes.is_empty());
}
#[test]
fn test_l2_context_test_fixture() {
let ctx = L2Context::test_fixture();
assert_eq!(ctx.project, PathBuf::from("/tmp/test-project"));
assert_eq!(ctx.language, Language::Rust);
assert!(ctx.changed_files.is_empty());
assert!(ctx.changed_functions().is_empty());
assert!(ctx.inserted_functions().is_empty());
assert!(ctx.deleted_functions().is_empty());
assert!(ctx.baseline_contents.is_empty());
assert!(ctx.current_contents.is_empty());
assert!(ctx.ast_changes.is_empty());
}
#[test]
fn test_function_change_fields() {
let change = FunctionChange {
id: FunctionId::new("src/lib.rs", "compute", 1),
name: "compute".to_string(),
old_source: "fn compute() { 1 + 1 }".to_string(),
new_source: "fn compute() { 2 + 2 }".to_string(),
};
assert_eq!(change.id.file, PathBuf::from("src/lib.rs"));
assert_eq!(change.id.qualified_name, "compute");
assert_eq!(change.name, "compute");
assert!(change.old_source.contains("1 + 1"));
assert!(change.new_source.contains("2 + 2"));
}
#[test]
fn test_inserted_function_fields() {
let inserted = InsertedFunction {
id: FunctionId::new("src/new.rs", "fresh_func", 1),
name: "fresh_func".to_string(),
source: "fn fresh_func() -> bool { true }".to_string(),
};
assert_eq!(inserted.id.file, PathBuf::from("src/new.rs"));
assert_eq!(inserted.id.qualified_name, "fresh_func");
assert_eq!(inserted.name, "fresh_func");
assert!(inserted.source.contains("true"));
}
#[test]
fn test_deleted_function_fields() {
let deleted = DeletedFunction {
id: FunctionId::new("src/old.rs", "stale_func", 1),
name: "stale_func".to_string(),
};
assert_eq!(deleted.id.file, PathBuf::from("src/old.rs"));
assert_eq!(deleted.id.qualified_name, "stale_func");
assert_eq!(deleted.name, "stale_func");
}
#[test]
fn test_contract_version_eq() {
assert_eq!(ContractVersion::Baseline, ContractVersion::Baseline);
assert_eq!(ContractVersion::Current, ContractVersion::Current);
assert_ne!(ContractVersion::Baseline, ContractVersion::Current);
assert_ne!(ContractVersion::Current, ContractVersion::Baseline);
}
#[test]
fn test_contract_version_hash_consistency() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(ContractVersion::Baseline);
set.insert(ContractVersion::Baseline); set.insert(ContractVersion::Current);
assert_eq!(set.len(), 2, "HashSet should deduplicate identical variants");
assert!(set.contains(&ContractVersion::Baseline));
assert!(set.contains(&ContractVersion::Current));
}
#[test]
fn test_l2_context_with_data() {
let file_a = PathBuf::from("src/alpha.rs");
let file_b = PathBuf::from("src/beta.rs");
let file_c = PathBuf::from("src/gamma.rs");
let changed = vec![
FunctionChange {
id: FunctionId::new("src/alpha.rs", "do_alpha", 1),
name: "do_alpha".to_string(),
old_source: "fn do_alpha() {}".to_string(),
new_source: "fn do_alpha() { todo!() }".to_string(),
},
FunctionChange {
id: FunctionId::new("src/alpha.rs", "do_alpha2", 5),
name: "do_alpha2".to_string(),
old_source: "fn do_alpha2() {}".to_string(),
new_source: "fn do_alpha2() { 42 }".to_string(),
},
];
let inserted = vec![InsertedFunction {
id: FunctionId::new("src/beta.rs", "new_beta", 1),
name: "new_beta".to_string(),
source: "fn new_beta() -> u32 { 0 }".to_string(),
}];
let deleted = vec![DeletedFunction {
id: FunctionId::new("src/gamma.rs", "old_gamma", 1),
name: "old_gamma".to_string(),
}];
let mut baseline = HashMap::new();
baseline.insert(file_a.clone(), "// alpha baseline".to_string());
baseline.insert(file_c.clone(), "// gamma baseline".to_string());
let mut current = HashMap::new();
current.insert(file_a.clone(), "// alpha current".to_string());
current.insert(file_b.clone(), "// beta current".to_string());
let ctx = L2Context::new(
PathBuf::from("/workspace"),
Language::Rust,
vec![file_a.clone(), file_b.clone(), file_c.clone()],
FunctionDiff {
changed,
inserted,
deleted,
},
baseline,
current,
HashMap::new(),
);
assert_eq!(ctx.changed_files.len(), 3);
assert_eq!(ctx.changed_functions().len(), 2);
assert_eq!(ctx.inserted_functions().len(), 1);
assert_eq!(ctx.deleted_functions().len(), 1);
assert_eq!(ctx.baseline_contents.len(), 2);
assert_eq!(ctx.current_contents.len(), 2);
assert_eq!(ctx.changed_functions()[0].name, "do_alpha");
assert_eq!(ctx.changed_functions()[1].name, "do_alpha2");
assert_eq!(ctx.inserted_functions()[0].name, "new_beta");
assert_eq!(ctx.deleted_functions()[0].name, "old_gamma");
assert_eq!(
ctx.baseline_contents.get(&file_a).unwrap(),
"// alpha baseline"
);
assert_eq!(
ctx.current_contents.get(&file_b).unwrap(),
"// beta current"
);
}
#[test]
fn test_function_change_clone() {
let original = FunctionChange {
id: FunctionId::new("src/lib.rs", "my_fn", 1),
name: "my_fn".to_string(),
old_source: "old".to_string(),
new_source: "new".to_string(),
};
let cloned = original.clone();
assert_eq!(cloned.id, original.id);
assert_eq!(cloned.name, original.name);
assert_eq!(cloned.old_source, original.old_source);
assert_eq!(cloned.new_source, original.new_source);
}
#[test]
fn test_inserted_function_clone() {
let original = InsertedFunction {
id: FunctionId::new("src/lib.rs", "ins_fn", 1),
name: "ins_fn".to_string(),
source: "fn ins_fn() {}".to_string(),
};
let cloned = original.clone();
assert_eq!(cloned.id, original.id);
assert_eq!(cloned.name, original.name);
assert_eq!(cloned.source, original.source);
}
#[test]
fn test_deleted_function_clone() {
let original = DeletedFunction {
id: FunctionId::new("src/lib.rs", "del_fn", 1),
name: "del_fn".to_string(),
};
let cloned = original.clone();
assert_eq!(cloned.id, original.id);
assert_eq!(cloned.name, original.name);
}
#[test]
fn test_contract_version_clone() {
let v1 = ContractVersion::Baseline;
let v2 = v1.clone();
assert_eq!(v1, v2);
let v3 = ContractVersion::Current;
let v4 = v3.clone();
assert_eq!(v3, v4);
}
const PYTHON_ADD: &str = "def add(a, b):\n return a + b\n";
#[test]
fn test_cfg_cache_miss_then_hit() {
let ctx = L2Context::test_fixture();
let fid = FunctionId::new("test.py", "add", 1);
let result = ctx.cfg_for(PYTHON_ADD, &fid, Language::Python);
assert!(result.is_ok(), "CFG build should succeed: {:?}", result.err());
let cfg = result.unwrap();
assert_eq!(cfg.function, "add");
drop(cfg);
let result2 = ctx.cfg_for(PYTHON_ADD, &fid, Language::Python);
assert!(result2.is_ok());
let cfg2 = result2.unwrap();
assert_eq!(cfg2.function, "add");
}
#[test]
fn test_dfg_cache_miss_then_hit() {
let ctx = L2Context::test_fixture();
let fid = FunctionId::new("test.py", "add", 1);
let result = ctx.dfg_for(PYTHON_ADD, &fid, Language::Python);
assert!(result.is_ok(), "DFG build should succeed: {:?}", result.err());
let dfg = result.unwrap();
assert_eq!(dfg.function, "add");
drop(dfg);
let result2 = ctx.dfg_for(PYTHON_ADD, &fid, Language::Python);
assert!(result2.is_ok());
let dfg2 = result2.unwrap();
assert_eq!(dfg2.function, "add");
}
#[test]
fn test_ssa_cache_miss_then_hit() {
let ctx = L2Context::test_fixture();
let fid = FunctionId::new("test.py", "add", 1);
let result = ctx.ssa_for(PYTHON_ADD, &fid, Language::Python);
assert!(result.is_ok(), "SSA build should succeed: {:?}", result.err());
let ssa = result.unwrap();
assert_eq!(ssa.function, "add");
drop(ssa);
let result2 = ctx.ssa_for(PYTHON_ADD, &fid, Language::Python);
assert!(result2.is_ok());
let ssa2 = result2.unwrap();
assert_eq!(ssa2.function, "add");
}
#[test]
fn test_contracts_cache_stores_and_retrieves() {
use crate::commands::contracts::types::ContractsReport;
let ctx = L2Context::test_fixture();
let fid = FunctionId::new("test.py", "add", 1);
let report = ContractsReport {
function: "add".to_string(),
file: PathBuf::from("test.py"),
preconditions: vec![],
postconditions: vec![],
invariants: vec![],
};
let report_clone = report.clone();
let result = ctx.contracts_for(&fid, ContractVersion::Baseline, || Ok(report));
assert!(result.is_ok());
let cached = result.unwrap();
assert_eq!(cached.function, "add");
drop(cached);
let result2 = ctx.contracts_for(&fid, ContractVersion::Baseline, || {
panic!("build_fn should not be called on cache hit");
});
assert!(result2.is_ok());
assert_eq!(result2.unwrap().function, report_clone.function);
}
#[test]
fn test_call_graph_set_and_get() {
let ctx = L2Context::test_fixture();
assert!(ctx.call_graph().is_none());
let cg = ProjectCallGraph::default();
assert!(ctx.set_call_graph(cg).is_ok());
assert!(ctx.call_graph().is_some());
}
#[test]
fn test_call_graph_double_set_fails() {
let ctx = L2Context::test_fixture();
let cg1 = ProjectCallGraph::default();
assert!(ctx.set_call_graph(cg1).is_ok());
let cg2 = ProjectCallGraph::default();
assert!(ctx.set_call_graph(cg2).is_err());
}
#[test]
fn test_change_impact_set_and_get() {
let ctx = L2Context::test_fixture();
assert!(ctx.change_impact().is_none());
let report = ChangeImpactReport {
changed_files: vec![PathBuf::from("src/main.rs")],
affected_tests: vec![],
affected_test_functions: vec![],
affected_functions: vec![],
detection_method: "call_graph".to_string(),
metadata: None,
};
assert!(ctx.set_change_impact(report).is_ok());
let stored = ctx.change_impact().unwrap();
assert_eq!(stored.changed_files.len(), 1);
assert_eq!(stored.detection_method, "call_graph");
}
#[test]
fn test_cache_fields_independent() {
let ctx = L2Context::test_fixture();
let fid = FunctionId::new("test.py", "add", 1);
let cfg_result = ctx.cfg_for(PYTHON_ADD, &fid, Language::Python);
assert!(cfg_result.is_ok());
drop(cfg_result);
assert!(
ctx.dfg_cache.get(&fid).is_none(),
"DFG cache should be empty when only CFG was built"
);
let dfg_result = ctx.dfg_for(PYTHON_ADD, &fid, Language::Python);
assert!(dfg_result.is_ok());
drop(dfg_result);
assert!(ctx.cfg_cache.get(&fid).is_some());
assert!(ctx.dfg_cache.get(&fid).is_some());
}
#[test]
fn test_cfg_cache_different_functions() {
let ctx = L2Context::test_fixture();
let source = "def foo(x):\n return x\n\ndef bar(y):\n return y + 1\n";
let fid_foo = FunctionId::new("multi.py", "foo", 1);
let fid_bar = FunctionId::new("multi.py", "bar", 4);
let r1 = ctx.cfg_for(source, &fid_foo, Language::Python);
assert!(r1.is_ok());
let cfg_foo = r1.unwrap();
assert_eq!(cfg_foo.function, "foo");
drop(cfg_foo);
let r2 = ctx.cfg_for(source, &fid_bar, Language::Python);
assert!(r2.is_ok());
let cfg_bar = r2.unwrap();
assert_eq!(cfg_bar.function, "bar");
drop(cfg_bar);
assert_eq!(ctx.cfg_cache.len(), 2);
assert_eq!(ctx.cfg_cache.get(&fid_foo).unwrap().function, "foo");
assert_eq!(ctx.cfg_cache.get(&fid_bar).unwrap().function, "bar");
}
#[test]
fn test_l2_context_default_is_not_first_run() {
let ctx = L2Context::test_fixture();
assert!(!ctx.is_first_run, "Default L2Context should not be first run");
}
#[test]
fn test_l2_context_with_first_run_true() {
let ctx = L2Context::test_fixture().with_first_run(true);
assert!(ctx.is_first_run, "with_first_run(true) should set is_first_run");
}
#[test]
fn test_l2_context_with_first_run_false() {
let ctx = L2Context::test_fixture().with_first_run(false);
assert!(!ctx.is_first_run, "with_first_run(false) should unset is_first_run");
}
#[test]
fn test_l2_context_with_first_run_chainable() {
let ctx = L2Context::new(
PathBuf::from("/tmp/test"),
Language::Python,
vec![],
FunctionDiff {
changed: vec![],
inserted: vec![],
deleted: vec![],
},
HashMap::new(),
HashMap::new(),
HashMap::new(),
)
.with_first_run(true);
assert!(ctx.is_first_run);
assert_eq!(ctx.project, PathBuf::from("/tmp/test"));
assert_eq!(ctx.language, Language::Python);
}
use super::super::daemon_client::DaemonClient;
struct MockDaemonWithCallGraph {
call_graph: ProjectCallGraph,
notifications: std::sync::Mutex<Vec<Vec<PathBuf>>>,
}
impl MockDaemonWithCallGraph {
fn new() -> Self {
Self {
call_graph: ProjectCallGraph::default(),
notifications: std::sync::Mutex::new(Vec::new()),
}
}
}
impl DaemonClient for MockDaemonWithCallGraph {
fn is_available(&self) -> bool {
true
}
fn query_call_graph(&self) -> Option<ProjectCallGraph> {
Some(self.call_graph.clone())
}
fn query_cfg(
&self,
_function_id: &FunctionId,
) -> Option<tldr_core::CfgInfo> {
None
}
fn query_dfg(
&self,
_function_id: &FunctionId,
) -> Option<tldr_core::DfgInfo> {
None
}
fn query_ssa(
&self,
_function_id: &FunctionId,
) -> Option<tldr_core::ssa::SsaFunction> {
None
}
fn notify_changed_files(&self, changed_files: &[PathBuf]) {
self.notifications
.lock()
.unwrap()
.push(changed_files.to_vec());
}
}
struct MockUnavailableDaemon;
impl DaemonClient for MockUnavailableDaemon {
fn is_available(&self) -> bool {
false
}
fn query_call_graph(&self) -> Option<ProjectCallGraph> {
None
}
fn query_cfg(&self, _fid: &FunctionId) -> Option<tldr_core::CfgInfo> {
None
}
fn query_dfg(&self, _fid: &FunctionId) -> Option<tldr_core::DfgInfo> {
None
}
fn query_ssa(&self, _fid: &FunctionId) -> Option<tldr_core::ssa::SsaFunction> {
None
}
fn notify_changed_files(&self, _files: &[PathBuf]) {}
}
#[test]
fn test_l2_context_default_daemon_not_available() {
let ctx = L2Context::test_fixture();
assert!(
!ctx.daemon_available(),
"Default L2Context should have NoDaemon (not available)"
);
}
#[test]
fn test_l2_context_with_daemon_sets_available() {
let ctx = L2Context::test_fixture()
.with_daemon(Box::new(MockDaemonWithCallGraph::new()));
assert!(
ctx.daemon_available(),
"L2Context with mock daemon should report available"
);
}
#[test]
fn test_l2_context_with_daemon_notifies_changed_files() {
use std::sync::Arc;
let notifications: Arc<std::sync::Mutex<Vec<Vec<PathBuf>>>> =
Arc::new(std::sync::Mutex::new(Vec::new()));
struct ArcTrackingDaemon {
notified: Arc<std::sync::Mutex<Vec<Vec<PathBuf>>>>,
}
impl DaemonClient for ArcTrackingDaemon {
fn is_available(&self) -> bool { true }
fn query_call_graph(&self) -> Option<ProjectCallGraph> { None }
fn query_cfg(&self, _fid: &FunctionId) -> Option<tldr_core::CfgInfo> { None }
fn query_dfg(&self, _fid: &FunctionId) -> Option<tldr_core::DfgInfo> { None }
fn query_ssa(&self, _fid: &FunctionId) -> Option<tldr_core::ssa::SsaFunction> { None }
fn notify_changed_files(&self, files: &[PathBuf]) {
self.notified.lock().unwrap().push(files.to_vec());
}
}
let changed = vec![PathBuf::from("src/lib.rs"), PathBuf::from("src/main.rs")];
let ctx = L2Context::new(
PathBuf::from("/tmp/test"),
Language::Rust,
changed.clone(),
FunctionDiff {
changed: vec![],
inserted: vec![],
deleted: vec![],
},
HashMap::new(),
HashMap::new(),
HashMap::new(),
);
let daemon = ArcTrackingDaemon {
notified: Arc::clone(¬ifications),
};
let ctx = ctx.with_daemon(Box::new(daemon));
assert!(ctx.daemon_available());
let recorded = notifications.lock().unwrap();
assert_eq!(
recorded.len(),
1,
"with_daemon should have called notify_changed_files exactly once"
);
assert_eq!(
recorded[0], changed,
"notify_changed_files should receive the context's changed_files"
);
}
#[test]
fn test_l2_context_daemon_not_available_call_graph_none() {
let ctx = L2Context::test_fixture()
.with_daemon(Box::new(MockUnavailableDaemon));
assert!(
ctx.call_graph().is_none(),
"Unavailable daemon should not provide call graph"
);
}
#[test]
fn test_l2_context_daemon_available_uses_cached_call_graph() {
let ctx = L2Context::test_fixture()
.with_daemon(Box::new(MockDaemonWithCallGraph::new()));
let cg = ctx.call_graph();
assert!(
cg.is_some(),
"Available daemon should provide cached call graph"
);
}
#[test]
fn test_l2_context_daemon_not_available_cfg_uses_local() {
let ctx = L2Context::test_fixture()
.with_daemon(Box::new(MockUnavailableDaemon));
let fid = FunctionId::new("test.py", "add", 1);
let result = ctx.cfg_for(PYTHON_ADD, &fid, Language::Python);
assert!(
result.is_ok(),
"cfg_for should fall back to local computation: {:?}",
result.err()
);
let cfg = result.unwrap();
assert_eq!(cfg.function, "add");
}
#[test]
fn test_l2_context_daemon_not_available_dfg_uses_local() {
let ctx = L2Context::test_fixture()
.with_daemon(Box::new(MockUnavailableDaemon));
let fid = FunctionId::new("test.py", "add", 1);
let result = ctx.dfg_for(PYTHON_ADD, &fid, Language::Python);
assert!(
result.is_ok(),
"dfg_for should fall back to local computation: {:?}",
result.err()
);
}
#[test]
fn test_l2_context_daemon_not_available_ssa_uses_local() {
let ctx = L2Context::test_fixture()
.with_daemon(Box::new(MockUnavailableDaemon));
let fid = FunctionId::new("test.py", "add", 1);
let result = ctx.ssa_for(PYTHON_ADD, &fid, Language::Python);
assert!(
result.is_ok(),
"ssa_for should fall back to local computation: {:?}",
result.err()
);
}
#[test]
fn test_l2_context_with_daemon_chainable() {
let ctx = L2Context::test_fixture()
.with_first_run(true)
.with_daemon(Box::new(MockDaemonWithCallGraph::new()));
assert!(ctx.is_first_run);
assert!(ctx.daemon_available());
}
#[test]
fn test_l2_context_daemon_accessor() {
let ctx = L2Context::test_fixture();
assert!(!ctx.daemon().is_available());
let ctx2 = L2Context::test_fixture()
.with_daemon(Box::new(MockDaemonWithCallGraph::new()));
assert!(ctx2.daemon().is_available());
}
#[test]
fn test_l2_context_local_call_graph_takes_precedence() {
let ctx = L2Context::test_fixture()
.with_daemon(Box::new(MockDaemonWithCallGraph::new()));
let local_cg = ProjectCallGraph::default();
assert!(ctx.set_call_graph(local_cg).is_ok());
let cg = ctx.call_graph();
assert!(cg.is_some(), "Local call graph should take precedence");
}
}