diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-17 22:04:41 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-17 22:04:41 +0300 |
| commit | 5fb1f17e3b16c8ef4b71648419251d76ae1227b2 (patch) | |
| tree | 994bdb3e5fe6f291226a48c7734106459e19b291 /tests/cli.sh | |
| parent | eb8cbbbcb9a613757ac4440246c43ebba6758afe (diff) | |
en0: make stats categories self-registering via STATS_CATEGORIES
Adding an EXIF stats category previously required editing four places: the
reset function, a new _stats_record_*, the body builder, and a new
_stats_render_* section. Introduce a STATS_CATEGORIES registry (the single
source of truth) and make the generic code iterate it instead.
- STATS_CATEGORIES: ordered, pipe-delimited specs
(count_array|prefix|heading|render_kind), declared -gra so it survives a
function-scoped source. The array order IS the overview display order.
- STATS_CATEGORY_BUCKETS: tab-delimited bucket ladders for the 'ordered'
histogram kinds (apertures wide->narrow, etc.).
- STATS_RECORD_FUNCTIONS: the per-photo recorder dispatch list.
Collapsed touch-points:
- reset_photo_exif_stats clears each registry count array via
_stats_category_arrays.
- accumulate_photo_stats dispatches recorders from STATS_RECORD_FUNCTIONS.
- _stats_build_body iterates STATS_CATEGORIES, dispatching each spec through
_stats_render_category (camera/ranked/ordered/month kinds).
- _stats_render_ordered_section reads its ladder from STATS_CATEGORY_BUCKETS;
the camera leaderboard is now ranked + a 'stats-leaderboard' list_class.
Adding a category is now: append one STATS_CATEGORIES entry (plus a
STATS_CATEGORY_BUCKETS row for an ordered ladder) and have a record function
tally into its array. No edits to reset, the body builder, or a per-category
render branch.
Behaviour-preserving: category order, bucket order, headings, counts and
links are unchanged. Verified byte-identical by diffing the stats/ output of
the pre-change binary against the new one over the same fixture album.
Added test_stats_categories_registry_is_single_source_of_truth (registered in
main()) asserting reset, the body builder and the bucket ladders all derive
from the registry; it fails if a category is added in only one place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'tests/cli.sh')
| -rwxr-xr-x | tests/cli.sh | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/tests/cli.sh b/tests/cli.sh index ad2a1d7..479a998 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -5877,6 +5877,84 @@ test_stats_collect_reads_cached_identify_output() { test::teardown } +# STATS_CATEGORIES is the single source of truth (task en0): proves the reset, +# the overview body builder, and the bucket ladders all derive from the registry, +# so a category can no longer be defined in only one place. A regression that +# added a category to (say) the body builder without registering it -- or +# registered one without resetting its array, or an 'ordered' kind without its +# bucket ladder -- would fail one of these assertions. +test_stats_categories_registry_is_single_source_of_truth() { + local spec + local array_name + local render_kind + local heading + local -A registry_arrays=() + local -A rendered_headings=() + local -a fields=() + local body + + test::setup + test::source_shuriken_lib + + # 1) reset_photo_exif_stats must clear exactly the registry's count arrays + # (no more, no less): a registered category gets a fresh empty array. + reset_photo_exif_stats + for spec in "${STATS_CATEGORIES[@]}"; do + IFS='|' read -r -a fields <<< "$spec" + array_name="${fields[0]}" + render_kind="${fields[3]}" + registry_arrays["$array_name"]=1 + # The array exists and is an (empty) associative array after reset. + if ! declare -p "$array_name" >/dev/null 2>&1; then + printf 'FAIL: reset did not declare registry array %s\n' \ + "$array_name" >&2 + exit 1 + fi + # Every 'ordered' category must supply a bucket ladder; nothing else may. + if [ "$render_kind" = ordered ]; then + if [ -z "${STATS_CATEGORY_BUCKETS[$array_name]:-}" ]; then + printf 'FAIL: ordered category %s has no bucket ladder\n' \ + "$array_name" >&2 + exit 1 + fi + fi + done + for array_name in "${!STATS_CATEGORY_BUCKETS[@]}"; do + if [ -z "${registry_arrays[$array_name]:-}" ]; then + printf 'FAIL: bucket ladder %s is not a registered category\n' \ + "$array_name" >&2 + exit 1 + fi + done + + # 2) The overview body builder renders only registry headings: feed one photo + # that lights up several categories, then assert every <h2> heading in the + # body comes from a STATS_CATEGORIES entry (so no out-of-band section). + reset_photo_exif_stats + accumulate_photo_stats 'a.jpg' <<'EXIF' + Geometry: 6000x4000+0+0 + exif:Make: Canon + exif:Model: Canon EOS 5D + exif:FNumber: 28/10 + exif:ISOSpeedRatings: 400 + exif:DateTimeOriginal: 2021:06:14 10:00:00 +EXIF + for spec in "${STATS_CATEGORIES[@]}"; do + IFS='|' read -r -a fields <<< "$spec" + rendered_headings["${fields[2]}"]=1 + done + body=$(_stats_build_body) + while IFS= read -r heading; do + if [ -z "${rendered_headings[$heading]:-}" ]; then + printf 'FAIL: body rendered heading %q not in STATS_CATEGORIES\n' \ + "$heading" >&2 + exit 1 + fi + done < <(grep -oP '(?<=<h2>).*?(?=</h2>)' <<< "$body") + + test::teardown +} + # Unit-test the shared Make+Model dedup helper (task mn0). The album tooltip and # stats leaderboard both rely on this, so cover dedup, plain concatenation and # the empty-field edge cases here in one place. @@ -6254,6 +6332,9 @@ main() { 'stats collect reads cached identify output' \ test_stats_collect_reads_cached_identify_output test::run_case \ + 'stats categories registry is single source of truth' \ + test_stats_categories_registry_is_single_source_of_truth + test::run_case \ 'camera label dedups make and model' \ test_camera_label_from_make_model test::run_case \ |
