skim-md-toc.test
#!/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"
test-bin/skim-md-toc.test · 3.6 KiB · d72d74260c