diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-18 16:50:03 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-18 16:50:03 +0300 |
| commit | eba54dbce3e8410bc2e9da69e7e4a26251573f87 (patch) | |
| tree | 3db38ffba253afe2311356d2e17aabb4dcbf29a7 /tests/cli.sh | |
| parent | 3a3ae4a4424136ad4a5be636c06fe036711558a2 (diff) | |
Fix CHRONOLOGICAL_ORDER to reject camera clock-reset EXIF dates
A real album (irregular.ninja/so.war.das) surfaced the bug: a Fujifilm
X100V whose battery died reset its clock to 2000-01-01, and every EXIF
timestamp on that roll was stamped with the bogus date instead of being
omitted. Trusting it at face value sorted that whole roll to the front
of the album, ahead of hundreds of correctly-dated 2020s photos.
EXIF years before 2001 are now treated the same as a missing date tag.
The no-date fallback also switches from source mtime to filename order,
since mtime is not reliable either -- bulk-copying/rsyncing an incoming
directory commonly rewrites every file's mtime to the transfer time,
unrelated to capture order -- while sequential camera filenames track
real shooting order even across a clock reset.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'tests/cli.sh')
| -rwxr-xr-x | tests/cli.sh | 98 |
1 files changed, 78 insertions, 20 deletions
diff --git a/tests/cli.sh b/tests/cli.sh index 0618163..a7b82b4 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -7279,13 +7279,14 @@ test_stats_collect_reads_cached_identify_output() { test::teardown } -# task 8v0: chronological_photo_files (album-photo-select.source.sh) orders -# photos by EXIF date taken, ascending, falling back to source mtime (and then -# filename) for photos with no usable EXIF date. Exercises the function -# directly (not a full --generate) for precise control over dates/mtimes, the -# same pattern test_stats_collect_reads_cached_identify_output above uses for -# the EXIF cache layer. -test_chronological_photo_files_sorts_by_exif_date_with_mtime_fallback() { +# task 8v0 (and its bugfix once irregular.ninja/so.war.das surfaced a real +# camera clock-reset roll): chronological_photo_files +# (album-photo-select.source.sh) orders photos by EXIF date taken, ascending, +# falling back to filename order for photos with no usable EXIF date. +# Exercises the function directly (not a full --generate) for precise control +# over dates/mtimes, the same pattern test_stats_collect_reads_cached_identify_output +# above uses for the EXIF cache layer. +test_chronological_photo_files_sorts_by_exif_date_with_filename_fallback() { local incoming_dir local dist_dir local cache_dir @@ -7306,12 +7307,14 @@ test_chronological_photo_files_sorts_by_exif_date_with_mtime_fallback() { done # a.jpg/d.jpg have no DateTimeOriginal/Digitized/DateTime tag at all (only - # an unrelated Make tag), so they must fall back to mtime. d.jpg's mtime is - # deliberately EARLIER than a.jpg's, and both are deliberately out of - # filename order, to prove the fallback is a real mtime sort rather than an - # accidental filename sort. - touch -d '2020-01-01 00:00:00' "$incoming_dir/d.jpg" - touch -d '2020-01-02 00:00:00' "$incoming_dir/a.jpg" + # an unrelated Make tag), so they must fall back to filename order. Their + # mtimes are deliberately set OUT of filename order (d.jpg newer than + # a.jpg) to prove the fallback is a real filename sort rather than an + # accidental mtime sort (mtime is not trustworthy: bulk-copying/rsyncing an + # incoming directory commonly rewrites every file's mtime to the transfer + # time regardless of capture order). + touch -d '2020-01-02 00:00:00' "$incoming_dir/d.jpg" + touch -d '2020-01-01 00:00:00' "$incoming_dir/a.jpg" # b.jpg/c.jpg have a real EXIF date, deliberately reversed relative to their # filenames (c.jpg is EARLIER than b.jpg) so a filename-sort bug would not @@ -7338,11 +7341,63 @@ test_chronological_photo_files_sorts_by_exif_date_with_mtime_fallback() { mapfile -t ordered < <(chronological_photo_files photos) - # Real EXIF dates (c.jpg 2021, b.jpg 2023) sort before the mtime fallback - # group (d.jpg 2020-01-01, a.jpg 2020-01-02): a genuine timestamp is never - # displaced by an approximate fallback, even though the fallback group's - # own dates are chronologically earlier. - test "${ordered[*]}" = 'c.jpg b.jpg d.jpg a.jpg' + # Real EXIF dates (c.jpg 2021, b.jpg 2023) sort before the filename + # fallback group (a.jpg, d.jpg): a genuine timestamp is never displaced by + # an approximate fallback. Within the fallback group, a.jpg sorts before + # d.jpg by filename despite its mtime being earlier -- proving mtime plays + # no part in the fallback order. + test "${ordered[*]}" = 'c.jpg b.jpg a.jpg d.jpg' + + test::teardown +} + +# Regression test for the real-world bug found via irregular.ninja/so.war.das: +# a Fujifilm X100V whose battery died reset its clock to 2000-01-01, and every +# EXIF timestamp on that roll (DateTimeOriginal/Digitized/DateTime, all +# identical) was stamped with that bogus date instead of omitting it. Trusting +# it at face value put an entire clock-reset roll at the very FRONT of an +# album ahead of hundreds of correctly (2020s-)dated photos -- the opposite of +# what CHRONOLOGICAL_ORDER promises. old.jpg's implausible year-2000 EXIF date +# must therefore be treated the same as a missing tag (filename fallback, group +# 1), not as a real group-0 timestamp. +test_chronological_photo_files_treats_implausible_exif_year_as_camera_clock_reset() { + local incoming_dir + local dist_dir + local cache_dir + local name + local -a ordered=() + + test::setup + test::source_shuriken_lib + + incoming_dir="$TEST_TMPDIR/incoming" + dist_dir="$TEST_TMPDIR/dist" + cache_dir="$TEST_TMPDIR/cache/exif" + mkdir -p "$incoming_dir" "$dist_dir/photos" "$cache_dir" + + for name in old.jpg real.jpg; do + printf 'fake\n' > "$incoming_dir/$name" + printf 'fake\n' > "$dist_dir/photos/$name" + done + + { + photo_cache_signature 'old.jpg' "$incoming_dir/old.jpg" + printf ' exif:DateTimeOriginal: 2000:01:01 05:50:52\n' + } > "$cache_dir/old.jpg.txt" + { + photo_cache_signature 'real.jpg' "$incoming_dir/real.jpg" + printf ' exif:DateTimeOriginal: 2026:06:07 13:19:23\n' + } > "$cache_dir/real.jpg.txt" + + export INCOMING_DIR="$incoming_dir" + export DIST_DIR="$dist_dir" + + mapfile -t ordered < <(chronological_photo_files photos) + + # real.jpg (a plausible 2026 date, group 0) sorts before old.jpg (an + # implausible 2000 clock-reset date, demoted to the group-1 filename + # fallback), even though 2000 is numerically earlier than 2026. + test "${ordered[*]}" = 'real.jpg old.jpg' test::teardown } @@ -7970,8 +8025,11 @@ main() { 'stats collect reads cached identify output' \ test_stats_collect_reads_cached_identify_output test::run_case \ - 'chronological_photo_files sorts by EXIF date with mtime fallback' \ - test_chronological_photo_files_sorts_by_exif_date_with_mtime_fallback + 'chronological_photo_files sorts by EXIF date with filename fallback' \ + test_chronological_photo_files_sorts_by_exif_date_with_filename_fallback + test::run_case \ + 'chronological_photo_files treats implausible EXIF year as camera clock reset' \ + test_chronological_photo_files_treats_implausible_exif_year_as_camera_clock_reset test::run_case \ 'album/stats decoupling boundary (pn0)' \ test_album_stats_decoupling_boundary |
