Skip to main content

xacli_testing/assert/
output.rs

1//! Output asserters for stdout/stderr
2
3use regex::Regex;
4
5use super::{helpers::truncate, Asserter};
6use crate::{AssertResult, ExecuteResult, Result};
7
8/// Output source type
9#[derive(Debug, Clone, Copy)]
10enum OutputKind {
11    Stdout,
12    Stderr,
13}
14
15impl OutputKind {
16    fn name(&self) -> &'static str {
17        match self {
18            OutputKind::Stdout => "stdout",
19            OutputKind::Stderr => "stderr",
20        }
21    }
22}
23
24/// Match rule for output assertion
25#[derive(Debug, Clone)]
26enum MatchRule {
27    Contains(String),
28    Exact(String),
29    Regex(String),
30    StartsWith(String),
31    EndsWith(String),
32    IsEmpty,
33}
34
35impl MatchRule {
36    fn matches(&self, output: &str) -> bool {
37        match self {
38            MatchRule::Contains(s) => output.contains(s),
39            MatchRule::Exact(s) => output == s,
40            MatchRule::Regex(pattern) => Regex::new(pattern)
41                .map(|r| r.is_match(output))
42                .unwrap_or(false),
43            MatchRule::StartsWith(s) => output.starts_with(s),
44            MatchRule::EndsWith(s) => output.ends_with(s),
45            MatchRule::IsEmpty => output.is_empty(),
46        }
47    }
48
49    fn description(&self) -> String {
50        match self {
51            MatchRule::Contains(s) => format!("contains \"{}\"", s),
52            MatchRule::Exact(s) => format!("exact \"{}\"", truncate(s)),
53            MatchRule::Regex(p) => format!("regex /{}/", p),
54            MatchRule::StartsWith(s) => format!("starts_with \"{}\"", s),
55            MatchRule::EndsWith(s) => format!("ends_with \"{}\"", s),
56            MatchRule::IsEmpty => "is_empty".to_string(),
57        }
58    }
59
60    fn match_type(&self) -> &'static str {
61        match self {
62            MatchRule::Contains(_) => "contains",
63            MatchRule::Exact(_) => "exact",
64            MatchRule::Regex(_) => "regex",
65            MatchRule::StartsWith(_) => "starts_with",
66            MatchRule::EndsWith(_) => "ends_with",
67            MatchRule::IsEmpty => "is_empty",
68        }
69    }
70}
71
72/// Builder for output assertions
73pub struct OutputAsserter {
74    kind: OutputKind,
75    rules: Vec<MatchRule>,
76}
77
78impl OutputAsserter {
79    fn new(kind: OutputKind) -> Self {
80        Self {
81            kind,
82            rules: vec![],
83        }
84    }
85
86    /// Assert output contains the given string
87    pub fn contains(mut self, s: impl Into<String>) -> Box<Self> {
88        self.rules.push(MatchRule::Contains(s.into()));
89        Box::new(self)
90    }
91
92    /// Assert output exactly matches the given string
93    pub fn exact(mut self, s: impl Into<String>) -> Box<Self> {
94        self.rules.push(MatchRule::Exact(s.into()));
95        Box::new(self)
96    }
97
98    /// Assert output matches the given regex pattern
99    pub fn regex(mut self, pattern: impl Into<String>) -> Box<Self> {
100        self.rules.push(MatchRule::Regex(pattern.into()));
101        Box::new(self)
102    }
103
104    /// Assert output starts with the given string
105    pub fn starts_with(mut self, s: impl Into<String>) -> Box<Self> {
106        self.rules.push(MatchRule::StartsWith(s.into()));
107        Box::new(self)
108    }
109
110    /// Assert output ends with the given string
111    pub fn ends_with(mut self, s: impl Into<String>) -> Box<Self> {
112        self.rules.push(MatchRule::EndsWith(s.into()));
113        Box::new(self)
114    }
115
116    /// Assert output is empty
117    pub fn empty(mut self) -> Box<Self> {
118        self.rules.push(MatchRule::IsEmpty);
119        Box::new(self)
120    }
121}
122
123impl Asserter for OutputAsserter {
124    fn validate(&self, result: &ExecuteResult) -> Result<AssertResult> {
125        let kind = self.kind;
126        let rules = self.rules.clone();
127        let desc: Vec<_> = rules.iter().map(|r| r.description()).collect();
128        let name = kind.name().to_string();
129        let title = format!("{} {}", kind.name(), desc.join(" AND "));
130
131        let output = match kind {
132            OutputKind::Stdout => &result.stdout,
133            OutputKind::Stderr => &result.stderr,
134        };
135
136        let mut messages = Vec::new();
137        for rule in &rules {
138            if !rule.matches(output) {
139                messages.push(format!(
140                    "Expected: {} {}\nActual: {}\nMatch: {}",
141                    kind.name(),
142                    rule.description(),
143                    truncate(output),
144                    rule.match_type()
145                ));
146            }
147        }
148        Ok(AssertResult {
149            name,
150            title,
151            success: messages.is_empty(),
152            messages,
153        })
154    }
155}
156
157/// Create stdout asserter builder
158pub fn stdout() -> OutputAsserter {
159    OutputAsserter::new(OutputKind::Stdout)
160}
161
162/// Create stderr asserter builder
163pub fn stderr() -> OutputAsserter {
164    OutputAsserter::new(OutputKind::Stderr)
165}