zapretgui/tests/test_preset_switch_runtime.py
loop-uh 926a05fa75
Some checks failed
Zapret source guards / validate (push) Failing after 11s
Windows-сборка / Dev Dev 21.1.5.66 опубликован
Починить путь debug-лога после переноса логов в user
Пресеты содержат --debug=@logs/..., и winws2 резолвит этот путь от корня
установки: после переезда логов в user\logs он отвечал «bad file» и
отказывался стартовать. Новые строки пишутся в user/logs, существующие
пресеты чинятся при нормализации текста; абсолютные пути не трогаются.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 12:29:50 +03:00

759 lines
35 KiB
Python

from __future__ import annotations
import inspect
import os
from pathlib import Path
from types import SimpleNamespace
import threading
import tempfile
import unittest
from unittest.mock import Mock, patch
class Winws2PresetSwitchTests(unittest.TestCase):
def test_preset_cache_key_changes_when_same_size_file_content_changes(self) -> None:
from winws_runtime.runners.preset_runner_support import preset_cache_key
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
fixed_ns = 1_779_890_000_000_000_000
preset_path.write_text("--wf-tcp-out=443\n", encoding="utf-8")
os.utime(preset_path, ns=(fixed_ns, fixed_ns))
first_key = preset_cache_key(str(preset_path))
preset_path.write_text("--wf-udp-out=443\n", encoding="utf-8")
os.utime(preset_path, ns=(fixed_ns, fixed_ns))
second_key = preset_cache_key(str(preset_path))
self.assertNotEqual(first_key, second_key)
def test_winws2_compile_rebuilds_at_config_when_same_size_content_changes(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
preset_path = root / "selected.txt"
fixed_ns = 1_779_890_000_000_000_000
runner = object.__new__(Winws2StrategyRunner)
runner.work_dir = str(root)
runner.lists_dir = str(root / "lists")
runner.bin_dir = str(root / "bin")
runner._state_lock = threading.RLock()
runner._prepared_preset_cache = {}
preset_path.write_text("--wf-tcp-out=443\n", encoding="utf-8")
os.utime(preset_path, ns=(fixed_ns, fixed_ns))
first_artifact = runner._compile_preset_artifact(str(preset_path))
first_config = Path(first_artifact.launch_args[0][1:]).read_text(encoding="utf-8")
preset_path.write_text("--wf-udp-out=443\n", encoding="utf-8")
os.utime(preset_path, ns=(fixed_ns, fixed_ns))
second_artifact = runner._compile_preset_artifact(str(preset_path))
second_config = Path(second_artifact.launch_args[0][1:]).read_text(encoding="utf-8")
self.assertIn("--wf-tcp-out=443", first_config)
self.assertIn("--wf-udp-out=443", second_config)
self.assertNotEqual(first_artifact.cache_key, second_artifact.cache_key)
self.assertNotEqual(first_config, second_config)
def test_winws2_at_config_prunes_old_cached_files(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
preset_path = root / "selected.txt"
preset_path.write_text("--wf-tcp-out=443\n", encoding="utf-8")
config_dir = root / "user" / "tmp" / "winws2_at_config"
config_dir.mkdir(parents=True)
for index in range(70):
stale = config_dir / f"winws2_at_stale_{index:02d}.txt"
stale.write_text("--old\n", encoding="utf-8")
os.utime(stale, (index, index))
runner = object.__new__(Winws2StrategyRunner)
runner.work_dir = str(root)
runner.lists_dir = str(root / "lists")
runner.bin_dir = str(root / "bin")
runner._state_lock = threading.RLock()
runner._prepared_preset_cache = {}
artifact = runner._compile_preset_artifact(str(preset_path))
active_config = Path(artifact.launch_args[0][1:])
self.assertTrue(active_config.exists())
self.assertLessEqual(len(list(config_dir.glob("winws2_at_*.txt"))), 64)
self.assertFalse((config_dir / "winws2_at_stale_00.txt").exists())
def test_winws1_at_config_prunes_old_cached_files(self) -> None:
from winws_runtime.runners.zapret1_runner import Winws1StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
preset_path = root / "selected.txt"
preset_path.write_text("--wf-tcp=443\n", encoding="utf-8")
config_dir = root / "user" / "tmp" / "winws1_at_config"
config_dir.mkdir(parents=True)
for index in range(70):
stale = config_dir / f"winws1_at_stale_{index:02d}.txt"
stale.write_text("--old\n", encoding="utf-8")
os.utime(stale, (index, index))
runner = object.__new__(Winws1StrategyRunner)
runner.work_dir = str(root)
runner.lists_dir = str(root / "lists")
runner.bin_dir = str(root / "bin")
runner._state_lock = threading.RLock()
runner._prepared_preset_cache = {}
artifact = runner._compile_preset_artifact(str(preset_path))
active_config = Path(artifact.launch_args[0][1:])
self.assertTrue(active_config.exists())
self.assertLessEqual(len(list(config_dir.glob("winws1_at_*.txt"))), 64)
self.assertFalse((config_dir / "winws1_at_stale_00.txt").exists())
def test_winws1_fast_switch_skips_stale_request_before_stopping_process(self) -> None:
from winws_runtime.runners.zapret1_runner import Winws1StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp=443\n", encoding="utf-8")
runner = object.__new__(Winws1StrategyRunner)
runner._state_lock = threading.RLock()
runner._set_last_error = Mock()
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(validation_ok=True, validation_report="")
)
runner._refresh_artifact_if_source_changed_locked = Mock(
side_effect=AssertionError("stale winws1 switch must not refresh after compile")
)
runner.running_process = object()
runner.is_running = Mock(return_value=True)
runner._stop_process_only_locked = Mock(
side_effect=AssertionError("stale winws1 switch must not stop current process")
)
self.assertTrue(
runner.switch_preset_file_fast(
str(preset_path),
"Selected",
is_current=lambda: False,
)
)
runner._compile_preset_artifact.assert_called_once_with(str(preset_path))
runner._refresh_artifact_if_source_changed_locked.assert_not_called()
runner._stop_process_only_locked.assert_not_called()
def test_winws2_fast_switch_skips_stale_request_before_spawning_process(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=443\n", encoding="utf-8")
artifact = SimpleNamespace(validation_ok=True, validation_report="")
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner._set_last_error = Mock()
runner._compile_preset_artifact = Mock(return_value=artifact)
runner._refresh_artifact_if_source_changed_locked = Mock(return_value=artifact)
runner.running_process = object()
runner.is_running = Mock(return_value=True)
runner._artifact_for_handoff_locked = Mock(
side_effect=AssertionError("stale winws2 switch must not prepare handoff")
)
runner._spawn_process_locked = Mock(
side_effect=AssertionError("stale winws2 switch must not spawn process")
)
runner._stop_previous_process_after_handoff_locked = Mock()
self.assertTrue(
runner.switch_preset_file_fast(
str(preset_path),
"Selected",
is_current=lambda: False,
)
)
runner._compile_preset_artifact.assert_called_once_with(str(preset_path))
runner._refresh_artifact_if_source_changed_locked.assert_called_once_with(artifact)
runner._artifact_for_handoff_locked.assert_not_called()
runner._spawn_process_locked.assert_not_called()
runner._stop_previous_process_after_handoff_locked.assert_not_called()
def test_winws2_fast_switch_skips_restart_when_config_identical(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
artifact = SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
cache_key=None,
normalized_text="--wf-tcp-out=80\n",
launch_args=("@same_config.txt",),
)
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner._set_last_error = Mock()
runner._compile_preset_artifact = Mock(return_value=artifact)
runner._refresh_artifact_if_source_changed_locked = Mock(return_value=artifact)
runner.running_process = object()
runner.is_running = Mock(return_value=True)
runner._last_applied_base_launch_args = ("@same_config.txt",)
runner._spawn_process_locked = Mock(
side_effect=AssertionError("идентичный @config не должен перезапускать winws2")
)
runner._stop_previous_process_after_handoff_locked = Mock()
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
runner._spawn_process_locked.assert_not_called()
runner._stop_previous_process_after_handoff_locked.assert_not_called()
def test_winws2_fast_switch_records_applied_config_for_dedupe(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner.work_dir = tmp_dir
runner.running_process = Mock(pid=111)
runner.current_launch_label = "Old"
runner._preset_file_path = "old.txt"
runner.current_strategy_args = ["--wf-tcp-out=443"]
runner._set_last_error = Mock()
runner.is_running = Mock(return_value=True)
runner._last_applied_base_launch_args = ("@old_config.txt",)
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
cache_key=None,
normalized_text="--wf-tcp-out=80\n",
launch_args=("@new_config.txt",),
)
)
runner._spawn_process_locked = Mock(return_value=True)
runner._stop_previous_process_after_handoff_locked = Mock()
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertEqual(runner._last_applied_base_launch_args, ("@new_config.txt",))
def test_same_filter_exit_message_is_retryable_conflict(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
runner = object.__new__(Winws2StrategyRunner)
self.assertTrue(
runner._is_windivert_conflict_error(
"Error: A copy of winws2 is already running with the same filter",
1,
)
)
def test_fast_switch_uses_windivert_error_instead_of_version_header(self) -> None:
from winws_runtime.health.winws_exit_diagnosis import WinDivertDiagnosis
from winws_runtime.runners import zapret2_runner
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
runner = object.__new__(Winws2StrategyRunner)
runner.last_error = None
output = "\n".join(
(
"github version v1.0.1 lua_compat_ver 6",
"Loading hostlist /lists/youtube.txt",
"windivert: error opening filter: The service cannot be started, "
"either because it is disabled or because it has no enabled devices associated with it.",
)
)
diagnosis = WinDivertDiagnosis(
cause="Служба драйвера WinDivert (Monkey) отключена в системе",
solution="Выполните аварийную очистку драйвера и повторите запуск",
exit_code=34,
win32_error=1058,
)
with (
patch.object(zapret2_runner, "diagnose_winws_exit", return_value=diagnosis),
patch.object(zapret2_runner, "log"),
):
runner._set_spawn_exit_error(34, output)
self.assertNotIn("github version", str(runner.last_error))
self.assertIn("WinDivert", str(runner.last_error))
self.assertIn("код ошибки Windows 1058", str(runner.last_error))
self.assertIn("код завершения процесса 34", str(runner.last_error))
def test_runner_state_allows_handoff_start_from_running(self) -> None:
from winws_runtime.runners.preset_runner_support import PresetRunnerState, PresetRunnerStateMachine
machine = PresetRunnerStateMachine()
machine.transition(PresetRunnerState.STARTING, preset_path="old.txt", strategy_name="Old")
machine.transition(PresetRunnerState.RUNNING, preset_path="old.txt", strategy_name="Old", pid=111)
snapshot = machine.transition(PresetRunnerState.STARTING, preset_path="new.txt", strategy_name="New")
self.assertEqual(snapshot.state, PresetRunnerState.STARTING)
self.assertEqual(snapshot.preset_path, "new.txt")
def test_winws2_launch_logs_exact_command_and_at_config(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
config_path = Path(tmp_dir) / "winws2_at_test.txt"
config_path.write_text("--wf-tcp-out=443\n", encoding="utf-8")
preset_path = Path(tmp_dir) / "Default v1 (game filter).txt"
preset_path.write_text("--wf-tcp-out=443\n", encoding="utf-8")
runner = object.__new__(Winws2StrategyRunner)
runner.winws_exe = str(Path(tmp_dir) / "winws2.exe")
runner.work_dir = tmp_dir
artifact = SimpleNamespace(
launch_args=(f"@{config_path}",),
preset_path=str(preset_path),
)
with patch("winws_runtime.runners.zapret2_runner.log") as log_mock:
runner._log_winws2_launch_command(
cmd=[runner.winws_exe, *artifact.launch_args],
artifact=artifact,
)
messages = [str(call.args[0]) for call in log_mock.call_args_list]
self.assertTrue(any("Winws2 launch command:" in message for message in messages))
self.assertTrue(any(str(config_path) in message for message in messages))
self.assertTrue(any("Winws2 launch cwd:" in message for message in messages))
self.assertTrue(any("Winws2 launch @config:" in message for message in messages))
self.assertTrue(any("sha1=" in message for message in messages))
self.assertTrue(any(str(preset_path) in message for message in messages))
def test_runner_file_watcher_restart_path_is_removed(self) -> None:
from winws_runtime.runners import preset_runner_support, zapret1_runner, zapret2_runner
combined_source = "\n".join(
(
inspect.getsource(zapret1_runner),
inspect.getsource(zapret2_runner),
inspect.getsource(preset_runner_support),
)
)
self.assertNotIn("ConfigFileWatcher", combined_source)
self.assertNotIn("_on_config_changed", combined_source)
def test_winws2_fast_switch_starts_new_process_before_stopping_old(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
calls: list[str] = []
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner.work_dir = tmp_dir
runner.running_process = Mock(pid=111)
runner.current_launch_label = "Old"
runner._preset_file_path = "old.txt"
runner.current_strategy_args = ["--wf-tcp-out=443"]
runner._set_last_error = Mock()
runner.is_running = Mock(return_value=True)
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
cache_key=None,
normalized_text="--wf-tcp-out=80\n",
launch_args=("--wf-tcp-out=80",),
)
)
runner._spawn_process_locked = Mock(side_effect=lambda *_args, **_kwargs: calls.append("spawn") or True)
runner._stop_previous_process_after_handoff_locked = Mock(
side_effect=lambda *_args, **_kwargs: calls.append("stop_old")
)
runner._stop_process_only_locked = Mock()
runner._perform_standard_windivert_cleanup = Mock()
runner._ensure_windivert_ready_before_spawn = Mock(return_value=True)
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertEqual(calls, ["spawn", "stop_old"])
runner._stop_process_only_locked.assert_not_called()
runner._perform_standard_windivert_cleanup.assert_not_called()
runner._ensure_windivert_ready_before_spawn.assert_not_called()
runner._spawn_process_locked.assert_called_once()
spawned_artifact = runner._spawn_process_locked.call_args.args[0]
handoff_config = Path(str(spawned_artifact.launch_args[0])[1:]).read_text(encoding="utf-8")
self.assertIn("--wf-tcp-out=80", handoff_config)
self.assertIn("--wf-dup-check=0", handoff_config)
def test_fast_switch_does_not_need_runner_file_watcher(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner.running_process = None
runner._preset_file_path = ""
runner._set_last_error = Mock()
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
launch_args=("--wf-tcp-out=80",),
)
)
runner._spawn_process_locked = Mock(return_value=True)
runner._perform_standard_windivert_cleanup = Mock()
runner._ensure_windivert_ready_before_spawn = Mock(return_value=True)
with patch("winws_runtime.runners.zapret2_runner.get_all_winws_process_pids", return_value=[]):
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
def test_winws2_fast_switch_rebuilds_artifact_if_preset_changes_before_spawn(self) -> None:
from winws_runtime.runners.preset_runner_support import preset_cache_key
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner.work_dir = tmp_dir
runner.running_process = Mock()
runner._preset_file_path = ""
runner._set_last_error = Mock()
runner._perform_standard_windivert_cleanup = Mock()
runner._ensure_windivert_ready_before_spawn = Mock(return_value=True)
def compile_artifact(path: str):
text = Path(path).read_text(encoding="utf-8")
return SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(path),
cache_key=preset_cache_key(path),
normalized_text=text,
launch_args=(text.strip(),),
)
runner._compile_preset_artifact = Mock(side_effect=compile_artifact)
runner.is_running = Mock(return_value=True)
runner._spawn_process_locked = Mock(return_value=True)
runner._stop_previous_process_after_handoff_locked = Mock()
def refresh_artifact(artifact):
preset_path.write_text("--wf-tcp-out=443", encoding="utf-8")
return runner._compile_preset_artifact(str(preset_path))
runner._refresh_artifact_if_source_changed_locked = Mock(side_effect=refresh_artifact)
with patch("winws_runtime.runners.zapret2_runner.get_all_winws_process_pids", return_value=[]):
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertEqual(runner._compile_preset_artifact.call_count, 2)
spawned_artifact = runner._spawn_process_locked.call_args.args[0]
self.assertIn("--wf-tcp-out=443", spawned_artifact.normalized_text)
self.assertIn("--wf-dup-check=0", spawned_artifact.normalized_text)
def test_winws2_handoff_failure_keeps_old_process_running(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
old_process = Mock(pid=111)
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner.work_dir = tmp_dir
runner.running_process = old_process
runner.current_launch_label = "Old"
runner.current_strategy_args = ["--wf-tcp-out=443"]
runner._preset_file_path = "old.txt"
runner.last_error = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._set_last_error = Mock()
runner.is_running = Mock(return_value=True)
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
cache_key=None,
normalized_text="--wf-tcp-out=80\n",
launch_args=("--wf-tcp-out=80",),
)
)
runner._spawn_process_locked = Mock(return_value=False)
runner._stop_previous_process_after_handoff_locked = Mock()
runner._stop_process_only_locked = Mock()
runner._perform_standard_windivert_cleanup = Mock()
self.assertFalse(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertIs(runner.running_process, old_process)
self.assertEqual(runner.current_launch_label, "Old")
self.assertEqual(runner._preset_file_path, "old.txt")
runner._stop_previous_process_after_handoff_locked.assert_not_called()
runner._stop_process_only_locked.assert_not_called()
def test_fast_switch_retries_winws2_conflict_inside_switch_without_full_start_fallback(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner.running_process = None
runner._preset_file_path = ""
runner.last_error = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._set_last_error = Mock()
runner._perform_standard_windivert_cleanup = Mock()
runner._aggressive_windivert_cleanup = Mock()
runner._wait_after_aggressive_windivert_cleanup = Mock()
runner._ensure_windivert_ready_before_spawn = Mock(return_value=True)
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
launch_args=("--wf-tcp-out=80",),
)
)
runner._start_from_preset_file_locked = Mock(return_value=True)
def spawn_then_conflict_then_success(*_args, **_kwargs):
if runner._spawn_process_locked.call_count == 1:
runner._last_spawn_exit_code = 1
runner._last_spawn_stderr = "A copy of winws2 is already running with the same filter"
return False
return True
runner._spawn_process_locked = Mock(side_effect=spawn_then_conflict_then_success)
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertEqual(runner._spawn_process_locked.call_count, 2)
runner._start_from_preset_file_locked.assert_not_called()
runner._aggressive_windivert_cleanup.assert_called_once()
def test_fast_switch_retries_winws2_dll_init_failure_inside_switch(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner.running_process = None
runner._preset_file_path = ""
runner.last_error = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._set_last_error = Mock()
runner._perform_standard_windivert_cleanup = Mock()
runner._aggressive_windivert_cleanup = Mock()
runner._wait_after_aggressive_windivert_cleanup = Mock()
runner._ensure_windivert_ready_before_spawn = Mock(return_value=True)
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
launch_args=("--wf-tcp-out=80",),
)
)
runner._start_from_preset_file_locked = Mock(return_value=True)
def spawn_then_dll_init_failure_then_success(*_args, **_kwargs):
if runner._spawn_process_locked.call_count == 1:
runner._last_spawn_exit_code = 0xC0000142
runner._last_spawn_stderr = ""
return False
return True
runner._spawn_process_locked = Mock(side_effect=spawn_then_dll_init_failure_then_success)
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertEqual(runner._spawn_process_locked.call_count, 2)
runner._start_from_preset_file_locked.assert_not_called()
runner._aggressive_windivert_cleanup.assert_called_once()
def test_winws2_handoff_retries_dll_init_failure_before_stopping_old_process(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
old_process = Mock(pid=111)
calls: list[str] = []
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = threading.RLock()
runner.work_dir = tmp_dir
runner.running_process = old_process
runner.current_launch_label = "Old"
runner.current_strategy_args = ["--wf-tcp-out=443"]
runner._preset_file_path = "old.txt"
runner.last_error = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._set_last_error = Mock()
runner.is_running = Mock(return_value=True)
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
cache_key=None,
normalized_text="--wf-tcp-out=80\n",
launch_args=("--wf-tcp-out=80",),
)
)
runner._stop_previous_process_after_handoff_locked = Mock(
side_effect=lambda *_args, **_kwargs: calls.append("stop_old")
)
def spawn_then_dll_init_failure_then_success(*_args, **_kwargs):
calls.append("spawn")
if runner._spawn_process_locked.call_count == 1:
runner._last_spawn_exit_code = 0xC0000142
runner._last_spawn_stderr = ""
return False
return True
runner._spawn_process_locked = Mock(side_effect=spawn_then_dll_init_failure_then_success)
with patch("winws_runtime.runners.zapret2_runner.time.sleep") as sleep:
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertEqual(calls, ["spawn", "spawn", "stop_old"])
self.assertEqual(runner._spawn_process_locked.call_count, 2)
runner._stop_previous_process_after_handoff_locked.assert_called_once_with(
old_process,
"Old",
"old.txt",
)
sleep.assert_called_once()
def test_fast_switch_retries_winws1_conflict_inside_switch_without_full_start_fallback(self) -> None:
from winws_runtime.runners.zapret1_runner import Winws1StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp-out=80", encoding="utf-8")
runner = object.__new__(Winws1StrategyRunner)
runner.winws_exe = "winws.exe"
runner._state_lock = threading.RLock()
runner.running_process = None
runner._preset_file_path = ""
runner.last_error = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._set_last_error = Mock()
runner._prepare_cleanup_before_spawn_locked = Mock()
runner._ensure_windivert_ready_before_spawn = Mock(return_value=True)
runner._compile_preset_artifact = Mock(
return_value=SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(preset_path),
launch_args=("--wf-tcp-out=80",),
)
)
runner._start_from_preset_file_locked = Mock(return_value=True)
def spawn_then_conflict_then_success(*_args, **_kwargs):
if runner._spawn_process_locked.call_count == 1:
runner._last_spawn_exit_code = 1
runner._last_spawn_stderr = "A copy of winws is already running with the same filter"
return False
return True
runner._spawn_process_locked = Mock(side_effect=spawn_then_conflict_then_success)
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertEqual(runner._spawn_process_locked.call_count, 2)
runner._start_from_preset_file_locked.assert_not_called()
runner._prepare_cleanup_before_spawn_locked.assert_called_once_with(retry_count=1)
def test_winws1_fast_switch_rebuilds_artifact_if_preset_changes_before_spawn(self) -> None:
from winws_runtime.runners.preset_runner_support import preset_cache_key
from winws_runtime.runners.zapret1_runner import Winws1StrategyRunner
with tempfile.TemporaryDirectory() as tmp_dir:
preset_path = Path(tmp_dir) / "selected.txt"
preset_path.write_text("--wf-tcp=80", encoding="utf-8")
runner = object.__new__(Winws1StrategyRunner)
runner.winws_exe = "winws.exe"
runner._state_lock = threading.RLock()
runner.running_process = Mock()
runner._preset_file_path = ""
runner._set_last_error = Mock()
runner._prepare_cleanup_before_spawn_locked = Mock()
runner._ensure_windivert_ready_before_spawn = Mock(return_value=True)
def compile_artifact(path: str):
text = Path(path).read_text(encoding="utf-8")
return SimpleNamespace(
validation_ok=True,
validation_report="",
preset_path=str(path),
cache_key=preset_cache_key(path),
launch_args=(text.strip(),),
)
runner._compile_preset_artifact = Mock(side_effect=compile_artifact)
runner.is_running = Mock(return_value=True)
def change_preset_before_spawn():
preset_path.write_text("--wf-tcp=443", encoding="utf-8")
return True
runner._stop_process_only_locked = Mock(side_effect=change_preset_before_spawn)
runner._spawn_process_locked = Mock(return_value=True)
with patch("winws_runtime.runners.zapret1_runner.get_process_pids_by_name", return_value=[]):
self.assertTrue(runner.switch_preset_file_fast(str(preset_path), "Selected"))
self.assertEqual(runner._compile_preset_artifact.call_count, 2)
spawned_artifact = runner._spawn_process_locked.call_args.args[0]
self.assertEqual(spawned_artifact.launch_args, ("--wf-tcp=443",))
if __name__ == "__main__":
unittest.main()