Skip to main content

xacli_testing/assert/
mod.rs

1//! Assert module for CLI testing
2//!
3//! Provides asserters for validating command execution results.
4
5mod code;
6mod helpers;
7mod output;
8mod snapshot;
9
10pub use code::{code, failure, success};
11pub use output::{stderr, stdout};
12pub use snapshot::SnapshotManager;
13
14use crate::{ExecuteResult, Result, TestingError};
15
16pub trait Asserter {
17    fn validate(&self, result: &ExecuteResult) -> Result<AssertResult>;
18}
19
20/// Validation function type alias
21type ValidateFn = Box<dyn Fn(&ExecuteResult) -> Result<()>>;
22
23/// Asserter for validating execution results
24pub struct ExecuteAsserter {
25    pub name: String,
26    pub title: String,
27    validate: ValidateFn,
28}
29
30impl ExecuteAsserter {
31    /// Create a new asserter
32    pub fn new(name: impl Into<String>, title: impl Into<String>, validate: ValidateFn) -> Self {
33        Self {
34            name: name.into(),
35            title: title.into(),
36            validate,
37        }
38    }
39}
40
41impl Asserter for ExecuteAsserter {
42    fn validate(&self, result: &ExecuteResult) -> Result<AssertResult> {
43        match (self.validate)(result) {
44            Ok(()) => Ok(AssertResult {
45                name: self.name.clone(),
46                title: self.title.clone(),
47                success: true,
48                messages: vec![],
49            }),
50            Err(TestingError::Assertion(msg)) => Ok(AssertResult {
51                name: self.name.clone(),
52                title: self.title.clone(),
53                success: false,
54                messages: vec![msg],
55            }),
56            Err(e) => Ok(AssertResult {
57                name: self.name.clone(),
58                title: self.title.clone(),
59                success: false,
60                messages: vec![e.to_string()],
61            }),
62        }
63    }
64}
65
66/// Result of an assertion
67#[derive(Debug, Clone)]
68pub struct AssertResult {
69    pub name: String,
70    pub title: String,
71    pub success: bool,
72    pub messages: Vec<String>,
73}
74
75/// Combine multiple asserters with AND logic (all must pass)
76pub fn and(asserters: Vec<Box<dyn Asserter>>) -> Box<dyn Asserter> {
77    Box::new(ExecuteAsserter::new(
78        "and",
79        "All assertions must pass",
80        Box::new(move |result| {
81            for asserter in &asserters {
82                let res = asserter.validate(result)?;
83                if !res.success {
84                    return Err(TestingError::Assertion(res.messages.join("; ")));
85                }
86            }
87            Ok(())
88        }),
89    ))
90}
91
92/// Combine multiple asserters with OR logic (at least one must pass)
93pub fn or(asserters: Vec<Box<dyn Asserter>>) -> Box<dyn Asserter> {
94    Box::new(ExecuteAsserter::new(
95        "or",
96        "At least one assertion must pass",
97        Box::new(move |result| {
98            let mut errors = vec![];
99            for asserter in &asserters {
100                let res = asserter.validate(result)?;
101                if res.success {
102                    return Ok(());
103                }
104                errors.extend(res.messages);
105            }
106            Err(TestingError::Assertion(format!(
107                "None of the assertions passed: {}",
108                errors.join("; ")
109            )))
110        }),
111    ))
112}
113
114/// Create a snapshot asserter
115pub fn snapshot(name: &str) -> Box<dyn Asserter> {
116    let name = name.to_string();
117    let title = format!("Snapshot: {}", name);
118    let snapshot_name = name.clone();
119
120    Box::new(ExecuteAsserter::new(
121        name,
122        title,
123        Box::new(move |result| {
124            let manager = SnapshotManager::new();
125            let actual = format!("stdout:\n{}\nstderr:\n{}", result.stdout, result.stderr);
126
127            match manager.compare_or_create(&snapshot_name, &actual) {
128                Ok(true) => Ok(()),
129                Ok(false) => {
130                    let diff = manager
131                        .diff(&snapshot_name, &actual)
132                        .ok()
133                        .flatten()
134                        .unwrap_or_else(|| "Snapshot mismatch".to_string());
135                    Err(TestingError::Assertion(diff))
136                }
137                Err(e) => Err(e),
138            }
139        }),
140    ))
141}