|
| 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