Skip to content

Releases: jamesgober/test-lang

v1.0.0 — Stable API

Choose a tag to compare

@jamesgober jamesgober released this 01 Jul 12:41

test-lang v1.0.0 — API Freeze

The public surface is now stable. Everything that shipped in 0.2.0 — the four types, their methods, their trait implementations — is frozen under SemVer: no breaking change lands before a 2.0, and 1.x releases add capability without removing or altering what exists. There are no functional changes from 0.2.0; this release records the promise.

What is test-lang?

A snapshot test harness for language front-ends. Give it source, run that source through a lexer, parser, or diagnostics renderer, and assert the rendered result against a known-good block of text. When the output changes, you get a line-level unified diff pointing at exactly what moved, and accepting the new behavior is a copy-paste. It owns no grammar and depends on no other front-end crate — it works over core::fmt::Display and Debug, so the same harness serves a hand-written lexer or a generated parser without coupling to either. no_std + alloc supported.

The frozen surface

Two types you construct, one you receive on failure, and the pair that describes what differed:

  • Snapshot — a normalized, comparable rendering of some stage's output.
    • new, display, debug, per_line — capture from a string, a Display value, a pretty-printed Debug tree, or a sequence of items (one per line).
    • as_str, check — read the normalized text; compare against expected text, returning Result<(), Mismatch>.
    • Implements Display + the standard Debug/Clone/PartialEq/Eq/Hash.
  • Diff + Change — the line-level edit script (LCS with a common prefix/suffix fast path), rendered as a unified -expected/+actual diff.
    • Diff::lines, is_empty, changes; Change::marker.
  • Mismatch — the error from Snapshot::check, carrying the Diff, implementing Display and core::error::Error.
use test_lang::Snapshot;

let snapshot = Snapshot::per_line(["let", "x", "=", "1"]);
snapshot.check("let\nx\n=\n1").expect("token stream matches");

The full reference, with parameters and multiple runnable examples per item, is in docs/API.md — now marked stable.

What the freeze means

  • No breaking changes before 2.0. Method signatures, type names, variant names, and trait impls listed in docs/API.md are fixed.
  • 1.x is additive. New constructors, helpers, or trait impls may arrive in minor releases; nothing already present will be removed or changed.
  • Normalization is part of the contract. CRLF/CR collapse to LF, trailing whitespace is stripped, and trailing blank lines are trimmed — so a snapshot written on one platform matches output captured on another. This behavior is stable.

Breaking changes

None. The API is identical to 0.2.0. The only documentation correction: the std feature description now states accurately that the Error impl is core::error::Error in both std and no_std builds — the feature only controls whether the standard library is linked.

Verification

Re-ran the full gate on Windows x86_64 (Rust stable 1.95) and Linux (WSL2 Ubuntu); MSRV 1.85 verified locally; the CI matrix covers Linux / macOS / Windows × stable / 1.85:

cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features
cargo test --no-default-features
cargo build --examples
cargo +1.85 build --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo deny check

All green. Test counts at this tag: 30 unit + 8 integration + 6 property + 18 doctests.

What's next

The public API is frozen; future 1.x work is additive and driven by real usage — candidates include a by-value Display capture helper, a compact ({:?}) debug variant, and a convenience assertion macro. None are required, and none will break existing code.

Installation

[dependencies]
test-lang = "1"

# no_std + alloc
test-lang = { version = "1", default-features = false }

MSRV: Rust 1.85.

Documentation


Full diff: v0.2.0...v1.0.0.
Changelog: CHANGELOG.md.

v0.2.0 — Foundation

v0.2.0 — Foundation Pre-release
Pre-release

Choose a tag to compare

@jamesgober jamesgober released this 01 Jul 11:57

test-lang v0.2.0 — The Core Harness

The first release with working code. v0.1.0 was scaffold — structure, tooling, quality gates, no logic. v0.2.0 is the harness itself: capture a stage's output as normalized text, assert it against an expected block, and get a line-level diff when they differ. Two public types you build (Snapshot, and Diff/Change describing what changed), one error you receive (Mismatch). No runtime dependencies; no_std + alloc supported.

What is test-lang?

A snapshot test harness for language front-ends. Give it source, run that source through a lexer, parser, or diagnostics renderer, and assert the rendered result — the token stream, the syntax tree, the diagnostics — against a known-good block of text. When the output changes, you get a unified diff pointing at exactly what moved, and accepting the new behavior is a copy-paste. It owns no grammar and depends on no other front-end crate: it works over core::fmt::Display and Debug, so the same harness serves a hand-written lexer or a generated parser without coupling to either.

What's new in 0.2.0

Snapshot — capture, normalize, check

