zapret-kvn/tests/test_service_check.py
loop-uh 18f49a1573
Some checks failed
Windows project source guards / test (push) Failing after 26s
Тест скорости узла проверяет и известные сервисы: пауза и медленный ответ видны в карточке сервера
После замера скорости через тот же временный прокси к YouTube, Discord, Telegram и ещё восьми сервисам идёт серия запросов по одному соединению. По времени до первого байта решается: быстро, медленно, с паузами, открывается не каждый адрес или не открывается. Итог хранится в узле и показан строкой «Через этот узел» в карточке сервера со словами о проблемах. Раньше узел проверялся одним байтом generate_204 и скоростью файла.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 12:48:35 -04:00

290 lines
12 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 xray_fluent.network import service_check as sc
YOUTUBE = sc.SERVICES[0]
DISCORD = sc.SERVICES[1]
def _run(host: str, *times: float, failed: bool = False, reason: str = "") -> sc.EndpointRun:
return sc.EndpointRun(host, tuple(times), failed=failed, reason=reason)
class JudgeTests(unittest.TestCase):
def test_fast_answers_are_ok(self) -> None:
verdict = sc.judge(YOUTUBE, [_run("www.youtube.com", 90, 80, 85, 95), _run("i.ytimg.com", 60, 70, 65, 62)])
self.assertEqual(verdict.code, sc.CODE_OK)
self.assertIn("быстрые", verdict.text)
def test_one_long_wait_among_fast_ones_is_a_pause(self) -> None:
verdict = sc.judge(YOUTUBE, [_run("www.youtube.com", 90, 80, 2900, 95)])
self.assertEqual(verdict.code, sc.CODE_PAUSE)
self.assertIn("2.9 с", verdict.text)
def test_every_request_slow_is_slow_not_a_pause(self) -> None:
verdict = sc.judge(YOUTUBE, [_run("www.youtube.com", 3100, 3000, 2900, 3200)])
self.assertEqual(verdict.code, sc.CODE_SLOW)
def test_a_name_that_does_not_open_makes_the_service_partial(self) -> None:
verdict = sc.judge(DISCORD, [_run("discord.com", 100, 90), _run("gateway.discord.gg", failed=True, reason="timeout")])
self.assertEqual(verdict.code, sc.CODE_PARTIAL)
self.assertIn("gateway.discord.gg", verdict.text)
def test_nothing_opens_is_blocked_with_the_reason(self) -> None:
verdict = sc.judge(DISCORD, [_run("discord.com", failed=True, reason="ConnectionResetError")])
self.assertEqual(verdict.code, sc.CODE_BLOCKED)
self.assertIn("ConnectionResetError", verdict.text)
def test_no_runs_is_unknown(self) -> None:
self.assertEqual(sc.judge(YOUTUBE, []).code, sc.CODE_UNKNOWN)
def test_one_slow_answer_without_enough_samples_is_not_a_pause(self) -> None:
self.assertEqual(sc.judge(YOUTUBE, [_run("www.youtube.com", 90, 2900)]).code, sc.CODE_OK)
class _Response:
def __init__(self, will_close: bool = False) -> None:
self.will_close = will_close
def read(self, _size: int) -> bytes:
return b"x"
class _Connection:
"""Подставное соединение: каждый ответ «приходит» за заданное время по подставным часам."""
def __init__(self, clock: "_Clock", delays: list[float], *, fail_after: int | None = None, will_close: bool = False) -> None:
self.clock, self.delays, self.fail_after, self.will_close = clock, list(delays), fail_after, will_close
self.requests = 0
self.closed = False
def request(self, method, path, headers=None) -> None:
if self.fail_after is not None and self.requests >= self.fail_after:
raise ConnectionResetError("reset")
self.requests += 1
def getresponse(self) -> _Response:
self.clock.now += self.delays.pop(0) / 1000
return _Response(self.will_close)
def close(self) -> None:
self.closed = True
class _Clock:
def __init__(self) -> None:
self.now = 0.0
def __call__(self) -> float:
return self.now
class ProbeTests(unittest.TestCase):
def test_first_request_with_the_handshake_is_not_counted(self) -> None:
clock = _Clock()
connection = _Connection(clock, [800, 90, 80, 85, 95])
run = sc.probe_endpoint("127.0.0.1", 1, "www.youtube.com", "/", connect=lambda *_a: connection, clock=clock)
self.assertEqual([round(value) for value in run.ttfb_ms], [90, 80, 85, 95])
self.assertTrue(connection.closed)
self.assertFalse(run.failed)
def test_connection_that_closes_after_the_answer_gives_one_measurement(self) -> None:
clock = _Clock()
connection = _Connection(clock, [300], will_close=True)
run = sc.probe_endpoint("127.0.0.1", 1, "x.com", "/", connect=lambda *_a: connection, clock=clock)
self.assertEqual([round(value) for value in run.ttfb_ms], [300])
self.assertEqual(connection.requests, 1)
def test_reset_on_the_first_request_is_a_failure_with_its_name(self) -> None:
clock = _Clock()
connection = _Connection(clock, [], fail_after=0)
run = sc.probe_endpoint("127.0.0.1", 1, "x.com", "/", connect=lambda *_a: connection, clock=clock)
self.assertTrue(run.failed)
self.assertEqual((run.ttfb_ms, run.reason), ((), "ConnectionResetError"))
def test_reset_in_the_middle_keeps_what_was_measured(self) -> None:
clock = _Clock()
connection = _Connection(clock, [500, 90, 95], fail_after=3)
run = sc.probe_endpoint("127.0.0.1", 1, "x.com", "/", connect=lambda *_a: connection, clock=clock)
self.assertTrue(run.failed)
self.assertEqual([round(value) for value in run.ttfb_ms], [90, 95])
def test_proxy_that_refuses_the_tunnel_is_a_failure(self) -> None:
def refuse(*_args):
raise ConnectionRefusedError("no proxy")
run = sc.probe_endpoint("127.0.0.1", 1, "x.com", "/", connect=refuse)
self.assertTrue(run.failed)
def test_stop_ends_the_series(self) -> None:
clock = _Clock()
connection = _Connection(clock, [300, 90, 90, 90, 90])
run = sc.probe_endpoint("127.0.0.1", 1, "x.com", "/", connect=lambda *_a: connection, clock=clock, cancelled=lambda: True)
self.assertEqual(connection.requests, 0)
self.assertEqual(run.ttfb_ms, ())
class CheckAllTests(unittest.TestCase):
def test_every_service_of_the_catalog_gets_a_verdict(self) -> None:
def probe(_h, _p, host, path, cancelled):
return _run(host, 80, 90, 85, 88) if host != "gateway.discord.gg" else _run(host, failed=True, reason="timeout")
verdicts = sc.check_all("127.0.0.1", 1, probe=probe)
self.assertEqual(set(verdicts), {spec.key for spec in sc.SERVICES})
self.assertEqual(verdicts["youtube"].code, sc.CODE_OK)
self.assertEqual(verdicts["discord"].code, sc.CODE_PARTIAL)
def test_summary_line_names_every_service_with_a_mark(self) -> None:
verdicts = {
"youtube": sc.ServiceVerdict("youtube", sc.CODE_OK),
"discord": sc.ServiceVerdict("discord", sc.CODE_BLOCKED),
"telegram": {"code": "pause"},
}
self.assertEqual(sc.summary_text(verdicts), "✓ YouTube ✗ Discord ⚠ Telegram")
def test_catalog_is_free_of_duplicates(self) -> None:
self.assertEqual(len({spec.key for spec in sc.SERVICES}), len(sc.SERVICES))
self.assertTrue(all(spec.endpoints for spec in sc.SERVICES))
class NodeStorageTests(unittest.TestCase):
def test_services_survive_saving_and_old_nodes_load_without_them(self) -> None:
from xray_fluent.profiles.models import Node
node = Node(name="n", services={"youtube": {"code": "ok", "text": "быстро"}}, services_checked_at="2026-10-20T12:00:00+00:00")
restored = Node.from_dict(node.to_dict())
self.assertEqual(restored.services, node.services)
self.assertEqual(restored.services_checked_at, node.services_checked_at)
old = {key: value for key, value in node.to_dict().items() if not key.startswith("services")}
self.assertEqual(Node.from_dict(old).services, {})
class DetailTextTests(unittest.TestCase):
def test_card_lists_marks_and_words_only_for_problems(self) -> None:
from xray_fluent.profiles.models import Node
from xray_fluent.ui.node_detail_widget import NodeDetailWidget
node = Node(
name="n",
services={
"youtube": {"code": "ok", "text": "быстро"},
"discord": {"code": "pause", "text": "ответы идут с паузами"},
},
services_checked_at="2026-10-20T12:00:00+00:00",
)
text = NodeDetailWidget._services_text(_Format(), node)
self.assertIn("✓ YouTube ⚠ Discord", text)
self.assertIn("Discord: ответы идут с паузами", text)
self.assertNotIn("YouTube: быстро", text)
self.assertEqual(NodeDetailWidget._services_text(_Format(), Node(name="n")), "")
class WorkerAndControllerTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
from PyQt6.QtWidgets import QApplication
cls.app = QApplication.instance() or QApplication([])
def _worker(self, **kwargs):
from xray_fluent.network.speed_test_worker import SpeedTestWorker
return SpeedTestWorker([], "xray.exe", **kwargs)
def test_worker_emits_the_verdicts_only_when_asked(self) -> None:
from unittest.mock import patch
from xray_fluent.profiles.models import Node
verdicts = {"youtube": sc.ServiceVerdict("youtube", sc.CODE_OK, "быстро")}
node = Node(name="n")
for asked, expected in ((True, [(node.id, {"youtube": verdicts["youtube"].as_dict()})]), (False, [])):
worker = self._worker(check_services=asked)
got: list = []
worker.services.connect(lambda node_id, data: got.append((node_id, data)))
with patch.object(sc, "check_all", return_value=verdicts) as check:
worker._check_node_services(node, 4321)
self.assertEqual(got, expected)
self.assertEqual(check.call_count, 1 if asked else 0)
def test_failed_check_does_not_break_the_speed_result(self) -> None:
from unittest.mock import patch
from xray_fluent.profiles.models import Node
worker = self._worker(check_services=True)
with patch.object(sc, "check_all", side_effect=RuntimeError("boom")):
worker._check_node_services(Node(name="n"), 4321)
def test_stop_skips_the_check(self) -> None:
from unittest.mock import patch
from xray_fluent.profiles.models import Node
worker = self._worker(check_services=True)
worker._cancelled = True
with patch.object(sc, "check_all") as check:
worker._check_node_services(Node(name="n"), 4321)
check.assert_not_called()
def test_controller_stores_verdicts_in_the_node_and_refreshes_the_list(self) -> None:
from types import SimpleNamespace
from xray_fluent.application import worker_service
from xray_fluent.profiles.models import Node
node = Node(name="n", speed_mbps=3.0, is_alive=True)
emitted: list = []
worker = object()
controller = SimpleNamespace(
sender=lambda: worker,
_speed_worker=worker,
_get_node_by_id=lambda node_id: node if node_id == node.id else None,
speed_updated=SimpleNamespace(emit=lambda *args: emitted.append(args)),
)
worker_service.on_services_result(controller, node.id, {"youtube": {"code": "ok", "text": "быстро"}, "bad": "x"})
self.assertEqual(set(node.services), {"youtube"})
self.assertTrue(node.services_checked_at)
self.assertEqual(emitted, [(node.id, 3.0, True)])
# Чужой сигнал (не от текущего теста) игнорируется.
controller.sender = lambda: object()
worker_service.on_services_result(controller, node.id, {"x": {"code": "ok"}})
self.assertEqual(set(node.services), {"youtube"})
class _Format:
@staticmethod
def _format_ts(value: str) -> str:
return value[:10]
if __name__ == "__main__":
unittest.main()