Two posts in two days? Madness!
Posts in StackLang:
But really, it got a bit late yesterday so I figured I’d split this into two different posts.
In this post:
Full source code for StackLang: github:jpverkamp/stacklang
Generating tests
Version 1: A simple function
In a nutshell, I have a directory of examples with what should be consistent output. It would be nice if I could set up tests so that when I changed something, I could just cargo test
and verify that the output of the examples hasn’t changed. Even better, I could also check the vm
and compile --run
(and any possible other versions) output and make sure that they’re the same!
This actually turned out much easier than I expected:
// example_tests.rs
#[cfg(test)]
mod test {
use std::process::Command;
use std::str;
fn test(path: &str, target: &str) {
let vm_output = Command::new("cargo")
.arg("run")
.arg("--")
.arg("vm")
.arg(path)
.output()
.expect("failed to run vm");
assert_eq!(vm_output.status.success(), true, "vm exit code");
assert_eq!(
str::from_utf8(&vm_output.stdout).unwrap(),
target,
"vm output"
);
let compile_output = Command::new("cargo")
.arg("run")
.arg("--")
.arg("compile")
.arg(path)
.arg("--output")
.arg(format!("output/test-{}.c", path.replace("/", "-")))
.arg("--run")
.output()
.expect("failed to execute process");
assert_eq!(
compile_output.status.success(),
true,
"c compiler exit code"
);
assert_eq!(
str::from_utf8(&compile_output.stdout).unwrap(),
target,
"c compiler output"
);
}
#[test]
fn test_add2() {
test("examples/add2.stack", "12\n");
}
#[test]
fn test_basic_math() {
test("examples/basic-math.stack", "98\n");
}
...
}
That’s really it. I made a helper function test
that takes the path to the example/*.stack
file and the expected output. It then uses Command
to run the vm
version, check it’s status and output and then compile
with its status and output. If any of these fail, fail the test. Otherwise, all good.
I think I’d probably rather having a test for vm and one for compile… so let’s do that!
Version 2: Macros!
To split the tests, we need a macro to generate two functions:
macro_rules! make_tests {
($name:ident: $path:expr => $target:expr) => {
paste! {
#[test]
fn [< test_vm_ $name >]() {
let vm_output = Command::new("cargo")
.arg("run")
.arg("--")
.arg("vm")
.arg($path)
.output()
.expect("failed to run vm");
assert_eq!(vm_output.status.success(), true, "vm exit code");
assert_eq!(
str::from_utf8(&vm_output.stdout).unwrap(),
$target,
"vm output"
);
}
#[test]
fn [< test_compile_ $name >]() {
let compile_output = Command::new("cargo")
.arg("run")
.arg("--")
.arg("compile")
.arg($path)
.arg("--output")
.arg(format!("output/test-{}.c", $path.replace("/", "-")))
.arg("--run")
.output()
.expect("failed to execute process");
assert_eq!(
compile_output.status.success(),
true,
"c compiler exit code"
);
assert_eq!(
str::from_utf8(&compile_output.stdout).unwrap(),
$target,
"c compiler output"
);
}
}
};
}
That’s actually easier than I feared. One gotcha is that Rust macro_rules!
macros can’t (by default) make new identifiers. And we do need to do that, since the functions have to have different names. paste to the rescue! (that sounds silly). Basically, add [< ... >]
syntax that can build new identifiers out of old ones + syntax objects. Pretty straight forward that.
And to use it:
make_tests!(add2: "examples/add2.stack" => "12\n");
make_tests!(basic_math: "examples/basic-math.stack" => "98\n");
make_tests!(named_variables: "examples/double-named.stack" => "20\n");
make_tests!(name_2: "examples/name-two.stack" => "3\n");
...
I really like that! 😄 And when I add another backend? Just add to the macro and tests for FREE!
Test output
Using version 2, so we have test_vm_*
and test_compile_*
separate:
$ cargo test
Compiling stacklang v0.1.0 (/Users/jp/Projects/stacklang)
Finished test [unoptimized + debuginfo] target(s) in 0.31s
Running unittests src/main.rs (target/debug/deps/stacklang-d83b4a09c025c08b)
running 59 tests
test example_tests::test::test_compile_loop_apply ... FAILED
test example_tests::test::test_compile_arity_in_2 ... ok
test example_tests::test::test_compile_loop ... ok
test example_tests::test::test_compile_add2 ... ok
test example_tests::test::test_compile_if ... ok
test example_tests::test::test_compile_loop_list ... FAILED
test example_tests::test::test_compile_arity_out_2 ... ok
test example_tests::test::test_compile_list ... ok
test example_tests::test::test_compile_basic_math ... ok
test example_tests::test::test_compile_lists_of_lists ... ok
test example_tests::test::test_compile_arity_2_2 ... ok
test example_tests::test::test_compile_cond_recursion ... ok
test example_tests::test::test_vm_basic_math ... ok
test example_tests::test::test_vm_arity_out_2 ... ok
test example_tests::test::test_vm_arity_in_2 ... ok
test example_tests::test::test_vm_if ... ok
test example_tests::test::test_vm_add2 ... ok
test example_tests::test::test_vm_arity_2_2 ... ok
test example_tests::test::test_vm_cond_recursion ... ok
test example_tests::test::test_compile_name_2 ... ok
test example_tests::test::test_compile_named_variables ... ok
test example_tests::test::test_compile_recursion ... ok
test example_tests::test::test_compile_mutual_recursion ... ok
test lexer::test::test_binary ... ok
test lexer::test::test_brackets ... ok
test lexer::test::test_float_scientific ... ok
test lexer::test::test_floats ... ok
test lexer::test::test_hex ... ok
test lexer::test::test_identifiers ... ok
test lexer::test::test_integers ... ok
test lexer::test::test_negative_integers ... ok
test lexer::test::test_prefixed ... ok
test lexer::test::test_rationals ... ok
test lexer::test::test_strings ... ok
test lexer::test::test_symbolic ... ok
test parser::test::test_assignment_bang ... ok
test parser::test::test_boolean_literal ... ok
test parser::test::test_dotted_identifier ... ok
test parser::test::test_factorial ... ok
test parser::test::test_float ... ok
test parser::test::test_identifier ... ok
test parser::test::test_integer ... ok
test parser::test::test_list_naming ... ok
test parser::test::test_naming ... ok
test parser::test::test_simple_addition ... ok
test parser::test::test_simple_block ... ok
test parser::test::test_string_literal ... ok
test parser::test::test_symbolic_identifier ... ok
test example_tests::test::test_compile_recursive_helper ... ok
test example_tests::test::test_vm_recursive_helper ... ok
test example_tests::test::test_vm_recursion ... ok
test example_tests::test::test_vm_loop ... ok
test example_tests::test::test_vm_list ... ok
test example_tests::test::test_vm_named_variables ... ok
test example_tests::test::test_vm_lists_of_lists ... ok
test example_tests::test::test_vm_loop_apply ... ok
test example_tests::test::test_vm_name_2 ... ok
test example_tests::test::test_vm_loop_list ... ok
test example_tests::test::test_vm_mutual_recursion ... ok
failures:
---- example_tests::test::test_compile_loop_apply stdout ----
thread 'example_tests::test::test_compile_loop_apply' panicked at 'assertion failed: `(left == right)`
left: `false`,
right: `true`: c compiler exit code', src/example_tests.rs:73:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
---- example_tests::test::test_compile_loop_list stdout ----
thread 'example_tests::test::test_compile_loop_list' panicked at 'assertion failed: `(left == right)`
left: `false`,
right: `true`: c compiler exit code', src/example_tests.rs:85:5
failures:
example_tests::test::test_compile_loop_apply
example_tests::test::test_compile_loop_list
test result: FAILED. 57 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.74s
error: test failed, to rerun pass `--bin stacklang`
When I first started writing these, I had a couple more failures, mostly because I hadn’t actually implemented stacks yet (yes, these posts are out of order). But now, I’m only missing loop lists / apply in the compiler version. Everything else is good to go.
And now, whenever I do any major refactoring… the tests should show me what I mess up. Pretty cool.