aboutsummaryrefslogtreecommitdiff
path: root/tests/unit
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/unit
downloadtrbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.gz
trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.bz2
trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.zip
Inital commit.
Diffstat (limited to 'tests/unit')
-rw-r--r--tests/unit/directives.bats235
-rw-r--r--tests/unit/extraction.bats171
-rw-r--r--tests/unit/template_vars.bats160
3 files changed, 566 insertions, 0 deletions
diff --git a/tests/unit/directives.bats b/tests/unit/directives.bats
new file mode 100644
index 0000000..3a57951
--- /dev/null
+++ b/tests/unit/directives.bats
@@ -0,0 +1,235 @@
+#!/usr/bin/env bats
+# tests/unit/directives.bats
+# Verifies that chunk directive parsing (the awk one-liners used in the chunk
+# loop of trbldoc.sh) correctly reads @name, @exec, @file, and @ref, and that
+# the sed strip pass removes directives from chunk bodies without touching
+# non-directive body lines.
+
+load "../helpers/common.bash"
+
+FIXTURES="$BATS_TEST_DIRNAME/../fixtures"
+
+setup() {
+ setup_workspace
+ CACHE=".trblcache"
+ mkdir -p "$CACHE/chunks"
+}
+
+teardown() {
+ teardown_workspace
+}
+
+# ── helpers used in this file ───────────────────────────────────────────────
+
+# Write a chunk file directly (bypassing extraction) so directive tests
+# are independent of extraction correctness.
+make_chunk() {
+ local path="$1"
+ shift
+ mkdir -p "$(dirname "$path")"
+ printf '%s\n' "$@" > "$path"
+}
+
+# Read @name from a chunk file (mirrors trbldoc.sh logic).
+parse_name() {
+ awk '/@name .+/ {print $2}' "$1"
+}
+
+# Read @exec from a chunk file.
+parse_exec() {
+ awk '/@exec .+/ {$1=""; print}' "$1" | sed 's/^ //'
+}
+
+# Read @file from a chunk file.
+parse_file() {
+ awk '/@file .+/ {print $2}' "$1"
+}
+
+# Read @ref from a chunk file.
+parse_ref() {
+ awk '/@ref .+/ {print $2}' "$1"
+}
+# Parse chunk metadata using the same awk program as trbldoc.sh's
+# parse_chunk_metadata() helper.
+parse_chunk_metadata_record() {
+ awk '
+BEGIN {
+ us = sprintf("%c", 31)
+ from_p = ""
+ namespace_p = ""
+ priority_p = ""
+ program_p = ""
+ ref_p = ""
+}
+/^[ \t]*@file / && from_p == "" {
+ from_p = $0
+ sub(/^[ \t]*@file /, "", from_p)
+}
+/^[ \t]*@name / && namespace_p == "" {
+ namespace_p = $2
+}
+/^[ \t]*@priority / && priority_p == "" {
+ priority_p = $2
+}
+/^[ \t]*@exec / && program_p == "" {
+ program_p = $0
+ sub(/^[ \t]*@exec /, "", program_p)
+}
+/^[ \t]*@ref / && ref_p == "" {
+ ref_p = $2
+}
+END {
+ printf "%s%s%s%s%s%s%s%s%s\n", from_p, us, namespace_p, us, priority_p, us, program_p, us, ref_p
+}
+' "$1"
+}
+
+# Apply the directive strip (mirrors trbldoc.sh line 231).
+strip_directives() {
+ sed -i "s/^@.*//g" "$1"
+}
+
+# ── @name ───────────────────────────────────────────────────────────────────
+
+@test "@name: parsed correctly from chunk" {
+ local chunk="$CACHE/chunks/test_name"
+ make_chunk "$chunk" "@file src.c:1" "@exec md" "@name my/namespace" "body text"
+ local result
+ result="$(parse_name "$chunk")"
+ assert_equal "$result" "my/namespace"
+}
+
+@test "@name: absent when directive is missing" {
+ local chunk="$CACHE/chunks/test_no_name"
+ make_chunk "$chunk" "@file src.c:1" "@exec md" "body text"
+ local result
+ result="$(parse_name "$chunk")"
+ assert_equal "$result" ""
+}
+
+@test "@name: only the first token after @name is captured" {
+ local chunk="$CACHE/chunks/test_name_token"
+ make_chunk "$chunk" "@name some/path extra ignored"
+ local result
+ result="$(parse_name "$chunk")"
+ assert_equal "$result" "some/path"
+}
+
+# ── @exec ───────────────────────────────────────────────────────────────────
+
+@test "@exec: renderer name parsed correctly" {
+ local chunk="$CACHE/chunks/test_exec"
+ make_chunk "$chunk" "@file src.c:1" "@exec md2html --github" "@name ns"
+ local result
+ result="$(parse_exec "$chunk")"
+ assert_equal "$result" "md2html --github"
+}
+
+@test "@exec: absent when directive is missing" {
+ local chunk="$CACHE/chunks/test_no_exec"
+ make_chunk "$chunk" "@file src.c:1" "@name ns" "body"
+ local result
+ result="$(parse_exec "$chunk")"
+ assert_equal "$result" ""
+}
+
+# ── @file ───────────────────────────────────────────────────────────────────
+
+@test "@file: source path parsed correctly" {
+ local chunk="$CACHE/chunks/test_file"
+ make_chunk "$chunk" "@file src/lib.c:42" "@exec md" "@name ns"
+ local result
+ result="$(parse_file "$chunk")"
+ assert_equal "$result" "src/lib.c:42"
+}
+
+# ── @ref ────────────────────────────────────────────────────────────────────
+
+@test "@ref: reference name parsed correctly" {
+ local chunk="$CACHE/chunks/test_ref"
+ make_chunk "$chunk" "@file src.c:1" "@exec md" "@name ns" "@ref my/ref-name"
+ local result
+ result="$(parse_ref "$chunk")"
+ assert_equal "$result" "my/ref-name"
+}
+
+@test "@ref: absent when directive is missing" {
+ local chunk="$CACHE/chunks/test_no_ref"
+ make_chunk "$chunk" "@file src.c:1" "@exec md" "@name ns"
+ local result
+ result="$(parse_ref "$chunk")"
+ assert_equal "$result" ""
+}
+
+@test "metadata parser: directives with leading spaces or tabs are parsed" {
+ local chunk="$CACHE/chunks/test_directive_leading_whitespace"
+ local sep expected result
+ make_chunk \
+ "$chunk" \
+ " @file src/lib.c:42" \
+ $'\t@name docs/indented' \
+ " @priority 99" \
+ $' \t@exec md2html --github' \
+ " @ref refs/anchor"
+ sep="$(printf '\037')"
+ expected="src/lib.c:42${sep}docs/indented${sep}99${sep}md2html --github${sep}refs/anchor"
+ result="$(parse_chunk_metadata_record "$chunk")"
+ assert_equal "$result" "$expected"
+}
+
+# ── directive stripping (sed pass) ──────────────────────────────────────────
+
+@test "strip: @name line is removed from chunk body" {
+ local chunk="$CACHE/chunks/test_strip_name"
+ make_chunk "$chunk" "@file src.c:1" "@name my/ns" "real body content"
+ strip_directives "$chunk"
+ assert_not_contains "$chunk" "^@name"
+}
+
+@test "strip: @exec line is removed from chunk body" {
+ local chunk="$CACHE/chunks/test_strip_exec"
+ make_chunk "$chunk" "@exec md" "real body content"
+ strip_directives "$chunk"
+ assert_not_contains "$chunk" "^@exec"
+}
+
+@test "strip: @file line is removed from chunk body" {
+ local chunk="$CACHE/chunks/test_strip_file"
+ make_chunk "$chunk" "@file src.c:1" "real body content"
+ strip_directives "$chunk"
+ assert_not_contains "$chunk" "^@file"
+}
+
+@test "strip: @ref line is removed from chunk body" {
+ local chunk="$CACHE/chunks/test_strip_ref"
+ make_chunk "$chunk" "@ref some/ref" "real body content"
+ strip_directives "$chunk"
+ assert_not_contains "$chunk" "^@ref"
+}
+
+@test "strip: non-directive body lines are preserved after stripping" {
+ local chunk="$CACHE/chunks/test_strip_body"
+ make_chunk "$chunk" "@name ns" "@exec md" "keep this line" "and this one"
+ strip_directives "$chunk"
+ assert_contains "$chunk" "keep this line"
+ assert_contains "$chunk" "and this one"
+}
+
+@test "strip: directives from all-directives fixture are present before strip" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/directives.c" "$prog" "$CACHE"
+ # Before stripping, @ref should be present
+ chunk_contains "$CACHE" "@ref"
+}
+
+@test "strip: body content from all-directives fixture survives strip" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/directives.c" "$prog" "$CACHE"
+ # Strip all chunks
+ for f in "$CACHE/chunks/"*; do
+ strip_directives "$f"
+ done
+ chunk_contains "$CACHE" "Directive test body content"
+}
diff --git a/tests/unit/extraction.bats b/tests/unit/extraction.bats
new file mode 100644
index 0000000..21c6784
--- /dev/null
+++ b/tests/unit/extraction.bats
@@ -0,0 +1,171 @@
+#!/usr/bin/env bats
+# tests/unit/extraction.bats
+# Verifies that the AWK extraction programs correctly identify doc comment
+# boundaries and produce chunk files for each supported language.
+#
+# Tests operate directly on the AWK programs (via helpers/common.bash) without
+# running the full trbldoc.sh pipeline.
+
+load "../helpers/common.bash"
+
+FIXTURES="$BATS_TEST_DIRNAME/../fixtures"
+
+setup() {
+ setup_workspace
+ CACHE=".trblcache"
+ mkdir -p "$CACHE/chunks"
+}
+
+teardown() {
+ teardown_workspace
+}
+
+# ── C (/* ... */) ──────────────────────────────────────────────────────────
+
+@test "C: single doc comment produces one chunk" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/simple.c" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "1"
+}
+
+@test "C: two doc comments produce two chunks" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/multi_chunk.c" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "2"
+}
+
+@test "C: file with no doc comments produces no chunks" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/no_comments.c" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "0"
+}
+
+@test "C: chunk contains @exec directive for renderer" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/simple.c" "$prog" "$CACHE"
+ chunk_contains "$CACHE" "@exec"
+}
+
+@test "C: chunk contains @file directive with source file name" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/simple.c" "$prog" "$CACHE"
+ chunk_contains "$CACHE" "@file"
+}
+
+@test "C: chunk body lines are captured" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/simple.c" "$prog" "$CACHE"
+ chunk_contains "$CACHE" "Hello from a C comment"
+}
+
+@test "C: non-comment source lines are not captured in chunk" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/simple.c" "$prog" "$CACHE"
+ # 'int main' is code, not doc
+ if grep -rl "int main" "$CACHE/chunks" >/dev/null 2>&1; then
+ echo "FAILED: code line 'int main' was captured in a chunk" >&2
+ return 1
+ fi
+}
+
+@test "C: single-line /* */ comment produces no chunks" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ # no_comments.c contains a single-line /* ... */ comment on the first line
+ run_extraction "$FIXTURES/c/no_comments.c" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "0"
+}
+
+@test "C: two-chunk file produces chunks with distinct content" {
+ local prog
+ prog="$(awk_prog_c "$CACHE")"
+ run_extraction "$FIXTURES/c/multi_chunk.c" "$prog" "$CACHE"
+ chunk_contains "$CACHE" "First chunk content"
+ chunk_contains "$CACHE" "Second chunk content"
+}
+
+# ── Lua (--[[ ... ]]) ──────────────────────────────────────────────────────
+
+@test "Lua: single doc comment produces one chunk" {
+ local prog
+ prog="$(awk_prog_lua "$CACHE")"
+ run_extraction "$FIXTURES/lua/simple.lua" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "1"
+}
+
+@test "Lua: chunk body lines are captured" {
+ local prog
+ prog="$(awk_prog_lua "$CACHE")"
+ run_extraction "$FIXTURES/lua/simple.lua" "$prog" "$CACHE"
+ chunk_contains "$CACHE" "Hello from a Lua comment"
+}
+
+@test "Lua: non-comment source lines are not captured" {
+ local prog
+ prog="$(awk_prog_lua "$CACHE")"
+ run_extraction "$FIXTURES/lua/simple.lua" "$prog" "$CACHE"
+ if grep -rl "local x" "$CACHE/chunks" >/dev/null 2>&1; then
+ echo "FAILED: code line 'local x' was captured in a chunk" >&2
+ return 1
+ fi
+}
+
+@test "Lua: single-line --[[ ]] comment produces no chunks" {
+ local prog
+ prog="$(awk_prog_lua "$CACHE")"
+ run_extraction "$FIXTURES/lua/single_line.lua" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "0"
+}
+
+# ── Python ('''renderer ... ''') ───────────────────────────────────────────
+
+@test "Python: single doc comment produces one chunk" {
+ local prog
+ prog="$(awk_prog_py "$CACHE")"
+ run_extraction "$FIXTURES/py/simple.py" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "1"
+}
+
+@test "Python: chunk body lines are captured" {
+ local prog
+ prog="$(awk_prog_py "$CACHE")"
+ run_extraction "$FIXTURES/py/simple.py" "$prog" "$CACHE"
+ chunk_contains "$CACHE" "Hello from a Python doc comment"
+}
+
+@test "Python: single-line '''content''' on one line produces no chunks" {
+ local prog
+ prog="$(awk_prog_py "$CACHE")"
+ run_extraction "$FIXTURES/py/single_line.py" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "0"
+}
+
+# ── Markdown (whole file = one chunk) ──────────────────────────────────────
+
+@test "Markdown: whole file produces exactly one chunk" {
+ local prog
+ prog="$(awk_prog_md "$CACHE")"
+ run_extraction "$FIXTURES/md/simple.md" "$prog" "$CACHE"
+ assert_equal "$(chunk_count "$CACHE")" "1"
+}
+
+@test "Markdown: chunk contains @exec md directive" {
+ local prog
+ prog="$(awk_prog_md "$CACHE")"
+ run_extraction "$FIXTURES/md/simple.md" "$prog" "$CACHE"
+ chunk_contains "$CACHE" "@exec md"
+}
+
+@test "Markdown: chunk includes file body content" {
+ local prog
+ prog="$(awk_prog_md "$CACHE")"
+ run_extraction "$FIXTURES/md/simple.md" "$prog" "$CACHE"
+ chunk_contains "$CACHE" "Hello from a markdown file"
+}
diff --git a/tests/unit/template_vars.bats b/tests/unit/template_vars.bats
new file mode 100644
index 0000000..5d95667
--- /dev/null
+++ b/tests/unit/template_vars.bats
@@ -0,0 +1,160 @@
+#!/usr/bin/env bats
+# tests/unit/template_vars.bats
+# Unit tests for template variable substitution patterns.
+#
+# These tests replicate the escape_sed_pattern_literal logic from trbldoc.sh
+# and verify that:
+# - the canonical {{ var }} format (two brackets, one space each side) is matched
+# - the old {{varname}} format (two brackets, no spaces) is NOT matched
+# - the layout-specific patterns ({{ title }}, {{ nav }}, {{ yield }},
+# {{ breadcrumb }}) work correctly in sed address and substitution position
+
+load "../helpers/common.bash"
+
+setup() {
+ setup_workspace
+}
+
+teardown() {
+ teardown_workspace
+}
+
+# Replicate escape_sed_pattern_literal from trbldoc.sh.
+escape_pat() {
+ printf '%s' "$1" | sed -e 's/[][\\.^$*+?(){}|]/\\&/g'
+}
+
+# ── chunk template variables ──────────────────────────────────────────────────
+
+@test "template vars: {{ src_repo }} is matched and substituted" {
+ local pat result
+ pat=$(escape_pat '{{ src_repo }}')
+ result=$(printf '%s' 'Link: {{ src_repo }}/file.c' | sed -E "s|$pat|https://example.com|g")
+ assert_equal "$result" 'Link: https://example.com/file.c'
+}
+
+@test "template vars: {{src_repo}} without spaces is NOT matched" {
+ local pat result
+ pat=$(escape_pat '{{ src_repo }}')
+ result=$(printf '%s' 'Old: {{src_repo}}' | sed -E "s|$pat|https://example.com|g")
+ assert_equal "$result" 'Old: {{src_repo}}'
+}
+
+@test "template vars: {{ toc }} is matched and substituted" {
+ local pat result
+ pat=$(escape_pat '{{ toc }}')
+ result=$(printf '%s' 'Nav: {{ toc }}' | sed -E "s|$pat|<ul><li>nav</li></ul>|g")
+ assert_equal "$result" 'Nav: <ul><li>nav</li></ul>'
+}
+
+@test "template vars: {{toc}} without spaces is NOT matched" {
+ local pat result
+ pat=$(escape_pat '{{ toc }}')
+ result=$(printf '%s' 'Old: {{toc}}' | sed -E "s|$pat|<ul><li>nav</li></ul>|g")
+ assert_equal "$result" 'Old: {{toc}}'
+}
+
+@test "template vars: {{ ref_string }} is matched and substituted" {
+ local pat result
+ pat=$(escape_pat '{{ ref_string }}')
+ result=$(printf '%s' "See: {{ ref_string }}." | sed -E "s|$pat|<a href='/x'>x</a>|g")
+ assert_equal "$result" "See: <a href='/x'>x</a>."
+}
+
+@test "template vars: {{ref_string}} without spaces is NOT matched" {
+ local pat result
+ pat=$(escape_pat '{{ ref_string }}')
+ result=$(printf '%s' 'Old: {{ref_string}}' | sed -E "s|$pat|<a href='/x'>x</a>|g")
+ assert_equal "$result" 'Old: {{ref_string}}'
+}
+
+@test "template vars: multiple {{ src_repo }} occurrences are all substituted" {
+ local pat result
+ pat=$(escape_pat '{{ src_repo }}')
+ result=$(printf '%s' '{{ src_repo }}/a and {{ src_repo }}/b' | sed -E "s|$pat|https://r|g")
+ assert_equal "$result" 'https://r/a and https://r/b'
+}
+
+# ── layout template variables ─────────────────────────────────────────────────
+
+@test "template vars: layout {{ title }} is matched and substituted" {
+ local result
+ result=$(printf '%s' '<title>{{ title }}</title>' \
+ | sed -E "s|\\{\\{ title \\}\\}|My Page|g")
+ assert_equal "$result" '<title>My Page</title>'
+}
+
+@test "template vars: layout {{title}} without spaces is NOT matched" {
+ local result
+ result=$(printf '%s' '<title>{{title}}</title>' \
+ | sed -E "s|\\{\\{ title \\}\\}|My Page|g")
+ assert_equal "$result" '<title>{{title}}</title>'
+}
+
+@test "template vars: layout {{ breadcrumb }} is matched and substituted" {
+ local result
+ result=$(printf '%s' '<h1>{{ breadcrumb }}</h1>' \
+ | sed -E "s|\\{\\{ breadcrumb \\}\\}|Section|g")
+ assert_equal "$result" '<h1>Section</h1>'
+}
+
+@test "template vars: layout {{breadcrumb}} without spaces is NOT matched" {
+ local result
+ result=$(printf '%s' '{{breadcrumb}}' \
+ | sed -E "s|\\{\\{ breadcrumb \\}\\}|Section|g")
+ assert_equal "$result" '{{breadcrumb}}'
+}
+
+@test "template vars: layout {{ nav }} is matched and substituted" {
+ local result
+ result=$(printf '%s' '<ul>{{ nav }}</ul>' \
+ | sed -E "s|\\{\\{ nav \\}\\}|<li>item</li>|g")
+ assert_equal "$result" '<ul><li>item</li></ul>'
+}
+
+@test "template vars: layout old {{> nav}} partial syntax is NOT matched by {{ nav }} pattern" {
+ local result
+ result=$(printf '%s' '<ul>{{> nav}}</ul>' \
+ | sed -E "s|\\{\\{ nav \\}\\}|<li>item</li>|g")
+ assert_equal "$result" '<ul>{{> nav}}</ul>'
+}
+
+@test "template vars: layout {{ yield }} works as sed address to include a file" {
+ local tmpbody tmplayout output
+ tmpbody=$(mktemp)
+ tmplayout=$(mktemp)
+ printf '<p>body content</p>\n' > "$tmpbody"
+ printf '{{ yield }}\n' > "$tmplayout"
+ output=$(sed -E \
+ -e "/\\{\\{ yield \\}\\}/r $tmpbody" \
+ -e "/\\{\\{ yield \\}\\}/d" \
+ "$tmplayout")
+ rm -f "$tmpbody" "$tmplayout"
+ assert_equal "$output" '<p>body content</p>'
+}
+
+@test "template vars: layout old {{{yield}}} triple-brace is NOT matched by {{ yield }} pattern" {
+ local tmpbody tmplayout output
+ tmpbody=$(mktemp)
+ tmplayout=$(mktemp)
+ printf '<p>body content</p>\n' > "$tmpbody"
+ printf '{{{yield}}}\n' > "$tmplayout"
+ output=$(sed -E \
+ -e "/\\{\\{ yield \\}\\}/r $tmpbody" \
+ -e "/\\{\\{ yield \\}\\}/d" \
+ "$tmplayout")
+ rm -f "$tmpbody" "$tmplayout"
+ # {{{yield}}} should remain unchanged — the new pattern only matches {{ yield }}
+ assert_equal "$output" '{{{yield}}}'
+}
+
+@test "template vars: substitution value containing special sed chars is safe" {
+ # Verify that replacement values with & and \ don't corrupt the output
+ # (this exercises the escape_sed_replacement_literal path)
+ local pat esc_repl result
+ pat=$(escape_pat '{{ src_repo }}')
+ # Simulate escape_sed_replacement_literal for a URL containing &
+ esc_repl=$(printf '%s' 'https://example.com/a&b' | sed -e 's/[\\&|]/\\&/g')
+ result=$(printf '%s' '{{ src_repo }}/path' | sed -E "s|$pat|$esc_repl|g")
+ assert_equal "$result" 'https://example.com/a&b/path'
+}