clnrm_core/backend/
mod.rs

1//! Backend implementations for cleanroom testing
2//!
3//! This module provides backend implementations following core team best practices.
4
5use crate::error::Result;
6use crate::policy::Policy;
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10// Module structure for backends
11pub mod testcontainer;
12pub mod volume;
13
14pub use testcontainer::TestcontainerBackend;
15pub use volume::{VolumeMount, VolumeValidator};
16
17/// Command to execute with all configuration
18#[derive(Debug, Clone)]
19pub struct Cmd {
20    /// Binary or executable path
21    pub bin: String,
22    /// Arguments to pass to the command
23    pub args: Vec<String>,
24    /// Working directory
25    pub workdir: Option<PathBuf>,
26    /// Environment variables
27    pub env: HashMap<String, String>,
28    /// Policy constraints
29    pub policy: Policy,
30}
31
32/// Result of a command execution
33#[derive(Debug, Clone)]
34pub struct RunResult {
35    /// Exit code of the command
36    pub exit_code: i32,
37    /// Standard output
38    pub stdout: String,
39    /// Standard error
40    pub stderr: String,
41    /// Duration of execution in milliseconds
42    pub duration_ms: u64,
43    /// Individual step results
44    pub steps: Vec<crate::scenario::StepResult>,
45    /// Environment variables that were redacted in forensics
46    pub redacted_env: Vec<String>,
47    /// Backend used for execution
48    pub backend: String,
49    /// Whether execution was concurrent
50    pub concurrent: bool,
51    /// Step execution order (for deterministic ordering)
52    pub step_order: Vec<String>,
53}
54
55impl RunResult {
56    /// Create a new run result
57    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    /// Check if the command succeeded
72    pub fn success(&self) -> bool {
73        self.exit_code == 0
74    }
75
76    /// Check if the command failed
77    pub fn failed(&self) -> bool {
78        self.exit_code != 0
79    }
80}
81
82impl Cmd {
83    /// Create a new command
84    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    /// Add an argument
95    pub fn arg(mut self, arg: impl Into<String>) -> Self {
96        self.args.push(arg.into());
97        self
98    }
99
100    /// Add multiple arguments
101    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    /// Set working directory
109    pub fn workdir(mut self, workdir: PathBuf) -> Self {
110        self.workdir = Some(workdir);
111        self
112    }
113
114    /// Set environment variable
115    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    /// Set policy
121    pub fn policy(mut self, policy: Policy) -> Self {
122        self.policy = policy;
123        self
124    }
125}
126
127/// Trait for backend execution environments
128pub trait Backend: Send + Sync + std::fmt::Debug {
129    /// Run a command in the backend
130    fn run_cmd(&self, cmd: Cmd) -> Result<RunResult>;
131    /// Get the name of the backend
132    fn name(&self) -> &str;
133    /// Check if the backend is available
134    fn is_available(&self) -> bool;
135    /// Check if the backend supports hermetic execution
136    fn supports_hermetic(&self) -> bool;
137    /// Check if the backend supports deterministic execution
138    fn supports_deterministic(&self) -> bool;
139}
140
141/// Auto-backend wrapper for testcontainers
142#[derive(Debug)]
143pub struct AutoBackend {
144    /// The underlying testcontainers backend
145    inner: TestcontainerBackend,
146}
147
148impl AutoBackend {
149    /// Create a new AutoBackend with testcontainers
150    pub fn new(backend: TestcontainerBackend) -> Self {
151        Self { inner: backend }
152    }
153
154    /// Create testcontainers backend with default image
155    pub fn detect() -> Result<Self> {
156        let backend = TestcontainerBackend::new("alpine:latest")?;
157        Ok(Self { inner: backend })
158    }
159
160    /// Create backend from name (only supports testcontainers now)
161    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    /// Get the resolved backend name
175    pub fn resolved_backend(&self) -> String {
176        self.inner.name().to_string()
177    }
178
179    /// Check if testcontainers backend is available
180    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}