179 lines
6.1 KiB
Rust
179 lines
6.1 KiB
Rust
//! Integration tests for the compiler binary against the Hello World program.
|
|
//!
|
|
//! These tests exercise the full compilation pipeline:
|
|
//! hello.rpg → recursive-descent parser → 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
|
|
/// compilation 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)],
|
|
);
|
|
}
|
|
|
|
/// `--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}",
|
|
);
|
|
}
|