use std::path::{Path, PathBuf};
use tldr_core::ssa::SsaFunction;
use tldr_core::{CfgInfo, DfgInfo, ProjectCallGraph};
use super::types::FunctionId;
pub trait DaemonClient: Send + Sync {
fn is_available(&self) -> bool;
fn query_call_graph(&self) -> Option<ProjectCallGraph>;
fn query_cfg(&self, function_id: &FunctionId) -> Option<CfgInfo>;
fn query_dfg(&self, function_id: &FunctionId) -> Option<DfgInfo>;
fn query_ssa(&self, function_id: &FunctionId) -> Option<SsaFunction>;
fn notify_changed_files(&self, changed_files: &[PathBuf]);
}
pub struct NoDaemon;
impl DaemonClient for NoDaemon {
fn is_available(&self) -> bool {
false
}
fn query_call_graph(&self) -> Option<ProjectCallGraph> {
None
}
fn query_cfg(&self, _function_id: &FunctionId) -> Option<CfgInfo> {
None
}
fn query_dfg(&self, _function_id: &FunctionId) -> Option<DfgInfo> {
None
}
fn query_ssa(&self, _function_id: &FunctionId) -> Option<SsaFunction> {
None
}
fn notify_changed_files(&self, _changed_files: &[PathBuf]) {
}
}
pub struct LocalDaemonClient {
project: PathBuf,
socket_path: PathBuf,
available: bool,
}
impl LocalDaemonClient {
pub fn new(project: &Path) -> Self {
let socket_path = Self::compute_socket_path(project);
let available = socket_path.exists();
Self {
project: project.to_path_buf(),
socket_path,
available,
}
}
fn compute_socket_path(project: &Path) -> PathBuf {
let canonical = dunce::canonicalize(project).unwrap_or_else(|_| project.to_path_buf());
let path_str = canonical.to_string_lossy();
let digest = md5::compute(path_str.as_bytes());
let hash = format!("{:x}", digest);
let hash_prefix = &hash[..8];
let socket_dir = std::env::var("TLDR_SOCKET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir());
socket_dir.join(format!("tldr-{}-v1.0.sock", hash_prefix))
}
pub fn project(&self) -> &Path {
&self.project
}
pub fn socket_path(&self) -> &Path {
&self.socket_path
}
}
impl DaemonClient for LocalDaemonClient {
fn is_available(&self) -> bool {
self.available
}
fn query_call_graph(&self) -> Option<ProjectCallGraph> {
if !self.available {
return None;
}
None
}
fn query_cfg(&self, _function_id: &FunctionId) -> Option<CfgInfo> {
if !self.available {
return None;
}
None
}
fn query_dfg(&self, _function_id: &FunctionId) -> Option<DfgInfo> {
if !self.available {
return None;
}
None
}
fn query_ssa(&self, _function_id: &FunctionId) -> Option<SsaFunction> {
if !self.available {
return None;
}
None
}
fn notify_changed_files(&self, _changed_files: &[PathBuf]) {
if self.available {
}
}
}
pub fn create_daemon_client(project: &Path) -> Box<dyn DaemonClient> {
let client = LocalDaemonClient::new(project);
if client.is_available() {
Box::new(client)
} else {
Box::new(NoDaemon)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_no_daemon_is_not_available() {
let client = NoDaemon;
assert!(
!client.is_available(),
"NoDaemon should always report not available"
);
}
#[test]
fn test_no_daemon_query_call_graph_returns_none() {
let client = NoDaemon;
assert!(
client.query_call_graph().is_none(),
"NoDaemon should return None for call graph"
);
}
#[test]
fn test_no_daemon_query_cfg_returns_none() {
let client = NoDaemon;
let fid = FunctionId::new("test.py", "foo", 1);
assert!(
client.query_cfg(&fid).is_none(),
"NoDaemon should return None for CFG"
);
}
#[test]
fn test_no_daemon_query_dfg_returns_none() {
let client = NoDaemon;
let fid = FunctionId::new("test.py", "bar", 5);
assert!(
client.query_dfg(&fid).is_none(),
"NoDaemon should return None for DFG"
);
}
#[test]
fn test_no_daemon_query_ssa_returns_none() {
let client = NoDaemon;
let fid = FunctionId::new("test.py", "baz", 10);
assert!(
client.query_ssa(&fid).is_none(),
"NoDaemon should return None for SSA"
);
}
#[test]
fn test_no_daemon_notify_changed_files_is_noop() {
let client = NoDaemon;
client.notify_changed_files(&[
PathBuf::from("src/lib.rs"),
PathBuf::from("src/main.rs"),
]);
}
#[test]
fn test_daemon_client_no_daemon_fallback() {
let client = NoDaemon;
assert!(!client.is_available());
assert!(client.query_call_graph().is_none());
let fid = FunctionId::new("test.rs", "test_fn", 1);
assert!(client.query_cfg(&fid).is_none());
assert!(client.query_dfg(&fid).is_none());
assert!(client.query_ssa(&fid).is_none());
client.notify_changed_files(&[PathBuf::from("a.rs")]);
}
#[test]
fn test_daemon_client_trait_object_safe() {
let client: Box<dyn DaemonClient> = Box::new(NoDaemon);
assert!(!client.is_available());
assert!(client.query_call_graph().is_none());
let fid = FunctionId::new("test.rs", "f", 1);
assert!(client.query_cfg(&fid).is_none());
assert!(client.query_dfg(&fid).is_none());
assert!(client.query_ssa(&fid).is_none());
client.notify_changed_files(&[]);
}
#[test]
fn test_daemon_client_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<NoDaemon>();
assert_send_sync::<LocalDaemonClient>();
}
#[test]
fn test_local_daemon_client_no_socket() {
let client = LocalDaemonClient::new(Path::new("/tmp/nonexistent-bugbot-project-xyz"));
assert!(
!client.is_available(),
"No daemon should be running for a nonexistent project"
);
}
#[test]
fn test_local_daemon_client_unavailable_returns_none() {
let client = LocalDaemonClient::new(Path::new("/tmp/nonexistent-bugbot-project-xyz"));
let fid = FunctionId::new("test.rs", "func", 1);
assert!(client.query_call_graph().is_none());
assert!(client.query_cfg(&fid).is_none());
assert!(client.query_dfg(&fid).is_none());
assert!(client.query_ssa(&fid).is_none());
}
#[test]
fn test_local_daemon_client_notify_when_unavailable() {
let client = LocalDaemonClient::new(Path::new("/tmp/nonexistent-bugbot-project-xyz"));
client.notify_changed_files(&[PathBuf::from("src/lib.rs")]);
}
#[test]
fn test_local_daemon_client_socket_path_computation() {
let client = LocalDaemonClient::new(Path::new("/tmp/test-project-for-socket-path"));
let socket = client.socket_path();
let socket_name = socket.file_name().unwrap().to_string_lossy();
assert!(
socket_name.starts_with("tldr-"),
"Socket name should start with 'tldr-', got: {}",
socket_name
);
assert!(
socket_name.ends_with("-v1.0.sock"),
"Socket name should end with '-v1.0.sock', got: {}",
socket_name
);
}
#[test]
fn test_create_daemon_client_no_daemon() {
let client = create_daemon_client(Path::new("/tmp/nonexistent-bugbot-factory-test"));
assert!(
!client.is_available(),
"Factory should return NoDaemon for nonexistent project"
);
assert!(client.query_call_graph().is_none());
}
struct MockDaemon {
available: bool,
call_graph: Option<ProjectCallGraph>,
}
impl MockDaemon {
fn available_with_call_graph() -> Self {
Self {
available: true,
call_graph: Some(ProjectCallGraph::default()),
}
}
fn unavailable() -> Self {
Self {
available: false,
call_graph: None,
}
}
}
impl DaemonClient for MockDaemon {
fn is_available(&self) -> bool {
self.available
}
fn query_call_graph(&self) -> Option<ProjectCallGraph> {
self.call_graph.clone()
}
fn query_cfg(&self, _function_id: &FunctionId) -> Option<CfgInfo> {
None
}
fn query_dfg(&self, _function_id: &FunctionId) -> Option<DfgInfo> {
None
}
fn query_ssa(&self, _function_id: &FunctionId) -> Option<SsaFunction> {
None
}
fn notify_changed_files(&self, _changed_files: &[PathBuf]) {}
}
#[test]
fn test_mock_daemon_available_returns_call_graph() {
let mock = MockDaemon::available_with_call_graph();
assert!(mock.is_available());
assert!(
mock.query_call_graph().is_some(),
"Available mock daemon should return cached call graph"
);
}
#[test]
fn test_mock_daemon_unavailable_returns_none() {
let mock = MockDaemon::unavailable();
assert!(!mock.is_available());
assert!(mock.query_call_graph().is_none());
}
#[test]
fn test_mock_daemon_as_trait_object() {
let client: Box<dyn DaemonClient> = Box::new(MockDaemon::available_with_call_graph());
assert!(client.is_available());
assert!(client.query_call_graph().is_some());
let client2: Box<dyn DaemonClient> = Box::new(MockDaemon::unavailable());
assert!(!client2.is_available());
assert!(client2.query_call_graph().is_none());
}
struct TrackingDaemon {
notified: std::sync::Mutex<Vec<Vec<PathBuf>>>,
}
impl TrackingDaemon {
fn new() -> Self {
Self {
notified: std::sync::Mutex::new(Vec::new()),
}
}
fn notifications(&self) -> Vec<Vec<PathBuf>> {
self.notified.lock().unwrap().clone()
}
}
impl DaemonClient for TrackingDaemon {
fn is_available(&self) -> bool {
true
}
fn query_call_graph(&self) -> Option<ProjectCallGraph> {
None
}
fn query_cfg(&self, _function_id: &FunctionId) -> Option<CfgInfo> {
None
}
fn query_dfg(&self, _function_id: &FunctionId) -> Option<DfgInfo> {
None
}
fn query_ssa(&self, _function_id: &FunctionId) -> Option<SsaFunction> {
None
}
fn notify_changed_files(&self, changed_files: &[PathBuf]) {
self.notified
.lock()
.unwrap()
.push(changed_files.to_vec());
}
}
#[test]
fn test_daemon_cache_invalidation_on_changed_files() {
let daemon = TrackingDaemon::new();
assert!(daemon.is_available());
let files = vec![
PathBuf::from("src/lib.rs"),
PathBuf::from("src/main.rs"),
];
daemon.notify_changed_files(&files);
let notifications = daemon.notifications();
assert_eq!(notifications.len(), 1, "Should have recorded one notification");
assert_eq!(
notifications[0],
files,
"Notification should contain the changed files"
);
}
#[test]
fn test_daemon_multiple_notifications_accumulate() {
let daemon = TrackingDaemon::new();
daemon.notify_changed_files(&[PathBuf::from("a.rs")]);
daemon.notify_changed_files(&[PathBuf::from("b.rs"), PathBuf::from("c.rs")]);
let notifications = daemon.notifications();
assert_eq!(notifications.len(), 2, "Should have recorded two notifications");
assert_eq!(notifications[0].len(), 1);
assert_eq!(notifications[1].len(), 2);
}
}