compass

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