aboutsummaryrefslogtreecommitdiff
path: root/tests/helpers
diff options
context:
space:
mode:
authorAlexander M Pickering <alex@cogarr.net>2026-07-21 20:19:46 -0500
committerAlexander M Pickering <alex@cogarr.net>2026-07-21 20:19:46 -0500
commit657fb0007f39f07cc0401e0c5d03e25df6234aa4 (patch)
treef73fe23232dc80802f39caed738c38464a60ec6d /tests/helpers
downloadtrbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.gz
trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.bz2
trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.zip
Inital commit.
Diffstat (limited to 'tests/helpers')
-rw-r--r--tests/helpers/common.bash263
-rw-r--r--tests/helpers/profiler.bash53
2 files changed, 316 insertions, 0 deletions
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 "<p class=\"stub-md\">$content</p>"
+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 <<EOF
+BEGIN{}
+/\*\// {
+ if(in_comment) {print "EOF"}
+ in_comment=0
+}
+in_comment {print}
+/\/\* .+/ && !/\*\// {
+ print "chunkfile=\$(mktemp -p ${cache}/chunks)"
+ print "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 <<EOF
+BEGIN{}
+/\]\]/ {
+ if(in_comment) {print "EOF"}
+ in_comment=0
+}
+in_comment {print}
+/--\[\[ .+/ && !/\]\]$/ {
+ print "chunkfile=\$(mktemp -p ${cache}/chunks)"
+ print "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 <<EOF
+BEGIN{
+ print "chunkfile=\$(mktemp -p ${cache}/chunks)"
+ print "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 '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>fragment</title></head><body>'
+ cat "$file"
+ echo '</body></html>'
+ } > "$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}<&-
+}