docs: userguide for going it alone with the primitives; the README points there.
commit f2bd49de52f80d029eb0921c0d2617d95dea32fd · parent 394e07b878
· browse this tree
Ximianth, 2026-10-24 15:42:41 UTC
diff --git a/README.md b/README.md index 28a89af..fbec273 100644 --- a/README.md +++ b/README.md @@ -153,37 +153,9 @@ The doc comments in [`lib/testmark.bash`](lib/testmark.bash) are the full refere Going it alone -------------- -The runner is built on primitives you can call yourself, when you want something it doesn't do: - -| function | does | -| --- | --- | -| `testmark_parse HANDLE FILE` | parses FILE, and names it HANDLE for the other calls | -| `testmark_names HANDLE` | prints the hunk names, one per line, in document order | -| `testmark_body HANDLE NAME` | prints a hunk's body, exactly | -| `testmark_info HANDLE NAME` | prints a hunk's info string (`text` in ```` ```text ````) | -| `testmark_check HANDLE NAME <OUTPUT` | compares OUTPUT to the body: returns 1 and shows a diff on a mismatch; or under `TESTMARK_REGEN`, stages OUTPUT as the new body | -| `testmark_patch HANDLE NAME [INFO] <BODY` | stages a new body, and optionally a new info string | -| `testmark_write HANDLE` | writes what's staged into the file, in place | - -[`examples/test-todos-alone.sh`](examples/test-todos-alone.sh) does what the runner does for `todos.md`, by hand. -With the primitives, these are yours to get right: - -- **`testmark_write` at the end.** That's what makes `TESTMARK_REGEN=1` write anything. - Without it, regen stages everything, then throws it away, without a word. -- **Counting failures.** Run every case, and exit nonzero if any failed. -- **The exit status.** A check doesn't see it. Put it in the output: `< <(mytool || echo "exit: $?")` adds a line only when the tool fails. - -**State changes happen in the shell that parsed.** -`testmark_check`, `testmark_patch`, and `testmark_write` refuse to run in a subshell, where what they stage would be lost. -So feed a check with `testmark_check doc NAME < <(cmd)`, not `cmd | testmark_check doc NAME`; -and don't call these inside `$(...)`, or inside a test helper that runs its command there. - -**Output must be empty, or end in a newline, with no CRLF.** -A hunk's body can't hold anything else, so `testmark_check` rejects it outright, rather than failing forever. -Normalize it first. - -**A check never creates a hunk.** -Regen rewrites the body of a hunk that's there; a missing one is an error in both modes. +The runner is built on primitives you can call yourself: +to read and rewrite testmark docs as plain data, or to check output in a shape the runner doesn't have. +[docs/userguide/going-it-alone.md](docs/userguide/going-it-alone.md) covers them. Requirements diff --git a/docs/userguide/going-it-alone.md b/docs/userguide/going-it-alone.md new file mode 100644 index 0000000..7bbcac5 --- /dev/null +++ b/docs/userguide/going-it-alone.md @@ -0,0 +1,117 @@ +Going it alone: the primitives +============================== + +The runner (`testmarktest_rule_basic` and `testmarktest_run`) is built on a handful of primitives, and you can call them yourself. +Reach for them when the runner doesn't fit: +when you want to read testmark docs as plain data, or rewrite them, or check output in a shape the runner doesn't have. + +They give you more control, but also more to get right. +If the runner fits, use it. + + +The functions +------------- + +Every primitive takes a handle: a name you pick when you parse a doc, and use for that doc in every other call. + +| function | does | +| --- | --- | +| `testmark_parse HANDLE FILE` | parses FILE, and names it HANDLE for the other calls | +| `testmark_names HANDLE` | prints the hunk names, one per line, in document order | +| `testmark_body HANDLE NAME` | prints a hunk's body, exactly | +| `testmark_info HANDLE NAME` | prints a hunk's info string (`text` in ```` ```text ````) | +| `testmark_check HANDLE NAME <OUTPUT` | compares OUTPUT to the body: returns 1 and shows a diff on a mismatch; or under `TESTMARK_REGEN`, stages OUTPUT as the new body | +| `testmark_patch HANDLE NAME [INFO] <BODY` | stages a new body, and optionally a new info string | +| `testmark_write HANDLE` | writes what's staged into the file, in place | + +Errors come back as a nonzero status, with a `testmark: ...` line on stderr. +The doc comments in [`lib/testmark.bash`](../../lib/testmark.bash) are the full reference. + + +Reading and writing docs as data +-------------------------------- + +Read a hunk: + +```bash +testmark_parse doc settings.md +greeting=$(testmark_body doc greeting) +``` + +Replace a hunk's body, then write the file: + +```bash +testmark_patch doc greeting < <(echo "hello again") +testmark_write doc +``` + +Patches are staged until `testmark_write`, which writes them all at once, and then parses the file again. + + +A test loop by hand +------------------- + +[`examples/test-todos-alone.sh`](../../examples/test-todos-alone.sh) does by hand what the runner does for `examples/todos.md`: + +```bash +#!/bin/bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" +source ../lib/testmark.bash + +testmark_parse doc todos.md +mapfile -t names < <(testmark_names doc) +fails=0 +for name in "${names[@]}"; do + case $name in + */input) + case=${name%/input} + if testmark_check doc "$case/output" < <(testmark_body doc "$name" | ./todos.sh /dev/stdin); then + echo "PASS $case" + else + echo "FAIL $case" + fails=$((fails + 1)) + fi + ;; + esac +done +testmark_write doc +((fails == 0)) +``` + +The `case` statement picks out the hunks this test cares about. +It's the shape these loops tend to grow into: a doc often holds several kinds of hunks, and each kind gets its own branch. + +Everything the runner did for you is now yours to get right: + +- **`testmark_write` at the end.** That's what makes `TESTMARK_REGEN=1` write anything. + Without it, regen stages everything, then throws it away, without a word. + With nothing staged, it does nothing, so call it unconditionally. +- **Counting failures.** Run every case, and exit nonzero if any failed. + A bare check under `set -e` stops at the first mismatch, and you only see one. +- **The exit status.** A check doesn't see it. Put it in the output: `< <(mytool || echo "exit: $?")` adds a line only when the tool fails. +- **Clean shells.** The runner runs each case in a fresh bash; here, everything shares one shell, and one case's leftovers can reach the next. + + +Rules to know +------------- + +### state changes happen in the shell that parsed + +`testmark_check`, `testmark_patch`, and `testmark_write` refuse to run in a subshell, where what they stage would be lost. +So feed a check with `testmark_check doc NAME < <(cmd)`, not `cmd | testmark_check doc NAME`; +and don't call these inside `$(...)`, or inside a test helper that runs its command there. + +### end output with a newline, and don't use CRLF + +Hunk bodies are whole lines, and the parser reads CRLF line endings as LF, +so a body always ends in a newline (unless it's empty), and never holds a CRLF. +Output that doesn't fit that could never match, and regen couldn't fix it either: the next parse would read back something different. +So `testmark_check` refuses such output with an error, rather than letting the test fail on every run. +Fix it in the command you're checking, with `| tr -d '\r'`, or by printing the last newline. + +### the hunk must already exist + +Nothing adds hunks. +`testmark_check` and `testmark_patch` need the hunk to be there, in regen too, and `testmark_write` only rewrites the bodies of hunks that are there. +Start with an empty block, and let regen fill it.