From c37f33d3fc5e19858abcb21cc40548d6024a5707 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sun, 2 Aug 2026 11:24:03 +0200 Subject: [PATCH 1/2] docs(adr): fix ADR-011 module table and pin it with a contract test ADR-011's module table was wrong in two places, and both errors came from counting with a one-level glob instead of from the tree. `doubles/` was missing outright. The table is ordered by load order, so it was generated from the entrypoint's `source` lines -- and `doubles/` is the one module the entrypoint does not source, `assert/index.sh` does. It fell through silently while the prose directly above said "Seventeen", a number that came from counting directories. The two disagreed and nothing noticed. The module is now row 12, where it actually loads, and the row says where it is sourced from so the next reader does not have to rediscover the discrepancy. `learn/` was listed at 5 files / 240 lines. It is 14 files / 1296. The difference is `learn/lessons/`, which `src/learn/*.sh` does not descend into -- the same one-level-glob mistake `Makefile:67` made with nested tests. Every other row's line count had drifted by two or three from recent comment edits; those are refreshed too. `docs/project-overview.md` listed 15 of the 17 modules, missing `doubles` and `dev`. Spies and mocks are a headline feature to be absent from the overview of the source layout. The new test asserts every `src/` module has a row, and that the Files column sums to the number of tracked `.sh` files in `src/` -- 118. Summing catches an under-count in any single row without pinning every number to an exact value, which would make the table annoying to maintain for no extra safety. Mutation-tested rather than assumed: deleting the `doubles/` row turns two assertions red, changing `learn/` from 14 to 5 turns one red, and renaming the ADR turns all three red. That last one is why the file-exists assertion is there -- without it a rename would leave the greps matching nothing and the whole file passing while checking nothing, which is exactly how a layering contract rotted unnoticed in #946. 1633 sequential / 1592 parallel; both are baseline + the 3 new tests. --- ...dr-011-source-layout-and-build-pipeline.md | 42 ++++++++------ docs/project-overview.md | 4 ++ tests/unit/project/adr_module_table_test.sh | 57 +++++++++++++++++++ 3 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 tests/unit/project/adr_module_table_test.sh diff --git a/adrs/adr-011-source-layout-and-build-pipeline.md b/adrs/adr-011-source-layout-and-build-pipeline.md index d3c0cb4c..5e248cf1 100644 --- a/adrs/adr-011-source-layout-and-build-pipeline.md +++ b/adrs/adr-011-source-layout-and-build-pipeline.md @@ -44,27 +44,35 @@ statement. That is load-bearing, not stylistic — see the build section — and ## The modules -Seventeen, in the order the `bashunit` entrypoint sources them. The order is the dependency -layering: leaves first. +Seventeen, in load order. The order is the dependency layering: leaves first. | # | Module | Files | Lines | Owns | |---|---|---|---|---| | 1 | `dev/` | 1 | 18 | debug helpers; **excluded from the build** | -| 2 | `system/` | 4 | 192 | capability probing: OS, `command -v`, small I/O | -| 3 | `util/` | 4 | 476 | computation: strings, arithmetic, time | -| 4 | `api/` | 5 | 207 | the surface a user's test file calls (except assertions) | -| 5 | `config/` | 4 | 964 | `BASHUNIT_*` defaults, scratch dirs, parallel mode, rerun cache | -| 6 | `coverage/` | 13 | 2644 | line/branch tracking and the four report formats | -| 7 | `state/` | 6 | 477 | counters, per-test context, result payload, parallel aggregation | -| 8 | `console/` | 9 | 1281 | everything printed: palette, header, per-test lines, totals | -| 9 | `helper/` | 8 | 976 | naming, discovery, data providers, tags, encoding | -| 10 | `cli/` | 5 | 447 | the `doc`/`init`/`upgrade`/`watch` subcommand implementations | -| 11 | `assert/` | 11 | 2336 | every assertion | -| 12 | `reports/` | 7 | 467 | JUnit, TAP, JSON, GHA and HTML writers | -| 13 | `runner/` | 11 | 2172 | the file loop, per-test execution, retry, result parsing | -| 14 | `benchmark/` | 4 | 221 | the bench implementation (`runner/bench.sh` is its loop) | -| 15 | `learn/` | 5 | 240 | the interactive tutorial | -| 16 | `main/` | 8 | 1475 | flag parsing per subcommand and the run lifecycle | +| 2 | `system/` | 4 | 189 | capability probing: OS, `command -v`, small I/O | +| 3 | `util/` | 4 | 474 | computation: strings, arithmetic, time | +| 4 | `api/` | 5 | 205 | the surface a user's test file calls (except assertions) | +| 5 | `config/` | 4 | 961 | `BASHUNIT_*` defaults, scratch dirs, parallel mode, rerun cache | +| 6 | `coverage/` | 13 | 2640 | line/branch tracking and the four report formats | +| 7 | `state/` | 6 | 474 | counters, per-test context, result payload, parallel aggregation | +| 8 | `console/` | 9 | 1278 | everything printed: palette, header, per-test lines, totals | +| 9 | `helper/` | 8 | 948 | naming, discovery, data providers, tags, encoding | +| 10 | `cli/` | 5 | 445 | the `doc`/`init`/`upgrade`/`watch` subcommand implementations | +| 11 | `assert/` | 11 | 2326 | every assertion | +| 12 | `doubles/` | 4 | 505 | spies and mocks — **sourced by `assert/index.sh`, not the entrypoint** | +| 13 | `reports/` | 7 | 465 | JUnit, TAP, JSON, GHA and HTML writers | +| 14 | `runner/` | 11 | 2190 | the file loop, per-test execution, retry, result parsing | +| 15 | `benchmark/` | 4 | 219 | the bench implementation (`runner/bench.sh` is its loop) | +| 16 | `learn/` | 14 | 1296 | the interactive tutorial (9 of those files are `learn/lessons/`) | +| 17 | `main/` | 8 | 1473 | flag parsing per subcommand and the run lifecycle | + +The file counts sum to 118, which is every `.sh` file in `src/` — that is the check to re-run +when editing this table, because both of its previous errors came from counting the wrong way. +`doubles/` was absent entirely: the table was generated from the entrypoint's `source` lines, +and `doubles/` is the one module the entrypoint does not source, so it fell through while the +prose above said "seventeen" from a directory count. And `learn/` was listed as 5 files / 240 +lines because a one-level `src/learn/*.sh` glob does not descend into `learn/lessons/` — the +same one-level-glob mistake that `Makefile:67` made with nested tests. Count from the tree. Namespaces track directories: `src/runner/` holds `bashunit::runner::*`. Two exceptions are deliberate — `assert/` holds bare `assert_*` (the public API is unprefixed) and `console/` diff --git a/docs/project-overview.md b/docs/project-overview.md index 2f78e6e4..d651504e 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -34,12 +34,16 @@ point holding **only `source` lines** — the code lives in the sibling files be | `helper` | naming, test discovery, data providers, tags, encoding | | `cli` | the `doc`, `init`, `upgrade` and `watch` subcommands | | `assert` | every assertion | +| `doubles` | spies and mocks | | `reports` | JUnit, TAP, JSON, GitHub Actions and HTML writers | | `runner` | the file loop, per-test execution, retry, result parsing | | `benchmark` | the bench implementation | | `learn` | the interactive tutorial | | `main` | flag parsing per subcommand and the run lifecycle | +There is also a `dev` module holding debug helpers, which is deliberately excluded from the +built binary. + The released `bashunit` is a **single file**: `build.sh` walks the `source` statements from the entrypoint, inlines every module in dependency order, and strips the `source` lines. diff --git a/tests/unit/project/adr_module_table_test.sh b/tests/unit/project/adr_module_table_test.sh new file mode 100644 index 00000000..3e2dde00 --- /dev/null +++ b/tests/unit/project/adr_module_table_test.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# Anti-drift contract for ADR-011's module table. +# +# That table has been wrong twice, both times because it was counted with a +# one-level glob rather than from the tree: +# +# * `doubles/` was missing outright. The table was generated from the +# entrypoint's `source` lines, and `doubles/` is the one module the +# entrypoint does not source -- `assert/index.sh` does -- so it fell through +# while the prose above it said "seventeen" from a directory count. +# * `learn/` was listed at 5 files / 240 lines because `src/learn/*.sh` does +# not descend into `learn/lessons/`, which holds 9 more files. +# +# Neither is visible to a human reading the ADR, which is what makes a test the +# right tool. Checking the file counts *sum* to `src/` catches an under-count in +# any single row without pinning every number to an exact value. + +ADR="" +ROOT_DIR="" + +function set_up_before_script() { + ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + ADR="$ROOT_DIR/adrs/adr-011-source-layout-and-build-pipeline.md" +} + +# Without this, renaming the ADR would make every other assertion here compare +# empty output against empty output and pass while checking nothing -- the +# failure mode that let a layering contract rot unnoticed in #946. +function test_the_adr_being_checked_exists() { + assert_file_exists "$ADR" +} + +function adr_table_modules() { + grep -E '^\| [0-9]+ \| `[a-z]+/`' "$ADR" | + sed 's/^| *[0-9]* *| *`\([a-z]*\)\/` *|.*/\1/' | LC_ALL=C sort +} + +function src_modules() { + (cd "$ROOT_DIR" && git ls-files 'src/**.sh' | cut -d/ -f2 | LC_ALL=C sort -u) +} + +function test_every_src_module_has_a_row_in_the_adr_table() { + assert_same "$(src_modules)" "$(adr_table_modules)" +} + +function adr_table_file_total() { + grep -E '^\| [0-9]+ \| `[a-z]+/`' "$ADR" | awk -F'|' '{ sum += $4 } END { print sum + 0 }' +} + +function src_file_total() { + (cd "$ROOT_DIR" && git ls-files 'src/**.sh' | wc -l | tr -d ' ') +} + +function test_adr_table_file_counts_account_for_every_src_file() { + assert_same "$(src_file_total)" "$(adr_table_file_total)" +} From 064998860928c003e0cf036b8b29d78c4437b8b6 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sun, 2 Aug 2026 11:30:57 +0200 Subject: [PATCH 2/2] fix(test): enumerate src/ with find so the Bash 3.0 container can run the ADR contract The Bash 3.0 jobs run in a container where git refuses to read the repo (dubious-ownership), so `git ls-files` exited non-zero and produced nothing. Both count-based assertions then compared against an empty listing and failed for a reason unrelated to the ADR. `find` needs no git and no index. --- tests/unit/project/adr_module_table_test.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/project/adr_module_table_test.sh b/tests/unit/project/adr_module_table_test.sh index 3e2dde00..38ee2556 100644 --- a/tests/unit/project/adr_module_table_test.sh +++ b/tests/unit/project/adr_module_table_test.sh @@ -36,8 +36,12 @@ function adr_table_modules() { sed 's/^| *[0-9]* *| *`\([a-z]*\)\/` *|.*/\1/' | LC_ALL=C sort } +# Enumerated with `find`, not `git ls-files`: the Bash 3.0 jobs run in a container +# where git refuses to read the repo (dubious-ownership), so `git ls-files` exits +# non-zero and yields nothing -- which reads here as "src/ has no modules" and +# fails for a reason that has nothing to do with the ADR. function src_modules() { - (cd "$ROOT_DIR" && git ls-files 'src/**.sh' | cut -d/ -f2 | LC_ALL=C sort -u) + (cd "$ROOT_DIR" && find src -name '*.sh' | cut -d/ -f2 | LC_ALL=C sort -u) } function test_every_src_module_has_a_row_in_the_adr_table() { @@ -49,7 +53,7 @@ function adr_table_file_total() { } function src_file_total() { - (cd "$ROOT_DIR" && git ls-files 'src/**.sh' | wc -l | tr -d ' ') + (cd "$ROOT_DIR" && find src -name '*.sh' | wc -l | tr -d ' ') } function test_adr_table_file_counts_account_for_every_src_file() {