feat: color-coded status output + --no-color flag
- Green for entries with valid EXIF date - Yellow for entries with file system creation fallback - Red for skipped entries (no date found) - --no-color flag disables all ANSI escape sequences - Flag works before or after the path argument - Switched from sys.argv to argparse for proper flag parsing
This commit is contained in:
1 parent
c284840dd3
commit
53ba943698
2 files changed
+206
-6
No files matched your search
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Verify year-based final statistics aggregation in organize.py.
|
||||
|
||||
Creates test images with different EXIF years (and some without EXIF),
|
||||
runs the organize function, and checks the output summary.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
# Make sure we can import organize
|
||||
HERE = Path(__file__).parent.absolute()
|
||||
if str(HERE) not in sys.path:
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from PIL import Image
|
||||
from PIL.ExifTags import Base as ExifBase
|
||||
|
||||
from organize import organize
|
||||
|
||||
|
||||
def _make_image(tmpdir: Path, name: str, year: int | None = None) -> Path:
|
||||
"""Create a test image. If *year* is given, embed EXIF DateTimeOriginal."""
|
||||
img = Image.new("RGB", (16, 16), color="red")
|
||||
path = tmpdir / name
|
||||
|
||||
if year is not None:
|
||||
exif_data = img.getexif()
|
||||
exif_data[ExifBase.DateTimeOriginal] = f"{year:04d}:06:15 12:00:00"
|
||||
img.save(path, exif=exif_data.tobytes())
|
||||
else:
|
||||
img.save(path) # no EXIF
|
||||
|
||||
img.close()
|
||||
return path
|
||||
|
||||
|
||||
def test_stats_summary_format() -> None:
|
||||
"""Check that the summary section is printed after per-line output."""
|
||||
import io
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tmp = Path(td)
|
||||
|
||||
# Create images with known years
|
||||
_make_image(tmp, "img_2023.jpg", year=2023)
|
||||
_make_image(tmp, "img_2024a.jpg", year=2024)
|
||||
_make_image(tmp, "img_2024b.jpg", year=2024)
|
||||
_make_image(tmp, "img_2025.jpg", year=2025)
|
||||
_make_image(tmp, "no_exif.jpg", year=None) # skipped in our test
|
||||
|
||||
# We just check it doesn't crash and returns correct exit
|
||||
# We'll capture stdout to verify the summary
|
||||
old_stdout = sys.stdout
|
||||
captured = io.StringIO()
|
||||
sys.stdout = captured
|
||||
try:
|
||||
organize(tmp, no_color=True)
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
|
||||
output = captured.getvalue()
|
||||
|
||||
# Verify the summary section exists
|
||||
assert "=== Year Summary ===" in output, "Year summary header missing"
|
||||
|
||||
# Verify per-year counts
|
||||
assert "2023: 1 entries" in output, "Year 2023 count wrong"
|
||||
assert "2024: 2 entries" in output, "Year 2024 count wrong"
|
||||
assert "2025: 1 entries" in output, "Year 2025 count wrong"
|
||||
|
||||
# Verify category counts
|
||||
assert "Total with EXIF date:" in output
|
||||
assert "Total with FS date:" in output
|
||||
assert "Total skipped:" in output
|
||||
|
||||
# Summary appears after per-line output (no interleaving check via text position)
|
||||
|
||||
print("test_stats_summary_format: PASS")
|
||||
|
||||
|
||||
def test_stats_counts_match() -> None:
|
||||
"""Verify that category totals add up correctly."""
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tmp = Path(td)
|
||||
|
||||
# 3 with EXIF, 1 without (uses FS date via mtime), 1 corrupt (skipped)
|
||||
_make_image(tmp, "a_2022.jpg", year=2022)
|
||||
_make_image(tmp, "b_2023.jpg", year=2023)
|
||||
_make_image(tmp, "c_2024.jpg", year=2024)
|
||||
|
||||
# Image without EXIF — falls back to FS date
|
||||
img = Image.new("RGB", (8, 8), color="blue")
|
||||
p = tmp / "no_exif.jpg"
|
||||
img.save(p)
|
||||
img.close()
|
||||
|
||||
# Corrupt file with valid extension — skipped
|
||||
(tmp / "corrupt.jpg").write_text("not an image")
|
||||
|
||||
old_stdout = sys.stdout
|
||||
captured = io.StringIO()
|
||||
sys.stdout = captured
|
||||
try:
|
||||
organize(tmp, no_color=True)
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
|
||||
output = captured.getvalue()
|
||||
|
||||
# Verify counts
|
||||
assert "Total with EXIF date: 3" in output
|
||||
assert "Total with FS date: 1" in output
|
||||
assert "Total skipped: 1" in output
|
||||
assert "Total processed: 5" in output
|
||||
|
||||
print("test_stats_counts_match: PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import io
|
||||
test_stats_summary_format()
|
||||
test_stats_counts_match()
|
||||
print("All stats tests passed!")
|
||||
Reference in new issue
Block a user