diff --git a/Cargo.lock b/Cargo.lock index daea36d8..c5caeed2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,6 +1205,7 @@ version = "0.6.0" dependencies = [ "anyhow", "clap", + "regex", "xshell", ] diff --git a/examples/memory_management/expected_output.txt b/examples/memory_management/expected_output.txt index e92dd84d..b3d4846f 100644 --- a/examples/memory_management/expected_output.txt +++ b/examples/memory_management/expected_output.txt @@ -41,7 +41,7 @@ Checkpoint 21 PrintOnDrop(A) has been dropped Checkpoint 22 -thread '' panicked at examples/memory_management/src/lib.rs:30:9: +thread '' ({{re:\d+}}) panicked at examples/memory_management/src/lib.rs:30:9: consume_and_panic executed with value B note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace PrintOnDrop(B) has been dropped diff --git a/examples/simple/expected_output.txt b/examples/simple/expected_output.txt index 13c0d002..30bca2e3 100644 --- a/examples/simple/expected_output.txt +++ b/examples/simple/expected_output.txt @@ -1,7 +1,7 @@ 17 s[2] = 7 -thread '' panicked at examples/simple/src/generated.rs:192:39: +thread '' ({{re:\d+}}) panicked at examples/simple/src/generated.rs:192:39: called `Option::unwrap()` on a `None` value note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace s[4] = Rust panic happened diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 621b021b..1d3fcfa8 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -12,3 +12,4 @@ publish = false xshell = "0.2.7" anyhow = "1.0" clap = { version = "4.3.12", features = ["derive"] } +regex = "1" diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index bf65b539..e5fb49db 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -1,4 +1,6 @@ -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; +use regex::Regex; +use std::fs; use xshell::{Shell, cmd}; fn check_crate(sh: &Shell) -> Result<()> { @@ -34,8 +36,9 @@ fn check_examples(sh: &Shell, fix: bool) -> Result<()> { if fix { sh.copy_file("./actual_output.txt", "./expected_output.txt")?; } - cmd!(sh, "diff actual_output.txt expected_output.txt") - .run() + let expected_path = format!("examples/{}/expected_output.txt", example); + let actual_path = format!("examples/{}/actual_output.txt", example); + compare_expected_with_actual(&expected_path, &actual_path) .with_context(|| format!("Example `{example}` output differs from expected."))?; cmd!(sh, "cargo fmt --check") .run() @@ -45,6 +48,60 @@ fn check_examples(sh: &Shell, fix: bool) -> Result<()> { Ok(()) } +/// Compare `expected_path` to `actual_path` line-by-line. +/// Supports one extension in expected: +/// - Inline regex placeholders: `{{re:...}}` which are treated as raw regex +/// segments embedded within an otherwise literal line +fn compare_expected_with_actual(expected_path: &str, actual_path: &str) -> Result<()> { + let expected = fs::read_to_string(expected_path) + .with_context(|| format!("Failed to read {expected_path}"))?; + let actual = + fs::read_to_string(actual_path).with_context(|| format!("Failed to read {actual_path}"))?; + + let expected_lines: Vec<&str> = expected.lines().collect(); + let actual_lines: Vec<&str> = actual.lines().collect(); + + if expected_lines.len() != actual_lines.len() { + bail!( + "Line count differs: expected {} lines, actual {} lines", + expected_lines.len(), + actual_lines.len() + ); + } + + for (idx, (exp, act)) in expected_lines.iter().zip(actual_lines.iter()).enumerate() { + let line_no = idx + 1; + // Escape literals, splice in `{{re:...}}` as raw regex + let mut pattern = String::new(); + let mut rest = *exp; + while let Some(start) = rest.find("{{re:") { + let (head, tail) = rest.split_at(start); + pattern.push_str(®ex::escape(head)); + if let Some(end) = tail.find("}}") { + let re_body = &tail[5..end]; // after '{{re:' up to before '}}' + pattern.push_str(re_body); + rest = &tail[end + 2..]; + } else { + // No closing, treat literally + pattern.push_str(®ex::escape(tail)); + rest = ""; + } + } + pattern.push_str(®ex::escape(rest)); + // Anchor the pattern to the full line + let anchored = format!("^{}$", pattern); + let re = Regex::new(&anchored) + .with_context(|| format!("Invalid constructed regex on expected line {line_no}"))?; + if !re.is_match(act) { + bail!( + "Line {line_no} differs.\n expected: {exp}\n actual: {act}\n pattern: {anchored}" + ); + } + } + + Ok(()) +} + pub fn main(fix: bool) -> Result<()> { let sh = &Shell::new()?; println!("Cargo version = {}", cmd!(sh, "cargo --version").read()?);