| module | STDSNAP | ||||
|---|---|---|---|---|---|
| tag | v0.3.0 | ||||
| phase | P4 wave | ||||
| stable | stable | ||||
| since | v0.3.0 | ||||
| synopsis | Snapshot testing: serialize an M tree, diff against a baseline | ||||
| labels |
|
||||
| errors |
|
||||
| conformance | |||||
| see_also |
|
||||
| created | 2026-05-07 | ||||
| last_modified | 2026-05-10 | ||||
| revisions | 4 | ||||
| doc_type |
|
Capture a deterministic dump of an M tree on the first run; on
subsequent runs, compare the live tree against the saved baseline.
A mismatch is reported as a STDASSERT failure with the snapshot
path so a human can inspect the diff. Cuts the wrist-deep
hand-written eq^STDASSERT chains for tests that verify large
parsed JSON trees, FileMan record exports, etc.
| Extrinsic | Signature | Action / Returns |
|---|---|---|
serialize |
$$serialize^STDSNAP(.data) |
Canonical line-per-leaf text dump (no trailing LF). |
save |
do save^STDSNAP(path, .data) |
Write serialize(data) to path via STDFS. |
matches |
$$matches^STDSNAP(path, .data) |
1 iff serialize(data) byte-equals path's content. |
asserts |
do asserts^STDSNAP(.pass, .fail, path, .data, desc) |
STDASSERT-style integration: pass on match, fail on mismatch. |
; First run: capture the baseline
NEW report
DO buildReport^MYAPP(.report)
DO save^STDSNAP("snapshots/report.snap",.report)
; Subsequent runs: compare against the baseline
NEW report
DO buildReport^MYAPP(.report)
IF '$$matches^STDSNAP("snapshots/report.snap",.report) DO ...
; Inside a test suite, drop straight into STDASSERT integration:
tReportShape(pass,fail) ;@TEST "report tree matches the snapshot"
NEW report
DO buildReport^MYAPP(.report)
DO asserts^STDSNAP(.pass,.fail,"snapshots/report.snap",.report,"report tree")
QUIT
; Update workflow: when the report shape legitimately changes,
; re-save the snapshot. (No CLI flag for this in v1 — caller
; toggles between save and matches by hand.)
DO save^STDSNAP("snapshots/report.snap",.report)
Each leaf in the tree emits exactly one line. Lines are emitted in
M's $ORDER walk order — natural M-collation, which sorts numeric
subscripts numerically and string subscripts as strings.
(subscripts)=value
| Component | Format |
|---|---|
subscripts |
M-syntax comma-separated list. Numeric subscripts unquoted; string subscripts wrapped in "..." with embedded " doubled. |
= |
Literal separator. |
value |
Numeric leaves: unquoted (canonical numeric form). Everything else: wrapped in "..." with embedded " doubled. |
Example dumps:
| Input tree | Serialised |
|---|---|
data("a")="hello" |
("a")="hello" |
data(1)="first" |
(1)="first" |
data("k")="say ""hi""" |
("k")="say ""hi""" |
Two leaves: data("a")="x", data("b")="y" |
("a")="x"\n("b")="y" |
Nested: data("user","name")="alice", data("user","age")=42, data("system")="ok" |
("system")="ok"\n("user","age")=42\n("user","name")="alice" |
The format is deterministic by construction: $QUERY walks
descendants in M-collation order; each leaf produces exactly one
line; quoting rules are positional and reversible. Two
serialize() calls on the same tree return byte-identical output —
which is exactly what makes file-based diffing reliable.
The format is also diff-friendly: a value change touches one
line, an added key adds one line, a removed key removes one line.
diff -u baseline.snap current.snap produces a tight unified diff
that a human can scan in seconds.
First run — caller doesn't have a snapshot yet:
- Build the data tree.
- Call
save^STDSNAP(path, .data)to write the baseline. - Commit the snapshot file alongside the test source.
Subsequent runs — caller has a baseline:
- Build the data tree (same producer, presumably the same shape).
- Call
matches^STDSNAP(path, .data)(orasserts^STDSNAPinside a test) to compare. - On mismatch: a human runs
diff -u baseline.snap current.snapto see what changed. If the change is intentional (the report shape legitimately moved), re-saveto update the baseline. If not, fix the producer.
There is no auto-update mode in v1 — callers must explicitly
re-save to refresh the baseline. This is a feature: forces a
human review before snapshot drift becomes invisible.
- Empty tree serialises to
"".savewrites a zero-byte file (well, almost: STDFS appends a trailing LF per its POSIX convention; readFile strips it; round-trip is clean). - Missing baseline file →
matches()returns0. The first run on a fresh checkout fails until you callsave()to seed the snapshot. This is intentional — silently passing the first run would leave snapshots accidentally absent. - Single root scalar (
set data="value"with no subscripts) is not currently serialised —$QUERYwalks only descendants. Callers that need root-scalar snapshots should pre-wrap in a one-key tree (data("value")=originalValue). Documented as a v0.x.y enhancement under T21. - Numeric vs string distinction. A value of
0and a value of"0"both serialise to0— M's canonical numeric form doesn't distinguish them in the dump. If you need byte-faithful type round-trip, prefer JSON encoding via STDJSON (which hass:/n:/t/f/ztype tags). - Numeric subscripts with leading zeros. M canonicalises
data("01")todata(1)if "01" parses as a canonical number? No — strings with leading zeros are not canonical numeric.$$isNumeric("01")returns 0 (since+"01"=1, not equal to"01"). Sodata("01","x")emits("01","x")="..."with the subscript quoted as a string — preserves the input.
Pure-M throughout: $QUERY, $ORDER, $DATA, $EXTRACT,
$LENGTH, $PIECE, $SELECT. ANSI-standard, no $Z*
extensions. Runs unchanged on YDB and IRIS. The test suite
(14 labels, 23 assertions) is the conformance gate.
STDASSERT— STDSNAP'sassertsintegrates with STDASSERT's pass/fail counters and recordPass/recordFail output protocol. Pair them inside any*TST.msuite.STDFS— STDSNAP'ssaveandmatchesusewriteFileandreadFilefor the I/O. STDSNAP files are plain text — a dropped LF onwriteFileand stripped LF onreadFilegive clean round-trip behaviour.STDJSON— alternative encoding for cases where byte-faithful type distinction matters (numeric0vs string"0"). STDSNAP's text format trades a small amount of fidelity for human-readable dumps.
Snapshot testing: serialize / save / matches / asserts; canonical
line-per-leaf dump via $QUERY walk. Companion track C7 added
--update-snapshots support via ^STDLIB($JOB,"stdsnap","update")=1
global-flag mechanism (m-stdlib 631b4e7, m-cli 8ef34a6) —
functionally equivalent to the originally-proposed STDSNAP_UPDATE
env var, with the global preferred for in-process control without a
shell round-trip.
T21 (root-scalar serialization, bundled diff helper) closed 2026-05-07.
Item 2 (auto-update flag) was delivered via C7's global mechanism;
items 1 and 3 closed as won't-fix-without-consumer-driver. v1 walks
$QUERY descendants only (a tree with a scalar at the root doesn't
serialise — covers the practical 80% case of parsed-JSON / FileMan
trees with subscripted leaves), and v1 reports the snapshot path on
mismatch (humans run diff -u baseline current themselves).