zapretgui/tests/test_strategy_families.py
loop-uh 8d4abf1034
All checks were successful
Zapret source guards / validate (push) Successful in 9s
Windows-сборка / Dev Dev 21.1.7.54 опубликован
Список готовых стратегий написан заново: советуемые из готовых пресетов, перебор «следующая», поиск и отборы
Что видит человек на странице профиля:
- первая группа «Советуем для этого сервиса»: стратегии, которые в готовых
  пресетах стоят на этом же сервисе, самые частые выше; метка «в N пресетах»;
- панель над списком: какая стратегия выбрана, кнопки «Работает» и
  «Не работает — следующая» (включает следующую из советуемых);
- поиск всегда на виду и ищет ещё по способу словами, описанию и автору;
  быстрые отборы: советуемые, работают у меня, не пробовал, избранное;
- значок слева показывает оценку человека (не пробовал / работает /
  не работает), способ обхода написан словами под названием;
- стратегии с одним названием сложены в одну строку с кнопкой «ещё N»;
- плитки шире, в ряд не больше трёх; экскурсия описывает новый экран.

Как устроено:
- profile/strategy_list — данные без окон: сведения о стратегии (facts),
  раскладка для профиля (plan), видимые строки (rows); всё чистые функции;
- profile/strategy_usage — частота стратегий считается из самих готовых
  пресетов: сервис опознаётся по условиям профиля;
- profile/ui/strategy_list — модель, отрисовка, список, панели, виджет.
  Экран выводится из состояния одной функцией _refresh; фонового потока,
  скрытых строк и обновления «на месте» больше нет;
- каталог стратегий теперь читает label, author и description.

Удалённые profile/strategy_grouping.py, profile/strategy_list_filter.py и
profile/ui/profile_strategy_list_widget.py уже ушли из дерева в коммите
5e82c42f: этот коммит возвращает main в рабочее состояние (страница профиля
ссылалась на удалённый файл).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:08:35 +03:00

118 lines
5.5 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 re
import unittest
from pathlib import Path
from types import SimpleNamespace
from profile.strategy_families import (
STRATEGY_FAMILIES,
strategy_count_text,
strategy_family,
strategy_family_key,
strategy_family_keys,
)
from profile.strategy_state import ProfileStrategyState
from profile.strategy_visuals import strategy_technique_keys
CATALOGS = Path(__file__).resolve().parents[1] / "src" / "system" / "strategy_catalogs"
def _entry(name: str, *desync: str):
return SimpleNamespace(name=name, args="\n".join(f"--lua-desync={value}" for value in desync))
def _catalog_entries(engine: str, name: str) -> dict:
text = (CATALOGS / engine / f"{name}.txt").read_text(encoding="utf-8")
entries = {}
for match in re.finditer(r"^\[(.*?)\]\s*$(.*?)(?=^\[|\Z)", text, flags=re.M | re.S):
body = match.group(2)
title = re.search(r"^name\s*=\s*(.*)$", body, flags=re.M)
entries[match.group(1)] = SimpleNamespace(
name=title.group(1).strip() if title else match.group(1),
args="\n".join(line for line in body.splitlines() if line.startswith("--")),
)
return entries
class StrategyFamilyKeyTests(unittest.TestCase):
def test_single_techniques(self) -> None:
self.assertEqual(strategy_family_key(("fake",)), "fake")
self.assertEqual(strategy_family_key(("multisplit",)), "split")
self.assertEqual(strategy_family_key(("split",)), "split")
self.assertEqual(strategy_family_key(("multidisorder",)), "disorder")
self.assertEqual(strategy_family_key(("hostfakesplit",)), "host")
self.assertEqual(strategy_family_key(()), "other")
self.assertEqual(strategy_family_key(("pass",)), "other")
def test_fake_combined_with_split_or_disorder(self) -> None:
self.assertEqual(strategy_family_key(("fake", "multisplit")), "fake_split")
self.assertEqual(strategy_family_key(("fake", "multidisorder")), "fake_disorder")
# Порядок строк в стратегии не важен: подмена остаётся подменой.
self.assertEqual(strategy_family_key(("multisplit", "fake")), "fake_split")
# Способы с поддельными частями сами относятся к «подмене и …».
self.assertEqual(strategy_family_key(("fakedsplit",)), "fake_split")
self.assertEqual(strategy_family_key(("fakemultidisorder",)), "fake_disorder")
# Изменение окна — добавка, группу она не меняет.
self.assertEqual(strategy_family_key(("wssize", "fake", "multisplit")), "fake_split")
def test_prelude_and_host_name_win_over_the_rest(self) -> None:
self.assertEqual(strategy_family_key(("send", "syndata", "multisplit")), "send")
self.assertEqual(strategy_family_key(("send", "syndata", "hostfakesplit")), "send")
self.assertEqual(strategy_family_key(("fake", "hostfakesplit")), "host")
def test_udp_length_change_is_its_own_family(self) -> None:
self.assertEqual(strategy_family_key(("fake", "udplen")), "fake_udplen")
self.assertEqual(strategy_family_key(("fake", "tamper")), "fake")
def test_every_family_has_russian_title_and_explanation(self) -> None:
keys = [family.key for family in STRATEGY_FAMILIES]
self.assertEqual(len(keys), len(set(keys)))
self.assertEqual(keys[-1], "other")
for family in STRATEGY_FAMILIES:
self.assertTrue(re.search("[а-яё]", family.title.lower()), family.key)
self.assertTrue(re.search("[а-яё]", family.description), family.key)
self.assertTrue(family.icon_name and family.color, family.key)
self.assertEqual(strategy_family("no-such-family").key, "other")
def test_count_text_uses_russian_plural_forms(self) -> None:
self.assertEqual(strategy_count_text(1), "1 стратегия")
self.assertEqual(strategy_count_text(3), "3 стратегии")
self.assertEqual(strategy_count_text(11), "11 стратегий")
self.assertEqual(strategy_count_text(97), "97 стратегий")
class StrategyFamilyCatalogTests(unittest.TestCase):
def test_rare_technique_folds_into_other(self) -> None:
entries = {f"fake-{n}": _entry(f"Fake {n}", "fake") for n in range(3)}
entries["split-0"] = _entry("Split 0", "multisplit")
entries["split-1"] = _entry("Split 1", "multisplit")
keys = strategy_family_keys(entries)
self.assertEqual({keys[f"fake-{n}"] for n in range(3)}, {"fake"})
self.assertEqual(keys["split-0"], "other")
self.assertEqual(keys["split-1"], "other")
def test_real_tcp_catalog_splits_into_readable_groups(self) -> None:
entries = _catalog_entries("winws2", "tcp")
keys = strategy_family_keys(entries)
counts: dict[str, int] = {}
for key in keys.values():
counts[key] = counts.get(key, 0) + 1
self.assertEqual(sum(counts.values()), len(entries))
self.assertGreaterEqual(len(counts), 6)
self.assertTrue(all(count >= 3 for key, count in counts.items() if key != "other"))
# Ни одна группа не забирает больше трети каталога: иначе деление
# ничего не упрощает.
self.assertLess(max(counts.values()), len(entries) / 3)
self.assertLess(counts.get("other", 0), len(entries) / 10)
if __name__ == "__main__":
unittest.main()