tests/test_performer_header.py
Rama master · réplica de lectura del repositorio original.
"""女优页头:五项资料与按名义分组的别名(ADR-0069),读的是 `performer_profile`(ADR-0067)。
全程临时账本,不联网。资料行由补女优资料后继的写入函数写下,页面是 minnano-av 504478
(篠田ゆう)与 494354(釈アリス)的真实页面裁剪而成,只留解析会读到的 canonical、`<h1>`
与资料表。
"""
import sqlite3
import tempfile
import unittest
from datetime import date
from pathlib import Path
from peach import minnano_av, performer_header, performer_name_display
from peach import web_contract as rm_web
from peach.entities import normalize_entity_name
from peach.genre_decisions import record_genre_decision
from peach.performer_profiles import read_profile, write_profile
from peach.web_entity import q_entity_shapes
from support.ledger import fresh_ledger
STAMP = "2026-09-24T00:00:00.000Z"
TODAY = date(2026, 9, 25)
SHINODA = """<!doctype html><html lang="ja"><head>
<link rel="canonical" href="https://www.minnano-av.com/actress504478.html">
</head><body>
<h1>篠田ゆう<span>しのだゆう / Shinoda Yu</span></h1>
<div class="act-profile">
<table width="100%" cellspacing="0" cellpadding="0" border="0">
<tr><td><h2>篠田ゆう (しのだゆう / Shinoda Yu)</h2></td></tr>
<tr><td><span>別名</span><p>篠崎ゆう子 (しのざきゆうこ / Shinozaki Yuuko)</p></td></tr>
<tr><td><span>別名</span><p>高木早希(ラグジュTV) (たかぎさき / Takagi Saki)</p></td></tr>
<tr><td><span>別名</span><p>桧山彩音(舞ワイフ) (ひやまあやね / Hiyama Ayane)</p></td></tr>
<tr><td><span>別名</span><p>橋本真紀(舞ワイフ) (はしもとまき / Hashimoto Maki)</p></td></tr>
<tr><td><span>別名</span><p>篠田杏奈(舞ワイフ) (しのだあんな / Shinoda Anna)</p></td></tr>
<tr><td><span>生年月日</span><p>1991年07月21日
(現在 <a href="actress_list.php?birthday=1991-07-21">35歳</a>)かに座</td> </p></td></tr>
<tr><td><span>サイズ</span><p>T155 / B86(<a href="actress_list.php?cup=F">Fカップ</a>) / W60 / H87 / S22</p></td></tr>
<tr><td><span>血液型</span><p><a href="actress_list.php?blood_type=O">O型</a></p></td></tr>
<tr><td><span>AV出演期間</span><p>2010年~2023年</p></td></tr>
<tr><td><span>デビュー作品</span><p>セキララ 〜今どき世代のゆるい性事情〜 03(2010年12月 02日)</p></td></tr>
<tr valign="top"><td><span>タグ</span><div class="tagarea">
<a href="actress_list.php?tag_a_id=28">美乳</a>
<a href="actress_list.php?tag_a_id=43">美尻</a>
<a href="actress_list.php?tag_a_id=238">レズ</a>
</td></tr>
</table></div></body></html>"""
ALICE = """<!doctype html><html lang="ja"><head>
<link rel="canonical" href="https://www.minnano-av.com/actress494354.html">
</head><body>
<h1>釈アリス<span>しゃくありす / Shaku Alice</span></h1>
<div class="act-profile">
<table width="100%" cellspacing="0" cellpadding="0" border="0">
<tr><td><span>サイズ</span><p>T169 / B88(<a href="actress_list.php?cup=E">Eカップ</a>) / W57 / H95 / S</p></td></tr>
<tr><td><span>AV出演期間</span><p>2024年-</p></td></tr>
</table></div></body></html>"""
class NameEntryTests(unittest.TestCase):
def test_alias_annotation_cells_have_no_names(self):
for marker in ("(注)", "(注)", "【注】"):
with self.subTest(marker=marker):
self.assertEqual(minnano_av.name_entries(
f"<p>{marker}現在は”楓カレン”が正式女優名です。…復活!! ( / )</p>"), [])
def test_alias_cells_split_names_and_carry_their_channel_note(self):
self.assertEqual(
[(entry["name"], entry["note"], entry["reading"]) for entry in
minnano_av.name_entries("<p>橋本真紀&桧山彩音(舞ワイフ名義)</p>")],
[("橋本真紀", "舞ワイフ", ""), ("桧山彩音", "舞ワイフ", "")])
# 读音栏里填的是店名:按注记收,不当读音。
self.assertEqual(minnano_av.name_entries("<p>あいな (吉原ソープ 薔薇の園 / Aina)</p>"),
[{"name": "あいな", "reading": "", "romaji": "Aina",
"note": "吉原ソープ 薔薇の園"}])
self.assertEqual(minnano_av.name_entries("美雲そら【旧名】 (みくもそら / Mikumo Sora)")[0]
["note"], "")
def test_the_raw_cells_keep_the_main_name_of_the_page(self):
"""现名那一组要页面主名;它不在资料表里,另记在原文的「名前」下。"""
self.assertEqual(minnano_av.profile(SHINODA)["raw"]["名前"], "篠田ゆう")
class Case(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name).resolve()
self.db = fresh_ledger(self.root)
self.connection = sqlite3.connect(self.db)
self.addCleanup(self.connection.close)
def entity(self, name: str, *aliases) -> int:
with self.connection:
cursor = self.connection.execute(
"INSERT INTO entity(kind,canonical_name,normalized_name,created_at,updated_at)"
" VALUES('performer',?,?,?,?)", (name, normalize_entity_name(name), STAMP, STAMP))
entity_id = int(cursor.lastrowid)
for written, source in aliases:
self.connection.execute(
"INSERT INTO entity_alias(entity_id,alias,normalized_alias,source)"
" VALUES(?,?,?,?)", (entity_id, written, normalize_entity_name(written), source))
return entity_id
def land(self, entity_id: int, page: str) -> None:
with self.connection:
write_profile(self.connection, entity_id, minnano_av.profile(page),
source="auto:performer-profile@1", source_url="https://example.invalid/",
fetched_at=STAMP)
def header(self, entity_id: int, canonical: str) -> dict:
return performer_header.header(self.connection, entity_id, canonical, today=TODAY)
class HeaderTests(Case):
def test_chinese_display_keeps_original_stage_names_and_only_changes_confirmed_glyphs(self):
for original, chinese in [('宮澤寧音', '宫泽宁音'), ('山田鈴奈', '山田铃奈'),
('本郷愛', '本乡爱'), ('天使萌', '天使萌'),
('篠田优', '篠田优'), ('槙泉奈', '槙泉奈')]:
with self.subTest(original=original):
page = {'name': original, 'aliases': []}
result = performer_name_display.writing(original, page, 'https://example.invalid/')
self.assertEqual(result['zh_cn'], chinese)
self.assertEqual(result['ja'], original)
self.assertEqual(page['name'], original)
def test_display_writings_keep_stage_names_and_raw_aliases_separate(self):
aliases = [('楓カレン', 'avdb'), ('田中柠檬', 'user:alias'),
('田中レモン', 'avdb'), ('かえでかれん', 'r18:performer'),
('Kaede Karen', 'r18:performer'), ('FC2', 'user:alias'),
('楓カレン(旧名)', 'user:alias')]
entity_id = self.entity('枫花恋', *aliases)
profile = {'kana': 'かえでかれん', 'raw': {'名前': '楓カレン', '別名': [
'楓カレン (かえでかれん / Kaede Karen)',
'田中レモン(旧名) (たなかれもん / Tanaka Remon)']}}
with self.connection:
write_profile(self.connection, entity_id, profile, source='auto:performer-profile@1',
source_url='https://example.invalid/', fetched_at=STAMP)
before = list(self.connection.iterdump())
rows = [{'alias': name, 'source': source} for name, source in aliases]
result = performer_name_display.display(self.connection, entity_id, '枫花恋', rows,
self.header(entity_id, '枫花恋')['name_groups'])
self.assertEqual(result['choices']['zh-CN'], [
{'value': '枫花恋', 'label': '枫花恋'}, {'value': '田中柠檬', 'label': '田中柠檬'}])
self.assertEqual(result['choices']['ja'], [
{'value': '枫花恋', 'label': '楓カレン'}, {'value': '田中レモン', 'label': '田中レモン'}])
self.assertEqual(result['groups']['zh-CN']['shown'], ['田中柠檬'])
self.assertEqual(result['current']['ja'], '楓カレン')
historical = performer_name_display.display(self.connection, entity_id, '田中柠檬', rows, None)
self.assertEqual(historical['current']['ja'], '田中レモン')
self.assertEqual(before, list(self.connection.iterdump()))
with self.connection:
self.connection.execute('INSERT INTO entity_external_ref(entity_id,provider,external_kind,external_id) '
"VALUES(?,'javdb','performer','sample')", (entity_id,))
self.connection.row_factory = sqlite3.Row
variants = performer_name_display.variants(self.connection)
self.assertEqual([(row['id'], row['canonical_name'], row['zh_cn'], row['ja']) for row in variants],
[(entity_id, '枫花恋', '枫花恋', '楓カレン')])
def test_missing_translation_keeps_current_spelling_and_latin_stage_names(self):
entity_id = self.entity('篠田ゆう', ('Alice', 'user:alias'))
self.land(entity_id, SHINODA)
result = performer_name_display.display(self.connection, entity_id, '篠田ゆう',
[{'alias': 'Alice', 'source': 'user:alias'}], None)
self.assertIsNone(result['current']['zh_cn'])
self.assertEqual(result['choices']['zh-CN'][0], {'value': '篠田ゆう', 'label': '篠田ゆう'})
latin = performer_name_display.writing('Alice', {'name': '', 'aliases': []}, '')
self.assertEqual((latin['zh_cn'], latin['ja']), ('Alice', 'Alice'))
def test_cached_alias_annotations_stay_in_raw_profile_only(self):
entity_id = self.entity("枫花恋", ("楓カレン", "avdb"),
("田中レモン", "auto:performer-alias@1"))
annotation = '(注)現在は”楓カレン”が正式女優名です。…復活!! ( / )'
profile = {
"kana": "かえでかれん", "raw": {
"名前": "楓カレン", "別名": [
"楓カレン (かえでかれん / Kaede Karen)",
"田中レモン(旧名) (たなかれもん / Tanaka Remon)", annotation]}}
with self.connection:
write_profile(self.connection, entity_id, profile,
source="auto:performer-profile@1", source_url="https://example.invalid/",
fetched_at=STAMP)
groups = self.header(entity_id, "枫花恋")["name_groups"]
self.assertEqual(groups["shown"], ["楓カレン", "田中レモン"])
self.assertEqual(groups["total"], 2)
self.assertEqual([(group["label"], [entry["name"] for entry in group["names"]])
for group in groups["groups"]],
[("现名", ["楓カレン"]), ("旧名义", ["田中レモン"])])
raw = read_profile(self.connection, entity_id)["raw"]
self.assertIn(annotation, raw["別名"])
def test_five_facts_and_the_name_groups_of_the_header(self):
shinoda = self.entity(
"篠田优", ("篠田ゆう", "avdb"), ("Yu Shinoda", "r18:performer"),
("しのだゆう", "r18:performer"), ("篠田杏奈", "user:alias"), ("城田優子", "auto:x@1"),
("ゆうちゃん企画", "javinizer:planning-alias"))
self.land(shinoda, SHINODA)
head = self.header(shinoda, "篠田优")
self.assertEqual(head["profile"], {
"birth_date": "1991-07-21", "age": 35, "height": 155, "bust": 86, "waist": 60,
"hip": 87, "cup": "F", "debut_date": "2010-12-02",
"debut_title": "セキララ 〜今どき世代のゆるい性事情〜 03",
"active": {"from": 2010, "to": 2023}, "tags": ["美乳", "美臀", "百合"]})
groups = head["name_groups"]
self.assertEqual(groups["reading"], "しのだゆう")
self.assertEqual(groups["shown"], ["篠田ゆう", "篠崎ゆう子", "高木早希"])
self.assertEqual(groups["total"], 7)
self.assertEqual([(group["label"], [entry["name"] for entry in group["names"]])
for group in groups["groups"]],
[("现名", ["篠田ゆう"]), ("旧名义", ["篠崎ゆう子"]),
("舞ワイフ", ["桧山彩音", "橋本真紀", "篠田杏奈"]),
("ラグジュTV", ["高木早希"]), ("其它", ["城田優子"])])
self.assertEqual(groups["groups"][0]["names"][0]["reading"], "しのだゆう")
def test_an_open_ended_period_is_still_active(self):
alice = self.entity("释爱丽丝")
self.land(alice, ALICE)
profile = self.header(alice, "释爱丽丝")["profile"]
self.assertEqual(profile["active"], {"from": 2024, "ongoing": True})
self.assertEqual((profile["height"], profile["cup"]), (169, "E"))
self.assertNotIn("birth_date", profile)
def test_without_a_profile_the_ledger_aliases_form_one_untitled_group(self):
alice = self.entity("释爱丽丝", ("Alice Shaku", "avdb"), ("しゃくありす", "avdb"),
("釈アリス", "avdb"))
head = self.header(alice, "释爱丽丝")
self.assertEqual(head["profile"], {})
self.assertEqual(head["name_groups"], {
"reading": "しゃくありす", "shown": ["釈アリス"], "total": 1,
"groups": [{"label": "", "names": [{"name": "釈アリス"}]}]})
def test_site_tags_speak_peach_chinese_and_follow_the_user_decisions(self):
raw = ["美人", "美少女", "清楚", "巨尻", "美尻", "ロリ→ギャル", "美乳美尻",
"カリビアン", "まだ知らない分類", " "]
self.assertEqual(performer_header.site_tags(raw),
["高颜值", "清纯", "巨臀", "美臀", "萝莉→辣妹", "美乳", "まだ知らない分類"])
with self.connection:
record_genre_decision(self.connection, "剛毛", "浓密阴毛", STAMP)
record_genre_decision(self.connection, "美尻", None, STAMP)
shinoda = self.entity("篠田优")
self.land(shinoda, SHINODA.replace(">レズ<", ">剛毛<"))
self.assertEqual(self.header(shinoda, "篠田优")["profile"]["tags"], ["美乳", "浓密阴毛"])
def test_age_turns_on_the_birthday(self):
self.assertEqual(performer_header.age_on("1991-07-21", date(2026, 7, 20)), 34)
self.assertEqual(performer_header.age_on("1991-07-21", date(2026, 7, 21)), 35)
self.assertIsNone(performer_header.age_on("1991-07", date(2026, 7, 21)))
def test_a_ledger_before_migration_0036_still_serves_the_header(self):
shinoda = self.entity("篠田优", ("篠田ゆう", "avdb"))
with self.connection:
self.connection.execute("DROP TABLE performer_profile")
head = self.header(shinoda, "篠田优")
self.assertEqual(head["profile"], {})
self.assertEqual(head["name_groups"]["shown"], ["篠田ゆう"])
self.assertEqual(performer_header.profiled(self.connection), set())
class ApiTests(Case):
def test_the_entity_api_and_the_skeleton_shapes_carry_the_profile(self):
shinoda = self.entity("篠田优", ("篠田ゆう", "avdb"))
self.land(shinoda, SHINODA)
(self.root / "logos").mkdir()
(self.root / "avatars").mkdir()
contract = rm_web.WebContract(self.db, avatar_root=self.root / "avatars",
logo_root=self.root / "logos")
page = rm_web.q_entity(contract, {"kind": "performer", "name": "篠田优"})
self.assertEqual(page["profile"]["cup"], "F")
self.assertEqual(page["name_groups"]["shown"][0], "篠田ゆう")
shapes = q_entity_shapes(contract, {})
parts = {item["id"]: item["parts"] for item in shapes["entities"]}
self.assertIn("facts", parts[shinoda])
if __name__ == "__main__":
unittest.main()