xacli_testing/assert/
output.rs1use regex::Regex;
4
5use super::{helpers::truncate, Asserter};
6use crate::{AssertResult, ExecuteResult, Result};
7
8#[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#[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
72pub 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 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 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 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 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 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 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
157pub fn stdout() -> OutputAsserter {
159 OutputAsserter::new(OutputKind::Stdout)
160}
161
162pub fn stderr() -> OutputAsserter {
164 OutputAsserter::new(OutputKind::Stderr)
165}