run: testmarktest_rule_basic, testmarktest_provide, and testmarktest_run, a table runner for fixture tests.
commit e85d9b9d379e50c8fe0fc8e8b521e73f34034f44 · parent 423732f80f
· browse this tree
Ximianth, 2026-10-24 14:37:29 UTC
A test file says which hunks are cases and which function exercises them, then runs its docs:
exercise_todos() { ./todos.sh /dev/stdin; }
testmarktest_rule_basic '*/input' exercise_todos
testmarktest_run todos.md
Each case runs in a fresh bash, under set -euo pipefail, in the script's directory, with the trigger hunk's body on stdin.
What it prints, stdout and stderr together, is checked against the case's `output` hunk,
with a line `exit status: N` added if it exits nonzero, and a marker if it lacks a final newline:
so a fixture for a tool that works is just its output, and a failing tool can't pass.
The run reports a line per case, writes each doc's regen changes, and fails if any case failed,
or any rule or doc matched nothing, which is how a typo'd glob would otherwise pass by running no cases.
A fresh process per case is what makes set -e dependable in case functions, whatever the caller's shell is doing;
it costs about half a millisecond a case over a subshell.
The case gets only its function and those given to testmarktest_provide, sent as `declare -f`; never the library.
The script is fed through a process substitution rather than -c:
bash's own errors in a case then read the same on 4.4 through 5.3 ("/dev/fd/63: line N: ..."), and land in fixtures that way.
diff --git a/lib/testmark.bash b/lib/testmark.bash index 050a507..9ed18c8 100644 --- a/lib/testmark.bash +++ b/lib/testmark.bash @@ -18,6 +18,8 @@ source "${BASH_SOURCE[0]%/*}/testmark/document.bash" source "${BASH_SOURCE[0]%/*}/testmark/patch.bash" # shellcheck source=lib/testmark/check.bash source "${BASH_SOURCE[0]%/*}/testmark/check.bash" +# shellcheck source=lib/testmark/run.bash +source "${BASH_SOURCE[0]%/*}/testmark/run.bash" ## Runs an implementation function in a known environment, whatever the caller's settings. ## @@ -34,6 +36,9 @@ _testmark_call() { return "$_testmark_st" } +# The outermost entry is the script bash is running, unless there's none: then it's this file, alone. +((${#BASH_SOURCE[@]} < 2)) || _testmark_call _testmarktest_init "${BASH_SOURCE[-1]}" + ## Parses FILE and makes it available under HANDLE. ## Parsing again under the same handle replaces the earlier document; if that parse fails, the handle is left unusable. ## @@ -83,3 +88,38 @@ testmark_write() { _testmark_call _testmark_write "$@"; } ## - @return 0 if it matches, or if TESTMARK_REGEN staged it; 1 on a mismatch, or any error. ## - @stderr on a mismatch, a line saying so, then a `diff -u` of the expected and actual output. testmark_check() { _testmark_call _testmark_check "$@"; } + +# The high road: a test file makes rules, then runs them over its docs. +# +# exercise_todos() { ./todos.sh /dev/stdin; } +# testmarktest_rule_basic '*/input' exercise_todos +# testmarktest_run todos.md +# +# Rules and provided functions accumulate for the rest of the script. + +## Makes every hunk whose name matches GLOB a case, to be exercised by FN. +## The case is the hunk's name minus its last `/`-segment; the hunk is its trigger. +## testmarktest_run runs FN for each case in a fresh bash, in the script's directory, with the trigger's body on stdin, +## and checks what FN prints, stdout and stderr together, against the case's `output` hunk, using testmark_check. +## Before the check, if FN's output doesn't end in a newline, it gets one, then a line `\ No newline at end of output`; +## and if FN exits nonzero, a line `exit status: N` is added. +## +## FN runs under `set -euo pipefail`. It sees exported variables, but no other variables, and no functions but itself and those given to testmarktest_provide. +## +## - @param $1 GLOB: a bash pattern, matched against whole hunk names; `*` matches `/` too. +## - @param $2 FN: the name of a defined function. +testmarktest_rule_basic() { _testmark_call _testmarktest_rule_basic "$@"; } + +## Sends more functions into every case, for case functions that call them. +## +## - @param $@ names of defined functions. +testmarktest_provide() { _testmark_call _testmarktest_provide "$@"; } + +## Runs every rule's cases in each DOC, in document order, then writes each DOC's regen changes (see testmark_check). +## A rule that matches no hunk in any DOC, or a DOC with no match for any rule, is a failure. +## +## - @param $@ DOC paths, relative to the directory of the script being run. +## - @stdout one line per case: PASS or FAIL, the doc, the trigger's name, and the function. +## - @stderr each failure's details. +## - @return 0 if every case passed; 1 otherwise. It always runs every case. +testmarktest_run() { _testmark_call _testmarktest_run "$@"; } diff --git a/lib/testmark/run.bash b/lib/testmark/run.bash new file mode 100644 index 0000000..39be5af --- /dev/null +++ b/lib/testmark/run.bash @@ -0,0 +1,154 @@ +## run.bash: the high road -- rules that make hunks into cases, and a runner that exercises each case and checks its output. +## Sourced by testmark.bash; the functions here assume they run inside `_testmark_call`. + +# Rules, in the order made, as parallel arrays: rule i is glob i and function i. +_testmarktest_globs=() +_testmarktest_fns=() +# Functions sent into every case, besides the rule's own. +_testmarktest_provided=() +# The directory of the script being run, absolute; empty if bash isn't running a script file. +# Set when the library is sourced, so a later `cd` can't move it. +_testmarktest_dir= + +## Sets _testmarktest_dir to the directory of script $1, if it's a file. +_testmarktest_init() { + local top=$1 + [[ -f $top ]] || return 0 + [[ $top == */* ]] || top=./$top + _testmarktest_dir=${top%/*} + _testmarktest_dir=${_testmarktest_dir:-/} + [[ $_testmarktest_dir == /* ]] || _testmarktest_dir=$PWD/$_testmarktest_dir +} + +## Fails with a message unless $2 is a defined function; $1 is the public function asking, for the message. +_testmarktest_need_fn() { + declare -F -- "$2" >/dev/null || { + _testmark_err "$1: \"$2\" is not a defined function" + return 1 + } +} + +_testmarktest_rule_basic() { + (($# == 2)) || { + _testmark_err "usage: testmarktest_rule_basic GLOB FN" + return 1 + } + [[ -n $1 ]] || { + _testmark_err "testmarktest_rule_basic: the glob is empty" + return 1 + } + _testmarktest_need_fn testmarktest_rule_basic "$2" || return 1 + _testmarktest_globs+=("$1") + _testmarktest_fns+=("$2") +} + +_testmarktest_provide() { + (($# > 0)) || { + _testmark_err "usage: testmarktest_provide FN..." + return 1 + } + local fn + for fn; do _testmarktest_need_fn testmarktest_provide "$fn" || return 1; done + _testmarktest_provided+=("$@") +} + +_testmarktest_run() { + (($# > 0)) || { + _testmark_err "usage: testmarktest_run DOC..." + return 1 + } + ((${#_testmarktest_fns[@]} > 0)) || { + _testmark_err "testmarktest_run: no rules; make them with testmarktest_rule_basic first" + return 1 + } + [[ -n $_testmarktest_dir ]] || { + _testmark_err "testmarktest_run: no script file to find the docs beside; run it from a script" + return 1 + } + local -a matched=() + local i doc failed=0 + for i in "${!_testmarktest_fns[@]}"; do matched[i]=0; done + for doc; do _testmarktest_doc "$doc" || failed=1; done + for i in "${!_testmarktest_fns[@]}"; do + ((matched[i] > 0)) || { + _testmark_err "rule \"${_testmarktest_globs[i]}\" ${_testmarktest_fns[i]}: matched no hunk" + failed=1 + } + done + return "$failed" +} + +## Runs every case in doc $1, reports each, and writes the doc's regen patches. +## Counts each rule's triggers in the caller's `matched`. +_testmarktest_doc() { + # The handle shows in the check layer's messages, so it's named for who's speaking. + local doc=$1 path=$1 h=testmarktest_run + [[ $path == /* ]] || path=$_testmarktest_dir/$path + _testmark_parse "$h" "$path" || { + printf 'FAIL %s\n' "$doc" + return 1 + } + local -a names + local name i failed=0 any= + mapfile -t names < <(_testmark_names "$h") + for name in "${names[@]}"; do + for i in "${!_testmarktest_globs[@]}"; do + # shellcheck disable=SC2053 # the glob is a pattern on purpose + [[ $name == ${_testmarktest_globs[i]} ]] || continue + matched[i]=$((matched[i] + 1)) any=1 + if _testmarktest_case "$h" "$name" "${_testmarktest_fns[i]}"; then + printf 'PASS %s: %s (%s)\n' "$doc" "$name" "${_testmarktest_fns[i]}" + else + printf 'FAIL %s: %s (%s)\n' "$doc" "$name" "${_testmarktest_fns[i]}" + failed=1 + fi + done + done + [[ -n $any ]] || { + _testmark_err "$doc: no hunk matches any rule" + failed=1 + } + _testmark_write "$h" || failed=1 + return "$failed" +} + +## Exercises the case of trigger $2 in document $1 with function $3, and checks its output against the case's `output` hunk. +_testmarktest_case() { + [[ $2 == */* ]] || { + _testmark_err "$1: hunk \"$2\": has no \"/\", so it names no case" + return 1 + } + local out=${2%/*}/output stream st + _testmark_hunk "$1" "$out" || return 1 + # The exit status follows the output after a NUL, which bash can't hold; so a second NUL means the output held one. + { + IFS= read -r -d '' stream + if IFS= read -r -d '' st; then + _testmark_err "$1: hunk \"$2\": output contains a NUL byte" + return 1 + fi + } < <( + _testmarktest_exec "$1" "$2" "$3" + printf '\0%s' "$?" + ) + [[ -z $stream || $stream == *$'\n' ]] || stream+=$'\n\\ No newline at end of output\n' + ((st == 0)) || stream+="exit status: $st"$'\n' + _testmark_check "$1" "$out" < <(printf '%s' "$stream") +} + +## Runs function $3 in a fresh bash, in the script's directory, with the body of hunk $2 of document $1 on stdin. +## stdout and stderr both go to stdout. Runs in a process substitution, so its `cd` and `unset` change nothing outside. +_testmarktest_exec() { + # _testmark_call's `local LC_ALL=C` would reach the case if the caller exported LC_ALL; unsetting it uncovers the caller's own. + unset -v LC_ALL + cd "$_testmarktest_dir" 2>&1 || return + "$BASH" <(_testmarktest_script "$3") < <(_testmark_body "$1" "$2") 2>&1 +} + +## Prints the script a case runs: strict mode, the functions it may call, and a call to function $1. +## This is the one place a case's script is built. +_testmarktest_script() { + printf 'set -euo pipefail\n' + declare -f -- "$1" "${_testmarktest_provided[@]}" + printf '%q\n' "$1" +} diff --git a/tests/test-run.sh b/tests/test-run.sh new file mode 100755 index 0000000..5d9f38e --- /dev/null +++ b/tests/test-run.sh @@ -0,0 +1,410 @@ +#!/bin/bash +## The high road: testmarktest_rule_basic, testmarktest_provide, and testmarktest_run. +## Each case writes a doc and a test script, then runs the script in a fresh bash, from another directory. +## These assert in plain bash, not through the runner itself, so a broken runner can't pass its own tests. +set -euo pipefail +cd "$(dirname "$0")" +# shellcheck source=tests/lib.sh +source ./lib.sh + +unset TESTMARK_REGEN +mkdir t + +## Writes t/doc.md and t/test.sh, runs the script from $work, and checks what it printed and its status. +## The script gets a line sourcing the library first. $work in the output shows as WORK. +## +## - @param $1 description +## - @param $2 t/doc.md's content +## - @param $3 the rest of t/test.sh +## - @param $4 the expected stdout, then a line `--- stderr`, then the expected stderr, then `--- status N` +run_case() { + printf '%s' "$2" >t/doc.md + printf 'source %q\n%s\n' "$ROOT/lib/testmark.bash" "$3" >t/test.sh + local rc=0 got + "$BASH" t/test.sh >"$work/out" 2>"$work/err" </dev/null || rc=$? + got="$(<"$work/out")"$'\n'"--- stderr"$'\n'"$(<"$work/err")"$'\n'"--- status $rc" + got=${got//$work/WORK} + expect_eq "$1" "$got" "$4" +} + +## Checks t/doc.md's bytes, as `bytes` quotes them. +## +## - @param $1 description +## - @param $2 the expected document, as a printf format +doc_is() { + expect_eq "$1" "$(bytes t/doc.md)" "$(esc "$(printf -- "$2" && printf x)" | sed 's/x"$/"/')" +} + +## Prints a doc with one case, `a`: `a/in` holds "hello", and `a/output` holds $1, which must be empty or end in a newline. +one() { + printf '[testmark]:# (a/in)\n```\nhello\n```\n\n[testmark]:# (a/output)\n```\n%s```\n' "$1" +} + +# --- passing, failing, and what the output stream holds --- + +run_case "a match passes" "$(one $'hello\n')" ' +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_cat) +--- stderr + +--- status 0" + +run_case "a mismatch fails, with a diff" "$(one $'bye\n')" ' +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run doc.md' "FAIL doc.md: a/in (exercise_cat) +--- stderr +testmark: testmarktest_run: hunk \"a/output\": output doesn't match; set TESTMARK_REGEN=1 to accept it +--- WORK/t/doc.md: a/output (expected) ++++ WORK/t/doc.md: a/output (actual) +@@ -1 +1 @@ +-bye ++hello +--- status 1" + +run_case "stderr is in the stream, in order" "$(one $'out\nerr\nout\n')" ' +exercise_both() { echo out; echo err >&2; echo out; } +testmarktest_rule_basic "*/in" exercise_both +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_both) +--- stderr + +--- status 0" + +run_case "a nonzero exit is a line of output" "$(one $'hello\nexit status: 3\n')" ' +exercise_fail() { cat; return 3; } +testmarktest_rule_basic "*/in" exercise_fail +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_fail) +--- stderr + +--- status 0" + +run_case "no final newline gets a marker" "$(one $'x\n\\ No newline at end of output\n')" ' +exercise_x() { printf x; } +testmarktest_rule_basic "*/in" exercise_x +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_x) +--- stderr + +--- status 0" + +run_case "the marker, then the exit status" "$(one $'x\n\\ No newline at end of output\nexit status: 1\n')" ' +exercise_x() { printf x; false; } +testmarktest_rule_basic "*/in" exercise_x +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_x) +--- stderr + +--- status 0" + +run_case "empty output matches an empty hunk" "$(one '')" ' +exercise_quiet() { :; } +testmarktest_rule_basic "*/in" exercise_quiet +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_quiet) +--- stderr + +--- status 0" + +run_case "CRLF fails the case" "$(one '')" ' +exercise_crlf() { printf "a\r\n"; } +testmarktest_rule_basic "*/in" exercise_crlf +testmarktest_run doc.md' "FAIL doc.md: a/in (exercise_crlf) +--- stderr +testmark: testmarktest_run: hunk \"a/output\": output must be empty or end in a newline, and contain no CRLF; normalize it before checking +--- status 1" + +run_case "a NUL byte fails the case" "$(one '')" ' +exercise_nul() { printf "a\0b\n"; } +testmarktest_rule_basic "*/in" exercise_nul +testmarktest_run doc.md' "FAIL doc.md: a/in (exercise_nul) +--- stderr +testmark: testmarktest_run: hunk \"a/in\": output contains a NUL byte +--- status 1" + +# --- the case's process --- + +run_case "stdin is the trigger's body, exactly" $'[testmark]:# (a/in)\n```\n two\\\n\n```\n\n[testmark]:# (a/output)\n```\n two\\\n\n|\n```\n\n[testmark]:# (b/in)\n```\n```\n\n[testmark]:# (b/output)\n```\n|\n```\n' ' +exercise_bar() { cat; echo "|"; } +testmarktest_rule_basic "*/in" exercise_bar +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_bar) +PASS doc.md: b/in (exercise_bar) +--- stderr + +--- status 0" + +run_case "set -e works in a case, even with the run under if and ||" "$(one $'exit status: 1\n')" ' +exercise_e() { false; echo unreached; } +testmarktest_rule_basic "*/in" exercise_e +if testmarktest_run doc.md; then echo ran; fi +testmarktest_run doc.md || echo failed' "PASS doc.md: a/in (exercise_e) +ran +PASS doc.md: a/in (exercise_e) +--- stderr + +--- status 0" + +run_case "pipefail is on in a case" "$(one $'exit status: 1\n')" ' +exercise_p() { false | true; echo unreached; } +testmarktest_rule_basic "*/in" exercise_p +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_p) +--- stderr + +--- status 0" + +run_case "nounset is on in a case" "$(one $'/dev/fd/63: line 4: nope: unbound variable\nexit status: 1\n')" ' +exercise_u() { echo "$nope"; } +testmarktest_rule_basic "*/in" exercise_u +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_u) +--- stderr + +--- status 0" + +run_case "the caller's set -e doesn't stop a run partway" '[testmark]:# (a/in) +``` +``` + +[testmark]:# (a/output) +``` +wrong +``` + +[testmark]:# (b/in) +``` +``` + +[testmark]:# (b/output) +``` +```' ' +set -euo pipefail +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run doc.md +echo unreached' "FAIL doc.md: a/in (exercise_cat) +PASS doc.md: b/in (exercise_cat) +--- stderr +testmark: testmarktest_run: hunk \"a/output\": output doesn't match; set TESTMARK_REGEN=1 to accept it +--- WORK/t/doc.md: a/output (expected) ++++ WORK/t/doc.md: a/output (actual) +@@ -1 +0,0 @@ +-wrong +--- status 1" + +run_case "the case runs in the script's dir; docs are found there" "$(one "$work/t"$'\n')" ' +exercise_pwd() { pwd; } +testmarktest_rule_basic "*/in" exercise_pwd +cd / +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_pwd) +--- stderr + +--- status 0" + +run_case "exported variables cross; others don't" "$(one $'yes\nunset\n')" ' +export EXPORTED=yes +NOT_EXPORTED=no +exercise_env() { echo "$EXPORTED"; echo "${NOT_EXPORTED-unset}"; } +testmarktest_rule_basic "*/in" exercise_env +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_env) +--- stderr + +--- status 0" + +run_case "the caller's exported LC_ALL crosses, not the library's" "$(one $'C.UTF-8\n')" ' +export LC_ALL=C.UTF-8 +exercise_lc() { echo "$LC_ALL"; } +testmarktest_rule_basic "*/in" exercise_lc +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_lc) +--- stderr + +--- status 0" + +run_case "an unset LC_ALL stays unset in the case" "$(one $'unset\n')" ' +unset LC_ALL +exercise_lc() { echo "${LC_ALL-unset}"; } +testmarktest_rule_basic "*/in" exercise_lc +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_lc) +--- stderr + +--- status 0" + +run_case "other functions don't cross, the library included" "$(one $'/dev/fd/63: line 5: helper: command not found\nexit status: 127\n')" ' +helper() { echo helped; } +exercise_h() { declare -F | grep testmark || :; helper; } +testmarktest_rule_basic "*/in" exercise_h +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_h) +--- stderr + +--- status 0" + +run_case "provided functions cross" "$(one $'helped\n')" ' +helper() { echo helped; } +exercise_h() { helper; } +testmarktest_provide helper +testmarktest_rule_basic "*/in" exercise_h +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_h) +--- stderr + +--- status 0" + +# --- regen --- + +run_case "regen writes the output" "$(one $'bye\n')" ' +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +TESTMARK_REGEN=1 testmarktest_run doc.md && testmarktest_run doc.md' "PASS doc.md: a/in (exercise_cat) +PASS doc.md: a/in (exercise_cat) +--- stderr + +--- status 0" +expect_eq "regen wrote the doc" "$(<t/doc.md)" "$(one $'hello\n')" + +run_case "regen writes every doc, and only what changed" "$(one $'hello\n')" ' +printf "%s\n" "[testmark]:# (b/in)" "\`\`\`" "x" "\`\`\`" "" "[testmark]:# (b/output)" "\`\`\`text" "\`\`\`" >"${BASH_SOURCE%/*}/two.md" +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +TESTMARK_REGEN=1 testmarktest_run doc.md two.md +cat "${BASH_SOURCE%/*}/two.md"' "PASS doc.md: a/in (exercise_cat) +PASS two.md: b/in (exercise_cat) +[testmark]:# (b/in) +\`\`\` +x +\`\`\` + +[testmark]:# (b/output) +\`\`\`text +x +\`\`\` +--- stderr + +--- status 0" +expect_eq "regen left an unchanged doc as it was" "$(<t/doc.md)" "$(one $'hello\n')" + +# --- what makes a run fail, besides a case --- + +run_case "a missing output hunk fails the case" '[testmark]:# (a/in) +``` +```' ' +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run doc.md +TESTMARK_REGEN=1 testmarktest_run doc.md' "FAIL doc.md: a/in (exercise_cat) +FAIL doc.md: a/in (exercise_cat) +--- stderr +testmark: testmarktest_run: no hunk named \"a/output\" +testmark: testmarktest_run: no hunk named \"a/output\" +--- status 1" + +run_case "a trigger without a / fails" '[testmark]:# (in) +``` +```' ' +exercise_cat() { cat; } +testmarktest_rule_basic "*in" exercise_cat +testmarktest_run doc.md' "FAIL doc.md: in (exercise_cat) +--- stderr +testmark: testmarktest_run: hunk \"in\": has no \"/\", so it names no case +--- status 1" + +run_case "a rule matching nothing fails the run, after every case runs" "$(one $'hello\n')" ' +exercise_cat() { cat; } +testmarktest_rule_basic "*/inn" exercise_cat +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run doc.md' "PASS doc.md: a/in (exercise_cat) +--- stderr +testmark: rule \"*/inn\" exercise_cat: matched no hunk +--- status 1" + +run_case "a doc with no match fails the run" "$(one $'hello\n')" ' +printf "%s\n" "[testmark]:# (b/script)" "\`\`\`" "\`\`\`" >"${BASH_SOURCE%/*}/two.md" +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run two.md doc.md' "PASS doc.md: a/in (exercise_cat) +--- stderr +testmark: two.md: no hunk matches any rule +--- status 1" + +run_case "a doc that doesn't parse fails the run; the others still run" "$(one $'hello\n')" ' +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run nope.md doc.md' "FAIL nope.md +PASS doc.md: a/in (exercise_cat) +--- stderr +testmark: WORK/t/nope.md: not a readable file +--- status 1" + +run_case "a run with no rules is an error" "$(one '')" ' +testmarktest_run doc.md' " +--- stderr +testmark: testmarktest_run: no rules; make them with testmarktest_rule_basic first +--- status 1" + +# --- several rules --- + +run_case "rules pick out their own hunks, in document order" '[testmark]:# (one/script) +``` +echo one +``` + +[testmark]:# (one/output) +``` +one +``` + +[testmark]:# (two/in) +``` +two +``` + +[testmark]:# (two/output) +``` +two +``` + +[testmark]:# (three/script) +``` +echo three +``` + +[testmark]:# (three/output) +``` +three +```' ' +exercise_script() { bash; } +exercise_cat() { cat; } +testmarktest_rule_basic "*/script" exercise_script +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run doc.md' "PASS doc.md: one/script (exercise_script) +PASS doc.md: two/in (exercise_cat) +PASS doc.md: three/script (exercise_script) +--- stderr + +--- status 0" + +run_case "a glob's * matches /; the case is all but the last segment" '[testmark]:# (a/b/c/in) +``` +deep +``` + +[testmark]:# (a/b/c/output) +``` +deep +```' ' +exercise_cat() { cat; } +testmarktest_rule_basic "*/in" exercise_cat +testmarktest_run doc.md' "PASS doc.md: a/b/c/in (exercise_cat) +--- stderr + +--- status 0" + +# --- arguments --- + +defined() { :; } +expect_fail "rule: no arguments" 'testmark: usage: testmarktest_rule_basic GLOB FN' testmarktest_rule_basic +expect_fail "rule: one argument" 'testmark: usage: testmarktest_rule_basic GLOB FN' testmarktest_rule_basic '*/in' +expect_fail "rule: three arguments" 'testmark: usage: testmarktest_rule_basic GLOB FN' testmarktest_rule_basic '*/in' defined output +expect_fail "rule: an empty glob" 'testmark: testmarktest_rule_basic: the glob is empty' testmarktest_rule_basic '' defined +expect_fail "rule: not a function" 'testmark: testmarktest_rule_basic: "cat" is not a defined function' testmarktest_rule_basic '*/in' cat +expect_fail "provide: no arguments" 'testmark: usage: testmarktest_provide FN...' testmarktest_provide +expect_fail "provide: not a function" 'testmark: testmarktest_provide: "nope" is not a defined function' testmarktest_provide defined nope +expect_fail "run: no arguments" 'testmark: usage: testmarktest_run DOC...' testmarktest_run +expect_eq "refused calls leave no rules" "${#_testmarktest_fns[@]} ${#_testmarktest_provided[@]}" "0 0" +got=$("$BASH" -c 'source "$1"; f() { :; }; testmarktest_rule_basic "*" f; testmarktest_run doc.md' _ "$ROOT/lib/testmark.bash" 2>&1) || got+=" (status $?)" +expect_eq "run: not from a script file" "$got" 'testmark: testmarktest_run: no script file to find the docs beside; run it from a script (status 1)' + +exit "$RESULT"