//! 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 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}", ); }