Skip to main content

test_lang/
lib.rs

1//! # test_lang
2//!
3//! A snapshot test harness for language front-ends: give it source, run that
4//! source through a stage, and assert the rendered result — the token stream,
5//! the syntax tree, or the diagnostics — against an expected block of text.
6//!
7//! ## Why a snapshot harness
8//!
9//! The output of a lexer, parser, or diagnostics renderer is a *shape*: a list
10//! of tokens, a tree of nodes, a set of caret-annotated errors. Asserting that
11//! shape field by field is verbose and brittle — one added node and a dozen
12//! index-based assertions shift. A snapshot test instead captures the whole
13//! rendered shape as text and compares it against a known-good block. When the
14//! output changes, you get a line-level diff pointing at exactly what moved, and
15//! accepting the new behavior is a copy-paste.
16//!
17//! This crate owns no grammar and depends on no other front-end crate. It works
18//! on anything that can render itself to text — a
19//! [`Display`](core::fmt::Display) value, a [`Debug`](core::fmt::Debug) tree, or
20//! an iterator of displayable items — so the same harness serves a hand-written
21//! lexer, a generated parser, or a diagnostics layer without coupling to any of
22//! them.
23//!
24//! ## The two types
25//!
26//! - [`Snapshot`] — a normalized, comparable rendering of some output. Build one
27//!   with [`Snapshot::display`], [`Snapshot::debug`], [`Snapshot::per_line`], or
28//!   [`Snapshot::new`], then call [`Snapshot::check`].
29//! - [`Diff`] — the line-level difference reported when a check fails, rendered
30//!   as a unified `-expected`/`+actual` diff. A failed check hands it back inside
31//!   a [`Mismatch`].
32//!
33//! ## Example
34//!
35//! Snapshot a token stream and assert it:
36//!
37//! ```
38//! use test_lang::Snapshot;
39//!
40//! // Whatever your lexer produces — here, a stand-in that yields display strings.
41//! fn lex(source: &str) -> Vec<String> {
42//!     source.split_whitespace().map(str::to_string).collect()
43//! }
44//!
45//! let tokens = lex("let x = 1");
46//! let snapshot = Snapshot::per_line(&tokens);
47//!
48//! snapshot.check("let\nx\n=\n1").expect("token stream matches");
49//! ```
50//!
51//! When the output drifts, the returned [`Mismatch`] shows precisely what
52//! changed:
53//!
54//! ```
55//! use test_lang::Snapshot;
56//!
57//! let snapshot = Snapshot::per_line(["let", "y", "=", "1"]);
58//! let err = snapshot.check("let\nx\n=\n1").unwrap_err();
59//!
60//! // `-x` was expected; `+y` was produced in its place.
61//! assert!(err.to_string().contains("-x"));
62//! assert!(err.to_string().contains("+y"));
63//! ```
64//!
65//! ## `no_std`
66//!
67//! The crate is `no_std` + `alloc` when the default `std` feature is disabled.
68//! Every type is available in both modes; the only difference is which crate the
69//! [`Error`](core::error::Error) impl is anchored to.
70
71#![cfg_attr(not(feature = "std"), no_std)]
72#![cfg_attr(docsrs, feature(doc_cfg))]
73#![deny(missing_docs)]
74#![forbid(unsafe_code)]
75#![deny(clippy::unwrap_used)]
76#![deny(clippy::expect_used)]
77
78extern crate alloc;
79
80mod diff;
81mod snapshot;
82
83pub use diff::{Change, Diff};
84pub use snapshot::{Mismatch, Snapshot};