zapretgui/tests/test_name_speed.py
loop-uh ea7b618c38
All checks were successful
Zapret source guards / validate (push) Successful in 10s
Windows-сборка / Dev Dev 21.1.7.188 опубликован
Единый источник слов для всех проверок, маскирование внешнего адреса, видеоузлы YouTube вашей сети
block_view решает строки и итоги сети, компьютера, хостингов, звонков, Telegram, DNS, скорости, места фильтра и сравнения: карточки и текстовый отчёт берут их оттуда, а журнал строит движок из того же словаря. Внешний адрес в отчёте скрыт (109.252.1.xxx), провайдер и сеть остаются. Видеоузлы берутся со страницы ролика и от redirector.googlevideo.com, у узла названа его сеть, а медленный первый узел сравнивается с запасным.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 11:02:18 -04:00

287 lines
13 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 unittest
from unittest.mock import patch
from diagnostics import name_speed as ns
from diagnostics import sections
def _run(name: str, kbps: float | None, kind: str = ns.RUN_OK) -> ns.NameRun:
if kbps is None:
return ns.NameRun(name, kind)
return ns.NameRun(name, ns.RUN_OK, int(kbps * 1024), 1.0)
class _Line:
"""Линия: скорость по имени; список — замеры по очереди."""
def __init__(self, **speeds) -> None:
self.speeds = {name.replace("_", "."): value for name, value in speeds.items()}
self.asked: list[str] = []
def fetch(self, name: str) -> ns.NameRun:
self.asked.append(name)
value = self.speeds.get(name, 5000.0)
if isinstance(value, list):
value = value.pop(0) if len(value) > 1 else value[0]
if isinstance(value, str):
return ns.NameRun(name, value)
return _run(name, value)
def _verdicts(line: _Line, names) -> tuple[float | None, dict[str, ns.NameVerdict]]:
control, verdicts = ns.judge(ns.collect(names, line.fetch))
return control, {item.name: item for item in verdicts}
class NameSpeedTests(unittest.TestCase):
def test_name_that_is_slow_twice_is_throttled(self) -> None:
line = _Line(youtube_com=30.0)
control, verdicts = _verdicts(line, ["youtube.com", "github.com"])
self.assertEqual(control, 5000.0)
self.assertEqual(verdicts["youtube.com"].code, ns.SLOW)
self.assertIn("замедляют", verdicts["youtube.com"].text)
self.assertEqual(verdicts["github.com"].code, ns.FINE)
# Медленное имя перепроверено, и контроль в конце повторён.
self.assertEqual(line.asked.count("youtube.com"), 2)
self.assertEqual(line.asked[-1], ns.SERVER_HOST)
def test_single_slow_measurement_is_not_believed(self) -> None:
line = _Line(youtube_com=[30.0, 4000.0])
_control, verdicts = _verdicts(line, ["youtube.com"])
self.assertEqual(verdicts["youtube.com"].code, ns.FINE)
def test_line_that_sagged_gives_no_verdict(self) -> None:
# Контроль в начале быстрый, в конце — такой же медленный, как имя: просела сама линия.
line = _Line(**{"mirror_yandex_ru": [5000.0, 5000.0, 40.0], "youtube_com": 30.0})
_control, verdicts = _verdicts(line, ["youtube.com"])
self.assertEqual(verdicts["youtube.com"].code, ns.UNKNOWN)
self.assertIn("просела", verdicts["youtube.com"].text)
def test_cold_first_control_does_not_hide_throttling(self) -> None:
# Первая загрузка идёт медленнее («холодная») — в счёт идёт лучший из двух контролей.
line = _Line(**{"mirror_yandex_ru": [300.0, 5000.0, 5000.0], "youtube_com": 30.0})
_control, verdicts = _verdicts(line, ["youtube.com"])
self.assertEqual(verdicts["youtube.com"].code, ns.SLOW)
def test_slow_but_not_much_slower_than_control_is_fine(self) -> None:
# Медленный тариф: с именем сайта не быстрее и не медленнее, чем без него.
line = _Line(**{"mirror_yandex_ru": 200.0, "youtube_com": 150.0})
_control, verdicts = _verdicts(line, ["youtube.com"])
self.assertEqual(verdicts["youtube.com"].code, ns.FINE)
self.assertEqual(line.asked.count("youtube.com"), 1)
def test_failed_control_gives_no_verdicts_and_asks_nothing(self) -> None:
line = _Line(**{"mirror_yandex_ru": ns.RUN_CONNECT})
control, verdicts = _verdicts(line, ["youtube.com"])
self.assertEqual((control, verdicts), (None, {}))
self.assertNotIn("youtube.com", line.asked)
def test_name_that_does_not_connect_is_unknown_not_slow(self) -> None:
line = _Line(youtube_com=ns.RUN_CONNECT, discord_com=ns.RUN_REFUSED)
_control, verdicts = _verdicts(line, ["youtube.com", "discord.com"])
self.assertEqual(verdicts["youtube.com"].code, ns.UNKNOWN)
self.assertIn("не прошло", verdicts["youtube.com"].text)
self.assertIn("не отдал", verdicts["discord.com"].text)
def test_number_of_names_is_limited_and_repeats_are_dropped(self) -> None:
line = _Line()
names = [f"site{index}.example" for index in range(ns.MAX_NAMES + 4)]
ns.collect([names[0], *names], line.fetch)
asked = [name for name in line.asked if name != ns.SERVER_HOST]
self.assertEqual(asked, names[: ns.MAX_NAMES])
def test_stop_ends_collection(self) -> None:
line = _Line()
facts = ns.collect(["a.example", "b.example"], line.fetch, should_stop=lambda: len(line.asked) >= 3)
self.assertEqual([runs[0].name for runs in facts.names], ["a.example"])
def test_little_data_over_the_whole_time_is_a_speed_not_a_failure(self) -> None:
crawl = ns.NameRun("x", ns.RUN_OK, 9_000, ns.SAMPLE_SECONDS)
early = ns.NameRun("x", ns.RUN_OK, 9_000, 0.1)
self.assertAlmostEqual(crawl.kbps, 9_000 / 1024 / ns.SAMPLE_SECONDS)
self.assertIsNone(early.kbps)
class CutStreamTests(unittest.TestCase):
"""Поток, который пошёл и оборвался, — не «быстрая загрузка»."""
CONTROL = ns.NameRun(ns.SERVER_HOST, ns.RUN_OK, ns.ENOUGH_BYTES, 0.2)
def _fetch(self, by_name):
calls: list[str] = []
def fetch(name: str) -> ns.NameRun:
calls.append(name)
if name == ns.SERVER_HOST:
return self.CONTROL
queue = by_name[name]
return queue.pop(0) if len(queue) > 1 else queue[0]
return fetch, calls
def test_stream_cut_twice_is_a_cut_by_name(self) -> None:
# Сброс после 24 КБ за 0,05 с раньше читался как «быстро»: байты за короткое время.
fetch, calls = self._fetch({"rr1.googlevideo.com": [ns.NameRun("rr1.googlevideo.com", ns.RUN_CUT, 24_000, 0.05)]})
_control, verdicts = ns.judge(ns.collect(["rr1.googlevideo.com"], fetch))
self.assertEqual(calls.count("rr1.googlevideo.com"), 2)
self.assertEqual(verdicts[0].code, ns.CUT)
self.assertIn("поток дважды оборвался (самое большее после 23 КБ)", verdicts[0].text)
self.assertIsNone(ns.NameRun("x", ns.RUN_CUT, 24_000, 0.05).kbps)
def test_single_cut_followed_by_a_full_stream_is_not_a_finding(self) -> None:
fine = ns.NameRun("a.example", ns.RUN_OK, ns.ENOUGH_BYTES, 0.2)
fetch, _calls = self._fetch({"a.example": [ns.NameRun("a.example", ns.RUN_CUT, 24_000, 0.05), fine]})
_control, verdicts = ns.judge(ns.collect(["a.example"], fetch))
self.assertEqual(verdicts[0].code, ns.FINE)
def test_cut_while_the_line_itself_fails_is_not_blamed_on_the_name(self) -> None:
cut = ns.NameRun("a.example", ns.RUN_CUT, 24_000, 0.05)
facts = ns.NameFacts((self.CONTROL,), ((cut, cut),), (ns.NameRun(ns.SERVER_HOST, ns.RUN_CONNECT),))
_control, verdicts = ns.judge(facts)
self.assertEqual(verdicts[0].code, ns.UNKNOWN)
def test_download_reports_a_reset_in_the_middle_as_a_cut(self) -> None:
import socket
import ssl
import subprocess
import tempfile
import threading
from pathlib import Path
if not __import__("shutil").which("openssl"):
self.skipTest("нет openssl для пробного сертификата")
with tempfile.TemporaryDirectory() as folder:
key, cert = Path(folder) / "k.pem", Path(folder) / "c.pem"
subprocess.run(
["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", str(key), "-out", str(cert),
"-days", "1", "-subj", "/CN=test"],
check=True, capture_output=True,
)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(str(cert), str(key))
listener = socket.socket()
listener.bind(("127.0.0.1", 0))
listener.listen(2)
port = listener.getsockname()[1]
def serve() -> None:
for _ in range(1):
raw, _addr = listener.accept()
try:
tls = context.wrap_socket(raw, server_side=True)
tls.recv(4096)
tls.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 9000000\r\n\r\n" + b"x" * 30_000)
# Жёсткий сброс посреди потока — так фильтр рвёт соединение после первых килобайт.
raw.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, __import__("struct").pack("ii", 1, 0))
raw.close()
except OSError:
pass
thread = threading.Thread(target=serve, daemon=True)
thread.start()
original = socket.socket.connect
def connect(sock, address):
return original(sock, (address[0], port))
with patch.object(socket.socket, "connect", connect):
run = ns.download("127.0.0.1", "rr1.googlevideo.com", seconds=2.0)
thread.join(2)
listener.close()
self.assertEqual(run.kind, ns.RUN_CUT)
self.assertGreater(run.received, 20_000)
class NameChoiceTests(unittest.TestCase):
"""Какие имена идут на замер: открывшиеся, не контрольные, сначала по одному с сервиса."""
def test_main_host_of_each_service_first_then_second_hosts(self) -> None:
from types import SimpleNamespace
from unittest.mock import patch
from diagnostics.verdict import ReachState
def probe(host: str, *, main: bool = False, video: bool = False, state=ReachState.OK):
return SimpleNamespace(
host=host, reach_state=state, target=SimpleNamespace(main=main, discover_googlevideo=video)
)
services = {
"yt": SimpleNamespace(label="YouTube", control=False),
"dc": SimpleNamespace(label="Discord", control=False),
"ya": SimpleNamespace(label="Яндекс", control=True),
}
collected = {
# Порядок адресов — как у сервиса на самом деле: сайт, превью, видеосервер.
"yt": [probe("www.youtube.com", main=True), probe("i.ytimg.com"), probe("rr1.googlevideo.com", video=True)],
"dc": [probe("discord.com", main=True), probe("gateway.discord.gg", state=ReachState.DPI)],
"ya": [probe("ya.ru", main=True)],
}
asked: list[str] = []
def collect(names, fetch, **_kwargs):
asked.extend(names)
return ns.NameFacts((ns.NameRun(ns.SERVER_HOST, ns.RUN_CONNECT),))
run = SimpleNamespace(dns_cancelled=lambda: False, pause=lambda _seconds: True, probe_cancel=None)
with (
patch.object(sections.net_access, "known_address", return_value="192.0.2.1"),
patch.object(sections.name_speed, "collect", side_effect=collect),
):
self.assertIsNone(sections._name_speed(run, collected, services))
# Закрытый адрес и контрольный сайт не меряются; видеосервер YouTube — после главных адресов.
self.assertEqual(asked, ["www.youtube.com", "discord.com", "rr1.googlevideo.com"])
class SpeedProblemsTests(unittest.TestCase):
SPEED = {
"names": {
"items": [
{"name": "YouTube", "host": "www.youtube.com", "state": "warn", "text": "0.2 Мбит/с против 40 Мбит/с"},
{"name": "GitHub", "host": "github.com", "state": "ok", "text": "40 Мбит/с"},
]
}
}
def test_throttled_site_becomes_a_problem(self) -> None:
found = sections.speed_problems(self.SPEED, zapret_running=False)
self.assertEqual(len(found), 1)
self.assertEqual((found[0]["level"], found[0]["target"], found[0]["action"]), ("warn", "www.youtube.com", "start_zapret"))
self.assertIn("YouTube открывается, но медленно", found[0]["text"])
def test_with_zapret_running_the_preset_is_named_not_zapret(self) -> None:
found = sections.speed_problems(self.SPEED, zapret_running=True)
self.assertEqual(found[0]["action"], "strategy")
self.assertIn("выбранный пресет", found[0]["advice"][0])
def test_no_speed_section_no_problems(self) -> None:
self.assertEqual(sections.speed_problems(None, zapret_running=False), [])
self.assertEqual(sections.speed_problems({"names": None}, zapret_running=False), [])
if __name__ == "__main__":
unittest.main()