test-lang is a snapshot test harness for language front-ends. Give it source, run that source through a stage — a lexer, a parser, a diagnostics renderer — and assert the rendered result against a known-good block of text. When the output changes, you get a line-level diff pointing at exactly what moved, and accepting the new behavior is a copy-paste.
It owns no grammar and takes no runtime dependencies. The harness works over anything that renders itself to text — a Display value, a Debug tree, or an iterator of displayable items — so the same two types serve a hand-written lexer, a generated parser, or a diagnostics layer without coupling to any of them. It is no_std + alloc capable and normalizes line endings and trailing whitespace so a snapshot captured on Windows matches one written on Linux.
MSRV is 1.85+ (Rust 2024 edition). Part of the -lang language-construction family.
Stable — API frozen at1.0.0. The public surface will not change in a breaking way before a 2.0; 1.x releases are additive. Seedocs/API.mdfor the frozen surface andCHANGELOG.mdfor history.
Latest local Criterion means (cargo bench, Windows x86_64, Rust stable). The workload that matters in a test suite is the check on the matching path — the case a green suite runs on every pass:
- Capture (
per_line, 16 lines): ~0.5 µs - Check, matching (16 lines): ~1.2 µs
- Check, matching (256 lines): ~19 µs
The check is a line-level diff with a common prefix/suffix fast path: identical leading and trailing lines are matched in linear time before the quadratic LCS engine runs, so a near-miss snapshot only pays for the region that actually changed.
[dependencies]
test-lang = "1"no_std + alloc (drops the std::error::Error anchor, keeps everything else):
[dependencies]
test-lang = { version = "1", default-features = false }Snapshot a token stream, one token per line, and assert it:
use test_lang::Snapshot;
// Whatever your lexer produces — here a stand-in that yields display strings.
fn lex(source: &str) -> Vec<String> {
source.split_whitespace().map(str::to_string).collect()
}
let snapshot = Snapshot::per_line(lex("let x = 1"));
snapshot.check("let\nx\n=\n1").expect("token stream matches");When the output drifts, the returned Mismatch shows precisely what changed:
use test_lang::Snapshot;
let snapshot = Snapshot::per_line(["let", "y", "=", "1"]);
let err = snapshot.check("let\nx\n=\n1").unwrap_err();
// `-x` was expected; `+y` was produced in its place.
assert!(err.to_string().contains("-x"));
assert!(err.to_string().contains("+y"));- Three capture modes —
Snapshot::displayfor aDisplayvalue,Snapshot::debugfor a pretty-printedDebugtree, andSnapshot::per_linefor a token stream (one item per line, so the diff points at the exact token). - Cross-platform normalization — CRLF/CR collapse to LF, trailing whitespace is stripped, and trailing blank lines are trimmed. A snapshot written by hand in a test matches output captured on any platform.
- Line-level diffs — a failed
checkreturns aMismatchwhoseDisplayis a unified-expected/+actualdiff;Mismatch::diffexposes theDifffor programmatic inspection. - No runtime dependencies — built on
core::fmtandalloconly.no_stdcapable. - No panics —
checkreturnsResult; the test author decides whether tounwrap,expect, or propagate.
For a complete reference with examples, see docs/API.md.
Snapshot— a normalized, comparable rendering of compiler output;new/display/debug/per_line/check.Diff&Change— the line-level edit script, rendered as a unified diff.Mismatch— the error returned bySnapshot::check, carrying theDiff.
Runnable examples: examples/tokens.rs, examples/ast.rs, examples/diagnostics.rs.
cargo run --example tokens
cargo run --example ast
cargo run --example diagnosticsSee REPS.md for engineering standards and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.
Licensed under either of
- Apache License, Version 2.0 — LICENSE-APACHE
- MIT License — LICENSE-MIT
at your option.