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/unit/directives.bats | 235 ++++++++++++++++++++++++++++++++++++++++++ tests/unit/extraction.bats | 171 ++++++++++++++++++++++++++++++ tests/unit/template_vars.bats | 160 ++++++++++++++++++++++++++++ 3 files changed, 566 insertions(+) create mode 100644 tests/unit/directives.bats create mode 100644 tests/unit/extraction.bats create mode 100644 tests/unit/template_vars.bats (limited to 'tests/unit') 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||g") + assert_equal "$result" 'Nav: ' +} + +@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||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|x|g") + assert_equal "$result" "See: x." +} + +@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|x|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 }}' \ + | sed -E "s|\\{\\{ title \\}\\}|My Page|g") + assert_equal "$result" 'My Page' +} + +@test "template vars: layout {{title}} without spaces is NOT matched" { + local result + result=$(printf '%s' '{{title}}' \ + | sed -E "s|\\{\\{ title \\}\\}|My Page|g") + assert_equal "$result" '{{title}}' +} + +@test "template vars: layout {{ breadcrumb }} is matched and substituted" { + local result + result=$(printf '%s' '

{{ breadcrumb }}

' \ + | sed -E "s|\\{\\{ breadcrumb \\}\\}|Section|g") + assert_equal "$result" '

Section

' +} + +@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' '' \ + | sed -E "s|\\{\\{ nav \\}\\}|
  • item
  • |g") + assert_equal "$result" '' +} + +@test "template vars: layout old {{> nav}} partial syntax is NOT matched by {{ nav }} pattern" { + local result + result=$(printf '%s' '' \ + | sed -E "s|\\{\\{ nav \\}\\}|
  • item
  • |g") + assert_equal "$result" '' +} + +@test "template vars: layout {{ yield }} works as sed address to include a file" { + local tmpbody tmplayout output + tmpbody=$(mktemp) + tmplayout=$(mktemp) + printf '

    body content

    \n' > "$tmpbody" + printf '{{ yield }}\n' > "$tmplayout" + output=$(sed -E \ + -e "/\\{\\{ yield \\}\\}/r $tmpbody" \ + -e "/\\{\\{ yield \\}\\}/d" \ + "$tmplayout") + rm -f "$tmpbody" "$tmplayout" + assert_equal "$output" '

    body content

    ' +} + +@test "template vars: layout old {{{yield}}} triple-brace is NOT matched by {{ yield }} pattern" { + local tmpbody tmplayout output + tmpbody=$(mktemp) + tmplayout=$(mktemp) + printf '

    body content

    \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' +} -- cgit v1.2.3-70-g09d2