Files
photoanalyzer/tests/unit/test_album_evidence.py

118 lines
4.0 KiB
Python

"""Unit + property tests for the album-evidence aggregation helpers (US03-01):
conflict/dominant rules, deterministic ordering, location cleaning, and the content
version's stability and sensitivity.
"""
import random
from collections import Counter
from pathlib import Path
from photo_pipeline.services.albums import (
_clean,
_counts,
_dominant,
_version,
album_label,
)
def test_album_label_is_library_relative_and_keeps_nested_albums_distinct():
lib = (Path("/lib"),)
assert album_label("/lib/Urlaub/Rom/a.jpg", lib) == "Urlaub/Rom"
assert album_label("/lib/Urlaub/Venedig/a.jpg", lib) == "Urlaub/Venedig"
assert album_label("/lib/a.jpg", lib) == "(root)" # directly in the root
assert album_label("/elsewhere/x/a.jpg", lib) == "x" # outside → bare parent
assert album_label("/lib/x/a.jpg", ()) == "x" # no configured root → bare parent
# Donor parity lives in tests/characterization/test_pa_album.py.
def test_clean_drops_empty_and_literal_null_locations():
assert _clean(None) is None
assert _clean("") is None
assert _clean(" ") is None
assert _clean("null") is None
assert _clean("NULL") is None
assert _clean(" Rome ") == "Rome"
def test_counts_order_by_frequency_then_value():
counts = _counts(Counter({"beach": 1, "sunset": 3, "sea": 1}))
assert counts == [
{"value": "sunset", "count": 3},
{"value": "beach", "count": 1}, # tie broken alphabetically
{"value": "sea", "count": 1},
]
def test_dominant_requires_a_strict_winner():
assert _dominant(Counter()) == (None, False)
assert _dominant(Counter({2019: 3})) == (2019, False)
assert _dominant(Counter({2019: 3, 2020: 1})) == (2019, False)
# A tie is a conflict: no dominant value, flagged.
assert _dominant(Counter({2019: 2, 2020: 2})) == (None, True)
def test_version_is_stable_and_row_order_independent():
a = [("id1", "/f/a.jpg", "sfw", "analyzed", "d", None, 1, None, None, None, None, 2019)]
b = list(a)
two = a + [("id2", "/f/b.jpg", "sfw", "analyzed", "e", None, 2, None, None, None, None, 2019)]
assert _version(a) == _version(b)
# Reversed input order must not change the version (inputs are sorted).
assert _version(two) == _version(list(reversed(two)))
def test_version_changes_when_any_input_field_changes():
base = (
"id1",
"/f/a.jpg",
"sfw",
"analyzed",
"desc",
None,
1,
"outdoor",
None,
None,
None,
2019,
)
baseline = _version([base])
# Flip each field in turn; every change must move the hash.
for index in range(len(base)):
changed = list(base)
current = changed[index]
changed[index] = 99 if isinstance(current, int) else "CHANGED"
assert _version([tuple(changed)]) != baseline, f"field {index} did not affect the version"
def test_property_dominant_matches_a_brute_force_definition():
rng = random.Random(1234)
for _ in range(500):
counter = Counter()
for _ in range(rng.randint(0, 6)):
counter[rng.randint(2000, 2005)] += 1
value, conflict = _dominant(counter)
if not counter:
assert value is None and conflict is False
continue
top = max(counter.values())
winners = [k for k, v in counter.items() if v == top]
if len(winners) == 1:
assert value == winners[0] and conflict is False
else:
assert value is None and conflict is True
def test_property_counts_is_a_permutation_sorted_by_frequency():
rng = random.Random(99)
for _ in range(300):
counter = Counter()
for _ in range(rng.randint(0, 20)):
counter[rng.choice("abcde")] += 1
counts = _counts(counter)
# Same multiset of (value, count) pairs as the counter.
assert {(c["value"], c["count"]) for c in counts} == set(counter.items())
# Non-increasing frequency.
freqs = [c["count"] for c in counts]
assert freqs == sorted(freqs, reverse=True)