clnrm_core/backend/
mod.rs1use crate::error::Result;
6use crate::policy::Policy;
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10pub mod testcontainer;
12pub mod volume;
13
14pub use testcontainer::TestcontainerBackend;
15pub use volume::{VolumeMount, VolumeValidator};
16
17#[derive(Debug, Clone)]
19pub struct Cmd {
20 pub bin: String,
22 pub args: Vec<String>,
24 pub workdir: Option<PathBuf>,
26 pub env: HashMap<String, String>,
28 pub policy: Policy,
30}
31
32#[derive(Debug, Clone)]
34pub struct RunResult {
35 pub exit_code: i32,
37 pub stdout: String,
39 pub stderr: String,
41 pub duration_ms: u64,
43 pub steps: Vec<crate::scenario::StepResult>,
45 pub redacted_env: Vec<String>,
47 pub backend: String,
49 pub concurrent: bool,
51 pub step_order: Vec<String>,
53}
54
55impl RunResult {
56 pub fn new(exit_code: i32, stdout: String, stderr: String, duration_ms: u64) -> Self {
58 Self {
59 exit_code,
60 stdout,
61 stderr,
62 duration_ms,
63 steps: Vec::new(),
64 redacted_env: Vec::new(),
65 backend: "unknown".to_string(),
66 concurrent: false,
67 step_order: Vec::new(),
68 }
69 }
70
71 pub fn success(&self) -> bool {
73 self.exit_code == 0
74 }
75
76 pub fn failed(&self) -> bool {
78 self.exit_code != 0
79 }
80}
81
82impl Cmd {
83 pub fn new(bin: impl Into<String>) -> Self {
85 Self {
86 bin: bin.into(),
87 args: Vec::new(),
88 workdir: None,
89 env: HashMap::new(),
90 policy: Policy::default(),
91 }
92 }
93
94 pub fn arg(mut self, arg: impl Into<String>) -> Self {
96 self.args.push(arg.into());
97 self
98 }
99
100 pub fn args(mut self, args: &[&str]) -> Self {
102 for arg in args {
103 self.args.push(arg.to_string());
104 }
105 self
106 }
107
108 pub fn workdir(mut self, workdir: PathBuf) -> Self {
110 self.workdir = Some(workdir);
111 self
112 }
113
114 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
116 self.env.insert(key.into(), value.into());
117 self
118 }
119
120 pub fn policy(mut self, policy: Policy) -> Self {
122 self.policy = policy;
123 self
124 }
125}
126
127pub trait Backend: Send + Sync + std::fmt::Debug {
129 fn run_cmd(&self, cmd: Cmd) -> Result<RunResult>;
131 fn name(&self) -> &str;
133 fn is_available(&self) -> bool;
135 fn supports_hermetic(&self) -> bool;
137 fn supports_deterministic(&self) -> bool;
139}
140
141#[derive(Debug)]
143pub struct AutoBackend {
144 inner: TestcontainerBackend,
146}
147
148impl AutoBackend {
149 pub fn new(backend: TestcontainerBackend) -> Self {
151 Self { inner: backend }
152 }
153
154 pub fn detect() -> Result<Self> {
156 let backend = TestcontainerBackend::new("alpine:latest")?;
157 Ok(Self { inner: backend })
158 }
159
160 pub fn from_name(name: &str) -> Result<Self> {
162 match name {
163 "testcontainers" | "auto" => Self::detect(),
164 _ => Err(crate::error::CleanroomError::new(
165 crate::error::ErrorKind::ConfigurationError,
166 format!(
167 "Unknown backend: {}. Only 'testcontainers' and 'auto' are supported",
168 name
169 ),
170 )),
171 }
172 }
173
174 pub fn resolved_backend(&self) -> String {
176 self.inner.name().to_string()
177 }
178
179 pub fn is_backend_available(name: &str) -> bool {
181 match name {
182 "testcontainers" | "auto" => TestcontainerBackend::is_available(),
183 _ => false,
184 }
185 }
186}
187
188impl Backend for AutoBackend {
189 fn run_cmd(&self, cmd: Cmd) -> Result<RunResult> {
190 self.inner.run_cmd(cmd)
191 }
192
193 fn name(&self) -> &str {
194 self.inner.name()
195 }
196
197 fn is_available(&self) -> bool {
198 self.inner.is_available()
199 }
200
201 fn supports_hermetic(&self) -> bool {
202 self.inner.supports_hermetic()
203 }
204
205 fn supports_deterministic(&self) -> bool {
206 self.inner.supports_deterministic()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_run_result_creation() {
216 let result = RunResult::new(0, "output".to_string(), "error".to_string(), 1000);
217
218 assert!(result.success());
219 assert!(!result.failed());
220 assert_eq!(result.exit_code, 0);
221 assert_eq!(result.stdout, "output");
222 assert_eq!(result.stderr, "error");
223 assert_eq!(result.duration_ms, 1000);
224 }
225
226 #[test]
227 fn test_run_result_failure() {
228 let result = RunResult::new(1, "output".to_string(), "error".to_string(), 500);
229
230 assert!(!result.success());
231 assert!(result.failed());
232 assert_eq!(result.exit_code, 1);
233 }
234
235 #[test]
236 fn test_cmd_creation() {
237 let cmd = Cmd::new("echo");
238 assert_eq!(cmd.bin, "echo");
239 assert!(cmd.args.is_empty());
240 assert!(cmd.workdir.is_none());
241 assert!(cmd.env.is_empty());
242 }
243
244 #[test]
245 fn test_cmd_with_args() {
246 let cmd = Cmd::new("echo").arg("hello").arg("world");
247
248 assert_eq!(cmd.args.len(), 2);
249 assert_eq!(cmd.args[0], "hello");
250 assert_eq!(cmd.args[1], "world");
251 }
252
253 #[test]
254 fn test_cmd_with_workdir() {
255 let cmd = Cmd::new("ls").workdir(PathBuf::from("/tmp"));
256 assert_eq!(cmd.workdir, Some(PathBuf::from("/tmp")));
257 }
258
259 #[test]
260 fn test_cmd_with_env() {
261 let cmd = Cmd::new("env").env("TEST", "value");
262 assert_eq!(cmd.env.get("TEST"), Some(&"value".to_string()));
263 }
264}