zapretgui/tests/test_winws2_launch_preset_validation.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

1051 lines
41 KiB
Python

from __future__ import annotations
from pathlib import Path
import sys
import tempfile
import unittest
PROJECT_SRC = Path(__file__).resolve().parents[1] / "src"
if str(PROJECT_SRC) not in sys.path:
sys.path.insert(0, str(PROJECT_SRC))
class Winws2LaunchPresetValidationTests(unittest.TestCase):
def test_launch_preparation_keeps_valid_preset_text_unchanged(self) -> None:
from winws_runtime.preset_launch_text import prepare_winws2_preset_text_for_launch
source = "\n".join(
(
"--wf-tcp-out=443",
"--filter-tcp=443",
"--hostlist=lists/youtube.txt",
"--payload=tls_client_hello",
"--lua-desync=fake:blob=tls_google",
"",
)
)
prepared = prepare_winws2_preset_text_for_launch(source, source_name="valid.txt")
self.assertEqual(prepared.text, source)
self.assertFalse(prepared.changed)
self.assertNotIn("--out-range=-d8", prepared.text)
def test_launch_filter_check_ignores_only_skipped_profiles(self) -> None:
from presets.mode_coordinator import PresetModeCoordinator
from settings.mode import ZAPRET2_MODE
source = "\n".join(
(
"--wf-tcp-out=443",
"--skip",
"--filter-tcp=443",
"--hostlist=lists/youtube.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
)
self.assertFalse(PresetModeCoordinator._has_required_filters(ZAPRET2_MODE, source))
def test_launch_filter_check_allows_some_skipped_profiles_when_one_is_enabled(self) -> None:
from presets.mode_coordinator import PresetModeCoordinator
from settings.mode import ZAPRET2_MODE
source = "\n".join(
(
"--wf-tcp-out=443",
"--skip",
"--filter-tcp=443",
"--hostlist=lists/disabled.txt",
"--lua-desync=fake:blob=tls_google",
"",
"--new",
"--filter-tcp=443",
"--hostlist=lists/enabled.txt",
"--lua-desync=multisplit:pos=sniext+1",
"",
)
)
self.assertTrue(PresetModeCoordinator._has_required_filters(ZAPRET2_MODE, source))
def test_winws1_launch_filter_check_ignores_only_skipped_profiles(self) -> None:
from presets.mode_coordinator import PresetModeCoordinator
from settings.mode import ZAPRET1_MODE
source = "\n".join(
(
"--wf-tcp=80,443",
"--skip",
"--filter-tcp=443",
"--hostlist=lists/youtube.txt",
"--dpi-desync=fake,split2",
"",
)
)
self.assertFalse(PresetModeCoordinator._has_required_filters(ZAPRET1_MODE, source))
def test_winws1_launch_filter_check_allows_some_skipped_profiles_when_one_is_enabled(self) -> None:
from presets.mode_coordinator import PresetModeCoordinator
from settings.mode import ZAPRET1_MODE
source = "\n".join(
(
"--wf-tcp=80,443",
"--skip",
"--filter-tcp=443",
"--hostlist=lists/disabled.txt",
"--dpi-desync=fake",
"",
"--new",
"--filter-tcp=443",
"--hostlist=lists/enabled.txt",
"--dpi-desync=fake,split2",
"",
)
)
self.assertTrue(PresetModeCoordinator._has_required_filters(ZAPRET1_MODE, source))
def test_start_validation_rejects_preset_with_only_skipped_profiles(self) -> None:
from winws_runtime.flow.start_preparation import validate_preset_selected_mode
from settings.mode import ZAPRET2_MODE
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "only-skipped.txt"
preset_path.write_text(
"\n".join(
(
"--wf-tcp-out=443",
"--skip",
"--filter-tcp=443",
"--hostlist=lists/youtube.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
),
encoding="utf-8",
)
with self.assertRaisesRegex(RuntimeError, "включ"):
validate_preset_selected_mode(
{"is_preset_file": True, "preset_path": str(preset_path)},
ZAPRET2_MODE,
)
def test_launch_preparation_allows_unknown_filename_when_syntax_is_valid(self) -> None:
from winws_runtime.preset_launch_text import prepare_winws2_preset_text_for_launch
source = "\n".join(
(
"--wf-tcp-out=443",
"--filter-tcp=443",
"--hostlist=unknown.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
)
prepared = prepare_winws2_preset_text_for_launch(source, source_name="placeholder.txt")
self.assertEqual(prepared.text, source)
def test_launch_preparation_rejects_strategy_tags_in_non_circular_preset(self) -> None:
from winws_runtime.preset_launch_text import prepare_winws2_preset_text_for_launch
source = "\n".join(
(
"--wf-tcp-out=443",
"--filter-tcp=443",
"--hostlist=lists/youtube.txt",
"--lua-desync=fake:blob=tls_google:strategy=1",
"",
)
)
with self.assertRaisesRegex(ValueError, "strategy"):
prepare_winws2_preset_text_for_launch(source, source_name="not-circular.txt")
def test_circular_detection_uses_official_lua_desync_line_not_name(self) -> None:
from winws_runtime.preset_launch_text import is_winws2_circular_preset_text
self.assertTrue(
is_winws2_circular_preset_text(
"\n".join(
(
"# Preset: anything",
"--in-range=-s5556 --lua-desync=circular:fails=4:retrans=2:maxseq=16384",
"--lua-desync=fake:strategy=1",
"",
)
)
)
)
self.assertFalse(
is_winws2_circular_preset_text(
"\n".join(
(
"# Preset: Default (circular)",
"--wf-tcp-out=443",
"--lua-desync=fake:blob=tls_google:strategy=1",
"",
)
)
)
)
self.assertFalse(
is_winws2_circular_preset_text(
"\n".join(
(
"--wf-tcp-out=443",
"--lua-desync=circular_quality:fails=4",
"--lua-desync=fake:strategy=1",
"",
)
)
)
)
def test_launch_preparation_allows_strategy_tags_in_circular_preset(self) -> None:
from winws_runtime.preset_launch_text import prepare_winws2_preset_text_for_launch
source = "\n".join(
(
"--wf-tcp-out=443",
"--lua-desync=circular",
"--lua-desync=fake:blob=tls_google:strategy=1",
"",
)
)
prepared = prepare_winws2_preset_text_for_launch(
source,
source_name="circular.txt",
source_is_circular=True,
)
self.assertEqual(prepared.text, source)
def test_launch_preparation_ignores_strategy_tags_in_comments(self) -> None:
from winws_runtime.preset_launch_text import prepare_winws2_preset_text_for_launch
source = "\n".join(
(
"# example: --lua-desync=fake:strategy=1",
"--wf-tcp-out=443",
"--filter-tcp=443",
"--hostlist=lists/youtube.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
)
prepared = prepare_winws2_preset_text_for_launch(source, source_name="comment.txt")
self.assertEqual(prepared.text, source)
def test_runner_checks_unknown_filename_by_file_existence(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
lists_dir = root / "lists"
lists_dir.mkdir()
runner = object.__new__(Winws2StrategyRunner)
runner.work_dir = str(root)
runner.lists_dir = str(lists_dir)
runner.bin_dir = str(root / "bin")
source = "\n".join(
(
"--wf-tcp-out=443",
"--filter-tcp=443",
"--hostlist=unknown.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
)
missing = runner._collect_missing_preset_references_from_text(source)
self.assertEqual(len(missing), 1)
self.assertIn("--hostlist=unknown.txt", missing[0][0])
(lists_dir / "unknown.txt").write_text("example.com\n", encoding="utf-8")
self.assertEqual(len(runner._collect_missing_preset_references_from_text(source)), 1)
(root / "unknown.txt").write_text("example.com\n", encoding="utf-8")
self.assertEqual(runner._collect_missing_preset_references_from_text(source), [])
def test_winws2_validation_treats_bare_list_file_as_workdir_relative(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
lists_dir = root / "lists"
lists_dir.mkdir()
(lists_dir / "tankix.txt").write_text("example.com\n", encoding="utf-8")
runner = object.__new__(Winws2StrategyRunner)
runner.work_dir = str(root)
runner.lists_dir = str(lists_dir)
runner.bin_dir = str(root / "bin")
source = "\n".join(
(
"--wf-tcp-out=443",
"--filter-tcp=443",
"--hostlist=tankix.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
)
missing = runner._collect_missing_preset_references_from_text(source)
self.assertEqual(len(missing), 1)
self.assertIn(str(root / "tankix.txt"), missing[0][1])
def test_winws2_validation_accepts_inline_lua_init_source(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
runner = object.__new__(Winws2StrategyRunner)
runner.work_dir = str(root)
runner.lists_dir = str(root / "lists")
runner.bin_dir = str(root / "bin")
source = (
"--lua-init=fake_unknown_256=string.rep(string.char(0),256);"
"fake_zero64=string.rep(string.char(0),64)\n"
)
self.assertEqual(runner._collect_missing_preset_references_from_text(source), [])
def test_winws2_compile_accepts_explicit_lists_paths_for_at_launch(self) -> None:
from threading import RLock
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
lists_dir = root / "lists"
lists_dir.mkdir()
(lists_dir / "tankix.txt").write_text("example.com\n", encoding="utf-8")
(lists_dir / "ipset-tankix.txt").write_text("1.2.3.4\n", encoding="utf-8")
preset_path = root / "selected.txt"
preset_path.write_text(
"\n".join(
(
"--wf-tcp-out=443",
"--filter-tcp=443",
"--hostlist=lists/tankix.txt",
"--ipset=lists/ipset-tankix.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
),
encoding="utf-8",
)
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = RLock()
runner._prepared_preset_cache = {}
runner.work_dir = str(root)
runner.lists_dir = str(lists_dir)
runner.bin_dir = str(root / "bin")
artifact = runner._compile_preset_artifact(str(preset_path))
self.assertTrue(artifact.validation_ok, artifact.validation_report)
self.assertEqual(len(artifact.launch_args), 1)
self.assertTrue(artifact.launch_args[0].startswith("@"))
at_config_path = Path(artifact.launch_args[0][1:])
self.assertTrue(at_config_path.exists())
self.assertEqual(at_config_path.parent, root / "user" / "tmp" / "winws2_at_config")
self.assertIn("--hostlist=lists/tankix.txt", at_config_path.read_text(encoding="utf-8"))
def test_winws2_compile_launches_safe_at_config_file(self) -> None:
from threading import RLock
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
lists_dir = root / "lists"
lists_dir.mkdir()
(lists_dir / "tankix.txt").write_text("example.com\n", encoding="utf-8")
preset_path = root / "selected with spaces.txt"
preset_path.write_text(
"\n".join(
(
"# Preset: selected with spaces",
"--wf-tcp-out=443",
"--new",
"--name=youtube.com (QUIC)",
"--filter-tcp=443",
"--hostlist=lists/tankix.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
),
encoding="utf-8",
)
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = RLock()
runner._prepared_preset_cache = {}
runner.work_dir = str(root)
runner.lists_dir = str(lists_dir)
runner.bin_dir = str(root / "bin")
artifact = runner._compile_preset_artifact(str(preset_path))
self.assertTrue(artifact.validation_ok, artifact.validation_report)
self.assertEqual(len(artifact.launch_args), 1)
self.assertTrue(artifact.launch_args[0].startswith("@"))
at_config_path = Path(artifact.launch_args[0][1:])
self.assertTrue(at_config_path.exists())
at_config_text = at_config_path.read_text(encoding="utf-8")
self.assertNotIn("# Preset:", at_config_text)
self.assertIn("'--name=youtube.com (QUIC)'", at_config_text)
self.assertIn("--hostlist=lists/tankix.txt", at_config_text)
at_config_path.unlink()
rebuilt = runner._compile_preset_artifact(str(preset_path))
self.assertTrue(Path(rebuilt.launch_args[0][1:]).exists())
def test_winws2_compile_resolves_windivert_filter_paths_without_duplicate_folder(self) -> None:
from threading import RLock
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
filter_dir = root / "windivert.filter"
filter_dir.mkdir()
(filter_dir / "windivert_part.discord_media.txt").write_text(
"true\n",
encoding="utf-8",
)
preset_path = root / "selected.txt"
preset_path.write_text(
"\n".join(
(
"--wf-tcp-out=443",
"--wf-raw-part=@windivert.filter/windivert_part.discord_media.txt",
"--filter-tcp=443",
"--lua-desync=fake:blob=tls_google",
"",
)
),
encoding="utf-8",
)
runner = object.__new__(Winws2StrategyRunner)
runner._state_lock = RLock()
runner._prepared_preset_cache = {}
runner.work_dir = str(root)
runner.lists_dir = str(root / "lists")
runner.bin_dir = str(root / "bin")
artifact = runner._compile_preset_artifact(str(preset_path))
self.assertTrue(artifact.validation_ok, artifact.validation_report)
self.assertEqual(len(artifact.launch_args), 1)
self.assertTrue(artifact.launch_args[0].startswith("@"))
self.assertNotIn("windivert.filter/windivert.filter", "/".join(artifact.launch_args))
def test_runner_reads_stdout_when_winws_exits_immediately(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
class FakeProcess:
def communicate(self, timeout=None):
return b"winws stdout diagnostic\n", b""
runner = object.__new__(Winws2StrategyRunner)
output = runner._read_process_startup_output(FakeProcess())
self.assertEqual(output, "winws stdout diagnostic")
def test_winws2_long_running_spawn_uses_file_output_not_pipes(self) -> None:
import subprocess
from types import SimpleNamespace
from unittest.mock import Mock, patch
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
runner = object.__new__(Winws2StrategyRunner)
runner.winws_exe = "winws2.exe"
runner.work_dir = tmp
runner.running_process = None
runner.current_launch_label = None
runner.current_strategy_args = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._prepare_state_for_spawn_locked = Mock()
runner._set_runner_state_locked = Mock()
runner._log_winws2_launch_command = Mock()
runner._create_startup_info = Mock(return_value=None)
runner._set_last_error = Mock()
runner._run_preset_dry_run_locked = Mock(return_value=True)
fake_process = SimpleNamespace(pid=2468, returncode=None)
artifact = SimpleNamespace(
launch_args=("@config.txt",),
preset_path="preset.txt",
normalized_text="--wf-tcp-out=443\n",
)
with (
patch("winws_runtime.runners.zapret2_runner.subprocess.Popen", return_value=fake_process) as popen_mock,
patch("winws_runtime.runners.zapret2_runner.wait_for_process_stable_start", return_value=True) as stable_start,
):
self.assertTrue(
runner._spawn_process_locked(
artifact,
"Preset",
preset_switch=False,
stable_start_window_seconds=0.35,
)
)
kwargs = popen_mock.call_args.kwargs
self.assertIsNot(kwargs["stdout"], subprocess.PIPE)
self.assertIsNot(kwargs["stderr"], subprocess.PIPE)
self.assertIs(kwargs["stdout"], kwargs["stderr"])
self.assertIsNot(kwargs["stdout"], subprocess.DEVNULL)
self.assertTrue(kwargs["stdout"].closed)
self.assertEqual(stable_start.call_args.kwargs["stable_window"], 0.35)
def test_winws2_preset_switch_waits_briefly_between_dry_run_and_real_spawn(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
events: list[str] = []
runner = object.__new__(Winws2StrategyRunner)
runner.winws_exe = "winws2.exe"
runner.work_dir = tmp
runner.running_process = None
runner.current_launch_label = None
runner.current_strategy_args = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._prepare_state_for_spawn_locked = Mock()
runner._set_runner_state_locked = Mock()
runner._log_winws2_launch_command = Mock()
runner._create_startup_info = Mock(return_value=None)
runner._set_last_error = Mock()
runner._run_preset_dry_run_locked = Mock(return_value=True)
fake_process = SimpleNamespace(pid=2468, returncode=None)
artifact = SimpleNamespace(
launch_args=("@config.txt",),
preset_path="preset.txt",
normalized_text="--wf-tcp-out=443\n",
)
def fake_popen(*_args, **_kwargs):
events.append("spawn")
return fake_process
with (
patch("winws_runtime.runners.zapret2_runner.subprocess.Popen", side_effect=fake_popen),
patch("winws_runtime.runners.zapret2_runner.wait_for_process_stable_start", return_value=True),
patch(
"winws_runtime.runners.zapret2_runner.time.sleep",
side_effect=lambda seconds: events.append(f"sleep:{seconds}"),
),
):
self.assertTrue(
runner._spawn_process_locked(
artifact,
"Preset",
preset_switch=True,
)
)
self.assertEqual(events[0].split(":")[0], "sleep")
self.assertEqual(events[1], "spawn")
def test_winws2_immediate_exit_reads_startup_output_file(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
runner = object.__new__(Winws2StrategyRunner)
runner.winws_exe = "winws2.exe"
runner.work_dir = tmp
runner.running_process = None
runner.current_launch_label = None
runner.current_strategy_args = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._prepare_state_for_spawn_locked = Mock()
runner._set_runner_state_locked = Mock()
runner._log_winws2_launch_command = Mock()
runner._create_startup_info = Mock(return_value=None)
runner._set_last_error = Mock()
runner._run_preset_dry_run_locked = Mock(return_value=True)
runner._read_process_startup_output = Mock(return_value="")
fake_process = SimpleNamespace(pid=2468, returncode=87)
artifact = SimpleNamespace(
launch_args=("@config.txt",),
preset_path="preset.txt",
normalized_text="--wf-tcp-out=443\n",
)
def fake_popen(*_args, **kwargs):
kwargs["stderr"].write(b"windivert: error opening filter: The parameter is incorrect.\\n")
kwargs["stderr"].flush()
return fake_process
with (
patch("winws_runtime.runners.zapret2_runner.subprocess.Popen", side_effect=fake_popen),
patch("winws_runtime.runners.zapret2_runner.wait_for_process_stable_start", return_value=False),
):
self.assertFalse(
runner._spawn_process_locked(
artifact,
"Preset",
preset_switch=False,
)
)
self.assertIn("parameter is incorrect", runner._last_spawn_stderr.lower())
runner._read_process_startup_output.assert_not_called()
def test_winws2_preset_switch_dll_init_failure_has_clear_error(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
runner = object.__new__(Winws2StrategyRunner)
runner.winws_exe = "winws2.exe"
runner.work_dir = tmp
runner.running_process = None
runner.current_launch_label = None
runner.current_strategy_args = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._prepare_state_for_spawn_locked = Mock()
runner._set_runner_state_locked = Mock()
runner._log_winws2_launch_command = Mock()
runner._create_startup_info = Mock(return_value=None)
runner._set_last_error = Mock()
runner._run_preset_dry_run_locked = Mock(return_value=True)
runner._read_process_startup_output = Mock(return_value="")
artifact = SimpleNamespace(
launch_args=("@config.txt",),
preset_path="preset.txt",
normalized_text="--wf-tcp-out=443\n",
)
with (
patch(
"winws_runtime.runners.zapret2_runner.subprocess.Popen",
return_value=SimpleNamespace(pid=2468, returncode=0xC0000142),
),
patch("winws_runtime.runners.zapret2_runner.wait_for_process_stable_start", return_value=False),
):
self.assertFalse(
runner._spawn_process_locked(
artifact,
"Preset",
preset_switch=True,
)
)
message = runner._set_last_error.call_args.args[0]
self.assertIn("Windows не смогла инициализировать DLL", message)
self.assertIn("0xC0000142", message)
self.assertNotIn("Preset switch failed", message)
def test_winws2_startup_output_summary_prefers_windivert_error_line(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
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",
)
)
self.assertEqual(
Winws2StrategyRunner._summarize_startup_output(output),
"windivert: error opening filter: The service cannot be started",
)
def test_winws2_retry_preserves_short_startup_stable_window(self) -> None:
from unittest.mock import Mock
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
runner = object.__new__(Winws2StrategyRunner)
runner._last_spawn_exit_code = 1058
runner._last_spawn_stderr = "transient"
runner._should_retry_transient_windivert_service_error = Mock(return_value=True)
runner._is_windivert_system_error = Mock(return_value=False)
runner._is_windivert_conflict_error = Mock(return_value=False)
runner._start_from_preset_file_locked = Mock(return_value=True)
self.assertTrue(
runner._maybe_retry_after_failed_spawn_locked(
"preset.txt",
"Preset",
cleanup_required=False,
retry_count=0,
stable_start_window_seconds=0.35,
)
)
self.assertEqual(
runner._start_from_preset_file_locked.call_args.kwargs["stable_start_window_seconds"],
0.35,
)
def test_winws2_retries_normal_start_once_after_dll_init_failure(self) -> None:
from unittest.mock import Mock
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
runner = object.__new__(Winws2StrategyRunner)
runner._last_spawn_exit_code = 0xC0000142
runner._last_spawn_stderr = ""
runner._should_retry_transient_windivert_service_error = Mock(return_value=False)
runner._is_windivert_system_error = Mock(return_value=False)
runner._is_windivert_conflict_error = Mock(return_value=False)
runner._maybe_run_windivert_auto_fix_after_failed_spawn = Mock(return_value=False)
runner._start_from_preset_file_locked = Mock(return_value=True)
self.assertTrue(
runner._maybe_retry_after_failed_spawn_locked(
"preset.txt",
"Preset",
cleanup_required=False,
retry_count=0,
stable_start_window_seconds=0.35,
)
)
runner._start_from_preset_file_locked.assert_called_once_with(
"preset.txt",
"Preset",
force_cleanup=True,
retry_count=1,
stable_start_window_seconds=0.35,
)
def test_winws2_does_not_retry_exit_87_when_stale_windivert_service_remains(self) -> None:
from unittest.mock import Mock, patch
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
runner = object.__new__(Winws2StrategyRunner)
runner._last_spawn_exit_code = 87
runner._last_spawn_stderr = ""
runner._should_retry_transient_windivert_service_error = Mock(return_value=False)
runner._is_windivert_system_error = Mock(return_value=False)
runner._is_windivert_conflict_error = Mock(return_value=False)
runner._aggressive_windivert_cleanup = Mock()
runner._start_from_preset_file_locked = Mock(return_value=True)
with patch(
"winws_runtime.runners.zapret2_runner.find_stale_windivert_delete_pending_services_runtime",
return_value=["Monkey"],
create=True,
) as find_stale:
retried = runner._maybe_retry_after_failed_spawn_locked(
"preset.txt",
"Preset",
cleanup_required=False,
retry_count=0,
stable_start_window_seconds=0.35,
)
self.assertFalse(retried)
find_stale.assert_called_once_with()
runner._start_from_preset_file_locked.assert_not_called()
runner._aggressive_windivert_cleanup.assert_called_once_with()
def test_winws2_dry_run_artifact_stays_inside_at_config(self) -> None:
from winws_runtime.runners.preset_runner_support import PreparedPresetArtifact
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
runner = object.__new__(Winws2StrategyRunner)
runner.work_dir = str(root)
artifact = PreparedPresetArtifact(
preset_path=str(root / "selected.txt"),
cache_key=None,
normalized_text="--wf-tcp-out=443\n",
launch_args=("@original.txt",),
validation_ok=True,
validation_report="",
)
dry_run_artifact = runner._artifact_for_dry_run_locked(artifact)
dry_run_config = Path(str(dry_run_artifact.launch_args[0])[1:]).read_text(encoding="utf-8")
self.assertEqual(len(dry_run_artifact.launch_args), 1)
self.assertTrue(str(dry_run_artifact.launch_args[0]).startswith("@"))
self.assertIn("--wf-tcp-out=443", dry_run_config)
self.assertIn("--wf-dup-check=0", dry_run_config)
self.assertIn("--dry-run", dry_run_config)
def test_winws2_dry_run_allows_second_delayed_retry_after_dll_init_failure(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from winws_runtime.runners.preset_runner_support import PreparedPresetArtifact
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
runner = object.__new__(Winws2StrategyRunner)
runner.winws_exe = "winws2.exe"
runner.work_dir = str(root)
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._create_startup_info = Mock(return_value=None)
runner._set_last_error = Mock()
runner._set_runner_state_locked = Mock()
artifact = PreparedPresetArtifact(
preset_path=str(root / "selected.txt"),
cache_key=None,
normalized_text="--wf-tcp-out=443\n",
launch_args=("@original.txt",),
validation_ok=True,
validation_report="",
)
with (
patch(
"winws_runtime.runners.zapret2_runner.subprocess.run",
side_effect=[
SimpleNamespace(returncode=3221225794, stdout=b"", stderr=b""),
SimpleNamespace(returncode=3221225794, stdout=b"", stderr=b""),
SimpleNamespace(returncode=0, stdout=b"", stderr=b""),
],
) as run_mock,
patch("winws_runtime.runners.zapret2_runner.time.sleep") as sleep_mock,
):
ok = runner._run_preset_dry_run_locked(
artifact,
"Preset",
preset_switch=False,
notify_failure=True,
)
self.assertTrue(ok)
self.assertEqual(run_mock.call_count, 3)
self.assertEqual(
[call.args[0] for call in sleep_mock.call_args_list],
[0.75, 2.0],
)
runner._set_runner_state_locked.assert_not_called()
runner._set_last_error.assert_not_called()
self.assertEqual(runner._last_spawn_exit_code, 0)
def test_winws2_spawn_stops_before_real_process_when_dry_run_fails(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
runner = object.__new__(Winws2StrategyRunner)
runner.winws_exe = "winws2.exe"
runner.work_dir = "C:/Zapret/Dev"
runner.running_process = None
runner.current_launch_label = None
runner.current_strategy_args = None
runner._last_spawn_exit_code = None
runner._last_spawn_stderr = ""
runner._set_last_error = Mock()
runner._prepare_state_for_spawn_locked = Mock()
runner._set_runner_state_locked = Mock()
runner._log_winws2_launch_command = Mock()
runner._create_startup_info = Mock(return_value=None)
runner._run_preset_dry_run_locked = Mock(return_value=False)
artifact = SimpleNamespace(
launch_args=("@config.txt",),
preset_path="preset.txt",
normalized_text="--wf-tcp-out=443\n",
)
with patch("winws_runtime.runners.zapret2_runner.subprocess.Popen") as popen_mock:
self.assertFalse(
runner._spawn_process_locked(
artifact,
"Preset",
preset_switch=False,
)
)
popen_mock.assert_not_called()
def test_winws1_compile_resolves_bare_list_paths_for_launch(self) -> None:
from threading import RLock
from winws_runtime.runners.zapret1_runner import Winws1StrategyRunner
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
lists_dir = root / "lists"
lists_dir.mkdir()
(lists_dir / "tankix.txt").write_text("example.com\n", encoding="utf-8")
preset_path = root / "selected.txt"
preset_path.write_text(
"\n".join(
(
"--wf-tcp=443",
"--filter-tcp=443",
"--hostlist=tankix.txt",
"--dpi-desync=fake",
"",
)
),
encoding="utf-8",
)
runner = object.__new__(Winws1StrategyRunner)
runner._state_lock = RLock()
runner._prepared_preset_cache = {}
runner.work_dir = str(root)
runner.lists_dir = str(lists_dir)
runner.bin_dir = str(root / "bin")
artifact = runner._compile_preset_artifact(str(preset_path))
self.assertTrue(artifact.validation_ok, artifact.validation_report)
self.assertEqual(len(artifact.launch_args), 1)
self.assertTrue(artifact.launch_args[0].startswith("@"))
at_config_text = Path(str(artifact.launch_args[0])[1:]).read_text(encoding="utf-8")
self.assertIn(f"--hostlist={lists_dir / 'tankix.txt'}", at_config_text)
self.assertNotIn("--hostlist=tankix.txt", at_config_text)
def test_winws1_compile_uses_at_config_for_launch(self) -> None:
from threading import RLock
from winws_runtime.runners.zapret1_runner import Winws1StrategyRunner
from profile.parser import parse_preset_text
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
lists_dir = root / "lists"
lists_dir.mkdir()
(lists_dir / "youtube.txt").write_text("youtube.com\n", encoding="utf-8")
preset_path = root / "selected.txt"
preset_path.write_text(
"\n".join(
(
"# Preset: Selected",
"--wf-tcp=443",
"--comment=youtube.com (интерфейс)",
"--filter-tcp=443",
"--hostlist=youtube.txt",
"--dpi-desync=fake,split2",
"",
)
),
encoding="utf-8",
)
runner = object.__new__(Winws1StrategyRunner)
runner._state_lock = RLock()
runner._prepared_preset_cache = {}
runner.work_dir = str(root)
runner.lists_dir = str(lists_dir)
runner.bin_dir = str(root / "bin")
artifact = runner._compile_preset_artifact(str(preset_path))
self.assertTrue(artifact.validation_ok, artifact.validation_report)
preset = parse_preset_text(preset_path.read_text(encoding="utf-8"), engine="winws1")
self.assertEqual(preset.profiles[0].name, "youtube.com (интерфейс)")
self.assertEqual(len(artifact.launch_args), 1)
self.assertTrue(artifact.launch_args[0].startswith("@"))
at_config_path = Path(str(artifact.launch_args[0])[1:])
at_config_text = at_config_path.read_text(encoding="utf-8")
self.assertNotIn("--name=", at_config_text)
self.assertIn("'--comment=youtube.com (интерфейс)'", at_config_text)
self.assertIn(f"--hostlist={lists_dir / 'youtube.txt'}", at_config_text)
self.assertNotIn("# Preset:", at_config_text)
def test_launch_preparation_rejects_invalid_ranges_and_payload(self) -> None:
from winws_runtime.preset_launch_text import prepare_winws2_preset_text_for_launch
cases = (
("--out-range=8", "out-range"),
("--in-range=bad", "in-range"),
("--payload=not_a_payload", "payload"),
)
for line, expected in cases:
with self.subTest(line=line):
source = "\n".join(
(
"--wf-tcp-out=443",
"--filter-tcp=443",
"--hostlist=lists/youtube.txt",
line,
"--lua-desync=fake:blob=tls_google",
"",
)
)
with self.assertRaisesRegex(ValueError, expected):
prepare_winws2_preset_text_for_launch(source, source_name="bad.txt")
def test_profile_editor_rejects_non_engine_range_value(self) -> None:
from profile.parser import parse_preset_text
from profile.editable_settings import EditableProfileSettings, with_editable_profile_settings
from settings.mode import ENGINE_WINWS2
preset = parse_preset_text(
"\n".join(
(
"--filter-tcp=443",
"--hostlist=lists/youtube.txt",
"--lua-desync=fake:blob=tls_google",
"",
)
),
engine=ENGINE_WINWS2,
source_name="editable.txt",
)
with self.assertRaisesRegex(ValueError, "packet range"):
with_editable_profile_settings(
preset,
0,
EditableProfileSettings(
filter_kind="hostlist",
filter_value="lists/youtube.txt",
in_range="x",
out_range="d8",
),
)
if __name__ == "__main__":
unittest.main()