booru-viewer/tests/gui/test_search_controller.py
pax 562c03071b test: Phase 2 — add 64 tests for extracted pure functions
5 new test files covering the pure-function extractions from Phase 1:
- test_search_controller.py (24): tag building, blacklist filtering, backfill
- test_window_state.py (16): geometry parsing, splitter parsing, hyprctl cmds
- test_media_controller.py (9): prefetch ring-expansion ordering
- test_post_actions.py (10): batch message detection, library membership
- test_popout_controller.py (3): video sync dict shape

All import-pure (no PySide6, no mpv, no httpx). Total suite: 186 tests.
2026-04-10 15:20:57 -05:00

219 lines
6.3 KiB
Python

"""Tests for search_controller -- tag building, blacklist filtering, backfill decisions.
Pure Python. No Qt, no network, no QApplication.
"""
from __future__ import annotations
from typing import NamedTuple
import pytest
from booru_viewer.gui.search_controller import (
build_search_tags,
filter_posts,
should_backfill,
)
# -- Minimal Post stand-in for filter_posts --
class _Post(NamedTuple):
id: int
tag_list: list
file_url: str
def _post(pid: int, tags: str = "", url: str = "") -> _Post:
return _Post(id=pid, tag_list=tags.split() if tags else [], file_url=url)
# ======================================================================
# build_search_tags
# ======================================================================
# -- Rating mapping --
def test_danbooru_rating_uses_single_letter():
result = build_search_tags("cat_ears", "explicit", "danbooru", 0, "All")
assert "rating:e" in result
def test_gelbooru_rating_uses_full_word():
result = build_search_tags("", "questionable", "gelbooru", 0, "All")
assert "rating:questionable" in result
def test_e621_maps_general_to_safe():
result = build_search_tags("", "general", "e621", 0, "All")
assert "rating:s" in result
def test_e621_maps_sensitive_to_safe():
result = build_search_tags("", "sensitive", "e621", 0, "All")
assert "rating:s" in result
def test_moebooru_maps_general_to_safe():
result = build_search_tags("", "general", "moebooru", 0, "All")
assert "rating:safe" in result
def test_all_rating_adds_nothing():
result = build_search_tags("cat", "all", "danbooru", 0, "All")
assert "rating:" not in result
# -- Score filter --
def test_score_filter():
result = build_search_tags("", "all", "danbooru", 50, "All")
assert "score:>=50" in result
def test_score_zero_adds_nothing():
result = build_search_tags("", "all", "danbooru", 0, "All")
assert "score:" not in result
# -- Media type filter --
def test_media_type_animated():
result = build_search_tags("", "all", "danbooru", 0, "Animated")
assert "animated" in result
def test_media_type_video():
result = build_search_tags("", "all", "danbooru", 0, "Video")
assert "video" in result
def test_media_type_gif():
result = build_search_tags("", "all", "danbooru", 0, "GIF")
assert "animated_gif" in result
def test_media_type_audio():
result = build_search_tags("", "all", "danbooru", 0, "Audio")
assert "audio" in result
# -- Combined --
def test_combined_has_all_tokens():
result = build_search_tags("1girl", "explicit", "danbooru", 10, "Video")
assert "1girl" in result
assert "rating:e" in result
assert "score:>=10" in result
assert "video" in result
# ======================================================================
# filter_posts
# ======================================================================
def test_removes_blacklisted_tags():
posts = [_post(1, tags="cat dog"), _post(2, tags="bird")]
seen: set = set()
filtered, drops = filter_posts(posts, bl_tags={"dog"}, bl_posts=set(), seen_ids=seen)
assert len(filtered) == 1
assert filtered[0].id == 2
assert drops["bl_tags"] == 1
def test_removes_blacklisted_posts_by_url():
posts = [_post(1, url="http://a.jpg"), _post(2, url="http://b.jpg")]
seen: set = set()
filtered, drops = filter_posts(posts, bl_tags=set(), bl_posts={"http://a.jpg"}, seen_ids=seen)
assert len(filtered) == 1
assert filtered[0].id == 2
assert drops["bl_posts"] == 1
def test_deduplicates_across_batches():
"""Dedup works against seen_ids accumulated from prior batches.
Within a single batch, the list comprehension fires before the
update, so same-id posts in one batch both survive -- cross-batch
dedup catches them on the next call."""
posts_batch1 = [_post(1)]
seen: set = set()
filter_posts(posts_batch1, bl_tags=set(), bl_posts=set(), seen_ids=seen)
assert 1 in seen
# Second batch with same id is deduped
posts_batch2 = [_post(1), _post(2)]
filtered, drops = filter_posts(posts_batch2, bl_tags=set(), bl_posts=set(), seen_ids=seen)
assert len(filtered) == 1
assert filtered[0].id == 2
assert drops["dedup"] == 1
def test_respects_previously_seen_ids():
posts = [_post(1), _post(2)]
seen: set = {1}
filtered, drops = filter_posts(posts, bl_tags=set(), bl_posts=set(), seen_ids=seen)
assert len(filtered) == 1
assert filtered[0].id == 2
assert drops["dedup"] == 1
def test_all_three_interact():
"""bl_tags, bl_posts, and cross-batch dedup all apply in sequence."""
# Seed seen_ids so post 3 is already known
seen: set = {3}
posts = [
_post(1, tags="bad", url="http://a.jpg"), # hit by bl_tags
_post(2, url="http://blocked.jpg"), # hit by bl_posts
_post(3), # hit by dedup (in seen)
_post(4), # survives
]
filtered, drops = filter_posts(
posts, bl_tags={"bad"}, bl_posts={"http://blocked.jpg"}, seen_ids=seen,
)
assert len(filtered) == 1
assert filtered[0].id == 4
assert drops["bl_tags"] == 1
assert drops["bl_posts"] == 1
assert drops["dedup"] == 1
def test_empty_lists_pass_through():
posts = [_post(1), _post(2)]
seen: set = set()
filtered, drops = filter_posts(posts, bl_tags=set(), bl_posts=set(), seen_ids=seen)
assert len(filtered) == 2
assert drops == {"bl_tags": 0, "bl_posts": 0, "dedup": 0}
def test_filter_posts_mutates_seen_ids():
posts = [_post(10), _post(20)]
seen: set = set()
filter_posts(posts, bl_tags=set(), bl_posts=set(), seen_ids=seen)
assert seen == {10, 20}
# ======================================================================
# should_backfill
# ======================================================================
def test_backfill_yes_when_under_limit_and_api_not_short():
assert should_backfill(collected_count=10, limit=40, last_batch_size=40) is True
def test_backfill_no_when_collected_meets_limit():
assert should_backfill(collected_count=40, limit=40, last_batch_size=40) is False
def test_backfill_no_when_api_returned_short():
assert should_backfill(collected_count=10, limit=40, last_batch_size=20) is False
def test_backfill_no_when_both_met():
assert should_backfill(collected_count=40, limit=40, last_batch_size=20) is False