From 657fb0007f39f07cc0401e0c5d03e25df6234aa4 Mon Sep 17 00:00:00 2001 From: Alexander M Pickering Date: Tue, 21 Jul 2026 20:19:46 -0500 Subject: Inital commit. --- tests/helpers/common.bash | 263 ++++++++++++++++++++++++++++++++++++++++++++ tests/helpers/profiler.bash | 53 +++++++++ 2 files changed, 316 insertions(+) create mode 100644 tests/helpers/common.bash create mode 100644 tests/helpers/profiler.bash (limited to 'tests/helpers') diff --git a/tests/helpers/common.bash b/tests/helpers/common.bash new file mode 100644 index 0000000..779cc96 --- /dev/null +++ b/tests/helpers/common.bash @@ -0,0 +1,263 @@ +# tests/helpers/common.bash +# Sourced by every bats test file via `load`. +# Provides: workspace isolation, PATH stubs, assertion helpers. + +# ── workspace setup/teardown ──────────────────────────────────────────────── + +# Call in setup(): creates an isolated temp directory and cds into it. +# A stub for md2html is pre-installed in a fake bin/ on PATH. +setup_workspace() { + TEST_TMPDIR="$(mktemp -d)" + # trap 'rm -rf $TEST_TMPDIR' EXIT + # Fake bin injected before the real PATH so stubs take priority + STUB_BIN="$TEST_TMPDIR/bin" + mkdir -p "$STUB_BIN" + install_stub_md2html "$STUB_BIN" + ORIG_DIR="$(pwd)" + export PATH="$STUB_BIN:$PATH" + # Work inside the temp dir so .trblcache is isolated + pushd "$TEST_TMPDIR" >/dev/null +} + +# Call in teardown(): removes the temp directory. +teardown_workspace() { + popd >/dev/null 2>&1 || true + rm -rf "$TEST_TMPDIR" +} + +# ── stubs ─────────────────────────────────────────────────────────────────── + +# md2html stub: echoes a predictable HTML marker for easy assertion. +# Accepts any flags; always succeeds. +install_stub_md2html() { + local bin_dir="$1" + cat > "$bin_dir/md2html" <<'EOF' +#!/usr/bin/env bash +# Stub: find the last non-flag argument as the input file, or read stdin. +# Flags (args starting with -) from the md alias are ignored. +input="" +for arg in "$@"; do + [[ "$arg" == -* ]] || input="$arg" +done +if [[ -n "$input" ]]; then + content="$(cat "$input")" +else + content="$(cat)" +fi +echo "

$content

