Files
langrpg/tests/hello_rpg.rs
2026-03-12 21:41:30 -07:00

206 lines
6.8 KiB
Rust

//! Integration tests for the compiler binary against the Hello World program.
//!
//! These tests exercise the full compilation pipeline:
//! hello.rpg → BNF validation → AST lowering → LLVM codegen → native binary
use std::process::Command;
/// `CARGO_BIN_EXE_rust-langrpg` is injected by Cargo for integration tests and
/// always points at the freshly-built binary under `target/`.
const BIN: &str = env!("CARGO_BIN_EXE_rust-langrpg");
/// Absolute path to hello.rpg, resolved at compile time relative to the crate
/// root so the test works regardless of the working directory.
const HELLO_RPG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/hello.rpg");
// ─────────────────────────────────────────────────────────────────────────────
// Helper
// ─────────────────────────────────────────────────────────────────────────────
fn run(args: &[&str]) -> std::process::Output {
Command::new(BIN)
.args(args)
.output()
.unwrap_or_else(|e| panic!("failed to spawn '{}': {e}", BIN))
}
// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────
/// The compiler should exit 0 when given hello.rpg (no -o flag — the output
/// executable is written to a.out but the important thing is no error).
#[test]
fn hello_rpg_exits_ok() {
let out_path = std::env::temp_dir().join("hello_rpg_exits_ok.out");
let out = run(&["-o", out_path.to_str().unwrap(), HELLO_RPG]);
assert!(
out.status.success(),
"expected exit 0 for hello.rpg\nstderr: {}",
String::from_utf8_lossy(&out.stderr),
);
}
/// When -o is supplied the output file must be created as a non-empty compiled
/// artifact (executable binary).
#[test]
fn hello_rpg_produces_output_file() {
let out_path = std::env::temp_dir().join("hello_rpg_test_output.out");
let out = run(&["-o", out_path.to_str().unwrap(), HELLO_RPG]);
assert!(
out.status.success(),
"compiler failed with -o flag\nstderr: {}",
String::from_utf8_lossy(&out.stderr),
);
assert!(
out_path.exists(),
"output file '{}' was not created",
out_path.display(),
);
let metadata = std::fs::metadata(&out_path)
.unwrap_or_else(|e| panic!("could not stat output file: {e}"));
assert!(
metadata.len() > 0,
"output file is empty — expected a compiled artifact",
);
}
/// The compiler must print the file name to stderr with an "ok:" prefix when
/// BNF validation succeeds.
#[test]
fn hello_rpg_reports_ok_on_stderr() {
let out_path = std::env::temp_dir().join("hello_rpg_reports_ok.out");
let out = run(&["-o", out_path.to_str().unwrap(), HELLO_RPG]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("ok:"),
"expected stderr to contain 'ok:'\ngot: {stderr}",
);
}
/// `--emit-ir` must print LLVM IR to stdout and exit 0.
///
/// The IR must contain:
/// * At least one `define` (a function definition)
/// * A reference to `rpg_dsply` (the DSPLY runtime call)
/// * A `@main` entry point (the C main wrapper)
#[test]
fn hello_rpg_emit_ir() {
let out = run(&["--emit-ir", HELLO_RPG]);
assert!(
out.status.success(),
"expected exit 0 with --emit-ir\nstderr: {}",
String::from_utf8_lossy(&out.stderr),
);
let ir = String::from_utf8_lossy(&out.stdout);
assert!(
ir.contains("define"),
"--emit-ir should produce at least one LLVM function definition\nIR:\n{}",
&ir[..ir.len().min(2000)],
);
assert!(
ir.contains("rpg_dsply"),
"--emit-ir should reference the rpg_dsply runtime symbol\nIR:\n{}",
&ir[..ir.len().min(2000)],
);
assert!(
ir.contains("@main"),
"--emit-ir should contain a @main entry-point wrapper\nIR:\n{}",
&ir[..ir.len().min(2000)],
);
}
/// `--emit-tree` must print the BNF parse tree to stdout and exit 0.
///
/// The tree must mention `program` (the top-level grammar rule).
#[test]
fn hello_rpg_emit_tree() {
let out = run(&["--emit-tree", HELLO_RPG]);
assert!(
out.status.success(),
"expected exit 0 with --emit-tree\nstderr: {}",
String::from_utf8_lossy(&out.stderr),
);
let tree = String::from_utf8_lossy(&out.stdout);
assert!(
!tree.trim().is_empty(),
"--emit-tree output is empty — expected a parse tree",
);
assert!(
tree.contains("program"),
"--emit-tree output should reference the <program> rule\n{}",
&tree[..tree.len().min(1000)],
);
}
/// `--no-link` should produce a `.o` object file and exit 0.
#[test]
fn hello_rpg_no_link_produces_object() {
let obj_path = std::env::temp_dir().join("hello_rpg_test.o");
let out = run(&["--no-link", "-o", obj_path.to_str().unwrap(), HELLO_RPG]);
assert!(
out.status.success(),
"expected exit 0 with --no-link\nstderr: {}",
String::from_utf8_lossy(&out.stderr),
);
assert!(
obj_path.exists(),
"object file '{}' was not created",
obj_path.display(),
);
let metadata = std::fs::metadata(&obj_path)
.unwrap_or_else(|e| panic!("could not stat object file: {e}"));
assert!(
metadata.len() > 0,
"object file is empty — expected compiled LLVM output",
);
// A valid ELF object file starts with the ELF magic bytes 0x7f 'E' 'L' 'F'.
let bytes = std::fs::read(&obj_path)
.unwrap_or_else(|e| panic!("could not read object file: {e}"));
assert!(
bytes.starts_with(b"\x7fELF"),
"expected an ELF object file, got unexpected magic bytes: {:?}",
&bytes[..bytes.len().min(4)],
);
}
/// Passing a non-existent file should cause the compiler to exit non-zero and
/// print an error to stderr.
#[test]
fn nonexistent_source_exits_error() {
let out = run(&["no_such_file_xyz.rpg"]);
assert!(
!out.status.success(),
"expected non-zero exit for a missing source file",
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("error"),
"expected an error message on stderr\ngot: {stderr}",
);
}