From 959f5d78ad512a8d9cf01f0debcbcd7a35e50d6f Mon Sep 17 00:00:00 2001 From: pepedinho <2spii94@gmail.com> Date: Tue, 12 May 2026 11:33:01 +0200 Subject: [PATCH 1/4] conf: add multi expect for a single command --- src/config.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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)] From da2c41f26e7d9fab67adcc817859c1fecf348f24 Mon Sep 17 00:00:00 2001 From: pepedinho <2spii94@gmail.com> Date: Tue, 12 May 2026 11:33:53 +0200 Subject: [PATCH 2/4] ui: rework ui details and error messages --- Cargo.lock | 1 + Cargo.toml | 1 + src/engine.rs | 27 +++++++++++++++++++++------ src/reporter/tui.rs | 6 +++--- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d6b347..d9e4d0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -705,6 +705,7 @@ dependencies = [ "anyhow", "clap", "crossterm", + "regex", "rexpect", "serde", "serde_yaml", diff --git a/Cargo.toml b/Cargo.toml index 2c455a5..23b96b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ rexpect = "0.7" anyhow = "1.0.102" clap = {version = "4.6", features = ["derive"]} crossterm = "0.29.0" +regex = "1.10" diff --git a/src/engine.rs b/src/engine.rs index 7b8cb31..d641862 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,3 +1,4 @@ +use regex::Regex; use rexpect::spawn; use crate::config::TestConfig; @@ -33,12 +34,13 @@ 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 clean_msg = clean_error_msg(&e.to_string()); + + reporter.on_step_failure(&test.command, &clean_msg); + anyhow::bail!(e.to_string()); + } } if let Err(e) = p.exp_string(&config.shell_interactions.prompt) { @@ -58,3 +60,16 @@ impl Runner for QemuRunner { Ok(()) } } + +fn clean_error_msg(raw_error: &str) -> String { + let mut msg = raw_error + .replace("\\r\\n", " ↵ ") + .replace("\\n", " ↵ ") + .replace("\\\"", "\""); + + if let Ok(re) = Regex::new(r"\\u\{1b\}\[[0-9;]*[mK]") { + msg = re.replace_all(&msg, "").to_string(); + } + + msg +} diff --git a/src/reporter/tui.rs b/src/reporter/tui.rs index 3d312a5..62527e8 100644 --- a/src/reporter/tui.rs +++ b/src/reporter/tui.rs @@ -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 ); @@ -168,7 +168,7 @@ impl TuiReporter { let _ = execute!( out, SetForegroundColor(Color::Red), - Print(format!(" │ Reason: {}\n", reason)), + Print(format!(" ↳ Reason: {}\n", reason)), ResetColor ); lines += 1; @@ -273,7 +273,7 @@ mod tests { prompt: "$".to_string(), tests: vec![CommandTest { command: "ls".to_string(), - expect: "file".to_string(), + expect: vec!["file".to_string()], }], }, } From 4b4025a448a937c4a7392225fa82f0f70845313f Mon Sep 17 00:00:00 2001 From: pepedinho <2spii94@gmail.com> Date: Tue, 12 May 2026 12:56:17 +0200 Subject: [PATCH 3/4] ui: add a diff view in error messages --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + src/engine.rs | 56 +++++++++++++++++++++++++++++++++-------- src/lib.rs | 4 +++ src/reporter/console.rs | 5 +--- src/reporter/tui.rs | 5 ++-- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9e4d0c..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" @@ -709,6 +715,7 @@ dependencies = [ "rexpect", "serde", "serde_yaml", + "similar", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 23b96b1..9db7366 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ 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/engine.rs b/src/engine.rs index d641862..a5d5bb5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,8 +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; @@ -36,9 +38,24 @@ impl Runner for QemuRunner { for expect in &test.expect { if let Err(e) = p.exp_string(expect) { - let clean_msg = clean_error_msg(&e.to_string()); - - reporter.on_step_failure(&test.command, &clean_msg); + // let clean_msg = clean_error_msg(&e.to_string()); + 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()); } } @@ -61,15 +78,34 @@ impl Runner for QemuRunner { } } -fn clean_error_msg(raw_error: &str) -> String { - let mut msg = raw_error - .replace("\\r\\n", " ↵ ") - .replace("\\n", " ↵ ") - .replace("\\\"", "\""); +fn clean_output(raw: &str) -> String { + let mut msg = raw.replace("\r\n", " ↵ ").replace('\n', " ↵ "); - if let Ok(re) = Regex::new(r"\\u\{1b\}\[[0-9;]*[mK]") { + 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 62527e8..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 { @@ -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; From 8e5aee26cfe97f6ae27e223bf68728a55d283279 Mon Sep 17 00:00:00 2001 From: pepe <91209772+pepedinho@users.noreply.github.com> Date: Tue, 12 May 2026 12:59:00 +0200 Subject: [PATCH 4/4] clean: remove deadcode --- src/engine.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/engine.rs b/src/engine.rs index a5d5bb5..530c09e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -38,7 +38,6 @@ impl Runner for QemuRunner { for expect in &test.expect { if let Err(e) = p.exp_string(expect) { - // let clean_msg = clean_error_msg(&e.to_string()); let fail_msg = match &e { rexpect::error::Error::Timeout { expected: exp, got, ..