zapretgui/tests/test_udp_burst.py
loop-uh 598364898d
All checks were successful
Zapret source guards / validate (push) Successful in 10s
BlockCheck: проверка «не замирает ли UDP после первых пакетов»; честнее формулировки про звонки и Telegram
Обычная проверка звонков шлёт один запрос и по ответу говорит «UDP
проходит». Фильтр умеет пропускать начало потока и глушить его дальше —
звонок или игра соединяются и замирают через секунды. Теперь на два сервера
разных владельцев уходит по тридцать запросов подряд, дважды с разных портов:
если ответы обрываются примерно на одном и том же пакете везде — это
предупреждение «UDP замирает». Обрыв только на одном сервере считается его
собственным ограничением, а не фильтром.

Формулировки: ответ проверочного сервера звонков — это «UDP доходит», а не
«звонок пройдёт»; открытая дорога до дата-центров Telegram не значит, что
приложение работает (его могут резать уже после соединения).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:24:33 +03:00

141 lines
5.6 KiB
Python

"""UDP по числу пакетов: поток, который замирает после первых двух десятков."""
from __future__ import annotations
import socket
import threading
import unittest
from concurrent.futures import ThreadPoolExecutor
from blockcheck.ui.result_cards_model import build_cards
from diagnostics import udp_burst as ub
FULL = (True,) * ub.PACKETS
def _cut(at: int) -> tuple[bool, ...]:
return (True,) * at + (False,) * (ub.PACKETS - at)
def _facts(google, cloudflare):
return (("Google", tuple(google)), ("Cloudflare", tuple(cloudflare)))
class JudgeTests(unittest.TestCase):
def test_whole_series_answered_means_no_freeze(self) -> None:
verdict = ub.judge(_facts([FULL, FULL], [FULL, FULL]))
self.assertEqual(verdict.code, ub.BURST_OK)
self.assertEqual(verdict.servers[0], ("Google", ("30 из 30", "30 из 30")))
def test_two_lost_packets_are_still_a_whole_series(self) -> None:
lossy = tuple(index not in (7, 19) for index in range(ub.PACKETS))
self.assertEqual(ub.judge(_facts([lossy, FULL], [FULL, FULL])).code, ub.BURST_OK)
def test_same_cut_on_both_servers_and_both_repeats_is_a_freeze(self) -> None:
verdict = ub.judge(_facts([_cut(25), _cut(24)], [_cut(25), _cut(26)]))
self.assertEqual(verdict.code, ub.BURST_FREEZE)
self.assertIn("после 25 пакетов", verdict.text)
def test_cut_on_one_server_only_is_that_servers_limit_not_a_filter(self) -> None:
verdict = ub.judge(_facts([_cut(25), _cut(25)], [FULL, FULL]))
self.assertEqual(verdict.code, ub.BURST_UNKNOWN)
self.assertIn("ограничение самого сервера", verdict.text)
def test_cut_that_does_not_repeat_or_lands_elsewhere_is_not_a_freeze(self) -> None:
self.assertEqual(ub.judge(_facts([_cut(25), FULL], [_cut(25), _cut(25)])).code, ub.BURST_UNKNOWN)
self.assertEqual(ub.judge(_facts([_cut(12), _cut(12)], [_cut(25), _cut(25)])).code, ub.BURST_UNKNOWN)
# Обрыв в самом начале или на последнем пакете — не «начало проходит, дальше тишина».
self.assertIsNone(ub._cut_at(_cut(3)))
self.assertIsNone(ub._cut_at(_cut(ub.PACKETS - 1)))
def test_nothing_answered_gives_no_verdict_here(self) -> None:
silent = (False,) * ub.PACKETS
self.assertIsNone(ub.judge(_facts([None, None], [silent, None])))
class _Stun:
"""Сервер на этом компьютере: отвечает на первые ``limit`` запросов, дальше молчит."""
def __init__(self, limit: int) -> None:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind(("127.0.0.1", 0))
self.sock.settimeout(2)
self.port = self.sock.getsockname()[1]
self.limit = limit
threading.Thread(target=self._serve, daemon=True).start()
def _serve(self) -> None:
seen = 0
try:
while True:
data, peer = self.sock.recvfrom(2048)
seen += 1
if seen <= self.limit:
self.sock.sendto(b"\x01\x01\x00\x00" + data[4:20], peer)
except OSError:
pass
finally:
self.sock.close()
class SendTests(unittest.TestCase):
def _burst(self, limit: int):
server = _Stun(limit)
return ub.send_burst("127.0.0.1", server.port, gap=0.002, tail_wait=0.3)
def test_every_answer_is_matched_to_its_request(self) -> None:
self.assertEqual(self._burst(999), FULL)
def test_stream_that_goes_silent_is_seen_as_a_cut(self) -> None:
run = self._burst(25)
self.assertEqual((sum(run), ub._cut_at(run)), (25, 25))
def test_unreachable_server_is_not_an_error(self) -> None:
free = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
free.bind(("127.0.0.1", 0))
port = free.getsockname()[1]
free.close()
run = ub.send_burst("127.0.0.1", port, gap=0.001, tail_wait=0.1)
self.assertTrue(run is None or not any(run))
def test_servers_are_asked_twice_each(self) -> None:
asked: list[str] = []
def send(host, port):
asked.append(host)
return FULL
with ThreadPoolExecutor(4) as pool:
facts = ub.check_bursts(send, submit=pool.submit)
self.assertEqual(sorted(asked), sorted([host for _n, host, _p in ub.SERVERS] * ub.REPEATS))
self.assertEqual(ub.judge(facts).code, ub.BURST_OK)
class CardTests(unittest.TestCase):
def _card(self, state, text):
voice = {"level": "ok", "headline": "Серверы отвечают", "items": [{"name": "Google STUN", "state": "ok", "ok": True, "text": "отвечает"}],
"burst": {"state": state, "text": text, "servers": [{"name": "Google", "series": ["25 из 30", "25 из 30"]}]}}
return build_cards({"voice": voice})[0]
def test_frozen_udp_turns_the_voice_card_into_a_warning(self) -> None:
card = self._card("freeze", "ответы прекращаются после 25 пакетов")
self.assertEqual(card.level, "warn")
self.assertEqual(card.lines[-1].name, "UDP замирает")
rows = card.sections[1].lines
self.assertEqual((rows[0].state, rows[1].text), ("warn", "ответов: 25 из 30, 25 из 30"))
def test_passing_series_is_shown_without_alarm(self) -> None:
card = self._card("ok", "серия проходит целиком")
self.assertEqual((card.level, card.sections[1].lines[0].state), ("ok", "ok"))
if __name__ == "__main__":
unittest.main()