Testing
formawasm's tests are the primary specification of correct behavior. The strict-clippy + typed-error setup means runtime failures are typed errors; the test suite confirms they get raised at the right shapes and that the happy path produces bytes that validate, instantiate, and run.
Test layout
Tests live under tests/, one file per concern:
tests/
backend_smoke.rs # WasmBackend::generate end-to-end
cli.rs # formawasm CLI integration tests
layout_*.rs # one per layout family
lower_*.rs # one per IR variant family
milestone_*.rs # per-phase end-to-end milestones
sieve.rs # Phase 1c milestone (named for the algorithm)
preflight.rs # rejection cases
survey.rs # public-surface classification
types.rs # ResolvedType → wasm valtype mapping
wit.rs # WIT emission
wit_snapshots.rs # insta snapshots of emitted WIT
component.rs # component wrap
wasm_opt_size.rs # (feature: wasm-opt) size-comparison
The split mirrors src/lower/: one source submodule, one test file. Adding a new IR variant means adding both src/lower/<family>.rs::lower_<variant> and tests/lower_<variant>.rs.
Test conventions
Tests return Result<(), TestError> where TestError = Box<dyn std::error::Error + Send + Sync>. Use ? to propagate errors; never bare assert! / panic! — return Err(...) instead.
#![allow(unused)] fn main() { type TestError = Box<dyn std::error::Error + Send + Sync>; type TestResult = Result<(), TestError>; #[test] fn empty_module_generates_a_valid_component() -> TestResult { let backend = WasmBackend::new(); let module = IrModule::new(); let bytes = backend.generate(&module)?; validate_component(&bytes) } }
This pattern is enforced by the panic_in_result_fn clippy lint. It also matches the project-wide style of typed errors over panics: a test failure is a value, not a control-flow exception.
What every milestone test does
End-to-end milestone tests follow a consistent shape:
- Hand-build the IR for the feature (see Extending the Backend for the pattern).
- Run through the full pipeline: typically via
Pipeline::emit(module, &WasmBackend::new()), sometimes directly viaWasmBackend::new().generate(&module)if no pre-codegen passes are needed. - Validate as a Component-Model artifact using
wasmparser::ValidatorwithWasmFeatures::default(). - Instantiate under wasmtime with
Config::wasm_component_model(true). - Call exports and assert on results using
instance.get_typed_func(or, for richer types, thebindgen!macro on a generated WIT file).
Example, condensed from tests/backend_smoke.rs:
#![allow(unused)] fn main() { let bytes = backend.generate(&module)?; validate_component(&bytes)?; let mut config = Config::new(); config.wasm_component_model(true); let engine = Engine::new(&config)?; let component = Component::from_binary(&engine, &bytes)?; let linker = Linker::<()>::new(&engine); let mut store = Store::new(&engine, ()); let instance = linker.instantiate(&mut store, &component)?; let fib = instance.get_typed_func::<(i32,), (i32,)>(&mut store, "fib")?; let (got,) = fib.call(&mut store, (10,))?; if got != 55 { return Err(format!("fib(10) = {got}, want 55").into()); } }
Snapshot tests for WIT
tests/wit_snapshots.rs uses insta to capture the full WIT text emitted for representative IR shapes. Each snapshot lives in tests/snapshots/wit_snapshots__<test>.snap and is committed alongside the test.
When the WIT emitter changes, snapshots may diverge. Run:
cargo insta review
to walk through diffs and accept or reject each one. CI compares against the committed snapshots and fails if any diverge without a corresponding accept.
Running tests
The Makefile wraps cargo test with nice -n 19 ionice -c 3 so a heavy compile doesn't starve the desktop:
make test # default-features
make test-wasm-opt # with the wasm-opt cargo feature on
TEST_ARGS='--test sieve' make test # one file
For CI parity:
make ci # fmt + clippy + doc + test + deny + test-wasm-opt
The make ci target compiles binaryen on a clean tree (the wasm-opt step), so expect a few minutes the first time.
What's not tested directly
A few things deliberately don't have unit tests:
- The bump allocator — exercised by every milestone test that constructs an aggregate. A direct test would lock in the
__allocABI, which we may want to evolve toward GC. - String-pool internal layout — same. Exercised through string-literal lowering tests.
__heap_ptr's exact starting value — depends on static-data size; tested via "the module instantiates" rather than by spelling out the integer.
When in doubt: a new feature gets a lower_<feature>.rs test. A change that touches only internal layout gets covered by the existing milestone that exercises the feature end-to-end.