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