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