diff --git a/Cargo.lock b/Cargo.lock index 2d6b347..db1bff5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,6 +612,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "smallvec" version = "1.15.1" @@ -705,9 +711,11 @@ dependencies = [ "anyhow", "clap", "crossterm", + "regex", "rexpect", "serde", "serde_yaml", + "similar", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2c455a5..9db7366 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ rexpect = "0.7" anyhow = "1.0.102" clap = {version = "4.6", features = ["derive"]} crossterm = "0.29.0" +regex = "1.10" +similar = "2.4.0" diff --git a/src/config.rs b/src/config.rs index a856aec..323fa7b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; #[derive(Debug, Deserialize)] pub struct TestConfig { @@ -18,7 +18,25 @@ pub struct ShellInteractions { #[derive(Debug, Deserialize)] pub struct CommandTest { pub command: String, - pub expect: String, + #[serde(deserialize_with = "deserialize_expect")] + pub expect: Vec, +} + +fn deserialize_expect<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrVec { + String(String), + Vec(Vec), + } + + match StringOrVec::deserialize(deserializer)? { + StringOrVec::String(s) => Ok(vec![s]), + StringOrVec::Vec(v) => Ok(v), + } } #[cfg(test)] diff --git a/src/engine.rs b/src/engine.rs index 7b8cb31..530c09e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,7 +1,10 @@ +use regex::Regex; use rexpect::spawn; +use similar::{ChangeTag, TextDiff}; use crate::config::TestConfig; use crate::traits::{Reporter, Runner}; +use crate::{RED, RESET}; pub struct QemuRunner; @@ -33,12 +36,27 @@ impl Runner for QemuRunner { anyhow::bail!(e.to_string()); } - if let Err(e) = p.exp_string(&test.expect) { - reporter.on_step_failure( - &test.command, - &format!("Expected '{}' not found", test.expect), - ); - anyhow::bail!(e.to_string()); + for expect in &test.expect { + if let Err(e) = p.exp_string(expect) { + let fail_msg = match &e { + rexpect::error::Error::Timeout { + expected: exp, got, .. + } => format_diff(exp, got), + rexpect::error::Error::EOF { + expected: exp, got, .. + } => { + format!( + "EOF (OS Crashed)!\n {RED}│{RESET} Expected: '{}'\n {RED}│{RESET} Got: '{}'", + exp, + clean_output(got) + ) + } + _ => e.to_string(), + }; + + reporter.on_step_failure(&test.command, &fail_msg); + anyhow::bail!(e.to_string()); + } } if let Err(e) = p.exp_string(&config.shell_interactions.prompt) { @@ -58,3 +76,35 @@ impl Runner for QemuRunner { Ok(()) } } + +fn clean_output(raw: &str) -> String { + let mut msg = raw.replace("\r\n", " ↵ ").replace('\n', " ↵ "); + + if let Ok(re) = Regex::new(r"\x1b\[[0-9;]*[mK]|\\u\{1b\}\[[0-9;]*[mK]") { + msg = re.replace_all(&msg, "").to_string(); + } + msg +} + +fn format_diff(expected: &str, got: &str) -> String { + let clean_got = clean_output(got); + + let diff = TextDiff::from_chars(expected, &clean_got); + let mut colored_got = String::new(); + + for change in diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => {} + ChangeTag::Insert => { + colored_got.push_str(&format!("\x1b[31m{}\x1b[0m", change.value())); + } + ChangeTag::Equal => { + colored_got.push_str(&format!("\x1b[32m{}\x1b[0m", change.value())); + } + } + } + format!( + "Timeout!\n {RED}│{RESET} Expected: '{}'\n {RED}│{RESET} Got: '{}'", + expected, colored_got + ) +} diff --git a/src/lib.rs b/src/lib.rs index 05af3d3..65025c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,7 @@ pub mod config; pub mod engine; pub mod reporter; pub mod traits; + +const GREEN: &str = "\x1b[32m"; +const RED: &str = "\x1b[31m"; +const RESET: &str = "\x1b[0m"; diff --git a/src/reporter/console.rs b/src/reporter/console.rs index ca80cb8..b997099 100644 --- a/src/reporter/console.rs +++ b/src/reporter/console.rs @@ -1,12 +1,9 @@ use crate::config::TestConfig; use crate::traits::Reporter; +use crate::{GREEN, RED, RESET}; pub struct ConsoleReporter; -const GREEN: &str = "\x1b[32m"; -const RED: &str = "\x1b[31m"; -const RESET: &str = "\x1b[0m"; - impl Reporter for ConsoleReporter { fn on_test_start(&self, config: &TestConfig) { println!("\n▶ 📼 WALKMAN PLAYING: {}", config.name); diff --git a/src/reporter/tui.rs b/src/reporter/tui.rs index 3d312a5..c834c95 100644 --- a/src/reporter/tui.rs +++ b/src/reporter/tui.rs @@ -6,7 +6,7 @@ use crossterm::{ terminal::{Clear, ClearType}, }; -use crate::{config::TestConfig, traits::Reporter}; +use crate::{RED, RESET, config::TestConfig, traits::Reporter}; #[derive(Debug, PartialEq)] enum Status { @@ -151,7 +151,7 @@ impl TuiReporter { let _ = execute!( out, SetForegroundColor(Color::DarkGrey), - Print(" │ ⌨️ Sending payload...\n"), + Print(" │ 📦 Sending payload...\n"), Print(" │ ⏳ Waiting for OS response...\n"), ResetColor ); @@ -167,8 +167,7 @@ impl TuiReporter { ); let _ = execute!( out, - SetForegroundColor(Color::Red), - Print(format!(" │ Reason: {}\n", reason)), + Print(format!(" {RED}↳ │{RESET} Reason: {}\n", reason)), ResetColor ); lines += 1; @@ -273,7 +272,7 @@ mod tests { prompt: "$".to_string(), tests: vec![CommandTest { command: "ls".to_string(), - expect: "file".to_string(), + expect: vec!["file".to_string()], }], }, }