use crate::state::modal::DependencyInfo;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::ErrorKind;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyCache {
pub install_list_signature: Vec<String>,
pub dependencies: Vec<DependencyInfo>,
}
pub fn compute_signature(packages: &[crate::state::PackageItem]) -> Vec<String> {
let mut names: Vec<String> = packages.iter().map(|p| p.name.clone()).collect();
names.sort();
names
}
pub fn load_cache(path: &PathBuf, current_signature: &[String]) -> Option<Vec<DependencyInfo>> {
let raw = match fs::read_to_string(path) {
Ok(contents) => contents,
Err(e) if e.kind() == ErrorKind::NotFound => {
tracing::debug!(path = %path.display(), "[Cache] Dependency cache not found");
return None;
}
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"[Cache] Failed to read dependency cache"
);
return None;
}
};
let cache: DependencyCache = match serde_json::from_str(&raw) {
Ok(cache) => cache,
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"[Cache] Failed to parse dependency cache"
);
return None;
}
};
let mut cached_sig = cache.install_list_signature.clone();
cached_sig.sort();
let mut current_sig = current_signature.to_vec();
current_sig.sort();
if cached_sig == current_sig {
tracing::info!(path = %path.display(), count = cache.dependencies.len(), "loaded dependency cache");
return Some(cache.dependencies);
}
tracing::debug!(path = %path.display(), "dependency cache signature mismatch, ignoring");
None
}
pub fn save_cache(path: &PathBuf, signature: &[String], dependencies: &[DependencyInfo]) {
let cache = DependencyCache {
install_list_signature: signature.to_vec(),
dependencies: dependencies.to_vec(),
};
if let Ok(s) = serde_json::to_string(&cache) {
let _ = fs::write(path, s);
tracing::debug!(path = %path.display(), count = dependencies.len(), "saved dependency cache");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::modal::{DependencyInfo, DependencySource, DependencyStatus};
use crate::state::{PackageItem, Source};
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_path(label: &str) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!(
"pacsea_deps_cache_{label}_{}_{}.json",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
path
}
fn sample_packages() -> Vec<PackageItem> {
vec![
PackageItem {
name: "ripgrep".into(),
version: "14.0.0".into(),
description: String::new(),
source: Source::Official {
repo: "extra".into(),
arch: "x86_64".into(),
},
popularity: None,
out_of_date: None,
orphaned: false,
},
PackageItem {
name: "fd".into(),
version: "9.0.0".into(),
description: String::new(),
source: Source::Aur,
popularity: Some(42.0),
out_of_date: None,
orphaned: false,
},
]
}
fn sample_dependencies() -> Vec<DependencyInfo> {
vec![DependencyInfo {
name: "gcc-libs".into(),
version: ">=13".into(),
status: DependencyStatus::ToInstall,
source: DependencySource::Official {
repo: "core".into(),
},
required_by: vec!["ripgrep".into()],
depends_on: Vec::new(),
is_core: true,
is_system: false,
}]
}
#[test]
fn compute_signature_orders_package_names() {
let mut packages = sample_packages();
packages.reverse();
let signature = compute_signature(&packages);
assert_eq!(signature, vec![String::from("fd"), String::from("ripgrep")]);
}
#[test]
fn load_cache_rejects_signature_mismatch() {
let path = temp_path("mismatch");
let packages = sample_packages();
let signature = compute_signature(&packages);
let deps = sample_dependencies();
save_cache(&path, &signature, &deps);
let mismatched_signature = vec!["ripgrep".into(), "zellij".into()];
assert!(load_cache(&path, &mismatched_signature).is_none());
let _ = fs::remove_file(&path);
}
#[test]
fn save_and_load_cache_roundtrip() {
let path = temp_path("roundtrip");
let packages = sample_packages();
let signature = compute_signature(&packages);
let deps = sample_dependencies();
let expected = deps.clone();
save_cache(&path, &signature, &deps);
let reloaded = load_cache(&path, &signature).expect("expected cache to load");
assert_eq!(reloaded.len(), expected.len());
let dep = &reloaded[0];
let expected_dep = &expected[0];
assert_eq!(dep.name, expected_dep.name);
assert_eq!(dep.version, expected_dep.version);
assert!(matches!(dep.status, DependencyStatus::ToInstall));
assert!(matches!(
dep.source,
DependencySource::Official { ref repo } if repo == "core"
));
assert_eq!(dep.required_by, expected_dep.required_by);
assert_eq!(dep.depends_on, expected_dep.depends_on);
assert_eq!(dep.is_core, expected_dep.is_core);
assert_eq!(dep.is_system, expected_dep.is_system);
let _ = fs::remove_file(&path);
}
}