use sqry_core::cache::{PruneOptions, PruneOutputMode};
use std::fs;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
use sqry_core::test_support::verbosity;
use std::sync::Once;
static INIT: Once = Once::new();
fn init_logging() {
INIT.call_once(|| {
verbosity::init(env!("CARGO_PKG_NAME"));
});
}
fn create_cache_entry(
cache_dir: &std::path::Path,
name: &str,
size_bytes: u64,
age_days: u64,
) -> std::path::PathBuf {
let user_dir = cache_dir.join("test_user");
let lang_dir = user_dir.join("rust");
let hash_dir = lang_dir.join("abc123def456");
let path_dir = hash_dir.join("path1");
fs::create_dir_all(&path_dir).unwrap();
let file_path = path_dir.join(format!("{name}.bin"));
let size = size_bytes.try_into().unwrap_or(usize::MAX);
let content = vec![0u8; size];
fs::write(&file_path, content).unwrap();
let mtime = SystemTime::now() - Duration::from_secs(age_days * 24 * 3600);
filetime::set_file_mtime(&file_path, filetime::FileTime::from_system_time(mtime)).unwrap();
file_path
}
#[test]
fn test_large_directory_age_prune() {
init_logging();
log::info!("Testing cache prune with 1000 entries (performance benchmark)");
let tmp_cache_dir = TempDir::new().unwrap();
let cache_dir = tmp_cache_dir.path();
let start = std::time::Instant::now();
for i in 0..1000 {
let age_days = if i < 300 {
45 } else if i < 700 {
20 } else {
5 };
create_cache_entry(cache_dir, &format!("file_{i}"), 1024, age_days);
}
let creation_time = start.elapsed();
log::debug!("Created 1000 entries in {creation_time:?}");
let options = PruneOptions::new()
.with_max_age(Duration::from_secs(30 * 24 * 3600))
.with_output_mode(PruneOutputMode::Human);
let prune_start = std::time::Instant::now();
log::debug!("Starting prune operation (max age: 30 days)");
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let report = engine.execute(cache_dir).unwrap();
let prune_time = prune_start.elapsed();
log::debug!("Prune completed in {prune_time:?}");
log::debug!("Entries considered: {}", report.entries_considered);
log::debug!("Entries removed: {}", report.entries_removed);
log::debug!("Entries remaining: {}", report.remaining_entries);
assert_eq!(report.entries_considered, 1000);
assert_eq!(report.entries_removed, 300); assert_eq!(report.remaining_entries, 700);
log::info!(
"✓ Pruned 1000 entries in {:?} - removed {}, kept {}",
prune_time,
report.entries_removed,
report.remaining_entries
);
assert!(
prune_time < Duration::from_secs(1),
"Prune took {prune_time:?}, expected < 1s for 1000 entries"
);
}
#[test]
fn test_mixed_age_and_size_prune() {
init_logging();
log::info!("Testing combined age + size policy (mixed eviction)");
let tmp_cache_dir = TempDir::new().unwrap();
let cache_dir = tmp_cache_dir.path();
for i in 0..100 {
let age_days = if i < 30 { 20 } else { 5 };
create_cache_entry(cache_dir, &format!("entry_{i}"), 1024, age_days);
}
let options = PruneOptions::new()
.with_max_age(Duration::from_secs(14 * 24 * 3600))
.with_max_size(50 * 1024) .with_output_mode(PruneOutputMode::Human);
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let report = engine.execute(cache_dir).unwrap();
log::debug!(
"Mixed policy results: removed {}, remaining {} ({} bytes)",
report.entries_removed,
report.remaining_entries,
report.remaining_bytes
);
log::info!(
"✓ Mixed policy: removed {} entries (age + size), kept {} bytes <= 50KB",
report.entries_removed,
report.remaining_bytes
);
assert!(
report.entries_removed >= 30,
"Should remove at least 30 old entries"
);
assert!(
report.remaining_bytes <= 50 * 1024,
"Should cap to 50KB, got {} bytes",
report.remaining_bytes
);
}
#[test]
fn test_dry_run_no_side_effects() {
init_logging();
log::info!("Testing dry-run mode has no side effects");
let tmp_cache_dir = TempDir::new().unwrap();
let cache_dir = tmp_cache_dir.path();
let mut file_paths = Vec::new();
for i in 0..50 {
let path = create_cache_entry(cache_dir, &format!("old_{i}"), 2048, 45);
file_paths.push(path);
}
let options = PruneOptions::new()
.with_max_age(Duration::from_secs(30 * 24 * 3600))
.with_dry_run(true)
.with_output_mode(PruneOutputMode::Human);
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let report = engine.execute(cache_dir).unwrap();
log::debug!(
"Dry-run report: {} entries would be removed",
report.entries_removed
);
assert_eq!(report.entries_removed, 50);
let mut existing_count = 0;
for path in &file_paths {
assert!(
path.exists(),
"Dry-run should not delete files, but {path:?} is missing"
);
existing_count += 1;
}
log::info!(
"✓ Dry-run: reported {} removals, verified {} files still exist",
report.entries_removed,
existing_count
);
}
#[test]
fn test_handles_partial_failures() {
let tmp_cache_dir = TempDir::new().unwrap();
let cache_dir = tmp_cache_dir.path();
for i in 0..10 {
create_cache_entry(cache_dir, &format!("entry_{i}"), 1024, 45);
}
let readonly_dir = cache_dir
.join("test_user")
.join("rust")
.join("readonly_hash");
fs::create_dir_all(&readonly_dir).unwrap();
let readonly_file = readonly_dir.join("readonly.bin");
fs::write(&readonly_file, vec![0u8; 1024]).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&readonly_file).unwrap().permissions();
perms.set_mode(0o444); fs::set_permissions(&readonly_file, perms).unwrap();
}
let options = PruneOptions::new()
.with_max_age(Duration::from_secs(30 * 24 * 3600))
.with_output_mode(PruneOutputMode::Human);
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let result = engine.execute(cache_dir);
assert!(result.is_ok(), "Prune should handle partial failures");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&readonly_file).unwrap().permissions();
perms.set_mode(0o644);
fs::set_permissions(&readonly_file, perms).unwrap();
}
}
#[test]
fn test_custom_path_outside_default() {
let tmp_cache_dir = TempDir::new().unwrap();
let custom_cache = tmp_cache_dir.path().join("custom_cache");
fs::create_dir_all(&custom_cache).unwrap();
for i in 0..20 {
create_cache_entry(&custom_cache, &format!("custom_{i}"), 512, 40);
}
let options = PruneOptions::new()
.with_max_age(Duration::from_secs(30 * 24 * 3600))
.with_output_mode(PruneOutputMode::Human);
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let report = engine.execute(&custom_cache).unwrap();
assert_eq!(report.entries_removed, 20);
}
#[test]
fn test_respects_lock_files() {
let tmp_cache_dir = TempDir::new().unwrap();
let cache_dir = tmp_cache_dir.path();
let data_path = create_cache_entry(cache_dir, "locked_entry", 1024, 45);
let lock_path = data_path.with_extension("bin.lock");
fs::write(&lock_path, b"12345").unwrap();
let options = PruneOptions::new()
.with_max_age(Duration::from_secs(30 * 24 * 3600))
.with_output_mode(PruneOutputMode::Human);
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let report = engine.execute(cache_dir).unwrap();
assert_eq!(report.entries_removed, 1);
assert!(!data_path.exists(), "Data file should be deleted");
assert!(!lock_path.exists(), "Lock file should be deleted");
}
#[test]
fn test_size_policy_removes_oldest_first() {
init_logging();
log::info!("Testing size policy removes oldest entries first");
let tmp_cache_dir = TempDir::new().unwrap();
let cache_dir = tmp_cache_dir.path();
for i in 0..10 {
let age_days = 10 + i; create_cache_entry(cache_dir, &format!("file_{i}"), 10 * 1024, age_days);
}
log::debug!("Created 10 entries, each 10KB (100KB total)");
let options = PruneOptions::new()
.with_max_size(50 * 1024)
.with_output_mode(PruneOutputMode::Human);
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let report = engine.execute(cache_dir).unwrap();
log::debug!("Size policy results:");
log::debug!(" Removed: {}", report.entries_removed);
log::debug!(" Remaining: {}", report.remaining_entries);
log::debug!(" Remaining bytes: {}", report.remaining_bytes);
assert_eq!(report.entries_removed, 5);
assert_eq!(report.remaining_entries, 5);
assert!(report.remaining_bytes <= 50 * 1024);
for i in 5..10 {
let path = cache_dir
.join("test_user/rust/abc123def456/path1")
.join(format!("file_{i}.bin"));
assert!(!path.exists(), "Oldest file {i} should be deleted");
}
for i in 0..5 {
let path = cache_dir
.join("test_user/rust/abc123def456/path1")
.join(format!("file_{i}.bin"));
assert!(path.exists(), "Newest file {i} should remain");
}
log::info!(
"✓ Size policy removed oldest 5 entries first: {} bytes remaining <= 50KB",
report.remaining_bytes
);
}
#[test]
fn test_empty_cache_returns_zero_report() {
let tmp_cache_dir = TempDir::new().unwrap();
let cache_dir = tmp_cache_dir.path();
let options = PruneOptions::new()
.with_max_age(Duration::from_secs(7 * 24 * 3600))
.with_output_mode(PruneOutputMode::Human);
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let report = engine.execute(cache_dir).unwrap();
assert_eq!(report.entries_considered, 0);
assert_eq!(report.entries_removed, 0);
assert_eq!(report.remaining_entries, 0);
}
#[test]
fn test_cache_within_limits_no_removal() {
let tmp_cache_dir = TempDir::new().unwrap();
let cache_dir = tmp_cache_dir.path();
for i in 0..5 {
create_cache_entry(cache_dir, &format!("recent_{i}"), 2048, 3);
}
let options = PruneOptions::new()
.with_max_age(Duration::from_secs(7 * 24 * 3600)) .with_max_size(100 * 1024) .with_output_mode(PruneOutputMode::Human);
let engine = sqry_core::cache::PruneEngine::new(options).unwrap();
let report = engine.execute(cache_dir).unwrap();
assert_eq!(report.entries_considered, 5);
assert_eq!(report.entries_removed, 0);
assert_eq!(report.remaining_entries, 5);
}