use std::fs;
use std::path::{Path, PathBuf};
use clap::Args;
use serde::Serialize;
use crate::output::OutputFormat;
use super::error::DaemonResult;
use super::ipc::send_command;
use super::types::DaemonCommand;
#[derive(Debug, Clone, Args)]
pub struct CacheClearArgs {
#[arg(long, short = 'p', default_value = ".")]
pub project: PathBuf,
}
#[derive(Debug, Clone, Serialize)]
pub struct CacheClearOutput {
pub status: String,
pub files_removed: usize,
pub bytes_freed: u64,
pub size_freed_human: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl CacheClearArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(self.run_async(format, quiet))
}
async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
let project = self.project.canonicalize().unwrap_or_else(|_| {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(&self.project)
});
self.try_stop_daemon(&project).await;
let (files_removed, bytes_freed) = self.clear_cache_files(&project)?;
let output = if files_removed == 0 {
CacheClearOutput {
status: "ok".to_string(),
files_removed: 0,
bytes_freed: 0,
size_freed_human: "0 B".to_string(),
message: Some("No cache directory found".to_string()),
}
} else {
CacheClearOutput {
status: "ok".to_string(),
files_removed,
bytes_freed,
size_freed_human: format_bytes(bytes_freed),
message: Some(format!("Cache cleared: {} file(s) removed", files_removed)),
}
};
self.print_output(&output, format, quiet)
}
async fn try_stop_daemon(&self, project: &Path) {
let cmd = DaemonCommand::Shutdown;
let _ = send_command(project, &cmd).await;
}
fn clear_cache_files(&self, project: &Path) -> DaemonResult<(usize, u64)> {
let cache_dir = project.join(".tldr").join("cache");
if !cache_dir.exists() {
return Ok((0, 0));
}
let mut files_removed = 0;
let mut bytes_freed = 0u64;
let entries: Vec<_> = fs::read_dir(&cache_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.metadata().map(|m| m.is_file()).unwrap_or(false))
.collect();
for entry in entries {
let path = entry.path();
if let Ok(metadata) = entry.metadata() {
bytes_freed += metadata.len();
}
if fs::remove_file(&path).is_ok() {
files_removed += 1;
}
}
Ok((files_removed, bytes_freed))
}
fn print_output(
&self,
output: &CacheClearOutput,
format: OutputFormat,
quiet: bool,
) -> anyhow::Result<()> {
if quiet {
return Ok(());
}
match format {
OutputFormat::Json | OutputFormat::Compact => {
println!("{}", serde_json::to_string_pretty(output)?);
}
OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
if output.files_removed == 0 {
println!("No cache directory found");
} else {
println!(
"Cache cleared: {} file(s) removed ({})",
output.files_removed, output.size_freed_human
);
}
}
}
Ok(())
}
}
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_cache_clear_args_default() {
let args = CacheClearArgs {
project: PathBuf::from("."),
};
assert_eq!(args.project, PathBuf::from("."));
}
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(0), "0 B");
assert_eq!(format_bytes(500), "500 B");
assert_eq!(format_bytes(1024), "1.0 KB");
assert_eq!(format_bytes(1536), "1.5 KB");
assert_eq!(format_bytes(1048576), "1.0 MB");
assert_eq!(format_bytes(1073741824), "1.0 GB");
}
#[test]
fn test_cache_clear_output_serialization() {
let output = CacheClearOutput {
status: "ok".to_string(),
files_removed: 26,
bytes_freed: 1048576,
size_freed_human: "1.0 MB".to_string(),
message: Some("Cache cleared: 26 file(s) removed".to_string()),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("ok"));
assert!(json.contains("26"));
assert!(json.contains("1048576"));
assert!(json.contains("1.0 MB"));
}
#[test]
fn test_cache_clear_output_empty() {
let output = CacheClearOutput {
status: "ok".to_string(),
files_removed: 0,
bytes_freed: 0,
size_freed_human: "0 B".to_string(),
message: Some("No cache directory found".to_string()),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("No cache directory found"));
}
#[test]
fn test_clear_cache_files_no_cache_dir() {
let temp = TempDir::new().unwrap();
let args = CacheClearArgs {
project: temp.path().to_path_buf(),
};
let result = args.clear_cache_files(temp.path());
assert!(result.is_ok());
let (files, bytes) = result.unwrap();
assert_eq!(files, 0);
assert_eq!(bytes, 0);
}
#[test]
fn test_clear_cache_files_with_files() {
let temp = TempDir::new().unwrap();
let cache_dir = temp.path().join(".tldr").join("cache");
fs::create_dir_all(&cache_dir).unwrap();
fs::write(cache_dir.join("salsa_cache.bin"), "test data 1").unwrap();
fs::write(cache_dir.join("call_graph.json"), r#"{"edges":[]}"#).unwrap();
fs::write(cache_dir.join("test.pkl"), "pickle data").unwrap();
let args = CacheClearArgs {
project: temp.path().to_path_buf(),
};
let result = args.clear_cache_files(temp.path());
assert!(result.is_ok());
let (files, bytes) = result.unwrap();
assert_eq!(files, 3);
assert!(bytes > 0);
assert!(!cache_dir.join("salsa_cache.bin").exists());
assert!(!cache_dir.join("call_graph.json").exists());
assert!(!cache_dir.join("test.pkl").exists());
}
#[test]
fn test_clear_cache_files_preserves_directory() {
let temp = TempDir::new().unwrap();
let cache_dir = temp.path().join(".tldr").join("cache");
fs::create_dir_all(&cache_dir).unwrap();
fs::write(cache_dir.join("test.bin"), "data").unwrap();
let args = CacheClearArgs {
project: temp.path().to_path_buf(),
};
args.clear_cache_files(temp.path()).unwrap();
assert!(cache_dir.exists());
}
#[tokio::test]
async fn test_cache_clear_no_cache() {
let temp = TempDir::new().unwrap();
let args = CacheClearArgs {
project: temp.path().to_path_buf(),
};
let result = args.run_async(OutputFormat::Json, true).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_cache_clear_with_files() {
let temp = TempDir::new().unwrap();
let cache_dir = temp.path().join(".tldr").join("cache");
fs::create_dir_all(&cache_dir).unwrap();
fs::write(cache_dir.join("test.bin"), "test data").unwrap();
let args = CacheClearArgs {
project: temp.path().to_path_buf(),
};
let result = args.run_async(OutputFormat::Json, true).await;
assert!(result.is_ok());
assert!(!cache_dir.join("test.bin").exists());
}
}