"""Characterize EXIF behavior (donor: build_exif_caption_from_result, read_existing_exif, write_exif merge/idempotency, _rec_has_nsfw, filter_nsfw_tagged). Uses the real exiftool on synthetic temp files.""" import subprocess import pytest import photo_analyzer as pa from conftest import EXIFTOOL needs_exiftool = pytest.mark.skipif(not EXIFTOOL, reason="exiftool not on PATH") RESULT = { "description": "A dog on a beach.", "tags": ["dog", "beach"], "mood": "joyful", "location_hint": "Rome, Italy", "approx_year": 2015, } def test_caption_golden_full(): assert pa.build_exif_caption_from_result(RESULT) == ( "A dog on a beach. | Tags: dog, beach | Mood: joyful | " "Location: Rome, Italy | ~2015") def test_caption_golden_sparse(): assert pa.build_exif_caption_from_result( {"description": "X.", "tags": [], "location_hint": None}) == "X." def test_rec_has_nsfw_list_scalar_case(): assert pa._rec_has_nsfw({"Keywords": ["beach", "NSFW "]}) assert pa._rec_has_nsfw({"Subject": "nsfw"}) assert not pa._rec_has_nsfw({"Keywords": ["sfw"], "Subject": ["beach"]}) assert not pa._rec_has_nsfw({}) @needs_exiftool def test_write_exif_roundtrip_and_idempotent(img): caption = pa.build_exif_caption_from_result(RESULT) assert pa.write_exif(str(img), caption, RESULT["tags"]) rec = pa.read_existing_exif(str(img)) assert rec["ImageDescription"] == caption assert rec["XPComment"] == caption assert rec["Subject"] == RESULT["tags"] assert rec["Keywords"] == RESULT["tags"] # second write with the same caption must not duplicate anything assert pa.write_exif(str(img), caption, RESULT["tags"]) rec2 = pa.read_existing_exif(str(img)) assert rec2["ImageDescription"] == caption assert rec2["Keywords"] == RESULT["tags"] @needs_exiftool def test_write_exif_preserves_existing_metadata(img): subprocess.run( ["exiftool", "-m", "-overwrite_original", "-Artist=Original Artist", "-ImageDescription=User caption", "-Keywords=userkw", str(img)], check=True, capture_output=True) assert pa.write_exif(str(img), "AI caption", ["aitag"]) rec = pa.read_existing_exif(str(img)) # donor behavior: AI caption prepended, user caption kept after ' | ' assert rec["ImageDescription"] == "AI caption | User caption" kws = rec["Keywords"] if isinstance(rec["Keywords"], list) else [rec["Keywords"]] assert "userkw" in kws and "aitag" in kws, "keywords merge, never replace" full = subprocess.run(["exiftool", "-j", "-Artist", str(img)], capture_output=True, text=True) assert "Original Artist" in full.stdout, "non-owned field preserved" @needs_exiftool def test_filter_nsfw_tagged_splits_and_normalizes(img, tmp_path): from conftest import write_fixture clean = write_fixture("fx-blocks-02", tmp_path / "clean.jpg") subprocess.run(["exiftool", "-m", "-overwrite_original", "-Keywords=nsfw", "-Subject=nsfw", str(img)], check=True, capture_output=True) kept, skipped = pa.filter_nsfw_tagged([str(img), str(clean)]) assert kept == [str(clean)] assert skipped == [str(img)] def test_filter_nsfw_tagged_empty_list_noop(): assert pa.filter_nsfw_tagged([]) == ([], [])