add: for sample, fix bugs, and make test harness
This commit is contained in:
170
tests/samples.rs
Normal file
170
tests/samples.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
//! Integration tests — compile every `.rpg` sample and validate stdout
|
||||
//! against the accompanying `.rpg.golden` file.
|
||||
//!
|
||||
//! Each test follows the same three-step pipeline:
|
||||
//! 1. `rust-langrpg -o <tmp> <sample>.rpg` — compile
|
||||
//! 2. `<tmp>` — execute
|
||||
//! 3. compare stdout with `<sample>.rpg.golden`
|
||||
//!
|
||||
//! # Adding a new sample
|
||||
//!
|
||||
//! Drop the `.rpg` source and a `.rpg.golden` file into `samples/` and add a
|
||||
//! one-line `#[test]` that calls `run_sample("your_file.rpg")`.
|
||||
|
||||
use std::{fs, path::PathBuf, process::Command};
|
||||
|
||||
/// Path to the freshly-built compiler binary, injected by Cargo.
|
||||
const BIN: &str = env!("CARGO_BIN_EXE_rust-langrpg");
|
||||
|
||||
/// Absolute path to the `samples/` directory, resolved at compile time.
|
||||
const SAMPLES_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/samples");
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Shared driver
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compile `sample_name` from `samples/`, run the resulting binary, and assert
|
||||
/// that its stdout matches `samples/<sample_name>.golden` line-for-line.
|
||||
///
|
||||
/// Temp binaries are written to the OS temp directory with a name derived from
|
||||
/// the sample so parallel test execution doesn't cause collisions.
|
||||
fn run_sample(sample_name: &str) {
|
||||
let src = PathBuf::from(SAMPLES_DIR).join(sample_name);
|
||||
let golden_path = PathBuf::from(SAMPLES_DIR).join(format!("{}.golden", sample_name));
|
||||
|
||||
assert!(
|
||||
src.exists(),
|
||||
"source file not found: {}",
|
||||
src.display()
|
||||
);
|
||||
assert!(
|
||||
golden_path.exists(),
|
||||
"golden file not found: {}\n\
|
||||
Create it with the expected stdout to enable this test.",
|
||||
golden_path.display()
|
||||
);
|
||||
|
||||
// ── 1. Compile ────────────────────────────────────────────────────────────
|
||||
|
||||
// Sanitise the stem so it is safe to embed in a file name (e.g. "for.rpg"
|
||||
// → "for_rpg") and prefix with a fixed string to make it easy to identify.
|
||||
let safe_stem = sample_name.replace('.', "_").replace(std::path::MAIN_SEPARATOR, "_");
|
||||
let exe = std::env::temp_dir().join(format!("rpg_sample_test_{}.out", safe_stem));
|
||||
|
||||
let compile_out = Command::new(BIN)
|
||||
.args(["-o", exe.to_str().unwrap(), src.to_str().unwrap()])
|
||||
.output()
|
||||
.unwrap_or_else(|e| panic!("failed to spawn compiler for '{}': {e}", sample_name));
|
||||
|
||||
assert!(
|
||||
compile_out.status.success(),
|
||||
"compilation of '{}' failed (exit {})\nstderr:\n{}",
|
||||
sample_name,
|
||||
compile_out.status,
|
||||
String::from_utf8_lossy(&compile_out.stderr),
|
||||
);
|
||||
|
||||
assert!(
|
||||
exe.exists(),
|
||||
"compiler exited 0 for '{}' but no executable was produced at '{}'",
|
||||
sample_name,
|
||||
exe.display(),
|
||||
);
|
||||
|
||||
// ── 2. Execute ────────────────────────────────────────────────────────────
|
||||
|
||||
let run_out = Command::new(&exe)
|
||||
.output()
|
||||
.unwrap_or_else(|e| panic!("failed to run compiled binary for '{}': {e}", sample_name));
|
||||
|
||||
assert!(
|
||||
run_out.status.success(),
|
||||
"binary for '{}' exited non-zero ({})\nstdout:\n{}\nstderr:\n{}",
|
||||
sample_name,
|
||||
run_out.status,
|
||||
String::from_utf8_lossy(&run_out.stdout),
|
||||
String::from_utf8_lossy(&run_out.stderr),
|
||||
);
|
||||
|
||||
// ── 3. Compare against golden ─────────────────────────────────────────────
|
||||
|
||||
let actual_raw = String::from_utf8_lossy(&run_out.stdout);
|
||||
let expected_raw = fs::read_to_string(&golden_path).unwrap_or_else(|e| {
|
||||
panic!("could not read golden file '{}': {e}", golden_path.display())
|
||||
});
|
||||
|
||||
// Normalise both sides: trim trailing whitespace from every line and ignore
|
||||
// a trailing blank line, so golden files don't need a precise final newline.
|
||||
let normalise = |s: &str| -> Vec<String> {
|
||||
s.lines().map(|l| l.trim_end().to_string()).collect()
|
||||
};
|
||||
|
||||
let actual_lines = normalise(&actual_raw);
|
||||
let expected_lines = normalise(&expected_raw);
|
||||
|
||||
if actual_lines != expected_lines {
|
||||
let mut msg = format!(
|
||||
"stdout mismatch for '{}'\n\n\
|
||||
── expected ({} line{}) ──────────────────────\n",
|
||||
sample_name,
|
||||
expected_lines.len(),
|
||||
if expected_lines.len() == 1 { "" } else { "s" },
|
||||
);
|
||||
for line in &expected_lines {
|
||||
msg.push_str(" ");
|
||||
msg.push_str(line);
|
||||
msg.push('\n');
|
||||
}
|
||||
msg.push_str(&format!(
|
||||
"\n── actual ({} line{}) ────────────────────────\n",
|
||||
actual_lines.len(),
|
||||
if actual_lines.len() == 1 { "" } else { "s" },
|
||||
));
|
||||
for line in &actual_lines {
|
||||
msg.push_str(" ");
|
||||
msg.push_str(line);
|
||||
msg.push('\n');
|
||||
}
|
||||
|
||||
// Highlight the first diverging line to make failures easy to spot.
|
||||
for (i, (exp, act)) in expected_lines.iter().zip(actual_lines.iter()).enumerate() {
|
||||
if exp != act {
|
||||
msg.push_str(&format!(
|
||||
"\nfirst difference at line {}:\n expected: {:?}\n actual: {:?}\n",
|
||||
i + 1,
|
||||
exp,
|
||||
act,
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if actual_lines.len() != expected_lines.len() {
|
||||
msg.push_str(&format!(
|
||||
"\nline count differs: expected {}, got {}\n",
|
||||
expected_lines.len(),
|
||||
actual_lines.len(),
|
||||
));
|
||||
}
|
||||
|
||||
panic!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// One #[test] per sample
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn sample_hello() {
|
||||
run_sample("hello.rpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_for() {
|
||||
run_sample("for.rpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_fib() {
|
||||
run_sample("fib.rpg");
|
||||
}
|
||||
Reference in New Issue
Block a user