zapretgui/tests/test_blockcheck_probe_budget.py
loop-uh f88541fa6b Перестроить BlockCheck на единый планировщик проб
Проверки шли семью последовательными фазами, и один хост резолвился до пяти
раз: в preflight, в каждой TLS-пробе, в ISP-пробе и в ping-пробе. Теперь этапа
три — опорная точка с однократным резолвом, параллельная группа проб под общим
дедлайном и чистый анализ, — а массовый preflight убран: те же проверки делает
планировщик, и отдельная фаза означала только двойной коннект к каждому хосту.

Вместе с этим:

- вердикт вынесен в отдельный движок с сигнатурами, данные списков — в
  data_lists, подписи DPI для интерфейса — в ui/dpi_labels;
- удалён blockcheck2.py и файлы данных, которые он обслуживал;
- наблюдатель за зависаниями интерфейса ставится из entry.py, где живёт event
  loop, а не из post-startup задач;
- profile payload и background worker gate приведены к одному пути обновления.

Проверки: compileall, architecture_checks, 369 тестов затронутых областей.
Полный прогон — 4121 passed; падения в preset-UI (Qt/fluent на Linux),
preset_folder_actions (тест не знает про папку 1.10.0) и build_resource_layout
(состояние приватного репозитория) существовали и до этих правок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:46:08 +03:00

146 lines
4.7 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.

"""Бюджет HTTPS-пробы: хост с несколькими адресами не стоит N таймаутов.
Регрессия: ``test_https`` перебирал все A/AAAA-записи подряд, каждую с полным
таймаутом. Хост с четырьмя адресами и блокировкой стоил 40 секунд на одну
пробу, а таких проб на цель приходится три.
"""
from __future__ import annotations
import socket
import sys
import time
import unittest
from pathlib import Path
from unittest.mock import patch
PROJECT_SRC = Path(__file__).resolve().parents[1] / "src"
if str(PROJECT_SRC) not in sys.path:
sys.path.insert(0, str(PROJECT_SRC))
from blockcheck import tls_tester # noqa: E402
from blockcheck.config import HTTPS_CONNECT_TIMEOUT, HTTPS_MAX_ADDRESSES # noqa: E402
from blockcheck.models import TestStatus # noqa: E402
FIVE_ADDRESSES = [
(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", (f"10.0.0.{i}", 443))
for i in range(1, 6)
]
class _Attempts:
def __init__(self, sleep_factor: float = 0.0):
self.timeouts: list[float] = []
self.sleep_factor = sleep_factor
class _FakeSocket:
def __init__(self, attempts: _Attempts):
self._attempts = attempts
self.timeout: float | None = None
def settimeout(self, value):
self.timeout = value
def close(self):
pass
class _FakeSSLSocket(_FakeSocket):
"""Соединение, которое всегда упирается в таймаут."""
def connect(self, _address):
self._attempts.timeouts.append(float(self.timeout or 0.0))
if self._attempts.sleep_factor:
time.sleep(float(self.timeout or 0.0) * self._attempts.sleep_factor)
raise socket.timeout("timed out")
class _FakeContext:
minimum_version = None
maximum_version = None
def __init__(self, attempts: _Attempts):
self._attempts = attempts
def wrap_socket(self, sock, server_hostname=None):
ssl_sock = _FakeSSLSocket(self._attempts)
ssl_sock.timeout = sock.timeout
return ssl_sock
class ProbeBudgetTests(unittest.TestCase):
def _run(
self,
*,
timeout: int,
sleep_factor: float = 0.0,
connect_timeout: float | None = None,
) -> tuple[_Attempts, object]:
attempts = _Attempts(sleep_factor=sleep_factor)
patches = [
patch("blockcheck.tls_tester.resolve_addrinfo", return_value=FIVE_ADDRESSES),
patch(
"blockcheck.tls_tester.socket.socket",
side_effect=lambda *a, **k: _FakeSocket(attempts),
),
patch(
"blockcheck.tls_tester.ssl.create_default_context",
side_effect=lambda: _FakeContext(attempts),
),
]
if connect_timeout is not None:
patches.append(
patch("blockcheck.tls_tester.HTTPS_CONNECT_TIMEOUT", connect_timeout)
)
for item in patches:
item.start()
try:
result = tls_tester.test_https(
"blocked.example", timeout=timeout, ip_family="ipv4",
)
finally:
for item in reversed(patches):
item.stop()
return attempts, result
def test_only_a_couple_of_addresses_are_tried(self) -> None:
attempts, result = self._run(timeout=30)
self.assertLessEqual(len(attempts.timeouts), HTTPS_MAX_ADDRESSES)
self.assertEqual(result.status, TestStatus.TIMEOUT)
def test_single_attempt_never_waits_longer_than_the_connect_timeout(self) -> None:
attempts, _ = self._run(timeout=30)
for value in attempts.timeouts:
self.assertLessEqual(value, HTTPS_CONNECT_TIMEOUT)
def test_total_wait_stays_within_the_call_budget(self) -> None:
"""Бюджет делится между адресами, а не выдаётся каждому целиком."""
budget = 2
attempts, _ = self._run(timeout=budget, sleep_factor=1.0, connect_timeout=0.5)
self.assertEqual(len(attempts.timeouts), HTTPS_MAX_ADDRESSES)
self.assertLessEqual(sum(attempts.timeouts), budget)
def test_wall_clock_respects_the_budget(self) -> None:
started = time.monotonic()
attempts, _ = self._run(timeout=2, sleep_factor=1.0)
elapsed = time.monotonic() - started
self.assertLess(elapsed, 3.0)
self.assertLessEqual(len(attempts.timeouts), HTTPS_MAX_ADDRESSES)
def test_exhausted_budget_stops_further_attempts(self) -> None:
attempts, _ = self._run(timeout=1, sleep_factor=1.0)
self.assertEqual(len(attempts.timeouts), 1)
if __name__ == "__main__":
unittest.main()