A Snapshot is the rendered output of some stage, held as normalized text. Four constructors cover the three things a front-end produces:

use test_lang::Snapshot;

// A token stream — one token per line, so the diff points at the exact token.
let tokens = Snapshot::per_line(["Ident(let)", "Ident(x)", "Eq", "Int(1)"]);

// A syntax tree — the pretty-printed `Debug` form.
#[derive(Debug)]
struct Binary { op: char, lhs: i64, rhs: i64 }
let tree = Snapshot::debug(&Binary { op: '+', lhs: 1, rhs: 2 });

// A rendered diagnostic — captured verbatim.
let diag = Snapshot::new("error: unexpected token\n  --> 1:5");

check compares against an expected block and returns a Result — no panic in the library, so the test author decides whether to unwrap, expect, or propagate with ?:

# use test_lang::Snapshot;
let snapshot = Snapshot::per_line(["let", "x", "=", "1"]);
snapshot.check("let\nx\n=\n1").expect("token stream matches");

Cross-platform normalization

The reason a snapshot written by hand on Linux matches output captured on Windows: both sides are normalized before comparison. CRLF and lone CR collapse to LF, trailing whitespace is stripped from every line, and trailing blank lines are trimmed. Interior blank lines and leading indentation — which are significant in a pretty-printed tree — are preserved.

use test_lang::Snapshot;

// Output captured on Windows: CRLF endings, a formatter's trailing space.
let captured = "error: bad token  \r\n  --> 1:1\r\n";
Snapshot::new(captured).check("error: bad token\n  --> 1:1").unwrap();

Diff / Change / Mismatch — the failure report

A failed check returns a Mismatch whose Display is a unified diff — - lines expected, + lines produced — and whose diff() accessor exposes the Diff for programmatic inspection:

use test_lang::{Change, Snapshot};

let err = Snapshot::per_line(["let", "y", "=", "1"])
    .check("let\nx\n=\n1")
    .unwrap_err();

assert!(err.to_string().contains("-x")); // expected `x`
assert!(err.to_string().contains("+y")); // produced `y`

let edits = err.diff().changes().filter(|(c, _)| *c != Change::Equal).count();
assert_eq!(edits, 2);

The diff engine is a longest-common-subsequence edit script with a common prefix/suffix fast path: identical leading and trailing lines are matched in linear time before the quadratic LCS ever runs, so a near-miss snapshot only pays for the region that actually changed, and the reported diff stays tight instead of re-aligning lines that never moved.

Property tests for the two invariants

tests/proptests.rs exercises the guarantees the harness rests on over a wide input space:

  • A snapshot always checks clean against its own captured text.
  • Normalization is idempotent — re-wrapping a snapshot's text is a no-op.
  • check succeeds exactly when the two texts normalize equal.
  • The diff is a faithful edit script: its +/ lines reconstruct the actual text, its -/ lines reconstruct the expected text.
  • A diff is empty if and only if the two normalized inputs are equal.
  • per_line equals a newline join of the rendered items.

Design decision — no concrete front-end dependencies

The v0.1.0 roadmap planned to wire the lexer, ast, parser, and diag crates into the harness "when first used." They are not used, and that is deliberate: operating over Display/Debug snapshots any front-end's output without a compile-time dependency on the crate that produced it. Taking concrete dependencies would couple test-lang's release cadence to four other crates for no functional gain, and would violate the REPS rule that cross-crate coupling flow through abstractions rather than concrete internals. This is recorded in dev/ROADMAP.md per the anti-deferral rule as a dropped task, not a deferred one.

Breaking changes

None — v0.1.0 exported no functional API. The unused serde feature and optional dependency from the scaffold were removed, and the scaffold's malformed Cargo.toml metadata (unquoted keywords / categories) was fixed.

Performance

Local Criterion means (cargo bench, Windows x86_64, Rust stable). The path that matters in a test suite is the check on the matching side — 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
  • Capture (4096 lines): ~78 µs

Verification

Run on Windows x86_64 (Rust stable 1.95.x) and on Linux via WSL2 Ubuntu; identical commands pass under the CI matrix (Linux / macOS / Windows × stable / 1.85):

cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features
cargo test --no-default-features
cargo build --examples
cargo +1.85 build --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo deny check

All green. Test counts at this tag: 30 unit + 8 integration + 6 property + 18 doctests.

What's next

  • 1.0.0 — API freeze. docs/API.md marked stable, the SemVer promise recorded, and the full test + benchmark suite green on all three platforms.

Installation

[dependencies]
test-lang = "0.2"

# no_std + alloc
test-lang = { version = "0.2", default-features = false }

MSRV: Rust 1.85.

Documentation


Changelog: CHANGELOG.md.