testmark-bash

test-check.sh

#!/bin/bash
## Checking output against hunks, and regenerating them under TESTMARK_REGEN.
## These assert in plain bash, not through testmark_check itself, so a broken check layer can't pass its own tests.
set -euo pipefail
cd "$(dirname "$0")"
# shellcheck source=tests/lib.sh
source ./lib.sh

DOC='[testmark]:# (a)\n```text\nalpha\n```\n\n[testmark]:# (empty)\n```\n```\n\n[testmark]:# (blank)\n```\n\n```\n\n[testmark]:# (glob)\n```\n*\n```\n'
printf -- "$DOC" >c.md
unset TESTMARK_REGEN

## Runs a command in this shell, with TESTMARK_REGEN set.
env_regen() { TESTMARK_REGEN=1 "$@"; }

## Parses c.md afresh as handle c, checks one output against one hunk, and checks the status.
##
## - @param $1 description
## - @param $2 hunk name
## - @param $3 the output, as a printf format
## - @param $4 the expected status
check_case() {
	local rc=0
	testmark_parse c c.md
	testmark_check c "$2" < <(printf -- "$3") 2>"$work/err" || rc=$?
	expect_eq "$1" "$rc" "$4"
}

# --- normal mode: exact comparison ---
check_case "match" a 'alpha\n' 0
check_case "a wrong expectation fails" a 'omega\n' 1
check_case "only the case differs" a 'ALPHA\n' 1
check_case "one newline more" a 'alpha\n\n' 1
check_case "empty hunk, empty output" empty '' 0
check_case "empty hunk, one newline" empty '\n' 1
check_case "one-blank-line hunk, one newline (diverges)" blank '\n' 0
check_case "one-blank-line hunk, empty output" blank '' 1
check_case "a body of glob characters matches only itself" glob 'x\n' 1
check_case "and itself" glob '*\n' 0

# --- output is normalized as bodies are, before comparing ---
check_case "a missing final newline is added" a 'alpha' 0
check_case "CRLF reads as LF" a 'alpha\r\n' 0
check_case "a lone CR is content" a 'alpha\r' 1
check_case "CR CR LF leaves one CR" a 'alpha\r\r\n' 1
check_case "empty output stays empty" empty '' 0

testmark_parse c c.md
expect_fail "a mismatch shows a diff" "$(
	cat <<-EOF
		testmark: $work/c.md:1: hunk "a": output doesn't match
		--- $work/c.md: a (expected)
		+++ $work/c.md: a (actual)
		@@ -1 +1,2 @@
		-alpha
		+omega
		+two
	EOF
)" testmark_check c a < <(printf 'omega\ntwo\n')
expect_ok "and stages nothing" testmark_write c
expect_eq "so the file is unchanged" "$(bytes c.md)" "$(esc "$(printf -- "$DOC")"$'\n')"

# --- regen mode ---
testmark_parse c c.md
expect_ok "regen: a mismatch succeeds" env_regen testmark_check c a <<<'omega'
expect_ok "regen: a match succeeds" env_regen testmark_check c blank <<<''
expect_ok "regen: and the write" testmark_write c
expect_eq "writes only the mismatch, keeping its info string" "$(bytes c.md)" \
	"$(esc "$(printf -- "${DOC/alpha/omega}")"$'\n')"
expect_ok "and then the same output checks clean" testmark_check c a <<<'omega'
printf -- "$DOC" >c.md

testmark_parse c c.md
expect_ok "regen: output with CRLF and no final newline" env_regen testmark_check c a < <(printf 'omega\r\ntwo')
testmark_write c
expect_eq "is written normalized" "$(bytes c.md)" "$(esc "$(printf -- "${DOC/alpha/omega\\ntwo}")"$'\n')"
printf -- "$DOC" >c.md

testmark_parse c c.md
TESTMARK_REGEN='' expect_ok "regen: TESTMARK_REGEN set but empty is regen too" testmark_check c a <<<'omega'
testmark_write c
expect_eq "and writes" "$(testmark_body c a)" "omega"
printf -- "$DOC" >c.md

