use regex::Regex;
use super::builder::AssertionResult;
#[derive(Debug, Clone)]
pub struct StdoutAssertion {
stdout: Option<String>,
contains: Vec<String>,
not_contains: Vec<String>,
matches: Vec<String>,
not_matches: Vec<String>,
}
impl StdoutAssertion {
pub fn new(stdout: Option<String>) -> Self {
Self {
stdout,
contains: Vec::new(),
not_contains: Vec::new(),
matches: Vec::new(),
not_matches: Vec::new(),
}
}
pub fn contains(mut self, s: &str) -> Self {
self.contains.push(s.to_string());
self
}
pub fn not_contains(mut self, s: &str) -> Self {
self.not_contains.push(s.to_string());
self
}
pub fn matches(mut self, pattern: &str) -> Self {
self.matches.push(pattern.to_string());
self
}
pub fn not_matches(mut self, pattern: &str) -> Self {
self.not_matches.push(pattern.to_string());
self
}
pub fn to_exist(&self) {
let result = self.evaluate_exists(true);
if !result.passed {
self.panic_with_context(&result);
}
}
pub fn to_be_empty(&self) {
let result = self.evaluate_exists(false);
if !result.passed {
self.panic_with_context(&result);
}
}
pub fn evaluate(&self) -> AssertionResult {
self.evaluate_exists(true)
}
pub fn evaluate_empty(&self) -> AssertionResult {
self.evaluate_exists(false)
}
fn evaluate_exists(&self, should_exist: bool) -> AssertionResult {
let mut failures: Vec<String> = Vec::new();
let stdout_exists = self.stdout.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
if should_exist && !stdout_exists {
failures.push("stdout was empty or not captured".to_string());
} else if !should_exist && stdout_exists {
let preview = self.format_stdout_preview();
failures.push(format!("stdout should be empty but was: {}", preview));
}
if should_exist && stdout_exists {
let stdout = self.stdout.as_ref().unwrap();
for pattern in &self.contains {
if !stdout.contains(pattern) {
failures.push(format!("stdout does not contain '{}'", pattern));
}
}
for pattern in &self.not_contains {
if stdout.contains(pattern) {
failures.push(format!("stdout contains '{}' but should not", pattern));
}
}
for pattern in &self.matches {
match Regex::new(pattern) {
Ok(re) => {
if !re.is_match(stdout) {
failures.push(format!("stdout does not match pattern '{}'", pattern));
}
}
Err(e) => {
failures.push(format!("invalid regex '{}': {}", pattern, e));
}
}
}
for pattern in &self.not_matches {
match Regex::new(pattern) {
Ok(re) => {
if re.is_match(stdout) {
failures.push(format!(
"stdout matches pattern '{}' but should not",
pattern
));
}
}
Err(e) => {
failures.push(format!("invalid regex '{}': {}", pattern, e));
}
}
}
}
let description = self.build_description(should_exist);
if failures.is_empty() {
AssertionResult::pass(description)
} else {
AssertionResult::fail(description, failures.join("; "))
}
}
fn build_description(&self, should_exist: bool) -> String {
let mut parts = vec!["stdout".to_string()];
if should_exist {
parts.push("exists".to_string());
} else {
parts.push("is empty".to_string());
}
for s in &self.contains {
parts.push(format!("contains '{}'", s));
}
for s in &self.not_contains {
parts.push(format!("not contains '{}'", s));
}
for s in &self.matches {
parts.push(format!("matches '{}'", s));
}
for s in &self.not_matches {
parts.push(format!("not matches '{}'", s));
}
parts.join(", ")
}
fn format_stdout_preview(&self) -> String {
match &self.stdout {
Some(s) if !s.is_empty() => {
if s.len() > 100 {
format!("\"{}...\"", &s[..97])
} else {
format!("\"{}\"", s)
}
}
_ => "(empty)".to_string(),
}
}
fn panic_with_context(&self, result: &AssertionResult) -> ! {
let reason = result.reason.as_deref().unwrap_or("unknown reason");
let preview = self.format_stdout_preview();
panic!(
"assertion failed: {}\n\n reason: {}\n stdout: {}\n",
result.description, reason, preview
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stdout_exists() {
let stdout = Some("hello world".to_string());
let assertion = StdoutAssertion::new(stdout);
let result = assertion.evaluate();
assert!(result.passed);
}
#[test]
fn test_stdout_empty() {
let stdout: Option<String> = None;
let assertion = StdoutAssertion::new(stdout);
let result = assertion.evaluate_empty();
assert!(result.passed);
}
#[test]
fn test_stdout_contains() {
let stdout = Some("hello world".to_string());
let assertion = StdoutAssertion::new(stdout).contains("world");
let result = assertion.evaluate();
assert!(result.passed);
}
#[test]
fn test_stdout_contains_fails() {
let stdout = Some("hello world".to_string());
let assertion = StdoutAssertion::new(stdout).contains("foo");
let result = assertion.evaluate();
assert!(!result.passed);
assert!(result.reason.unwrap().contains("does not contain"));
}
#[test]
fn test_stdout_not_contains() {
let stdout = Some("hello world".to_string());
let assertion = StdoutAssertion::new(stdout).not_contains("error");
let result = assertion.evaluate();
assert!(result.passed);
}
#[test]
fn test_stdout_matches() {
let stdout = Some("Success: 42 items processed".to_string());
let assertion = StdoutAssertion::new(stdout).matches(r"Success: \d+ items");
let result = assertion.evaluate();
assert!(result.passed);
}
#[test]
fn test_stdout_not_matches() {
let stdout = Some("all good".to_string());
let assertion = StdoutAssertion::new(stdout).not_matches(r"error|fail");
let result = assertion.evaluate();
assert!(result.passed);
}
#[test]
fn test_multiple_constraints() {
let stdout = Some("Success: 10 items done".to_string());
let assertion = StdoutAssertion::new(stdout)
.contains("Success")
.not_contains("error")
.matches(r"\d+ items");
let result = assertion.evaluate();
assert!(result.passed);
}
}