add: for sample, fix bugs, and make test harness
This commit is contained in:
11
samples/fib.rpg.golden
Normal file
11
samples/fib.rpg.golden
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
DSPLY Fibonacci Sequence:
|
||||||
|
DSPLY 0
|
||||||
|
DSPLY 1
|
||||||
|
DSPLY 1
|
||||||
|
DSPLY 2
|
||||||
|
DSPLY 3
|
||||||
|
DSPLY 5
|
||||||
|
DSPLY 8
|
||||||
|
DSPLY 13
|
||||||
|
DSPLY 21
|
||||||
|
DSPLY 34
|
||||||
13
samples/for.rpg
Normal file
13
samples/for.rpg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
**FREE
|
||||||
|
Ctl-Opt Main(For);
|
||||||
|
|
||||||
|
Dcl-Proc For;
|
||||||
|
dcl-s num int(10);
|
||||||
|
|
||||||
|
for num = 1 to 3;
|
||||||
|
dsply ('i = ' + %char(num));
|
||||||
|
endfor;
|
||||||
|
for num = 5 downto 1 by 1;
|
||||||
|
dsply ('i = ' + %char(num));
|
||||||
|
endfor;
|
||||||
|
End-Proc For;
|
||||||
8
samples/for.rpg.golden
Normal file
8
samples/for.rpg.golden
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
DSPLY i = 1
|
||||||
|
DSPLY i = 2
|
||||||
|
DSPLY i = 3
|
||||||
|
DSPLY i = 5
|
||||||
|
DSPLY i = 4
|
||||||
|
DSPLY i = 3
|
||||||
|
DSPLY i = 2
|
||||||
|
DSPLY i = 1
|
||||||
1
samples/hello.rpg.golden
Normal file
1
samples/hello.rpg.golden
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DSPLY Hello, World!
|
||||||
107
src/codegen.rs
107
src/codegen.rs
@@ -692,10 +692,13 @@ impl<'ctx> Codegen<'ctx> {
|
|||||||
self.builder.position_at_end(bb);
|
self.builder.position_at_end(bb);
|
||||||
|
|
||||||
// Call the RPG entry procedure.
|
// Call the RPG entry procedure.
|
||||||
// Try the bare name first (CTL-OPT MAIN procedures are not renamed),
|
// Try the `rpg_` prefix first (used for EXPORT-ed procedures) so that
|
||||||
// then the `rpg_` prefix used for EXPORT-ed procedures.
|
// a procedure named "main" resolves to @rpg_main rather than the @main
|
||||||
let callee = self.module.get_function(rpg_entry)
|
// wrapper we just created (which would cause infinite recursion).
|
||||||
.or_else(|| self.module.get_function(&format!("rpg_{}", rpg_entry)));
|
// Fall back to the bare name for CTL-OPT MAIN procedures, which are
|
||||||
|
// not exported and therefore not prefixed.
|
||||||
|
let callee = self.module.get_function(&format!("rpg_{}", rpg_entry))
|
||||||
|
.or_else(|| self.module.get_function(rpg_entry));
|
||||||
if let Some(rpg_fn) = callee {
|
if let Some(rpg_fn) = callee {
|
||||||
self.builder.build_call(rpg_fn, &[], "call_rpg").ok();
|
self.builder.build_call(rpg_fn, &[], "call_rpg").ok();
|
||||||
}
|
}
|
||||||
@@ -1011,14 +1014,33 @@ impl<'ctx> Codegen<'ctx> {
|
|||||||
let func = state.function;
|
let func = state.function;
|
||||||
let i64_t = self.context.i64_type();
|
let i64_t = self.context.i64_type();
|
||||||
|
|
||||||
// Allocate loop variable.
|
// Reuse the existing i64 alloca when the variable has already been
|
||||||
let loop_var = self.builder.build_alloca(i64_t, &f.var).unwrap();
|
// upgraded by a prior FOR loop (registered as Int(20), 8 bytes).
|
||||||
|
// If the variable comes from a DCL-S INT(10) declaration it only has a
|
||||||
|
// 4-byte slot, which is too small to hold an i64 — in that case (and
|
||||||
|
// when there is no existing local at all) allocate a fresh 8-byte slot.
|
||||||
|
// Both cases update state.locals so that the rest of the procedure
|
||||||
|
// reads/writes through the same pointer for the rest of its lifetime.
|
||||||
|
let loop_var = match state.locals.get(&f.var) {
|
||||||
|
Some((ptr, TypeSpec::Int(n))) => {
|
||||||
|
// Already upgraded to Int(20) (8 bytes) by a previous FOR —
|
||||||
|
// reuse it directly so all loops share the same variable.
|
||||||
|
if matches!(n.as_ref(), Expression::Literal(Literal::Integer(20))) {
|
||||||
|
*ptr
|
||||||
|
} else {
|
||||||
|
// Declared as a narrower int (e.g. INT(10) = 4 bytes).
|
||||||
|
// Allocate a fresh 8-byte slot; locals will be updated below.
|
||||||
|
self.builder.build_alloca(i64_t, &f.var).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self.builder.build_alloca(i64_t, &f.var).unwrap(),
|
||||||
|
};
|
||||||
let start = self.gen_expression(&f.start, state)?;
|
let start = self.gen_expression(&f.start, state)?;
|
||||||
let start_i = self.coerce_to_i64(start);
|
let start_i = self.coerce_to_i64(start);
|
||||||
self.builder.build_store(loop_var, start_i).ok();
|
self.builder.build_store(loop_var, start_i).ok();
|
||||||
// Store the loop variable with Int(20) so that byte_size() returns 8,
|
// Update the type to Int(20) so that byte_size() returns 8 and all
|
||||||
// matching the i64 alloca above. (Int(10) would give 4 bytes, causing
|
// subsequent loads/stores use a full i64 slot. (Int(10) gives 4 bytes,
|
||||||
// a 32-bit load from an 8-byte slot.)
|
// which would cause a width mismatch with the i64 alloca above.)
|
||||||
state.locals.insert(f.var.clone(), (loop_var, TypeSpec::Int(Box::new(Expression::Literal(Literal::Integer(20))))));
|
state.locals.insert(f.var.clone(), (loop_var, TypeSpec::Int(Box::new(Expression::Literal(Literal::Integer(20))))));
|
||||||
|
|
||||||
let cond_bb = self.context.append_basic_block(func, "for_cond");
|
let cond_bb = self.context.append_basic_block(func, "for_cond");
|
||||||
@@ -1830,7 +1852,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ir_hello_world() {
|
fn ir_hello_world() {
|
||||||
let src = include_str!("../hello.rpg");
|
let src = include_str!("../samples/hello.rpg");
|
||||||
let prog = lower(src).expect("lower hello.rpg");
|
let prog = lower(src).expect("lower hello.rpg");
|
||||||
let ir = emit_ir(&prog).expect("emit_ir hello.rpg");
|
let ir = emit_ir(&prog).expect("emit_ir hello.rpg");
|
||||||
// The IR must contain the dsply call and both main functions.
|
// The IR must contain the dsply call and both main functions.
|
||||||
@@ -1853,6 +1875,71 @@ END-PROC;
|
|||||||
assert!(ir.contains("for_cond") || ir.contains("br"), "FOR loop should emit branches:\n{}", ir);
|
assert!(ir.contains("for_cond") || ir.contains("br"), "FOR loop should emit branches:\n{}", ir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Two FOR loops on the same DCL-S variable (one ascending, one downto)
|
||||||
|
/// must share a single alloca — matching the semantics of for.rpg.
|
||||||
|
#[test]
|
||||||
|
fn ir_for_two_loops_same_var() {
|
||||||
|
let src = r#"
|
||||||
|
**FREE
|
||||||
|
Ctl-Opt Main(For);
|
||||||
|
|
||||||
|
Dcl-Proc For;
|
||||||
|
dcl-s num int(10);
|
||||||
|
|
||||||
|
for num = 1 to 3;
|
||||||
|
dsply ('i = ' + %char(num));
|
||||||
|
endfor;
|
||||||
|
for num = 5 downto 1 by 1;
|
||||||
|
dsply ('i = ' + %char(num));
|
||||||
|
endfor;
|
||||||
|
End-Proc For;
|
||||||
|
"#;
|
||||||
|
let ir = get_ir(src);
|
||||||
|
|
||||||
|
// Both loops must be present (two for_cond labels).
|
||||||
|
let for_cond_count = ir.matches("for_cond").count();
|
||||||
|
assert!(
|
||||||
|
for_cond_count >= 2,
|
||||||
|
"expected at least two for_cond blocks (one per loop), got {}:\n{}",
|
||||||
|
for_cond_count,
|
||||||
|
&ir[..ir.len().min(3000)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// The ascending loop uses sle (signed less-or-equal).
|
||||||
|
assert!(
|
||||||
|
ir.contains("icmp sle"),
|
||||||
|
"ascending loop should use 'icmp sle':\n{}",
|
||||||
|
&ir[..ir.len().min(3000)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// The downto loop uses sge (signed greater-or-equal).
|
||||||
|
assert!(
|
||||||
|
ir.contains("icmp sge"),
|
||||||
|
"downto loop should use 'icmp sge':\n{}",
|
||||||
|
&ir[..ir.len().min(3000)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// The decrement instruction must appear for the downto loop.
|
||||||
|
assert!(
|
||||||
|
ir.contains("sub i64"),
|
||||||
|
"downto loop should emit 'sub i64' for decrement:\n{}",
|
||||||
|
&ir[..ir.len().min(3000)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// There must be exactly one i64 alloca for 'num' — both loops share it.
|
||||||
|
// Count lines that both allocate i64 and mention 'num'.
|
||||||
|
let num_i64_allocas = ir
|
||||||
|
.lines()
|
||||||
|
.filter(|l| l.contains("alloca i64") && l.contains("num"))
|
||||||
|
.count();
|
||||||
|
assert_eq!(
|
||||||
|
num_i64_allocas, 1,
|
||||||
|
"both FOR loops must share a single i64 alloca for 'num', found {}:\n{}",
|
||||||
|
num_i64_allocas,
|
||||||
|
&ir[..ir.len().min(3000)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ir_if_stmt() {
|
fn ir_if_stmt() {
|
||||||
let src = r#"
|
let src = r#"
|
||||||
|
|||||||
19
src/lower.rs
19
src/lower.rs
@@ -1105,12 +1105,11 @@ impl Parser {
|
|||||||
fn parse_paren_ident(&mut self) -> Option<String> {
|
fn parse_paren_ident(&mut self) -> Option<String> {
|
||||||
if self.peek() != &Token::LParen { return None; }
|
if self.peek() != &Token::LParen { return None; }
|
||||||
self.advance(); // (
|
self.advance(); // (
|
||||||
let name = if let Token::Identifier(s) = self.peek().clone() {
|
// Use try_parse_ident_or_name so that names which collide with keywords
|
||||||
self.advance();
|
// (e.g. `For` in `Ctl-Opt Main(For)`) are accepted. Lowercase the
|
||||||
Some(s)
|
// result so it matches how procedure names are stored by token_as_name /
|
||||||
} else {
|
// expect_name (which always return lowercase strings for keyword tokens).
|
||||||
None
|
let name = self.try_parse_ident_or_name().map(|s| s.to_lowercase());
|
||||||
};
|
|
||||||
self.eat(&Token::RParen);
|
self.eat(&Token::RParen);
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
@@ -2500,7 +2499,7 @@ impl Parser {
|
|||||||
|
|
||||||
fn expect_ident(&mut self) -> Result<String, LowerError> {
|
fn expect_ident(&mut self) -> Result<String, LowerError> {
|
||||||
match self.advance() {
|
match self.advance() {
|
||||||
Token::Identifier(s) => Ok(s),
|
Token::Identifier(s) => Ok(s.to_lowercase()),
|
||||||
tok => Err(LowerError::new(format!("expected identifier, got {:?}", tok))),
|
tok => Err(LowerError::new(format!("expected identifier, got {:?}", tok))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2572,7 +2571,7 @@ impl Parser {
|
|||||||
/// lowercase or mixed-case spelling that the source would have used.
|
/// lowercase or mixed-case spelling that the source would have used.
|
||||||
fn token_as_name(tok: &Token) -> Option<String> {
|
fn token_as_name(tok: &Token) -> Option<String> {
|
||||||
match tok {
|
match tok {
|
||||||
Token::Identifier(s) => Some(s.clone()),
|
Token::Identifier(s) => Some(s.to_lowercase()),
|
||||||
|
|
||||||
// Statement / declaration keywords that are commonly used as names.
|
// Statement / declaration keywords that are commonly used as names.
|
||||||
Token::KwMain => Some("main".into()),
|
Token::KwMain => Some("main".into()),
|
||||||
@@ -2814,7 +2813,7 @@ mod tests {
|
|||||||
fn lower_dcl_c() {
|
fn lower_dcl_c() {
|
||||||
let p = lower_ok("DCL-C MAX_SIZE CONST(100);");
|
let p = lower_ok("DCL-C MAX_SIZE CONST(100);");
|
||||||
if let Declaration::Constant(c) = &p.declarations[0] {
|
if let Declaration::Constant(c) = &p.declarations[0] {
|
||||||
assert_eq!(c.name, "MAX_SIZE");
|
assert_eq!(c.name, "max_size");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2840,7 +2839,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lower_hello_rpg() {
|
fn lower_hello_rpg() {
|
||||||
let hello = include_str!("../hello.rpg");
|
let hello = include_str!("../samples/hello.rpg");
|
||||||
let p = lower_ok(hello);
|
let p = lower_ok(hello);
|
||||||
assert!(!p.procedures.is_empty(), "should have at least one procedure");
|
assert!(!p.procedures.is_empty(), "should have at least one procedure");
|
||||||
let proc = p.procedures.iter().find(|p| p.name == "main").expect("main proc");
|
let proc = p.procedures.iter().find(|p| p.name == "main").expect("main proc");
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ mod tests {
|
|||||||
/// to LLVM IR without errors.
|
/// to LLVM IR without errors.
|
||||||
#[test]
|
#[test]
|
||||||
fn hello_rpg_emits_ir() {
|
fn hello_rpg_emits_ir() {
|
||||||
let src = include_str!("../hello.rpg");
|
let src = include_str!("../samples/hello.rpg");
|
||||||
let prog = lower(src.trim()).expect("lower hello.rpg");
|
let prog = lower(src.trim()).expect("lower hello.rpg");
|
||||||
let ir = emit_ir(&prog).expect("emit_ir hello.rpg");
|
let ir = emit_ir(&prog).expect("emit_ir hello.rpg");
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const BIN: &str = env!("CARGO_BIN_EXE_rust-langrpg");
|
|||||||
|
|
||||||
/// Absolute path to hello.rpg, resolved at compile time relative to the crate
|
/// Absolute path to hello.rpg, resolved at compile time relative to the crate
|
||||||
/// root so the test works regardless of the working directory.
|
/// root so the test works regardless of the working directory.
|
||||||
const HELLO_RPG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/hello.rpg");
|
const HELLO_RPG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/samples/hello.rpg");
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Helper
|
// Helper
|
||||||
|
|||||||
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