" +EOF + chmod +x "$bin_dir/md2html" +} + + +# ── AWK program builders ───────────────────────────────────────────────────── +# These mirror the comment_form patterns from trbldoc.sh so extraction +# logic can be unit-tested without sourcing the whole script. + +CACHE_DIR=".trblcache" + +# Returns the AWK program string for C-style /* */ comments. +awk_prog_c() { + local cache="$1" + cat < \$chunkfile << \"EOF\"\\n@file " name ":" NR ; + if(\$2) {\$1=""; print "@exec " \$0} + in_comment=1; +} +EOF +} + +# Returns the AWK program string for Lua +awk_prog_lua() { + local cache="$1" + cat < \$chunkfile << \"EOF\"\\n@file " name ":" NR ; + if(\$2) {\$1=""; print "@exec " \$0} + in_comment=1; +} +EOF +} + +# Returns the AWK program string for Python. +# Handles '''renderer format (renderer glued directly to ''', no space required). +awk_prog_py() { + local cache="$1" + sed "s|__PY_CACHE__|${cache}|g" << 'PYEOF' +BEGIN{} +/'''/ { + if(in_comment) {print "EOF"} + in_comment=0 +} +in_comment {print} +/'''.+/ && !/'''$/ { + print "chunkfile=$(mktemp -p __PY_CACHE__/chunks)" + print "cat > $chunkfile << \"EOF\"\n@file " name ":" NR ; + renderer = $0 + sub(/'''/, "", renderer) + sub(/^[[:space:]]+/, "", renderer) + sub(/[[:space:]]+$/, "", renderer) + if (renderer != "") {print "@exec " renderer} + in_comment=1; +} +PYEOF +} + +# Returns the AWK program string for .md files +awk_prog_md() { + local cache="$1" + cat < \$chunkfile << \"EOF\"\\n@file " name ":1\\n@exec md" ; +} +{print \$0} +END{print "EOF"} +EOF +} + +# ── extraction helper ──────────────────────────────────────────────────────── + +# run_extraction FILE AWK_PROG CACHE_DIR +# Runs AWK against FILE, evals the output shell script, and populates CACHE_DIR/chunks. +run_extraction() { + local file="$1" + local awk_prog="$2" + local cache="$3" + mkdir -p "$cache/chunks" + local chunk_script + chunk_script=$(awk -v name="$file" "$awk_prog" < "$file") + eval "$chunk_script" +} + +# chunk_count CACHE_DIR +# Prints the number of chunk files produced. +chunk_count() { + find "$1/chunks" -type f 2>/dev/null | wc -l | tr -d ' ' +} + +# chunk_contains CACHE_DIR PATTERN +# Succeeds if any chunk file contains a line matching PATTERN (grep -q). +chunk_contains() { + grep -rl "$2" "$1/chunks" >/dev/null 2>&1 +} + +# ── assertion helpers ──────────────────────────────────────────────────────── + +# assert_file_exists PATH +assert_file_exists() { + if [[ ! -f "$1" ]]; then + echo "ASSERTION FAILED: expected file to exist: $1" >&2 + return 1 + fi +} + +# assert_dir_exists PATH +assert_dir_exists() { + if [[ ! -d "$1" ]]; then + echo "ASSERTION FAILED: expected directory to exist: $1" >&2 + return 1 + fi +} + +# assert_contains FILE PATTERN +assert_contains() { + if ! grep -q "$2" "$1" 2>/dev/null; then + echo "ASSERTION FAILED: '$2' not found in $1" >&2 + echo "--- file contents ---" >&2 + cat "$1" >&2 + return 1 + fi +} + +# assert_not_contains FILE PATTERN +assert_not_contains() { + if grep -q "$2" "$1" 2>/dev/null; then + echo "ASSERTION FAILED: '$2' unexpectedly found in $1" >&2 + return 1 + fi +} + +# assert_equal ACTUAL EXPECTED +assert_equal() { + if [[ "$1" != "$2" ]]; then + echo "ASSERTION FAILED: expected '$2', got '$1'" >&2 + return 1 + fi +} + +# ── HTML validation helpers ────────────────────────────────────────────────── + +# assert_valid_html FILE +# Validates FILE as a complete HTML5 document using tidy. +# Mustache template markers ({{...}}, {{{...}}}) are substituted with +# structurally valid HTML before linting to avoid false positives from +# template syntax that tidy cannot parse. +# Skips automatically if tidy is not installed. +# tidy exit codes: 0 = clean, 1 = warnings only, 2 = errors. +# This helper fails only on exit code 2 (actual errors). +assert_valid_html() { + local file="$1" + if ! command -v tidy >/dev/null 2>&1; then + skip "tidy not installed (apt install tidy)" + fi + local tmp + tmp=$(mktemp --suffix=.html) + # Replace {{ var }} template markers with valid HTML equivalents so tidy + # sees a structurally correct document. + sed \ + -e 's/{{[^}]*}}/placeholder/g' \ + "$file" > "$tmp" + local tidy_out rc + tidy_out=$(tidy -errors -quiet --show-warnings no -utf8 "$tmp" 2>&1) + rc=$? + rm -f "$tmp" + if [ "$rc" -ge 2 ]; then + echo "HTML VALIDATION FAILED: $file" >&2 + echo "$tidy_out" >&2 + return 1 + fi +} + +# assert_valid_html_fragment FILE +# Wraps FILE in a minimal HTML5 document shell, then validates with tidy. +# Use for HTML fragment files (e.g. rendered chunk index.html output) that +# are not complete documents on their own. +# Skips automatically if tidy is not installed. +assert_valid_html_fragment() { + local file="$1" + if ! command -v tidy >/dev/null 2>&1; then + skip "tidy not installed (apt install tidy)" + fi + local tmp + tmp=$(mktemp --suffix=.html) + { + echo 'fragment' + cat "$file" + echo '' + } > "$tmp" + local tidy_out rc + tidy_out=$(tidy -errors -quiet --show-warnings no -utf8 "$tmp" 2>&1) + rc=$? + rm -f "$tmp" + if [ "$rc" -ge 2 ]; then + echo "HTML VALIDATION FAILED: $file" >&2 + echo "$tidy_out" >&2 + return 1 + fi +} diff --git a/tests/helpers/profiler.bash b/tests/helpers/profiler.bash new file mode 100644 index 0000000..c3d5207 --- /dev/null +++ b/tests/helpers/profiler.bash @@ -0,0 +1,53 @@ +# Profiler implementation +profile() { + # Set default options + local file=profiler.log + + # Open a file descriptor for writing to $file and save it in $tracefd + exec {tracefd}>"$file" + # Send trace output to $tracefd + export BASH_XTRACEFD="$tracefd" + # Print microsecond time in trace output + export PS4='+ $EPOCHREALTIME ' + # Enable tracing, run script, and disable tracing + set -x + bash -x -- "$@" + set +x + # Un-redirect the trace output. This also closes the file descriptor. + unset BASH_XTRACEFD + export -n BASH_XTRACEFD PS4 + + # Remove "source" line from output and change last line to only include the + # timestamp with no name. + sed -i -e 1d -e '$s/\(+\+ [0-9\.]\+\) .*$/\1/' "$file" +} + +analyze() { + # Set defaults + local file=profiler.log + local -a sortcmd=(cat) tablecmd=(cat) + + # Declare variables + local timestamp nestlvl cmd next_timestamp next_nestlvl next_cmd duration + # Open file as a file descriptor so we can re-use the same stream + exec {fd}<"$file"; + # Read first line + read -r nestlvl timestamp cmd <&"$fd" + # Process each line + while read -r next_nestlvl next_timestamp next_cmd + do + duration="$(echo "scale=6; $next_timestamp" - "$timestamp" | bc)" + # Prepend leading zero + if [ "${duration:0:1}" = . ] + then + duration="0$duration" + fi + echo "$duration" "$nestlvl" "$cmd" + + timestamp="$next_timestamp" + nestlvl="$next_nestlvl" + cmd="$next_cmd" + done <&"$fd" | "${sortcmd[@]}" | "${tablecmd[@]}" + # Close file descriptor + exec {fd}<&- +} -- cgit v1.2.3-70-g09d2