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"