presolve_cli/
cache_commands.rs1#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
7
8use std::fmt;
9use std::path::Path;
10
11use presolve_compiler::persistent_cache::CacheInspectionReportV1;
12use presolve_compiler::platform::ContractVersion;
13use presolve_compiler::service::CompilerServiceHost;
14
15use crate::load_explicit_project_envelope_v1;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum CliCacheOperationV1 {
19 Inspect,
20 Verify,
21 Clean,
22}
23
24impl CliCacheOperationV1 {
25 #[must_use]
26 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::Inspect => "inspect",
29 Self::Verify => "verify",
30 Self::Clean => "clean",
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
36pub struct CliCacheOperationResultV1 {
37 pub operation: CliCacheOperationV1,
38 pub report: Option<CacheInspectionReportV1>,
39 pub removed_keys: Vec<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct CliCacheOperationErrorV1 {
44 pub code: &'static str,
45 pub message: String,
46}
47
48impl fmt::Display for CliCacheOperationErrorV1 {
49 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(output, "{}: {}", self.code, self.message)
51 }
52}
53
54impl std::error::Error for CliCacheOperationErrorV1 {}
55
56fn compiler_contract() -> ContractVersion {
57 ContractVersion::new(format!("presolve-compiler:{}", env!("CARGO_PKG_VERSION")))
58}
59
60pub fn run_project_cache_operation_v1(
63 configuration_path: &Path,
64 operation: CliCacheOperationV1,
65) -> Result<CliCacheOperationResultV1, CliCacheOperationErrorV1> {
66 let project_root = configuration_path
67 .parent()
68 .ok_or(CliCacheOperationErrorV1 {
69 code: "L9E001_CONFIGURATION_PATH_INVALID",
70 message: "configuration path must have a parent directory".into(),
71 })?;
72 let envelope =
73 load_explicit_project_envelope_v1(project_root, configuration_path).map_err(|error| {
74 CliCacheOperationErrorV1 {
75 code: error.code,
76 message: error.message,
77 }
78 })?;
79 let service_root = envelope.project_root.join(".presolve");
80 let cache_root = service_root.join("cache");
81 let service = CompilerServiceHost::start_with_cache(
82 &service_root,
83 Some(&cache_root),
84 compiler_contract(),
85 )
86 .map_err(|error| CliCacheOperationErrorV1 {
87 code: error.code,
88 message: error.message,
89 })?;
90 match operation {
91 CliCacheOperationV1::Inspect => {
92 service
93 .inspect_cache(&cache_root)
94 .map(|report| CliCacheOperationResultV1 {
95 operation,
96 report: Some(report),
97 removed_keys: Vec::new(),
98 })
99 }
100 CliCacheOperationV1::Verify => {
101 service
102 .verify_cache(&cache_root)
103 .map(|report| CliCacheOperationResultV1 {
104 operation,
105 report: Some(report),
106 removed_keys: Vec::new(),
107 })
108 }
109 CliCacheOperationV1::Clean => {
110 service
111 .clean_cache(&cache_root)
112 .map(|removed_keys| CliCacheOperationResultV1 {
113 operation,
114 report: None,
115 removed_keys,
116 })
117 }
118 }
119 .map_err(|error| CliCacheOperationErrorV1 {
120 code: error.code,
121 message: error.message,
122 })
123}
124
125#[cfg(test)]
126mod tests {
127 use std::fs;
128 use std::sync::atomic::{AtomicU64, Ordering};
129
130 use super::*;
131
132 static NEXT_ROOT: AtomicU64 = AtomicU64::new(1);
133
134 fn setup() -> (std::path::PathBuf, std::path::PathBuf) {
135 let root = std::env::temp_dir().join(format!(
136 "presolve-l9e-{}",
137 NEXT_ROOT.fetch_add(1, Ordering::Relaxed)
138 ));
139 if root.exists() {
140 fs::remove_dir_all(&root).unwrap();
141 }
142 fs::create_dir_all(&root).unwrap();
143 let config = root.join("presolve.json");
144 fs::write(
145 &config,
146 include_bytes!("../fixtures/configuration/minimum-cli-v1.json"),
147 )
148 .unwrap();
149 (root, config)
150 }
151
152 #[test]
153 fn l9e_inspects_and_verifies_only_the_configured_l6_cache_root() {
154 let (root, config) = setup();
155 fs::write(root.join("outside.txt"), "must survive").unwrap();
156 let inspection = run_project_cache_operation_v1(&config, CliCacheOperationV1::Inspect)
157 .unwrap()
158 .report
159 .unwrap();
160 assert!(inspection.enabled);
161 assert!(inspection.manifest_valid);
162 let verification = run_project_cache_operation_v1(&config, CliCacheOperationV1::Verify)
163 .unwrap()
164 .report
165 .unwrap();
166 assert_eq!(
167 inspection.to_canonical_json(),
168 verification.to_canonical_json()
169 );
170 assert_eq!(
171 fs::read_to_string(root.join("outside.txt")).unwrap(),
172 "must survive"
173 );
174 fs::remove_dir_all(root).unwrap();
175 }
176
177 #[test]
178 fn l9e_clean_delegates_to_l6_and_never_removes_project_siblings() {
179 let (root, config) = setup();
180 fs::write(root.join("outside.txt"), "must survive").unwrap();
181 run_project_cache_operation_v1(&config, CliCacheOperationV1::Inspect).unwrap();
182 let result = run_project_cache_operation_v1(&config, CliCacheOperationV1::Clean).unwrap();
183 assert!(result.removed_keys.is_empty());
184 assert!(root.join(".presolve/cache/entries").is_dir());
185 assert_eq!(
186 fs::read_to_string(root.join("outside.txt")).unwrap(),
187 "must survive"
188 );
189 fs::remove_dir_all(root).unwrap();
190 }
191}