zapretgui/tests/test_profile_icon_bundle.py
loop-uh 8d912eba13
All checks were successful
Zapret source guards / validate (push) Successful in 9s
Windows-сборка / Dev Dev 21.1.7.9 опубликован
Профили: настоящие значки вместо букв у Apple, Claude, Gemini, Notion, LinkedIn, ChatGPT, git.zapret.moe, LordFilm, Akamai, Railway и Google Cloud; для значков, которых нет в Simple Icons, заведён свой файл SVG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:02:08 +03:00

228 lines
9.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import os
import sys
import unittest
from pathlib import Path
PROJECT_SRC = Path(__file__).resolve().parents[1] / "src"
if str(PROJECT_SRC) not in sys.path:
sys.path.insert(0, str(PROJECT_SRC))
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication
import profile.ui.profile_icon as profile_icon
from profile.ui.simple_icons_bundle import SIMPLE_ICON_SVGS
def _catalog_simple_slugs() -> set[str]:
"""Собирает simple-слаги из каталога иконок тем же способом, что и генератор."""
import profile.icons as profile_icons
slugs: set[str] = set()
for attr_name in dir(profile_icons):
attr = getattr(profile_icons, attr_name)
if not isinstance(attr, dict):
continue
for value in attr.values():
icon_name = str(getattr(value, "icon_name", "") or "")
if not icon_name.startswith("simple:"):
continue
slug = icon_name.removeprefix("simple:").partition(":")[0]
slug = slug.strip().lower().replace("-", "")
if slug:
slugs.add(slug)
return slugs
class SimpleIconsBundleTests(unittest.TestCase):
"""Рантайм рисует брендовые иконки из сгенерированного бандла, без simplepycons."""
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
profile_icon._PROFILE_PIXMAP_CACHE.clear()
def test_bundle_covers_all_catalog_slugs(self) -> None:
"""При добавлении сервиса в profile/icons.py нужно перегенерировать бандл:
PYTHONPATH=src python tools/generate_profile_icon_bundle.py"""
catalog_slugs = _catalog_simple_slugs()
self.assertTrue(catalog_slugs, "каталог не дал ни одного simple-слага")
missing = sorted(catalog_slugs - set(SIMPLE_ICON_SVGS))
self.assertEqual(missing, [], f"бандл не покрывает слаги каталога: {missing}")
def test_bundle_entries_are_valid_svgs(self) -> None:
for slug, (primary_color, raw_svg) in SIMPLE_ICON_SVGS.items():
with self.subTest(slug=slug):
self.assertTrue(raw_svg.lstrip().startswith("<svg"), "не SVG")
if primary_color:
self.assertRegex(primary_color, r"^#[0-9A-Fa-f]{6}$")
def test_simple_icon_renders_from_bundle(self) -> None:
pixmap = profile_icon.profile_icon_pixmap(
"simple:youtube:YT",
color="#FF0000",
size=18,
)
self.assertFalse(pixmap.isNull())
cache_kinds = {key[0] for key in profile_icon._PROFILE_PIXMAP_CACHE}
self.assertIn("simple", cache_kinds, "иконка не отрисована из бандла")
self.assertNotIn("initials", cache_kinds, "не должно быть заглушки для иконки из бандла")
def test_vencord_icon_renders_from_bundle(self) -> None:
pixmap = profile_icon.profile_icon_pixmap(
"simple:vencord:VC",
color="#EB7396",
size=18,
)
self.assertFalse(pixmap.isNull())
self.assertTrue(any(key[0] == "simple" and key[1] == "vencord" for key in profile_icon._PROFILE_PIXMAP_CACHE))
def test_unknown_slug_falls_back_to_initials(self) -> None:
pixmap = profile_icon.profile_icon_pixmap(
"simple:definitely-not-in-bundle:NB",
color="#3B82F6",
size=18,
)
self.assertFalse(pixmap.isNull())
cache_kinds = {key[0] for key in profile_icon._PROFILE_PIXMAP_CACHE}
self.assertIn("initials", cache_kinds)
def test_runtime_never_imports_simplepycons(self) -> None:
"""Регрессия: импорт simplepycons (~3400 модулей, ~2.6с) морозил первую
отрисовку списка профилей. Рантайм обязан обходиться бандлом."""
for slug in list(SIMPLE_ICON_SVGS)[:5]:
profile_icon.profile_icon_pixmap(f"simple:{slug}:XX", color="", size=18)
self.assertNotIn("simplepycons", sys.modules)
def test_profile_icon_module_has_no_simplepycons_reference(self) -> None:
source = (PROJECT_SRC / "profile" / "ui" / "profile_icon.py").read_text(encoding="utf-8")
self.assertNotIn("import simplepycons", source)
self.assertNotIn("from simplepycons", source)
def _catalog_own_slugs() -> set[str]:
import profile.icons as profile_icons
slugs: set[str] = set()
for attr_name in dir(profile_icons):
attr = getattr(profile_icons, attr_name)
if not isinstance(attr, dict):
continue
for value in attr.values():
icon_name = str(getattr(value, "icon_name", "") or "")
if icon_name.startswith("own:"):
slugs.add(icon_name.removeprefix("own:").partition(":")[0].strip().lower())
return slugs
class OwnIconsTests(unittest.TestCase):
"""Свои SVG-значки — для сервисов, которых нет в Simple Icons."""
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
profile_icon._PROFILE_PIXMAP_CACHE.clear()
def _painted_pixels(self, pixmap) -> int:
image = pixmap.toImage()
return sum(
1
for x in range(image.width())
for y in range(image.height())
if image.pixelColor(x, y).alpha() > 40
)
def test_every_own_icon_used_by_the_catalog_exists(self) -> None:
from profile.ui.own_icons import OWN_ICON_SVGS
used = _catalog_own_slugs()
self.assertIn("openai", used)
self.assertEqual(sorted(used - set(OWN_ICON_SVGS)), [])
def test_own_icons_are_plain_one_color_svgs(self) -> None:
from profile.ui.own_icons import OWN_ICON_SVGS
for slug, (color, raw_svg) in OWN_ICON_SVGS.items():
with self.subTest(slug=slug):
self.assertRegex(color, r"^#[0-9A-Fa-f]{6}$")
self.assertTrue(raw_svg.startswith("<svg"))
self.assertIn('viewBox="0 0 24 24"', raw_svg)
# Цвет подставляет программа: своего fill у рисунка быть не должно.
self.assertNotIn("fill=", raw_svg.split(">", 1)[0])
# Имя не должно совпадать с набором Simple Icons: генератор
# перезаписывает его файл целиком.
self.assertNotIn(slug, SIMPLE_ICON_SVGS)
def test_chatgpt_profile_draws_the_openai_logo_in_the_asked_color(self) -> None:
from profile.icons import resolve_profile_icon
icon = resolve_profile_icon("chatgpt", ("--filter-tcp=80-65535", "--hostlist=lists/chatgpt.txt"))
self.assertEqual(icon.icon_name, "own:openai:AI")
pixmap = profile_icon.profile_icon_pixmap(icon.icon_name, color="#10A37F", size=18)
self.assertFalse(pixmap.isNull())
self.assertGreater(self._painted_pixels(pixmap), 100)
# Линии логотипа тонкие, поэтому края сглажены: смотрим на плотные точки.
image = pixmap.toImage()
solid = [
image.pixelColor(x, y)
for x in range(image.width())
for y in range(image.height())
if image.pixelColor(x, y).alpha() > 150
]
self.assertGreater(len(solid), 20)
for color in solid:
self.assertLessEqual(abs(color.red() - 0x10), 4)
self.assertLessEqual(abs(color.green() - 0xA3), 4)
self.assertLessEqual(abs(color.blue() - 0x7F), 4)
# Не буквы на цветной плашке: у запасного значка закрашен весь квадрат.
fallback = profile_icon.profile_icon_pixmap("own:no-such-icon:AI", color="#10A37F", size=18)
self.assertGreater(self._painted_pixels(fallback), self._painted_pixels(pixmap) + 60)
def test_group_of_chatgpt_profiles_can_show_the_logo_in_the_tile_header(self) -> None:
from profile.list_view_state import build_profile_list_view_state
from profile.state import ProfileListItem
items = tuple(
ProfileListItem(
key=f"gpt-{position}",
persistent_key=f"gpt-{position}",
profile_index=position,
display_name="chatgpt",
enabled=True,
in_preset=True,
strategy_id="pass",
strategy_name="pass",
match_lines=("--filter-tcp=443", "--hostlist=lists/chatgpt.txt"),
list_type="hostlist",
rating="",
favorite=False,
group="ai",
group_name="ИИ",
order=position,
profile_name="chatgpt",
)
for position in range(2)
)
state = build_profile_list_view_state(
items, active_profile_types={"all"}, search_query="", group_expanded={}
)
self.assertEqual(state.rows[0]["icon_name"], "own:openai:AI")
self.assertTrue(all(row["icon_in_header"] for row in state.rows[1:]))
if __name__ == "__main__":
unittest.main()