summaryrefslogtreecommitdiff
path: root/tests/cli.sh
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-17 22:04:41 +0300
committerPaul Buetow <paul@buetow.org>2026-06-17 22:04:41 +0300
commit5fb1f17e3b16c8ef4b71648419251d76ae1227b2 (patch)
tree994bdb3e5fe6f291226a48c7734106459e19b291 /tests/cli.sh
parenteb8cbbbcb9a613757ac4440246c43ebba6758afe (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-xtests/cli.sh81
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 \