ssh git@grit.deadkey.org man

mailmanpat/jenny · 2026-09-06 09:36:46 +08002026-9-6 +8git clone …

FILES / test / lib / assert.bash

NameLast CommitTimestampSize
..
assert.bash 41cf051 h fix(index): restore the original break-marker placement and dimming 26-08-24 22:33 7 KB
#!/usr/bin/env bash
# Minimal assertion + harness library for jenny's test suite.
# Plain bash on purpose: no dev dependency, runs anywhere jenny runs.
TESTS_RUN=0
TESTS_FAILED=0
CURRENT_TEST=""
# ANSI colours, disabled when not a tty or when NO_COLOR is set.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
C_DIM=$'\033[2m'; C_OFF=$'\033[0m'
else
C_RED=""; C_GREEN=""; C_YELLOW=""; C_DIM=""; C_OFF=""
fi
# Root of the repo under test, and the jenny entrypoint.
TEST_LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "$TEST_LIB_DIR/../.." && pwd)
JENNY="${JENNY_BIN:-$REPO_ROOT/bin/jenny}"
export JENNY_SHARE_DIR="${JENNY_SHARE_DIR:-$REPO_ROOT/share/jenny}"
# ---------------------------------------------------------------- test runner
# it <description> -- starts a new test case
it() {
CURRENT_TEST="$1"
TESTS_RUN=$((TESTS_RUN + 1))
}
pass() {
printf ' %sok%s %s\n' "$C_GREEN" "$C_OFF" "$CURRENT_TEST"
}
fail() {
TESTS_FAILED=$((TESTS_FAILED + 1))
printf ' %sFAIL%s %s\n' "$C_RED" "$C_OFF" "$CURRENT_TEST"
local line
while IFS= read -r line; do
printf ' %s%s%s\n' "$C_DIM" "$line" "$C_OFF"
done <<< "$1"
}
skip() {
printf ' %sskip%s %s %s(%s)%s\n' \
"$C_YELLOW" "$C_OFF" "$CURRENT_TEST" "$C_DIM" "$1" "$C_OFF"
}
suite() {
printf '\n%s\n' "$1"
}
# Call at the end of a test file. Exits non-zero if anything failed.
summary() {
printf '\n'
if [ "$TESTS_FAILED" -eq 0 ]; then
printf '%s%d passed%s\n' "$C_GREEN" "$TESTS_RUN" "$C_OFF"
return 0
fi
printf '%s%d of %d failed%s\n' "$C_RED" "$TESTS_FAILED" "$TESTS_RUN" "$C_OFF"
return 1
}
# ----------------------------------------------------------------- assertions
assert_eq() {
# assert_eq <actual> <expected> [label]
if [ "$1" = "$2" ]; then
pass
else
fail "${3:+$3
}expected: $2
actual: $1"
fi
}
assert_contains() {
# assert_contains <haystack> <needle> [label]
case "$1" in
*"$2"*) pass ;;
*) fail "${3:+$3
}expected to contain: $2
actual: $1" ;;
esac
}
assert_not_contains() {
case "$1" in
*"$2"*) fail "${3:+$3
}expected NOT to contain: $2
actual: $1" ;;
*) pass ;;
esac
}
assert_file_contains() {
# assert_file_contains <file> <fixed-string> [label]
if [ ! -f "$1" ]; then
fail "${3:+$3
}no such file: $1"
return
fi
if grep -qF -- "$2" "$1"; then
pass
else
fail "${3:+$3
}$1 does not contain: $2"
fi
}
assert_file_not_contains() {
if [ ! -f "$1" ]; then
fail "${3:+$3
}no such file: $1"
return
fi
if grep -qF -- "$2" "$1"; then
fail "${3:+$3
}$1 unexpectedly contains: $2"
else
pass
fi
}
assert_file_exists() {
if [ -f "$1" ]; then pass; else fail "${2:+$2
}expected file to exist: $1"; fi
}
assert_file_absent() {
if [ ! -e "$1" ]; then pass; else fail "${2:+$2
}expected file to be absent: $1"; fi
}
assert_success() {
# assert_success <status> [output]
if [ "$1" -eq 0 ]; then pass; else fail "expected exit 0, got $1
${2:-}"; fi
}
assert_failure() {
# assert_failure <status> [output]
if [ "$1" -ne 0 ]; then pass; else fail "expected non-zero exit, got 0
${2:-}"; fi
}
# --------------------------------------------------------------- blog fixture
# make_blog <dir> [posts_per_page]
# Builds the standard repro blog from the handoff document.
make_blog() {
local dir=$1 ppp=${2:-3}
rm -rf "$dir"
mkdir -p "$dir"
cat > "$dir/.blogrc" <<EOF
BLOG_HOST="http://localhost:8000"
BLOG_TITLE="Test Blog"
POSTS_PER_PAGE=$ppp
DIST=\$PWD/.dist
EOF
local i
for i in 01 02 03 04 05 06 07; do
printf '# Post %s\n\ntags: alpha\n\nBody %s\n' "$i" "$i" > "$dir/2024-01-$i post-$i.md"
done
printf '# Tricky & <Title> "quoted"\n\nBody\n' > "$dir/2024-02-01 tricky.md"
printf '# Real Title\n\n```\n# not a title\n```\n' > "$dir/2024-02-02 fenced.md"
printf '# MD Test\n\n- item one\n- item two\n\n**bold** and `code`\n' > "$dir/2024-03-01 mdtest.md"
}
# make_blog_min <dir> <count> [posts_per_page]
# A blog with exactly <count> plain posts and nothing tricky.
make_blog_min() {
local dir=$1 count=$2 ppp=${3:-3}
rm -rf "$dir"
mkdir -p "$dir"
cat > "$dir/.blogrc" <<EOF
BLOG_HOST="http://localhost:8000"
BLOG_TITLE="Test Blog"
POSTS_PER_PAGE=$ppp
DIST=\$PWD/.dist
EOF
local i n
for i in $(seq 1 "$count"); do
n=$(printf '%02d' "$i")
printf '# Post %s\n\nBody %s\n' "$n" "$n" > "$dir/2024-01-$n post-$n.md"
done
}
# copy_fixture <name> <dest>
# Copies a committed fixture blog to a scratch directory. Fixtures are
# committed rather than generated so the legacy layouts they carry stay
# byte-identical to what shipped before the record format changed.
copy_fixture() {
local name=$1 dest=$2
rm -rf "$dest"
cp -r "$REPO_ROOT/test/fixtures/$name" "$dest"
}
# run_jenny <dir> [args...]
# Runs jenny in <dir>, capturing stdout+stderr into $output and status into $status.
run_jenny() {
local dir=$1; shift
output=$(cd "$dir" && "$JENNY" "$@" 2>&1)
status=$?
return 0
}
# run_jenny_split <dir> [args...]
# Same, but keeps stderr separate in $stderr_output.
run_jenny_split() {
local dir=$1; shift
local errfile
errfile=$(mktemp)
output=$(cd "$dir" && "$JENNY" "$@" 2>"$errfile")
status=$?
stderr_output=$(cat "$errfile")
rm -f "$errfile"
return 0
}
# ---------------------------------------------------------------- link checker
# check_links <dist_dir>
# Extracts every href/src from every .html file under <dist_dir> and verifies
# that site-relative and document-relative targets resolve to a real file,
# treating <dist_dir> as the document root. Prints one line per broken link.
# Returns 0 when everything resolves.
check_links() {
local dist=$1
local broken=0
local html rel dir link target
while IFS= read -r html; do
rel=${html#"$dist"/}
dir=$(dirname "$rel")
# Pull href="..." and src="..." values.
while IFS= read -r link; do
[ -z "$link" ] && continue
# Skip external, protocol-relative, anchors, and data/mailto URIs.
case "$link" in
http://*|https://*|//*|\#*|data:*|mailto:*|javascript:*) continue ;;
esac
# Strip query string and fragment.
link=${link%%\#*}
link=${link%%\?*}
[ -z "$link" ] && continue
if [ "${link:0:1}" = "/" ]; then
target="$dist$link"
elif [ "$dir" = "." ]; then
target="$dist/$link"
else
target="$dist/$dir/$link"
fi
# A trailing slash (or a bare directory) means index.html.
if [ "${target%/}" != "$target" ] || [ -d "$target" ]; then
target="${target%/}/index.html"
fi
# With URL_STYLE=extensionless jenny emits /post/slug, which no file
# matches on disk -- Cloudflare Pages, Netlify and Vercel resolve it by
# appending .html. Model that here so the check reflects how the output
# is actually served, rather than passing only in the default style.
if [ ! -e "$target" ] && [ -e "$target.html" ]; then
target="$target.html"
fi
if [ ! -e "$target" ]; then
echo "$rel -> $link (resolved: ${target#"$dist"/})"
broken=$((broken + 1))
fi
done < <(grep -oE '(href|src)="[^"]*"' "$html" | sed -E 's/^(href|src)="//; s/"$//')
done < <(find "$dist" -name '*.html' -type f)
[ "$broken" -eq 0 ]
}
# feed_item_count <feed.xml>
feed_item_count() {
grep -c '<item>' "$1" 2>/dev/null || echo 0
}
# index_post_count <index.html> -- number of real post links (excludes break marker)
index_post_count() {
# Borrowed posts carry an extra class (post-link in-next), so the match is
# on the class prefix rather than the exact attribute value.
grep -oE '<li class="post-link[^"]*">' "$1" 2>/dev/null | wc -l | tr -d ' '
}