Skip to content

Commit a3c82ce

Browse files
authored
docs: add architecture map rule (module ownership + life of a run) (#816)
1 parent 6f43e6b commit a3c82ce

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

.claude/rules/architecture-map.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
paths:
3+
- "src/**/*.sh"
4+
- "bashunit"
5+
---
6+
7+
# Architecture Map: modules and the life of a run
8+
9+
Orientation for working in `src/` — which file owns what, and the exact call
10+
flow of a test run. Line numbers drift; function names are the stable anchors.
11+
12+
## The life of `./bashunit tests/`
13+
14+
```
15+
bashunit (entry) sources all src/*.sh; version gate; early flag scan
16+
└─ bashunit::main::cmd_test (main.sh: flag parsing, env exports)
17+
└─ bashunit::runner::load_test_files (runner.sh: the per-file loop)
18+
├─ console_header::print_header "Running N tests" — captures
19+
│ └─ helper::find_total_tests $() SUBSHELL: sources each file in a
20+
│ nested subshell just to count tests
21+
├─ source "$test_file" in the MAIN shell (workers inherit)
22+
├─ helper::check_duplicate_functions one awk pass per file
23+
├─ runner::functions_for_script pure bash: filter + sort by def line
24+
├─ helper::build_provider_map one awk pass per file (cached by path,
25+
│ but the count subshell's build dies
26+
│ with it → runs twice per file)
27+
└─ runner::call_test_functions per-fn loop
28+
└─ runner::run_test [--parallel: spawned as a & worker per test]
29+
├─ clock::now_to_slot duration start (if needed)
30+
├─ runner::execute_test_body $() subshell: the actual test fn
31+
│ └─ state::export_subshell_context printf payload "##K=V##…"
32+
├─ runner::parse_result sync: parse counts from payload
33+
│ parallel: + write .result via mktemp
34+
└─ console_results::print_successful_test / _failed_ / …
35+
└─ str::rpad + strip_ansi align per-test time (pure bash)
36+
└─ [--parallel] wait; parallel::aggregate_test_results over *.result files
37+
└─ console_results::render_result totals; deferred failure/skip blocks
38+
└─ rerun::persist; env::cleanup_run_output_dir; exit code
39+
```
40+
41+
Key inversion to remember: **tests run inside `$()` subshells; state comes back
42+
as an encoded single-line payload** (`##ASSERTIONS_FAILED=…##TEST_OUTPUT=<b64>##`),
43+
parsed by `runner::parse_result`/`state.sh`. Counters only exist in the main
44+
shell (or, in parallel, in per-test `.result` files aggregated at the end).
45+
46+
## Module ownership
47+
48+
| Module | Owns |
49+
|--------|------|
50+
| `bashunit` + `main.sh` | entry, subcommand routing, flag parsing, run lifecycle, exit codes, cleanup calls |
51+
| `runner.sh` | file loop, per-test execution, retry/timeout, result parsing, failure context |
52+
| `helpers.sh` | discovery (`find_files_recursive`), fn filtering, provider map, duplicate check, ids |
53+
| `state.sh` | counters, per-test payload encode/decode, TAP conversion |
54+
| `env.sh` | all `BASHUNIT_*` defaults/config files, scratch dirs (`_BASHUNIT_RUN_OUTPUT_DIR` + EXIT-trap cleanup) |
55+
| `parallel.sh` | worker temp tree, aggregation, stop-on-failure flag file |
56+
| `console_header.sh` / `console_results.sh` | header/totals rendering, deferred failed/skipped/incomplete/risky blocks (scratch files under the run dir) |
57+
| `assert*.sh` | assertions; `assertions.sh` re-exports; per-assertion path must stay fork-free |
58+
| `clock.sh` | time impl selection (EPOCHREALTIME > date > perl > …), return-slot reads |
59+
| `str.sh` / `math.sh` / `io.sh` / `globals.sh` | pure-bash utilities; `globals.sh` has `temp_file`/`temp_dir` (public test API) |
60+
| `test_doubles.sh` | spy/mock state via `_BASHUNIT_SPY_*` globals + files |
61+
| `coverage.sh` | DEBUG-trap line tracking; only active under `--coverage` |
62+
| `rerun.sh` | `.bashunit/last-failed` cache for `--rerun-failed` |
63+
| `reports.sh` | JUnit/HTML/TAP/JSON writers |
64+
| `check_os.sh` / `dependencies.sh` | one-fork OS detect; `command -v` probes (builtins, not forks) |
65+
| `doc.sh` `init.sh` `learn.sh` `upgrade.sh` `watch.sh` `benchmark.sh` | the non-`test` subcommands |
66+
67+
## Cross-cutting invariants
68+
69+
- **Bash 3.0 floor** (`.claude/rules/bash-style.md`): no `[[`, `declare -A`,
70+
`${var,,}`, `BASHPID`, negative indices. Subshells share `$$` and `RANDOM`
71+
state — you cannot make a per-worker unique token without a fork (`mktemp`).
72+
- **Return-slot pattern** (`_BASHUNIT_<PKG>_<FN>_OUT` globals) instead of `$()`
73+
captures on hot paths — bash-style.md documents it; `local` is dynamically
74+
scoped, so helpers must not write caller-named variables.
75+
- **Fork budget** (`.claude/rules/perf-fork-budget.md`): per-test paths are
76+
fork-free (sequential) or mktemp-only (parallel); budgets are enforced by
77+
`tests/acceptance/bashunit_*forks*_test.sh` on three platforms — a "harmless"
78+
`echo | sed` in a per-test path will fail CI.
79+
- **Binaries pinned at startup** (`$GREP`, `$MKTEMP`, `$CAT` in env.sh) so test
80+
doubles/PATH games can't hijack the framework's own plumbing.
81+
- **Snapshots assume 80-col non-tty width** (`tput cols` fallback); anything
82+
that changes rendering widths breaks `tests/acceptance/snapshots/`.

0 commit comments

Comments
 (0)