diff options
Diffstat (limited to 'tests/integration')
| -rw-r--r-- | tests/integration/cli.bats | 215 | ||||
| -rw-r--r-- | tests/integration/pipeline.bats | 842 |
2 files changed, 1057 insertions, 0 deletions
diff --git a/tests/integration/cli.bats b/tests/integration/cli.bats new file mode 100644 index 0000000..5ffc55c --- /dev/null +++ b/tests/integration/cli.bats @@ -0,0 +1,215 @@ +#!/usr/bin/env bats +# tests/integration/cli.bats +# Tests for trbldoc.sh command-line option parsing. +# +# Each test runs the full script (with md2html stubs) and asserts that +# the correct sources are scanned and outputs produced, based on the options +# passed. + +load "../helpers/common.bash" +load "../helpers/profiler.bash" + +FIXTURES="$BATS_TEST_DIRNAME/../fixtures" +_REPO_ROOT="${REPO_ROOT:-$(cd "$BATS_TEST_DIRNAME/../.." && pwd)}" +TRBLDOC="$_REPO_ROOT/trbldoc.sh" + +setup() { + setup_workspace + CACHE=".trblcache" + NS="$CACHE/namespace" +} + +teardown() { + teardown_workspace +} + +# ── helpers ────────────────────────────────────────────────────────────────── + +run_pipeline() { + bash "$TRBLDOC" "$@" # 2>/dev/null + #profile "$TRBLDOC" "$@" 2>/dev/null + #analyze >> profile.txt +} + +stage_fixture() { + local src="$1" + local dest="$2" + mkdir -p "$(dirname "$dest")" + cp -r "$src" "$dest" +} + +# ── error cases ────────────────────────────────────────────────────────────── + +@test "cli: no arguments exits non-zero with an error message" { + run bash "$TRBLDOC" 2>&1 + [ "$status" -ne 0 ] + [[ "$output" == *"trbldoc:"* ]] +} + +@test "cli: unknown option exits non-zero" { + run bash "$TRBLDOC" -z 2>&1 + [ "$status" -ne 0 ] +} + +@test "cli: option missing its required argument exits non-zero" { + run bash "$TRBLDOC" -s 2>&1 + [ "$status" -ne 0 ] +} + +# ── -s (source folder) ─────────────────────────────────────────────────────── + +@test "cli: -s flag specifies the source folder to scan" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_dir_exists "$NS/test/simple" +} + +@test "cli: multiple -s flags scan all specified folders" { + stage_fixture "$FIXTURES/c/simple.c" "src1/simple.c" + stage_fixture "$FIXTURES/c/multi_chunk.c" "src2/multi_chunk.c" + run_pipeline -s src1 -s src2 + assert_dir_exists "$NS/test/simple" + assert_dir_exists "$NS/test/alpha" +} + +# ── -o (output directory) ──────────────────────────────────────────────────── + +@test "cli: -o flag creates the specified output directory" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src -o my_output + assert_dir_exists "my_output" + assert_file_exists "my_output/test/simple/index.html" +} + +@test "cli: without -o the default output directory .trblcache/built is created" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_dir_exists "$CACHE/built" +} + +# ── -f (custom file extension) ─────────────────────────────────────────────── + +@test "cli: -f flag registers a custom file extension for scanning" { + mkdir -p src + # Create a .myext file with C-style doc comments. + # The -f option takes ext;start_regex;end_regex (AWK patterns without slashes). + cat > src/custom.myext <<'SRCEOF' +/* md +@name test/custom-ext +Hello from a custom extension. +*/ +int noop() {} +SRCEOF + run_pipeline -s src -f 'myext;\/\*;\*\/' + assert_dir_exists "$NS/test/custom-ext" +} + +@test "cli: -f registered extension produces correct chunk content" { + mkdir -p src + cat > src/custom.myext <<'SRCEOF' +/* md +@name test/custom-content +Content from custom ext. +*/ +int noop() {} +SRCEOF + run_pipeline -s src -f 'myext;\/\*;\*\/' + assert_contains "$NS/test/custom-content/index.html" "Content from custom ext" +} + +# ── TRBLDOC_MD (markdown renderer override) ────────────────────────────────── + +@test "cli: TRBLDOC_MD overrides the markdown renderer for .md files" { + mkdir -p src + cat > src/doc.md <<'EOF' +@name test/md-override +# Heading +Some content. +EOF + cat > custom_md.sh <<'EOF' +#!/usr/bin/env bash +# Stub: emit a recognisable marker instead of real HTML. +cat "$@" > /dev/null +echo "<p class=\"custom-md-renderer\">custom renderer was here</p>" +EOF + chmod +x custom_md.sh + TRBLDOC_MD="./custom_md.sh" run_pipeline -s src + assert_contains "$NS/test/md-override/index.html" "custom-md-renderer" +} + +@test "cli: TRBLDOC_MD override is not used when unset (default md2html stub runs)" { + stage_fixture "$FIXTURES/md/simple.md" "src/simple.md" + run_pipeline -s src + # The default stub wraps content in class=stub-md; confirm it ran. + assert_contains "$NS/test/md-simple/index.html" "stub-md" +} + +# ── built-in site assembly ─────────────────────────────────────────────────── + +@test "cli: built output is assembled by default without external zod" { + mkdir -p src + cat > src/simple.c <<'EOF' +/* md +@name test/simple +Hello from a C comment. +*/ +int main() { return 0; } +EOF + run_pipeline -s src + assert_file_exists "$CACHE/built/test/simple/index.html" + assert_contains "$CACHE/built/test/simple/index.html" "<!DOCTYPE html>" + assert_contains "$CACHE/built/test/simple/index.html" "Hello from a C comment" + assert_not_contains "$CACHE/built/test/simple/index.html" "{{{yield}}}" +} + +@test "cli: default main.layout includes stylesheet and excludes highlightjs mermaid and mathjax" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$NS/main.layout" '<link href="/stylesheets/style.css" rel="stylesheet">' + assert_not_contains "$NS/main.layout" "highlight.min.js" + assert_not_contains "$NS/main.layout" "highlightAll()" + assert_not_contains "$NS/main.layout" "mermaid.min.js" + assert_not_contains "$NS/main.layout" "mermaid.initialize" + assert_not_contains "$NS/main.layout" "mathjax" +} + +@test "cli: -l uses the provided main.layout template" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + cat > alt.layout <<'EOF' +<!DOCTYPE html> +<html lang="en"> +<head><meta charset="utf-8"><title>{{ title }}</title></head> +<body> +<header>ALT {{ breadcrumb }}</header> +<nav><ul>{{ nav }}</ul></nav> +<main>{{ yield }}</main> +</body> +</html> +EOF + run_pipeline -s src -l alt.layout + assert_contains "$NS/main.layout" "ALT {{ breadcrumb }}" + assert_not_contains "$NS/main.layout" "Generated by trbldoc" +} + +@test "cli: -l fails when layout template file does not exist" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run bash "$TRBLDOC" -s src -l does-not-exist.layout 2>&1 + [ "$status" -ne 0 ] + [[ "$output" == *"layout template not found"* ]] +} + +@test "cli: files without a registered extension are not scanned" { + mkdir -p src + # .xyz has no registered comment form; no output namespace should appear. + cat > src/ignored.xyz <<'SRCEOF' +/* md +@name test/should-not-exist +This should not be processed. +*/ +SRCEOF + run_pipeline -s src + if [[ -d "$NS/test/should-not-exist" ]]; then + echo "FAILED: .xyz file was unexpectedly processed" >&2 + return 1 + fi +} diff --git a/tests/integration/pipeline.bats b/tests/integration/pipeline.bats new file mode 100644 index 0000000..2f43304 --- /dev/null +++ b/tests/integration/pipeline.bats @@ -0,0 +1,842 @@ +#!/usr/bin/env bats +# tests/integration/pipeline.bats +# End-to-end tests for trbldoc.sh. +# +# External markdown rendering is stubbed via helpers/common.bash. +# Tests inspect both .trblcache/namespace intermediate output and the assembled +# .trblcache/built pages produced by trbldoc itself. +# +# Tests marked `skip` document known bugs from todo.md. Remove `skip` after +# the corresponding bug is fixed. + +load "../helpers/common.bash" + +FIXTURES="$BATS_TEST_DIRNAME/../fixtures" +# REPO_ROOT is exported by run_tests.sh; fall back to relative path when bats +# is invoked directly on this file. +_REPO_ROOT="${REPO_ROOT:-$(cd "$BATS_TEST_DIRNAME/../.." && pwd)}" +TRBLDOC="$_REPO_ROOT/trbldoc.sh" + +setup() { + setup_workspace + CACHE=".trblcache" + NS="$CACHE/namespace" +} + +teardown() { + teardown_workspace +} + +# ── helpers ───────────────────────────────────────────────────────────────── + +# Copy a fixture file into a temporary source tree and run the pipeline. +# Usage: run_pipeline SRC_DIR [additional source dirs...] +run_pipeline() { + bash "$TRBLDOC" "$@" +} + +# Copy a fixture into a local source dir for a clean run. +stage_fixture() { + local src="$1" # path to fixture file or dir + local dest="$2" # destination relative path in tmp workspace + mkdir -p "$(dirname "$dest")" + cp -r "$src" "$dest" +} + +install_busybox_tool_wrapper() { + local tool="$1" + local busybox_bin="$2" + cat > "$STUB_BIN/$tool" <<EOF +#!/bin/sh +if [ -n "\${TRBLDOC_TEST_TOOL_LOG:-}" ]; then + printf '%s\n' "$tool" >> "\$TRBLDOC_TEST_TOOL_LOG" +fi +exec "$busybox_bin" $tool "\$@" +EOF + chmod +x "$STUB_BIN/$tool" +} + +# ── cache structure ────────────────────────────────────────────────────────── + +@test "pipeline: .trblcache directory is created on run" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" +run_pipeline -s src + assert_dir_exists ".trblcache" +} + +@test "pipeline: namespace subdirectory is created on run" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_dir_exists ".trblcache/namespace" +} + +@test "pipeline: chunks subdirectory is created on run" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_dir_exists ".trblcache/chunks" +} + +@test "pipeline: global.meta is created" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_file_exists "$NS/global.meta" +} + +@test "pipeline: main.layout is created" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_file_exists "$NS/main.layout" +} + +@test "pipeline: main.layout passes HTML validation" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_valid_html "$NS/main.layout" +} + +@test "pipeline: main.layout has well-formed HTML structure" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + local layout="$NS/main.layout" + # <html> must appear before <head> + local html_line head_line body_line footer_line close_body_line + html_line=$(grep -n '<html' "$layout" | head -1 | cut -d: -f1) + head_line=$(grep -n '<head' "$layout" | head -1 | cut -d: -f1) + body_line=$(grep -n '<body' "$layout" | head -1 | cut -d: -f1) + footer_line=$(grep -n '<footer' "$layout" | head -1 | cut -d: -f1) + close_body_line=$(grep -n '</body>' "$layout" | head -1 | cut -d: -f1) + # <html> before <head> + [ "$html_line" -lt "$head_line" ] + # exactly one <head> + local head_count + head_count=$(grep -c '<head>' "$layout") + assert_equal "$head_count" "1" + # <footer> inside <body>: footer before </body> + [ "$footer_line" -lt "$close_body_line" ] +} + +@test "pipeline: nav.partial is created" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_file_exists "$NS/nav.partial" +} + +# ── namespace output ───────────────────────────────────────────────────────── + +@test "pipeline: named chunk creates namespace output folder" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_dir_exists "$NS/test/simple" +} + +@test "pipeline: namespace output folder contains index.html" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_file_exists "$NS/test/simple/index.html" +} + +@test "pipeline: built page exists for rendered namespace" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_file_exists "$CACHE/built/test/simple/index.html" +} + +@test "pipeline: built page applies layout tokens and contains rendered body" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$CACHE/built/test/simple/index.html" "<!DOCTYPE html>" + assert_contains "$CACHE/built/test/simple/index.html" "<title>test/simple</title>" + assert_contains "$CACHE/built/test/simple/index.html" "Hello from a C comment" + assert_contains "$CACHE/built/test/simple/index.html" "href='/test/simple'" +} + +@test "pipeline: index.html contains rendered chunk body" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$NS/test/simple/index.html" "Hello from a C comment" +} + +@test "pipeline: runs with BusyBox awk and xargs from PATH" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + + if ! command -v busybox >/dev/null 2>&1; then + skip "busybox is not installed" + fi + + local busybox_bin + busybox_bin="$(command -v busybox)" + export TRBLDOC_TEST_TOOL_LOG="$TEST_TMPDIR/busybox-tools.log" + : > "$TRBLDOC_TEST_TOOL_LOG" + + install_busybox_tool_wrapper awk "$busybox_bin" + install_busybox_tool_wrapper xargs "$busybox_bin" + + run bash "$TRBLDOC" -s src 2>&1 + [ "$status" -eq 0 ] + + assert_file_exists "$NS/test/simple/index.html" + assert_contains "$NS/test/simple/index.html" "Hello from a C comment" + + local awk_count xargs_count + awk_count=$(grep -c '^awk$' "$TRBLDOC_TEST_TOOL_LOG" || true) + xargs_count=$(grep -c '^xargs$' "$TRBLDOC_TEST_TOOL_LOG" || true) + [ "$awk_count" -gt 0 ] + [ "$xargs_count" -gt 0 ] +} + +@test "pipeline: rendered namespace index.html passes HTML validation" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_valid_html_fragment "$NS/test/simple/index.html" +} + +@test "pipeline: multi-chunk file creates two separate namespace folders" { + stage_fixture "$FIXTURES/c/multi_chunk.c" "src/multi_chunk.c" + run_pipeline -s src + assert_dir_exists "$NS/test/alpha" + assert_dir_exists "$NS/test/beta" +} + +@test "pipeline: each namespace folder has its own index.html" { + stage_fixture "$FIXTURES/c/multi_chunk.c" "src/multi_chunk.c" + run_pipeline -s src + assert_file_exists "$NS/test/alpha/index.html" + assert_file_exists "$NS/test/beta/index.html" +} + +@test "pipeline: each namespace index.html contains the correct body" { + stage_fixture "$FIXTURES/c/multi_chunk.c" "src/multi_chunk.c" + run_pipeline -s src + assert_contains "$NS/test/alpha/index.html" "First chunk content" + assert_contains "$NS/test/beta/index.html" "Second chunk content" +} + +@test "pipeline: two chunks with same @name both appear in the shared index.html" { + stage_fixture "$FIXTURES/c/shared_namespace.c" "src/shared_namespace.c" + run_pipeline -s src + assert_contains "$NS/test/shared/index.html" "Part one" + assert_contains "$NS/test/shared/index.html" "Part two" +} + +@test "pipeline: @priority orders chunks before namespace/source ordering" { + mkdir -p src + cat > src/priority_high.c <<'EOF' +/* md +@name test/priority-order +@priority 100 +Priority high +*/ +int high() { return 0; } +EOF + cat > src/priority_low.c <<'EOF' +/* md +@name test/priority-order +@priority 1 +Priority low +*/ +int low() { return 0; } +EOF + cat > src/priority_default.c <<'EOF' +/* md +@name test/priority-order +Priority default +*/ +int def() { return 0; } +EOF + run_pipeline -s src + local out="$NS/test/priority-order/index.html" + local line_high line_low line_default first_render second_render + line_high=$(grep -n "Priority high" "$out" | head -1 | cut -d: -f1) + line_low=$(grep -n "Priority low" "$out" | head -1 | cut -d: -f1) + line_default=$(grep -n "Priority default" "$out" | head -1 | cut -d: -f1) + [ "$line_high" -lt "$line_low" ] + [ "$line_low" -lt "$line_default" ] + first_render="$(cat "$out")" + run_pipeline -s src + second_render="$(cat "$out")" + assert_equal "$first_render" "$second_render" +} + +@test "pipeline: Lua file chunk creates correct namespace output" { + stage_fixture "$FIXTURES/lua/simple.lua" "src/simple.lua" + run_pipeline -s src + assert_file_exists "$NS/test/lua-simple/index.html" +} + +@test "pipeline: Python file chunk creates correct namespace output" { + stage_fixture "$FIXTURES/py/simple.py" "src/simple.py" + run_pipeline -s src + assert_file_exists "$NS/test/py-simple/index.html" +} + +@test "pipeline: Python chunk body appears in namespace output" { + stage_fixture "$FIXTURES/py/simple.py" "src/simple.py" + run_pipeline -s src + assert_contains "$NS/test/py-simple/index.html" "Hello from a Python doc comment" +} + +@test "pipeline: Markdown file chunk creates correct namespace output" { + stage_fixture "$FIXTURES/md/simple.md" "src/simple.md" + run_pipeline -s src + assert_file_exists "$NS/test/md-simple/index.html" +} + +@test "pipeline: @exec rst chunk is rendered using TRBLDOC_RST override" { + stage_fixture "$FIXTURES/c/rst_exec.c" "src/rst_exec.c" + cat > custom_rst.sh <<'EOF' +#!/usr/bin/env bash +if [[ $# -gt 0 ]]; then + content="$(cat "$1")" +else + content="$(cat)" +fi +echo "<section class=\"stub-rst\">$content</section>" +EOF + chmod +x custom_rst.sh + TRBLDOC_RST="./custom_rst.sh" run_pipeline -s src + assert_file_exists "$NS/test/rst-exec/index.html" + assert_contains "$NS/test/rst-exec/index.html" "stub-rst" + assert_contains "$NS/test/rst-exec/index.html" "RST Heading" +} + +@test "pipeline: file with no doc comments creates no namespace folders" { + stage_fixture "$FIXTURES/c/no_comments.c" "src/no_comments.c" + run_pipeline -s src + # Only the fixed namespace files (global.meta, nav.partial, etc.) should exist; + # no test/* content namespace folder should be present. + if [[ -d "$NS/test" ]]; then + echo "FAILED: unexpected namespace folder created for file with no doc comments" >&2 + ls "$NS/test" >&2 + return 1 + fi +} + +# ── global.meta ───────────────────────────────────────────────────────────── + +@test "pipeline: global.meta contains entry for the processed namespace" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$NS/global.meta" "test/simple" +} + +# ── nav.partial ────────────────────────────────────────────────────────────── + +@test "pipeline: nav.partial contains link for processed namespace" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$NS/nav.partial" "test/simple" +} + +@test "pipeline: nav.partial does not contain duplicate entries" { + stage_fixture "$FIXTURES/c/shared_namespace.c" "src/shared_namespace.c" + run_pipeline -s src + # Count occurrences of the shared namespace link; must be exactly 1. + local count + count=$(grep -c "test/shared" "$NS/nav.partial" || true) + assert_equal "$count" "1" +} + +@test "pipeline: nav.partial groups related namespaces as a tree" { + mkdir -p doc + cat > doc/doc.md <<'EOF' +# Doc +EOF + cat > doc/index.md <<'EOF' +# Index +EOF + cat > doc/love_loader.md <<'EOF' +# Love Loader +EOF + cat > doc/ui_notes.md <<'EOF' +# UI Notes +EOF + run_pipeline -s doc + assert_contains "$NS/nav.partial" "<li>doc/<ul>" + assert_contains "$NS/nav.partial" "href='/doc/doc.md'>doc.md</a>" + assert_contains "$NS/nav.partial" "href='/doc/index.md'>index.md</a>" + assert_contains "$NS/nav.partial" "href='/doc/love_loader.md'>love_loader.md</a>" + assert_contains "$NS/nav.partial" "href='/doc/ui_notes.md'>ui_notes.md</a>" +} + +@test "pipeline: dotted @name values are allowed" { + mkdir -p src + cat > src/dotted.c <<'EOF' +/* md +@name ui.element.Element +Dotted namespace. +*/ +int dotted() { return 0; } +EOF + run_pipeline -s src + assert_dir_exists "$NS/ui.element.Element" + assert_contains "$NS/nav.partial" "href='/ui.element.Element'" +} + +@test "pipeline: explicit slash-separated @name builds correct nav tree" { + mkdir -p src + cat > src/ui_parent.c <<'EOF' +/* md +@name ui +UI root documentation. +*/ +int ui_root() { return 0; } +EOF + cat > src/ui_element.c <<'EOF' +/* md +@name ui/element/Element +Element details. +*/ +int ui_element() { return 0; } +EOF + run_pipeline -s src + assert_contains "$NS/nav.partial" "href='/ui'>ui</a>/<ul>" + assert_contains "$NS/nav.partial" "href='/ui/element/Element'>Element</a>" + assert_dir_exists "$NS/ui/element/Element" +} + +# ── inline reference resolution ───────────────────────────────────────────── + +@test "pipeline: @lua/<path> tokens are resolved to Markdown links" { + stage_fixture "$FIXTURES/c/inline_ref.c" "src/inline_ref.c" + run_pipeline -s src + # ref_resolvers/lua.sh "Coroutines" returns https://www.lua.org/manual/5.2/manual.html#2.6 + assert_contains "$NS/test/inline-ref/index.html" "lua.org/manual" + assert_not_contains "$NS/test/inline-ref/index.html" "@lua/Coroutines" +} + +@test "pipeline: -R adds a custom resolver prefix" { + mkdir -p src + cat > src/custom_ref.c <<'EOF' +/* md +@name test/custom-ref +Link: @custom/topic +*/ +int noop() {} +EOF + cat > custom_resolver.sh <<'EOF' +#!/usr/bin/env bash +opt="$1" +path="$(cat)" +printf "https://example.test/%s/%s" "$opt" "$path" +EOF + chmod +x custom_resolver.sh + run_pipeline -s src -R 'custom=./custom_resolver.sh;modeA' + assert_contains "$NS/test/custom-ref/index.html" "https://example.test/modeA/topic" + assert_not_contains "$NS/test/custom-ref/index.html" "@custom/topic" +} + +@test "pipeline: -R overrides an existing resolver prefix" { + mkdir -p src + cat > src/lua_override.c <<'EOF' +/* md +@name test/lua-override +Link: @lua/Coroutines +*/ +int noop() {} +EOF + cat > lua_override.sh <<'EOF' +#!/usr/bin/env bash +path="$(cat)" +printf "https://override.test/%s" "$path" +EOF + chmod +x lua_override.sh + run_pipeline -s src -R 'lua=./lua_override.sh' + assert_contains "$NS/test/lua-override/index.html" "https://override.test/Coroutines" + assert_not_contains "$NS/test/lua-override/index.html" "lua.org/manual" +} + +@test "pipeline: @exec renderer receives TRBLCACHE environment variable (repro)" { + mkdir -p src + cat > src/renderer_cache_env.c <<'EOF' +/* ./require_trblcache.sh +@name test/renderer-cache-env +Renderer env test. +*/ +int noop() { return 0; } +EOF + cat > require_trblcache.sh <<'EOF' +#!/usr/bin/env bash +if [[ -z "${TRBLCACHE:-}" ]]; then + echo "TRBLCACHE is missing in renderer" >&2 + exit 23 +fi +echo "<p>cache=$TRBLCACHE</p>" +EOF + chmod +x require_trblcache.sh + run bash "$TRBLDOC" -s src 2>&1 + [ "$status" -eq 0 ] + assert_contains "$NS/test/renderer-cache-env/index.html" "cache=./.trblcache" + [[ "$output" != *"TRBLCACHE is missing in renderer"* ]] + [[ "$output" != *"renderer failed (exit 23)"* ]] +} + +@test "pipeline: @exec renderer receives FILEPATH environment variable (repro)" { + mkdir -p src + cat > src/renderer_filepath_env.c <<'EOF' +/* ./require_filepath.sh +@name test/renderer-filepath-env +Renderer FILEPATH env test. +*/ +int noop() { return 0; } +EOF + cat > require_filepath.sh <<'EOF' +#!/usr/bin/env bash +if [[ -z "${FILEPATH:-}" ]]; then + echo "FILEPATH is missing in renderer" >&2 + exit 24 +fi +if [[ "${FILEPATH}" != "src/renderer_filepath_env.c" ]]; then + echo "FILEPATH is incorrect in renderer: $FILEPATH" >&2 + exit 25 +fi +echo "<p>filepath=$FILEPATH</p>" +EOF + chmod +x require_filepath.sh + run bash "$TRBLDOC" -s src 2>&1 + [ "$status" -eq 0 ] + assert_contains "$NS/test/renderer-filepath-env/index.html" "filepath=src/renderer_filepath_env.c" + [[ "$output" != *"FILEPATH is missing in renderer"* ]] + [[ "$output" != *"FILEPATH is incorrect in renderer"* ]] + [[ "$output" != *"renderer failed (exit 24)"* ]] + [[ "$output" != *"renderer failed (exit 25)"* ]] +} + +@test "pipeline: @exec renderer receives LINENUM environment variable (repro)" { + mkdir -p src + cat > src/renderer_linenum_env.c <<'EOF' +int prelude = 0; + +/* ./require_linenum.sh +@name test/renderer-linenum-env +Renderer LINENUM env test. +*/ +int noop() { return 0; } +EOF + cat > require_linenum.sh <<'EOF' +#!/usr/bin/env bash +if [[ -z "${LINENUM:-}" ]]; then + echo "LINENUM is missing in renderer" >&2 + exit 26 +fi +if [[ "${LINENUM}" != "3" ]]; then + echo "LINENUM is incorrect in renderer: $LINENUM" >&2 + exit 27 +fi +echo "<p>linenum=$LINENUM</p>" +EOF + chmod +x require_linenum.sh + run bash "$TRBLDOC" -s src 2>&1 + [ "$status" -eq 0 ] + assert_contains "$NS/test/renderer-linenum-env/index.html" "linenum=3" + [[ "$output" != *"LINENUM is missing in renderer"* ]] + [[ "$output" != *"LINENUM is incorrect in renderer"* ]] + [[ "$output" != *"renderer failed (exit 26)"* ]] + [[ "$output" != *"renderer failed (exit 27)"* ]] +} +@test "pipeline: inline @exec function receives TRBLCACHE FILEPATH and LINENUM (repro)" { + mkdir -p src + cat > src/renderer_inline_function_env.c <<'EOF' +/* render_inline(){ if [[ -z "${TRBLCACHE:-}" || -z "${FILEPATH:-}" || -z "${LINENUM:-}" ]]; then echo "missing env in inline renderer function" >&2; return 28; fi; printf "<p>cache=%s filepath=%s linenum=%s</p>\n" "$TRBLCACHE" "$FILEPATH" "$LINENUM"; }; render_inline +@name test/renderer-inline-function-env +Renderer inline function env test. +*/ +int noop() { return 0; } +EOF + run bash "$TRBLDOC" -s src 2>&1 + [ "$status" -eq 0 ] + assert_contains "$NS/test/renderer-inline-function-env/index.html" "cache=./.trblcache" + assert_contains "$NS/test/renderer-inline-function-env/index.html" "filepath=src/renderer_inline_function_env.c" + assert_contains "$NS/test/renderer-inline-function-env/index.html" "linenum=1" + [[ "$output" != *"missing env in inline renderer function"* ]] + [[ "$output" != *"renderer failed (exit 28)"* ]] +} + +@test "pipeline: @name tokens resolve to internal refs before external resolvers" { + # Internal refs: @<name> resolves to the first chunk with @ref <name> + mkdir -p src + cat > src/target.c <<'EOF' +/* md +@name api/target +@ref my-target +Target documentation. +*/ +int target() {} +EOF + cat > src/source.c <<'EOF' +/* md +@name api/source +See also: @my-target for details. +*/ +int source() {} +EOF + run_pipeline -s src + # @my-target should resolve to the internal link from global.meta + assert_contains "$NS/api/source/index.html" "href='/api/target#my-target'" + assert_not_contains "$NS/api/source/index.html" "@my-target" +} + +# ── template variable substitution ───────────────────────────────────────── + +@test "pipeline: {{ src_repo }} is substituted with TRBLDOC_SRC_REPO value" { + stage_fixture "$FIXTURES/c/src_repo.c" "src/src_repo.c" + TRBLDOC_SRC_REPO="https://example.com/repo" run_pipeline -s src + assert_contains "$NS/test/src-repo/index.html" "https://example.com/repo" + assert_not_contains "$NS/test/src-repo/index.html" '{{ src_repo }}' +} + +@test "pipeline: {{ ref_string }} is substituted with the resolved reference link" { + stage_fixture "$FIXTURES/c/ref_string.c" "src/ref_string.c" + run_pipeline -s src + # @ref lua/Coroutines is resolved via ref_resolvers/lua.sh; the output + # contains the full ref name as link text and the resolved URL. + assert_contains "$NS/test/ref-string/index.html" "lua/Coroutines" + assert_contains "$NS/test/ref-string/index.html" "lua.org/manual" + assert_not_contains "$NS/test/ref-string/index.html" '{{ ref_string }}' +} +@test "pipeline: From attribution is linked to source when TRBLDOC_SRC_REPO is set" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + TRBLDOC_SRC_REPO="https://example.com/repo/blob/main" run_pipeline -s src + assert_contains "$NS/test/simple/index.html" "From <a href='https://example.com/repo/blob/main/src/simple.c#L1'>src/simple.c:1</a>" +} + +@test "pipeline: {{ toc }} is substituted with nav links after rendering" { + stage_fixture "$FIXTURES/c/toc.c" "src/toc.c" + run_pipeline -s src + # After substitution, the chunk output should contain the nav link for the + # namespace itself, and the literal marker should be gone. + assert_contains "$NS/test/toc-chunk/index.html" "/test/toc-chunk" + assert_not_contains "$NS/test/toc-chunk/index.html" '{{ toc }}' +} + +@test "pipeline: main.layout uses {{ title }} two-bracket format" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$NS/main.layout" '{{ title }}' + assert_not_contains "$NS/main.layout" '{{title}}' +} + +@test "pipeline: main.layout uses {{ nav }} format instead of {{> nav}}" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$NS/main.layout" '{{ nav }}' + assert_not_contains "$NS/main.layout" '{{> nav}}' +} + +@test "pipeline: main.layout uses {{ yield }} format instead of {{{yield}}}" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$NS/main.layout" '{{ yield }}' + assert_not_contains "$NS/main.layout" '{{{yield}}}' +} + +@test "pipeline: main.layout uses {{ breadcrumb }} format" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_contains "$NS/main.layout" '{{ breadcrumb }}' + assert_not_contains "$NS/main.layout" '{{breadcrumb}}' +} + +@test "pipeline: old-style {{src_repo}} without spaces is not substituted" { + mkdir -p src + cat > src/old_fmt.c <<'EOF' +/* md +@name test/old-fmt-src-repo +Old format: {{src_repo}} +*/ +void f() {} +EOF + TRBLDOC_SRC_REPO="https://example.com" run_pipeline -s src + assert_contains "$NS/test/old-fmt-src-repo/index.html" '{{src_repo}}' +} + +@test "pipeline: old-style {{toc}} without spaces is not substituted" { + mkdir -p src + cat > src/old_fmt_toc.c <<'EOF' +/* md +@name test/old-fmt-toc +Old format: {{toc}} +*/ +void f() {} +EOF + run_pipeline -s src + assert_contains "$NS/test/old-fmt-toc/index.html" '{{toc}}' +} + +# ── known defects (skipped – convert to active tests after bug fixes) ──────── + +@test "pipeline: chunk without @name uses source file path as namespace" { + # A chunk with no @name should use its own @file value (the source file path), + # not whatever file was last iterated in the scan loop. + mkdir -p src + cat > src/anon.c <<'EOF' +/* md +Chunk with no @name directive. +*/ +void anon() {} +EOF + run_pipeline -s src + # Expect a namespace folder derived from src/anon.c. + assert_dir_exists "$NS/src/anon.c" +} + +@test "pipeline: second run does not duplicate nav entries" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + run_pipeline -s src # second run + local count + count=$(grep -c "test/simple" "$NS/nav.partial" || true) + assert_equal "$count" "1" +} + +# ── logic bug regression tests ──────────────────────────────────────────── + +@test "pipeline: source folder with spaces in name is discovered via -s" { + # Bug: infolders is a space-separated string; folder names with spaces break discovery. + mkdir -p "src folder" + stage_fixture "$FIXTURES/c/simple.c" "src folder/simple.c" + run_pipeline -s "src folder" + assert_dir_exists "$NS/test/simple" +} + +@test "pipeline: source filename with spaces is processed correctly" { + # Bug: source_files and chunk_files use unquoted iteration which breaks on spaces. + # This fixture has no @name directive, so namespace is derived from source path. + mkdir -p src + cat > 'src/simple file.c' <<'EOF' +/* md +Hello from a C comment. +*/ +int main() { return 0; } +EOF + run_pipeline -s src + # Namespace is derived from source file path (no @name means use file path) + assert_file_exists "$NS/src/simple file.c/index.html" + assert_contains "$NS/src/simple file.c/index.html" "Hello from a C comment" +} + +@test "pipeline: nav entries with ampersands in {{ toc }} are substituted correctly" { + # Bug: toc_oneline sed substitution treats & as matched text. + mkdir -p src + cat > 'src/a.c' <<'EOF' +/* md +@name test/a +Navigation: {{ toc }} +*/ +int a() {} +EOF + cat > 'src/b.c' <<'EOF' +/* md +@name test/b&c +Chunk with ampersand. +*/ +int b() {} +EOF + run_pipeline -s src + # Verify the nav.partial contains the entries + assert_contains "$NS/nav.partial" "test/b&c" + # Verify the rendered output preserves this (not garbled by sed) + assert_contains "$NS/test/a/index.html" "test/b&c" + assert_not_contains "$NS/test/a/index.html" '{{ toc }}' +} + +# ── readme.md → top-level index ───────────────────────────────────────────── + +@test "pipeline: readme.md creates root-level index.html" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + run_pipeline -s src + assert_file_exists "$NS/index.html" +} + +@test "pipeline: readme.md content appears in root index.html" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + run_pipeline -s src + assert_contains "$NS/index.html" "Hello from the readme" +} + +@test "pipeline: README.md (uppercase) creates root-level index.html" { + mkdir -p src + cat > src/README.md <<'EOF' +# Overview +Uppercase README content. +EOF + run_pipeline -s src + assert_file_exists "$NS/index.html" + assert_contains "$NS/index.html" "Uppercase README content" +} + +@test "pipeline: readme.md with explicit @name uses custom namespace, not root" { + mkdir -p src + cat > src/readme.md <<'EOF' +@name custom/readme +# Custom Name +Content with custom name. +EOF + run_pipeline -s src + assert_file_exists "$NS/custom/readme/index.html" + if [[ -f "$NS/index.html" ]]; then + echo "ASSERTION FAILED: unexpected $NS/index.html created for readme with @name" >&2 + return 1 + fi +} + +@test "pipeline: root index.html from readme.md contains rendered HTML tags" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + run_pipeline -s src + assert_contains "$NS/index.html" "<p" +} + +@test "pipeline: root index.html from readme.md passes HTML fragment validation" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + run_pipeline -s src + assert_valid_html_fragment "$NS/index.html" +} + +@test "pipeline: nav.partial contains root href when readme.md is present" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + run_pipeline -s src + assert_contains "$NS/nav.partial" "href='/'" +} + +@test "pipeline: global.meta contains entry for index when readme.md is present" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + run_pipeline -s src + assert_contains "$NS/global.meta" "index:" +} + +@test "pipeline: readme.md alongside other source files produces both root index and other pages" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + assert_file_exists "$NS/index.html" + assert_file_exists "$NS/test/simple/index.html" + assert_contains "$NS/index.html" "Hello from the readme" + assert_contains "$NS/test/simple/index.html" "Hello from a C comment" +} + +@test "pipeline: root index.html source attribution points to readme.md" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + run_pipeline -s src + assert_contains "$NS/index.html" "readme.md" +} + +@test "pipeline: readme.md is assembled to the built root, not a subdirectory" { + stage_fixture "$FIXTURES/md/readme.md" "src/readme.md" + run_pipeline -s src + # Must land at the built root, not inside an "index" subdirectory. + assert_file_exists ".trblcache/built/index.html" + if [[ -f ".trblcache/built/index/index.html" ]]; then + echo "ASSERTION FAILED: readme was built to built/index/index.html instead of built/index.html" >&2 + return 1 + fi +} + +# ── known defects + +@test "pipeline: second run does not duplicate index.html content" { + stage_fixture "$FIXTURES/c/simple.c" "src/simple.c" + run_pipeline -s src + run_pipeline -s src + local count + count=$(grep -c "Hello from a C comment" "$NS/test/simple/index.html" || true) + assert_equal "$count" "1" +} |