# --- both modes ---
FENCE='body has a line starting with ```, which would end its code block'
for mode in normal regen; do
	run=()
	[[ $mode == normal ]] || run=(env_regen)
	testmark_parse c c.md
	expect_fail "$mode: absent hunk" "testmark: $work/c.md: no hunk named \"nope\"" "${run[@]}" testmark_check c nope <<<'x'
	expect_fail "$mode: a NUL byte" "testmark: $work/c.md:1: hunk \"a\": input contains a NUL byte" "${run[@]}" testmark_check c a < <(printf 'a\0b\n')
	expect_fail "$mode: arity" 'testmark: usage: testmark_check HANDLE NAME <ACTUAL' "${run[@]}" testmark_check c <<<'x'
	expect_ok "$mode: none of which staged anything" testmark_write c
	expect_eq "$mode: so the file is unchanged" "$(bytes c.md)" "$(esc "$(printf -- "$DOC")"$'\n')"

	# Checking a hunk twice: the same output is checked again like the first time, other output is an error, whether or not the first check matched.
	for first in alpha omega; do
		testmark_parse c c.md
		"${run[@]}" testmark_check c a <<<"$first" 2>/dev/null || :
		rc=0 want=0
		"${run[@]}" testmark_check c a <<<"$first" 2>/dev/null || rc=$?
		[[ $mode == regen || $first == alpha ]] || want=1
		expect_eq "$mode: first $first, then the same again" "$rc" "$want"
		expect_fail "$mode: first $first, then other output" "testmark: $work/c.md:1: hunk \"a\": checked twice, with different output" "${run[@]}" testmark_check c a <<<'other'
		expect_fail "$mode: first $first, then glob characters" "testmark: $work/c.md:1: hunk \"a\": checked twice, with different output" "${run[@]}" testmark_check c a <<<'*'
	done
done

testmark_parse c c.md
expect_fail "regen: output with a fence line" "testmark: $work/c.md:1: hunk \"a\": $FENCE" env_regen testmark_check c a < <(printf 'x\n```\n')

testmark_parse c c.md
env_regen testmark_check c a <<<'omega'
testmark_write c
expect_fail "a write drops what was checked: other output is a mismatch, not a second check" "$(
	cat <<-EOF
		testmark: $work/c.md:1: hunk "a": output doesn't match
		--- $work/c.md: a (expected)
		+++ $work/c.md: a (actual)
		@@ -1 +1 @@
		-omega
		+other
	EOF
)" testmark_check c a <<<'other'
printf -- "$DOC" >c.md

# --- the subshell guard, in both modes ---
SUBSHELL="testmark: c: called in a subshell (from a pipe, or inside \$(...)), where any change to the document is lost; instead of 'cmd | testmark_check HANDLE NAME', use 'testmark_check HANDLE NAME < <(cmd)'"
check_from_pipe() { printf 'omega\n' | "$@" testmark_check c a; }
check_in_subst() {
	local got
	# shellcheck disable=SC2034 # only the call inside $(...) matters, not its output
	got=$("$@" testmark_check c a <<<'omega')
}
testmark_parse c c.md
expect_fail "normal: from a pipe" "$SUBSHELL" check_from_pipe
expect_fail "normal: inside \$(...)" "$SUBSHELL" check_in_subst
expect_fail "regen: from a pipe" "$SUBSHELL" check_from_pipe env_regen
expect_fail "regen: inside \$(...)" "$SUBSHELL" check_in_subst env_regen
expect_ok "none of which staged anything" testmark_write c
expect_eq "so the file is unchanged" "$(bytes c.md)" "$(esc "$(printf -- "$DOC")"$'\n')"
expect_ok "regen with < <(...)" env_regen testmark_check c a < <(printf 'omega\n')

# --- names with shell syntax: plain data through checking and regen ---
names=('$(>pwned1)' '`>pwned2`' 'a]b' 'x[0]' '@' '*' '$HOME' "'q'" '"q"' 'a\b')
for name in "${names[@]}"; do
	printf '[testmark]:# (%s)\n```\nold\n```\n\n' "$name"
done >syntax.md
testmark_parse h syntax.md
for name in "${names[@]}"; do env_regen testmark_check h "$name" <<<"new $name"; done
# Each name's annotation is on line 5i+1 of syntax.md.
for i in "${!names[@]}"; do
	name=${names[i]}
	expect_fail "checked $name twice" "testmark: $work/syntax.md:$((5 * i + 1)): hunk \"$name\": checked twice, with different output" testmark_check h "$name" <<<'other'
done
testmark_write h
for name in "${names[@]}"; do
	expect_ok "regenerated $name" testmark_check h "$name" <<<"new $name"
done
expect_eq "nothing was executed" "$(echo pwned*)" "pwned*"

exit "$RESULT"

tests/test-check.sh · 7.1 KiB · 90280a0b42