#!/bin/bash # Tests for `skim-md-toc`. # # Run directly: `./skim-md-toc.test` (exits non-zero on any failure). # # Lives in `test-bin/` rather than `bin/`: everything in `bin/` gets # symlinked out to a wider audience, and tests shouldn't ride along. # # The fixtures are written inline as heredocs rather than kept as separate # files, because the assertions are about *line numbers* -- keeping the input # and its expected ranges adjacent in one place is what makes them reviewable. set -euo pipefail self_dir=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd) subject="$self_dir/../bin/skim-md-toc" tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT fails=0 ## check runs the subject against a fixture and diffs against expected output. ## ## - @param $1 a human label for the case. ## - @param $2 the fixture's markdown content. ## - @param $3 the expected stdout. check() { local label="$1" fixture="$2" want="$3" got file file="$tmpdir/fixture.md" printf '%s' "$fixture" >"$file" got=$("$subject" "$file") if [ "$got" != "$want" ]; then >&2 printf 'FAIL: %s\n' "$label" >&2 diff -u <(printf '%s\n' "$want") <(printf '%s\n' "$got") | sed 's/^/ /' || true fails=$((fails + 1)) fi } # The main fixture, exercising the interactions that make ranges non-obvious: # # - Frontmatter is skipped, so the closing `---` isn't read as a setext rule, # while the line numbering still counts those lines. # - Setext headings report the *text* line as their start, not the underline. # - A `#` inside a fenced block is not a heading and doesn't close a range. # - An `h2`'s range covers its `h3` children (read-the-whole-section). # - A range closes at the next heading of level <= its own, not any level. # - The final section runs to EOF. # # Line numbers are annotated here rather than in the fixture, # since the fixture content has to stay byte-exact. # # 1 --- # 2 tags: [x] # 3 --- # 4 # 5 Top Title # 6 ========= # 7 # 8 intro prose # 9 # 10 ## Section One # 11 # 12 some text # 13 # 14 ### Sub A # 15 # 16 ``` # 17 # not a heading # 18 ``` # 19 # 20 ### Sub B # 21 # 22 text # 23 # 24 Section Two # 25 ----------- # 26 # 27 tail text main_fixture=$( cat <<'EOF' --- tags: [x] --- Top Title ========= intro prose ## Section One some text ### Sub A ``` # not a heading ``` ### Sub B text Section Two ----------- tail text EOF ) main_fixture="$main_fixture"$'\n' check "nesting, setext, fences, frontmatter" "$main_fixture" \ '- `#` Top Title -- L5-27 - `##` Section One -- L10-23 - `###` Sub A -- L14-19 - `###` Sub B -- L20-23 - `##` Section Two -- L24-27' # A lone heading with no body still gets a range: itself to EOF. check "single heading to EOF" '# Only ' \ '- `#` Only -- L1-1' # A deeper heading followed by a shallower one closes at the shallower one. # (Guards the `<=` comparison against being written as `==`.) check "deep then shallow" '### Deep body # Shallow more ' \ ' - `###` Deep -- L1-4 - `#` Shallow -- L5-7' # Heading text containing the same ` -- ` separator we append. # The range is the last dash-pair; the text is passed through verbatim. check "dashes in heading text" '## bwrap -- **recommended** body ' \ ' - `##` bwrap -- **recommended** -- L1-3' # No headings at all: silent, exit 0. printf 'just prose\n\nmore prose\n' >"$tmpdir/plain.md" got=$("$subject" "$tmpdir/plain.md") if [ -n "$got" ]; then >&2 printf 'FAIL: headingless file should be silent, got: %q\n' "$got" fails=$((fails + 1)) fi if [ "$fails" -gt 0 ]; then >&2 printf '\n%d test(s) failed.\n' "$fails" exit 1 fi echo "ok"