Ctrl K
ComunidadGuías…

tests/test_follow_variants.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""追更变体判定的隔离测试。

标题取自 2026-08-25 实测的真实来源页:rule34video.com 的
`/models/lazyprocrastinator/`、kemono.cr 的 `fanbox/30917150` 帖子列表,以及 f95zone
`latest_data.php` 里 thread 50685 的记录。证据登记见 `docs/reference-sources.json`。
"""
import unittest
from dataclasses import dataclass

from peach.follow_variants import (
    PROVIDER_PRIORITY, VariantVerdict, classify, group_duplicates, normalize_handle,
)


ALIASES = ("LazyProcrastinator", "LazyProcrast", "Lazyprocastinator")


@dataclass(frozen=True)
class _Item:
    external_id: str
    provider: str
    release_key: str
    variant_kind: str
    published_at: str | None


class ClassifyTests(unittest.TestCase):
    def test_combined_resolution_and_frame_rate_are_version_markers(self):
        for spec, labels in (("4K60fps", "4K + 60fps"), ("1080p60fps", "1080p + 60fps"),
                             ("4K 60fps", "4K + 60fps")):
            for title in (f"Evening Movie ({spec})", f"Evening Movie {spec}"):
                with self.subTest(title=title):
                    verdict = classify(title)
                    self.assertEqual(verdict.release_key, "evening movie")
                    self.assertEqual(verdict.variant_label, labels)
        self.assertEqual(classify("Evening Movie Part 4K60fpsExtra").release_key,
                         "evening movie part 4k60fpsextra")

    def _key(self, title, **kwargs):
        return classify(title, creator_aliases=ALIASES, **kwargs)

    def test_alt_marker_groups_with_its_main_release(self):
        main = self._key("Fiona - Paizuri")
        alt = self._key("Fiona - Paizuri (Nude)")
        self.assertEqual(main.release_key, alt.release_key)
        self.assertEqual(main.variant_kind, "main")
        self.assertIsNone(main.variant_label)
        self.assertEqual(alt.variant_kind, "alt")
        self.assertEqual(alt.variant_label, "nude")

    def test_no_watermark_is_an_alt_not_a_separate_work(self):
        verdict = self._key("Bertha Riding (No Watermark)")
        self.assertEqual(verdict.variant_kind, "alt")
        self.assertEqual(verdict.variant_label, "no watermark")
        self.assertEqual(verdict.release_key, "bertha riding")
        unwatermarked = self._key("Juno Facial - 4K Unwatermarked")
        self.assertEqual(unwatermarked.release_key, "juno facial")
        self.assertEqual(unwatermarked.markers, ("4K", "no watermark"))

    def test_wip_markers_win_over_alt_markers(self):
        verdict = self._key("Mitsuru - School Movie [WIP] (nude)")
        self.assertEqual(verdict.variant_kind, "wip")
        self.assertEqual(verdict.variant_label, "WIP")
        self.assertIn("nude", verdict.markers)
        self.assertEqual(verdict.release_key, "mitsuru school movie")

    def test_bare_wip_token_is_recognized_without_brackets(self):
        self.assertEqual(self._key("Sayuri Cowgirl WIP").variant_kind, "wip")

    def test_trailing_digit_is_a_sequence_not_a_version(self):
        first = self._key("Sayuri - Cowgirl")
        second = self._key("Sayuri - Cowgirl 2")
        self.assertNotEqual(first.release_key, second.release_key)
        self.assertEqual(second.variant_kind, "main")

    def test_explicit_v2_is_an_alt_of_the_same_work(self):
        base = self._key("Riona Heartlily - Cowgirl")
        second = self._key("Riona Heartlily - Cowgirl v2")
        self.assertEqual(base.release_key, second.release_key)
        self.assertEqual(second.variant_label, "v2")

    def test_creator_handle_is_stripped_even_when_misspelled(self):
        tagged = self._key("Femboy Tighnari Footjob [Lazyprocrastinator]")
        misspelled = self._key("Femboy Tighnari Footjob (Lazyprocastinator)")
        bare = self._key("Femboy Tighnari Footjob")
        self.assertEqual(tagged.release_key, bare.release_key)
        self.assertEqual(misspelled.release_key, bare.release_key)

    def test_slash_separated_handle_group_is_stripped(self):
        verdict = classify(
            "Lazy Procrastinator Collection [2026-06-28] [LazyProcrastinator/LazyProcrast]",
            creator_aliases=ALIASES, semantics="release",
        )
        self.assertEqual(verdict.release_key, "lazy procrastinator collection")
        self.assertIsNone(verdict.version)

    def test_dated_collection_batches_are_not_versions(self):
        june = classify("Billyhhyb Collection [2026-06-28] [Billyhhyb]",
                        creator_aliases=ALIASES, semantics="release")
        july = classify("Billyhhyb Collection [2026-07-15] [Billyhhyb]",
                        creator_aliases=ALIASES, semantics="release")
        self.assertEqual(june.release_key, july.release_key)
        self.assertIsNone(june.version)
        self.assertIsNone(july.version)

    def test_work_semantics_keeps_date_brackets_in_the_key(self):
        june = self._key("Pack [2026-06-28]")
        july = self._key("Pack [2026-07-15]")
        self.assertNotEqual(june.release_key, july.release_key)
        self.assertIsNone(june.version)

    def test_release_semantics_group_successive_versions(self):
        june = classify("Lazy Procrastinator Collection", creator_aliases=ALIASES,
                        version="2026-06-28", semantics="release")
        august = classify("Lazy Procrastinator Collection", creator_aliases=ALIASES,
                          version="2026-08-24", semantics="release")
        self.assertEqual(june.release_key, august.release_key)
        self.assertNotEqual(june.version, august.version)

    def test_work_semantics_does_not_mine_versions_from_the_title(self):
        verdict = self._key("Mitsuru v3")
        self.assertIsNone(verdict.version)
        self.assertEqual(verdict.variant_label, "v3")

    def test_tag_list_bracket_is_kept_in_the_release_key(self):
        episode5 = self._key(
            "My Best Friend's Mom Is A Futa - Episode 5 [Anilingus, Light BDSM, Pegging JOI]")
        episode6 = self._key("My Best Friend's Mom Is A Futa - Episode 6 [Pegging JOI]")
        self.assertNotEqual(episode5.release_key, episode6.release_key)
        self.assertIn("anilingus", episode5.release_key)

    def test_brackets_do_not_change_the_key(self):
        with_parens = self._key("Cecilia - Belle Fingering (Zenless Zone Zero)")
        without = self._key("Cecilia - Belle Fingering Zenless Zone Zero")
        self.assertEqual(with_parens.release_key, without.release_key)

    def test_multi_marker_bracket_splits_on_commas(self):
        verdict = self._key("Yorha Commander (nude, 4k)")
        self.assertEqual(verdict.variant_kind, "alt")
        self.assertEqual(verdict.markers, ("nude", "4K"))
        self.assertEqual(verdict.release_key, "yorha commander")

    def test_resolution_and_framerate_labels_keep_their_number(self):
        self.assertEqual(self._key("Sayuri Handy [1080p]").variant_label, "1080p")
        self.assertEqual(self._key("Sayuri Handy [60fps]").variant_label, "60fps")

    def test_short_tokens_are_never_fuzzy_matched_as_handles(self):
        # riona/rinoa 只差一次换位,短 token 上模糊匹配必然误伤。
        self.assertIn("rinoa", self._key("Rinoa Heartilly").release_key)
        self.assertIn("riona", self._key("Riona Heartlily").release_key)

    def test_unknown_semantics_is_rejected(self):
        with self.assertRaises(ValueError):
            classify("x", semantics="whatever")

    def test_normalize_handle_folds_case_and_punctuation(self):
        self.assertEqual(normalize_handle("Lazy_Procrast!"), "lazyprocrast")


    def test_resolution_written_without_the_p_is_still_a_version(self):
        """`2B Camp [4K]` 与 `2B Camp [1080]` 是同一部片的两个版本。

        `[1080]` 此前留在 release_key 里(`2b camp 1080`),两张卡永远合不到一起,
        而 `[4K]` 那张的角标写着「2 个版本」,数的是 `[4K]` + `[WIP]`。
        只认已知分辨率档位:任意三四位数可能是年份或作品序号。
        """
        self.assertEqual(classify("2B Camp [4K]").release_key,
                         classify("2B Camp [1080]").release_key)
        self.assertEqual(classify("2B Camp [1080]").variant_label, "1080p")
        self.assertEqual(classify("Yuffie Photoshoot Part 3 [1080]").release_key,
                         classify("Yuffie Photoshoot Part 3 [4K]").release_key)
        # 不是分辨率的数字照旧留在键里。
        self.assertEqual(classify("Sayuri - Cowgirl 2").release_key, "sayuri cowgirl 2")
        self.assertEqual(classify("Scene [2023]").release_key, "scene 2023")
        self.assertEqual(classify("Part [1200]").release_key, "part 1200")


class GroupDuplicatesTests(unittest.TestCase):
    def test_cross_site_duplicates_fold_onto_one_primary(self):
        video = _Item("4542713", "rule34video", "fiona paizuri", "main", "2026-08-18")
        booru = _Item("998877", "rule34xxx", "fiona paizuri", "main", "2026-08-19")
        nude = _Item("4542721", "rule34video", "fiona paizuri", "alt", "2026-08-18")
        self.assertEqual(group_duplicates([booru, nude, video]), (video, video, video))

    def test_main_wins_over_alt_and_wip_regardless_of_provider(self):
        wip = _Item("1", "kemono", "mitsuru school movie", "wip", "2026-07-01")
        main = _Item("2", "f95zone", "mitsuru school movie", "main", "2026-08-01")
        self.assertEqual(group_duplicates([wip, main]), (main, main))

    def test_provider_priority_breaks_ties_between_equal_variants(self):
        self.assertLess(PROVIDER_PRIORITY["fanbox"], PROVIDER_PRIORITY["kemono"])
        self.assertLess(PROVIDER_PRIORITY["kemono"], PROVIDER_PRIORITY["rule34video"])
        low = _Item("1", "rule34video", "k", "main", "2026-08-01")
        high = _Item("2", "kemono", "k", "main", "2026-08-01")
        self.assertEqual(group_duplicates([low, high])[0], high)

    def test_items_without_a_release_key_are_skipped(self):
        orphan = _Item("1", "kemono", "", "main", None)
        self.assertEqual(group_duplicates([orphan]), (None,))

    def test_result_is_positionally_aligned_with_the_input(self):
        # 条目带 dict 字段就不可哈希,所以返回值必须按位置对齐,不能拿条目当键。
        keyed = _Item("1", "kemono", "k", "main", "2026-08-01")
        loose = _Item("2", "kemono", "", "main", "2026-08-01")
        self.assertEqual(group_duplicates([loose, keyed]), (None, keyed))

    def test_verdict_is_immutable(self):
        verdict = VariantVerdict("k", "main", None, None, ())
        with self.assertRaises(Exception):
            verdict.release_key = "other"


if __name__ == "__main__":
    unittest.main()