zapretgui/tests/test_strategy_search_runtime.py
loop-uh 917aceab71
All checks were successful
Zapret source guards / validate (push) Successful in 7s
Драйвер WinDivert и запуск winws: служба Monkey больше не зависает — программа сама держала её незакрытым хэндлом (CloseServiceHandle без argtypes падал с «int too long to convert»); новый слой winws_runtime/engine на типизированном WinAPI: запуск подтверждается строкой windivert initialized из вывода winws, остановка — сигналом Windows о выходе процесса, без пауз «на глаз»; кнопка «Стоп», выход и обновление выгружают драйвер и убирают его службу, когда им никто не пользуется (кроме Kaspersky), перезапуск и смена пресета драйвер не трогают; останавливаются только winws из папки программы, чужая копия запрета не убивается; реестр службы, DeleteFlag и тип запуска больше не правятся, штатное «отключена и помечена на удаление» у работающего драйвера не считается поломкой; застрявшая служба называется пользователю с причиной; оркестратор переведён на те же подтверждения; вывод winws первой версии пишется в файл; сквозные проверки на живой Windows в tests/e2e_windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:34:45 +03:00

234 lines
9.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.

"""Запуск winws2 одной стратегии и настоящее окружение подбора (без Windows)."""
from __future__ import annotations
import subprocess
import threading
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from blockcheck.strategy_search.engine import ScanFatal
from blockcheck.strategy_search.winws_session import WinwsSession
class FakeProcess:
"""Процесс, который печатает строки и живёт, пока его не остановят."""
def __init__(self, lines: list[bytes], *, exit_after_output: int | None = None, hang_output: bool = True) -> None:
self._lines = list(lines)
self._exit_after_output = exit_after_output
self._hang_output = hang_output
self.returncode = None
self.terminated = False
self._closed = threading.Event()
self.stdout = self
def __iter__(self):
for line in self._lines:
yield line
if self._exit_after_output is not None:
self.returncode = self._exit_after_output
return
if self._hang_output:
self._closed.wait(5)
def close(self):
self._closed.set()
def poll(self):
return self.returncode
def terminate(self):
self.terminated = True
self.returncode = 1
self._closed.set()
def kill(self):
self.terminate()
def wait(self, timeout=None):
return self.returncode
class WinwsSessionTests(unittest.TestCase):
def test_ready_line_makes_start_fast(self) -> None:
process = FakeProcess([b"github version v1.0.3\n", b"windivert initialized. capture is started.\n"])
session = WinwsSession(["winws2.exe", "@cfg"], cwd=".", popen=lambda *a, **k: process)
start = session.start()
self.assertTrue(start.ok)
self.assertLess(start.ready_ms, 1500)
self.assertTrue(session.alive())
self.assertTrue(session.stop())
self.assertTrue(process.terminated)
def test_exit_during_start_is_reported_with_output(self) -> None:
process = FakeProcess([b"lua: attempt to call nil value 'fakee'\n"], exit_after_output=1)
session = WinwsSession(["winws2.exe", "@cfg"], cwd=".", popen=lambda *a, **k: process)
start = session.start()
self.assertFalse(start.ok)
self.assertEqual(start.exit_code, 1)
self.assertIn("fakee", start.output_tail)
def test_output_is_merged_and_hidden(self) -> None:
captured = {}
def popen(command, **kwargs):
captured.update(kwargs, command=command)
return FakeProcess([b"windivert initialized\n"])
session = WinwsSession(["winws2.exe", "@cfg"], cwd="C:/Zapret", popen=popen)
session.start()
session.stop()
self.assertEqual(captured["command"], ["winws2.exe", "@cfg"])
self.assertEqual(captured["stdout"], subprocess.PIPE)
self.assertEqual(captured["stderr"], subprocess.STDOUT)
self.assertEqual(captured["cwd"], "C:/Zapret")
class UdpProbeTests(unittest.TestCase):
def test_any_udp_reply_means_path_is_open(self) -> None:
import socket
from blockcheck.strategy_search import probes, verdict
from blockcheck.strategy_search.verdict import UdpProbeSpec
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(("127.0.0.1", 0))
port = server.getsockname()[1]
def reply_garbage():
data, peer = server.recvfrom(4096)
server.sendto(b"not an A2S answer", peer)
thread = threading.Thread(target=reply_garbage, daemon=True)
thread.start()
try:
outcome = probes.probe_udp(UdpProbeSpec("Rust", "source_a2s", "127.0.0.1", port, "127.0.0.1"))
finally:
thread.join(2)
server.close()
# Сервер ответил не по формату, но ответил: UDP проходит, это не блокировка.
self.assertEqual(outcome.state, verdict.PROBE_OK)
class RealEnvironmentTests(unittest.TestCase):
def make_env(self, shutdown=None):
from blockcheck.strategy_search.environment import RealEnvironment
return RealEnvironment(shutdown_sync=shutdown or (lambda **_kw: SimpleNamespace(still_running=False)))
def test_kaspersky_needs_cooldown_between_strategies(self) -> None:
from blockcheck.strategy_search import environment
with patch("utils.antivirus_probe.is_kaspersky_present", return_value=True):
self.assertEqual(environment.strategy_pause_seconds(), environment.KASPERSKY_STRATEGY_COOLDOWN_SECONDS)
with patch("utils.antivirus_probe.is_kaspersky_present", return_value=False):
self.assertEqual(environment.strategy_pause_seconds(), 0.0)
# Не удалось узнать — перестраховка: пауза как с Kaspersky.
with patch("utils.antivirus_probe.is_kaspersky_present", side_effect=OSError("boom")):
self.assertEqual(environment.strategy_pause_seconds(), environment.KASPERSKY_STRATEGY_COOLDOWN_SECONDS)
def test_driver_service_is_checked_before_first_start_only(self) -> None:
env = self.make_env()
ready = SimpleNamespace(ready=True, blocker="", description="")
with (
patch(
"winws_runtime.health.windivert_diagnostics.ensure_windivert_ready_before_spawn",
return_value=ready,
) as check,
patch.object(env, "_winws2_path", return_value="winws2.exe"),
patch("builtins.open"),
):
env.start_session("cfg")
env.start_session("cfg")
self.assertEqual(check.call_count, 1)
def test_stuck_driver_service_stops_the_scan(self) -> None:
# Пока служба драйвера застряла, не запустится ни одна стратегия.
env = self.make_env()
blocked = SimpleNamespace(
ready=False,
blocker="stuck_entry",
description="Запись службы драйвера WinDivert (Monkey) осталась после остановки",
)
with (
patch(
"winws_runtime.health.windivert_diagnostics.ensure_windivert_ready_before_spawn",
return_value=blocked,
),
patch("winws_runtime.runtime.system_ops.recover_windivert_runtime") as recover,
patch.object(env, "_winws2_path", return_value="winws2.exe"),
):
with self.assertRaises(ScanFatal) as raised:
env.start_session("cfg")
self.assertIn("WinDivert", str(raised.exception))
recover.assert_called_once_with()
def test_driver_service_recovers_before_the_scan(self) -> None:
env = self.make_env()
blocked = SimpleNamespace(ready=False, blocker="stop_pending", description="выгружается")
ready = SimpleNamespace(ready=True, blocker="", description="")
with (
patch(
"winws_runtime.health.windivert_diagnostics.ensure_windivert_ready_before_spawn",
side_effect=[blocked, ready],
),
patch("winws_runtime.runtime.system_ops.recover_windivert_runtime") as recover,
patch.object(env, "_winws2_path", return_value="winws2.exe"),
patch("builtins.open"),
):
env.start_session("cfg")
recover.assert_called_once_with()
def test_recover_after_crash_stops_only_own_engines_without_blind_pause(self) -> None:
from blockcheck.strategy_search import environment
env = self.make_env()
with (
patch("winws_runtime.runtime.system_ops.stop_own_winws_processes_runtime") as stop_own,
patch.object(env, "strategy_pause_seconds", return_value=0.0),
patch.object(environment.time, "sleep") as sleep,
):
env.recover_after_crash()
stop_own.assert_called_once_with()
sleep.assert_not_called()
def test_recover_after_crash_keeps_kaspersky_cooldown(self) -> None:
from blockcheck.strategy_search import environment
env = self.make_env()
with (
patch("winws_runtime.runtime.system_ops.stop_own_winws_processes_runtime"),
patch.object(
env, "strategy_pause_seconds", return_value=environment.KASPERSKY_STRATEGY_COOLDOWN_SECONDS
),
patch.object(environment.time, "sleep") as sleep,
):
env.recover_after_crash()
sleep.assert_called_once_with(environment.KASPERSKY_STRATEGY_COOLDOWN_SECONDS)
def test_cleanup_marks_scan_guard_and_stops_zapret(self) -> None:
calls = []
env = self.make_env(lambda **kw: calls.append(kw["reason"]) or SimpleNamespace(still_running=False))
with patch("winws_runtime.runtime.scan_guard.mark_external_winws_scan_active") as guard:
env.pre_cleanup()
env.post_cleanup()
self.assertEqual(calls, ["blockcheck_pre_scan", "blockcheck_post_scan"])
self.assertEqual([call.args[0] for call in guard.call_args_list], [True, False])
def test_zapret_that_refuses_to_stop_blocks_the_scan(self) -> None:
env = self.make_env(lambda **_kw: SimpleNamespace(still_running=True))
with patch("winws_runtime.runtime.scan_guard.mark_external_winws_scan_active"):
with self.assertRaises(ScanFatal):
env.pre_cleanup()
if __name__ == "__main__":
unittest.main()