testmark-bash

tests: parse rules, one edge case per line.

commit 4469ad6c3958f5556f6773b418d85934eca6f6ca · parent 53a614c00a · browse this tree
Ximianth, 2026-10-23 09:44:23 UTC

Inputs are printf formats, so invisible bytes stay visible.
Most cases come from the conformance corpus gathered while researching this library,
and expect what testmark-rs does, except the ones named "-diverges".
The dump format matches the one the go and rs conformance harnesses print,
so expectations can be compared with theirs directly.
diff --git a/tests/test-parse.sh b/tests/test-parse.sh
new file mode 100755
index 0000000..5790d80
--- /dev/null
+++ b/tests/test-parse.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+## Parse rules, one edge per case: a document (as a printf format) and its expected dump or error.
+## Most cases come from the conformance corpus, and expect what testmark-rs does,
+## except those marked "diverges", where this library deliberately does otherwise.
+set -euo pipefail
+cd "$(dirname "$0")"
+# shellcheck source=tests/lib.sh
+source ./lib.sh
+
+MAL='malformed annotation; want "[testmark]:# (name)"'
+DANGLING='annotation not followed by a code block'
+
+## - @param $1 case name
+## - @param $2 document, as a printf format
+## - @param $3... expected dump lines
+parse_case() {
+	printf -- "$2" >in.md
+	dump_parse in.md >got # not in $(...), so a library bug that kills the shell kills this script too
+	expect_eq "$1" "$(<got)" "$(printf '%s\n' "${@:3}")"
+}
+
+# --- annotation line shape ---
+parse_case ann-basic 'prose\n\n[testmark]:# (x)\n```text\nhello\n```\n' 'hunk "x"' 'info "text"' 'body "hello\n"'
+parse_case ann-trailing-text '[testmark]:# (x reserved words)\n```\nhello\n```\n' 'hunk "x"' 'info ""' 'body "hello\n"'
+parse_case ann-space-before-close '[testmark]:# (x )\n```\nhello\n```\n' 'hunk "x"' 'info ""' 'body "hello\n"'
+parse_case ann-leading-tab-name '[testmark]:# (\tx)\n```\nhello\n```\n' 'hunk "x"' 'info ""' 'body "hello\n"'
+parse_case ann-paren-in-name '[testmark]:# (a(b))\n```\nhello\n```\n' 'hunk "a(b)"' 'info ""' 'body "hello\n"'
+parse_case ann-two-paren-groups '[testmark]:# (x) (y)\n```\nhello\n```\n' 'hunk "x)"' 'info ""' 'body "hello\n"'
+parse_case ann-trailing-space '[testmark]:# (x) \n```\nhello\n```\n' "error: testmark: in.md:1: $MAL"
+parse_case ann-no-space-after-hash '[testmark]:#(x)\n```\nhello\n```\n' "error: testmark: in.md:1: $MAL"
+parse_case ann-double-space '[testmark]:#  (x)\n```\nhello\n```\n' "error: testmark: in.md:1: $MAL"
+parse_case ann-sigil-only 'prose\n[testmark]:# \nmore prose\n' "error: testmark: in.md:2: $MAL"
+parse_case ann-open-paren-only '[testmark]:# (\n```\nhello\n```\n' "error: testmark: in.md:1: $MAL"
+parse_case ann-empty-parens '[testmark]:# ()\n```\nhello\n```\n' "error: testmark: in.md:1: $MAL"
+parse_case ann-space-in-parens '[testmark]:# ( )\n```\nhello\n```\n' "error: testmark: in.md:1: $MAL"
+parse_case ann-indented ' [testmark]:# (x)\n```\nhello\n```\n'
+parse_case ann-uppercase '[Testmark]:# (x)\n```\nhello\n```\n'
+parse_case ann-in-codeblock '```\n[testmark]:# (x)\n```\n'
+parse_case ann-malformed-in-codeblock '```\n[testmark]:# x\n```\n'
+parse_case struct-error-after-hunk '[testmark]:# (a)\n```\none\n```\n\n[testmark]:# b\n' "error: testmark: in.md:6: $MAL"
+
+# --- names: whitespace is ASCII (SP HT VT FF CR) ---
+parse_case name-vt '[testmark]:# (a\x0bb)\n```\nhello\n```\n' 'hunk "a"' 'info ""' 'body "hello\n"'
+parse_case name-lone-cr '[testmark]:# (a\rb)\n```\nhello\n```\n' 'hunk "a"' 'info ""' 'body "hello\n"'
+parse_case name-em-space-diverges '[testmark]:# (a\xe2\x80\x83b)\n```\nhello\n```\n' 'hunk "a\xe2\x80\x83b"' 'info ""' 'body "hello\n"'
+parse_case name-invalid-utf8-diverges '[testmark]:# (a\xffb)\n```\nhello\n```\n' 'hunk "a\xffb"' 'info ""' 'body "hello\n"'
+parse_case name-dup '[testmark]:# (x)\n```\none\n```\n\n[testmark]:# (x)\n```\ntwo\n```\n' 'error: testmark: in.md:6: duplicate hunk name "x"'
+parse_case name-dup-dangling '[testmark]:# (x)\n```\none\n```\n[testmark]:# (x)\nprose\n' 'error: testmark: in.md:5: duplicate hunk name "x"'
+
+# --- what follows an annotation ---
+parse_case ann-last-line-nl 'prose\n[testmark]:# (x)\n' "error: testmark: in.md:2: $DANGLING"
+parse_case ann-last-line-no-nl 'prose\n[testmark]:# (x)' "error: testmark: in.md:2: $DANGLING"
+parse_case ann-blank-then-fence '[testmark]:# (x)\n\n```\nhello\n```\n' "error: testmark: in.md:1: $DANGLING"
+parse_case ann-then-ann '[testmark]:# (x)\n[testmark]:# (y)\n```\nhello\n```\n' "error: testmark: in.md:1: $DANGLING"
+parse_case ann-then-tilde-fence '[testmark]:# (x)\n~~~\nhello\n~~~\n' "error: testmark: in.md:1: $DANGLING"
+parse_case ann-then-table-diverges '[testmark]:# (x)\n| a | b |\n|---|---|\n| 1 | 2 |\n' "error: testmark: in.md:1: $DANGLING"
+
+# --- fences ---
+parse_case fence-four-backticks '[testmark]:# (x)\n````\nhello\n````\n' 'hunk "x"' 'info "`"' 'body "hello\n"'
+parse_case fence-four-backticks-nested '[testmark]:# (x)\n````md\n```inner\n````\n' 'error: testmark: in.md:4: code block not closed'
+parse_case fence-close-trailing-text '[testmark]:# (x)\n```\nhello\n``` trailing\nafter\n' 'hunk "x"' 'info ""' 'body "hello\n"'
+parse_case fence-indented-close '[testmark]:# (x)\n```\nhello\n  ```\nstill body\n```\n' 'hunk "x"' 'info ""' 'body "hello\n  ```\nstill body\n"'
+parse_case fence-midline-backticks '[testmark]:# (x)\n```\nhello ```\n```\n' 'hunk "x"' 'info ""' 'body "hello ```\n"'
+parse_case fence-close-cr-only-eof '[testmark]:# (x)\n```\nhello\n```\r' 'hunk "x"' 'info ""' 'body "hello\n"'
+parse_case fence-plain-unclosed-eof '[testmark]:# (x)\n```\nhello\n```\n\n```\nunclosed plain block\n' 'error: testmark: in.md:6: code block not closed'
+parse_case fence-hunk-unclosed-eof 'prose\n[testmark]:# (x)\n```\nhello\n' 'error: testmark: in.md:3: code block not closed'
+parse_case struct-adjacent-hunks '[testmark]:# (a)\n```\none\n```\n[testmark]:# (b)\n```\ntwo\n```\n' \
+	'hunk "a"' 'info ""' 'body "one\n"' 'hunk "b"' 'info ""' 'body "two\n"'
+
+# --- info strings: after the first three backticks, trimmed of ASCII whitespace ---
+parse_case info-spaces '[testmark]:# (x)\n``` json  \nhello\n```\n' 'hunk "x"' 'info "json"' 'body "hello\n"'
+parse_case info-tab '[testmark]:# (x)\n```\tjson\nhello\n```\n' 'hunk "x"' 'info "json"' 'body "hello\n"'
+parse_case info-words '[testmark]:# (x)\n```text with spaces\nhello\n```\n' 'hunk "x"' 'info "text with spaces"' 'body "hello\n"'
+parse_case info-lone-cr-mid '[testmark]:# (x)\n```a\rb\nhello\n```\n' 'hunk "x"' 'info "a\rb"' 'body "hello\n"'
+parse_case info-trailing-nbsp-diverges '[testmark]:# (x)\n```json\xc2\xa0\nhello\n```\n' 'hunk "x"' 'info "json\xc2\xa0"' 'body "hello\n"'
+
+# --- bodies ---
+parse_case body-empty '[testmark]:# (x)\n```\n```\n' 'hunk "x"' 'info ""' 'body ""'
+parse_case body-one-blank-diverges '[testmark]:# (x)\n```\n\n```\n' 'hunk "x"' 'info ""' 'body "\n"'
+parse_case body-two-blank '[testmark]:# (x)\n```\n\n\n```\n' 'hunk "x"' 'info ""' 'body "\n\n"'
+parse_case body-trailing-spaces '[testmark]:# (x)\n```\na  \n\t\n```\n' 'hunk "x"' 'info ""' 'body "a  \n\t\n"'
+parse_case body-contains-annotation '[testmark]:# (x)\n```\n[testmark]:# (y)\n```\n' 'hunk "x"' 'info ""' 'body "[testmark]:# (y)\n"'
+parse_case body-lone-cr '[testmark]:# (x)\n```\na\rb\n```\n' 'hunk "x"' 'info ""' 'body "a\rb\n"'
+parse_case body-invalid-utf8-diverges '[testmark]:# (x)\n```\na\xffb\n```\n' 'hunk "x"' 'info ""' 'body "a\xffb\n"'
+
+# --- NUL is an error, wherever it is (diverges: bash can't hold it) ---
+parse_case nul-in-body '[testmark]:# (x)\n```\na\000b\n```\n' 'error: testmark: in.md:3: contains a NUL byte'
+parse_case nul-in-prose-first 'a\000b\n[testmark]:# (x\n' 'error: testmark: in.md:1: contains a NUL byte'
+parse_case nul-last-line 'prose\na\000b\n' 'error: testmark: in.md:2: contains a NUL byte'
+parse_case nul-last-line-no-nl 'prose\na\000b' 'error: testmark: in.md:2: contains a NUL byte'
+parse_case nul-alone '\000' 'error: testmark: in.md:1: contains a NUL byte'
+
+# --- line endings and file edges: every CRLF becomes LF first ---
+parse_case eol-crlf-whole-doc 'prose\r\n\r\n[testmark]:# (x)\r\n```\r\na\r\nb\r\n```\r\n' 'hunk "x"' 'info ""' 'body "a\nb\n"'
+parse_case eol-mixed-body '[testmark]:# (x)\n```\na\r\nb\nc\r\n```\n' 'hunk "x"' 'info ""' 'body "a\nb\nc\n"'
+parse_case info-crlf '[testmark]:# (x)\r\n```json\r\nhello\r\n```\r\n' 'hunk "x"' 'info "json"' 'body "hello\n"'
+parse_case body-cr-cr-lf '[testmark]:# (x)\n```\na\r\r\n```\n' 'hunk "x"' 'info ""' 'body "a\r\n"'
+parse_case eol-ann-cr-cr-lf '[testmark]:# (x)\r\r\n```\nhello\n```\n' "error: testmark: in.md:1: $MAL"
+parse_case eol-lone-cr-as-newline '[testmark]:# (x)\r```\rhello\r```\r' "error: testmark: in.md:1: $MAL"
+parse_case eol-bom-first-line '\xef\xbb\xbf[testmark]:# (x)\n```\nhello\n```\n'
+parse_case eol-no-final-newline '[testmark]:# (x)\n```\nhello\n```' 'hunk "x"' 'info ""' 'body "hello\n"'
+parse_case eol-empty-file ''
+parse_case eol-only-newline '\n'
+
+exit "$RESULT"