162 lines
6.2 KiB
Python
162 lines
6.2 KiB
Python
"""Führt den echten PowerShell-Wächter mit harmlosen Testprogrammen aus."""
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from syncvito import updater
|
|
|
|
pytestmark = pytest.mark.skipif(os.name != "nt", reason="Windows-Prozesslebenszyklus")
|
|
|
|
|
|
def wait_for_file(path: Path, timeout: float = 15) -> None:
|
|
deadline = time.monotonic() + timeout
|
|
while not path.exists():
|
|
if time.monotonic() > deadline:
|
|
pytest.fail(f"Testprozess hat keine Datei geschrieben: {path}")
|
|
time.sleep(0.05)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def helper_exe(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
|
root = tmp_path_factory.mktemp("watcher-helper")
|
|
source = root / "helper.cs"
|
|
source.write_text(
|
|
"""
|
|
using System;
|
|
using System.IO;
|
|
using System.Threading;
|
|
class Helper {
|
|
static int Main(string[] args) {
|
|
File.WriteAllLines(args[0], args);
|
|
var deadline = DateTime.UtcNow.AddSeconds(30);
|
|
while (!File.Exists(args[1])) {
|
|
if (DateTime.UtcNow > deadline) return 99;
|
|
Thread.Sleep(50);
|
|
}
|
|
return Int32.Parse(args[2]);
|
|
}
|
|
}
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
compiler = Path(os.environ.get("SYSTEMROOT", r"C:\Windows")) / "Microsoft.NET/Framework64/v4.0.30319/csc.exe"
|
|
if not compiler.exists():
|
|
pytest.skip("Windows-.NET-Compiler für Testprogramm nicht vorhanden")
|
|
executable = root / "SyncVitoWatcherTest.exe"
|
|
subprocess.run(
|
|
[str(compiler), "/nologo", "/target:winexe", f"/out:{executable}", str(source)],
|
|
check=True,
|
|
capture_output=True,
|
|
timeout=30,
|
|
)
|
|
return executable
|
|
|
|
|
|
@pytest.mark.parametrize("exit_code", [0, 5])
|
|
def test_watcher_waits_for_all_instances_and_real_setup_exit(
|
|
helper_exe: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, exit_code: int
|
|
) -> None:
|
|
root = tmp_path / "Ordner mit Leerzeichen und 'Apostroph'"
|
|
root.mkdir()
|
|
installer = root / "SetupTest.exe"
|
|
shutil.copy2(helper_exe, installer)
|
|
unrelated_dir = root / "anderer Installationspfad"
|
|
unrelated_dir.mkdir()
|
|
unrelated_exe = unrelated_dir / helper_exe.name
|
|
shutil.copy2(helper_exe, unrelated_exe)
|
|
children = []
|
|
releases = []
|
|
|
|
def start_client(exe: Path, name: str) -> subprocess.Popen:
|
|
ready = root / f"{name}.ready"
|
|
release = root / f"{name}.release"
|
|
releases.append(release)
|
|
process = subprocess.Popen([str(exe), str(ready), str(release), "0"])
|
|
children.append(process)
|
|
wait_for_file(ready)
|
|
return process
|
|
|
|
def launch_watcher(executable: str, arguments: list[str]) -> None:
|
|
children.append(subprocess.Popen([executable, *arguments], creationflags=subprocess.CREATE_NO_WINDOW))
|
|
|
|
try:
|
|
parent = start_client(helper_exe, "parent")
|
|
extra = start_client(helper_exe, "extra")
|
|
unrelated = start_client(unrelated_exe, "unrelated")
|
|
monkeypatch.setattr(updater, "_start_detached", launch_watcher)
|
|
output = root / "setup.arguments"
|
|
setup_release = root / "setup.release"
|
|
releases.append(setup_release)
|
|
log = root / "bootstrap.log"
|
|
arguments = [str(output), str(setup_release), str(exit_code), f"/DIR={root}", f"/LOG={root / 'setup.log'}"]
|
|
updater._start_installer_after_exit(installer, arguments, [parent.pid], log, helper_exe)
|
|
watcher = children[-1]
|
|
wait_for_file(log)
|
|
assert not output.exists()
|
|
releases[0].touch()
|
|
parent.wait(timeout=10)
|
|
time.sleep(0.5)
|
|
assert not output.exists(), "Weitere Instanz wurde nicht abgewartet"
|
|
releases[1].touch()
|
|
extra.wait(timeout=10)
|
|
wait_for_file(output)
|
|
assert unrelated.poll() is None, "Anderer Installationspfad darf weiterlaufen"
|
|
assert output.read_text(encoding="utf-8-sig").splitlines() == arguments
|
|
assert watcher.poll() is None, "Wächter muss auf Setup warten"
|
|
assert not (root / "update-result.json").exists()
|
|
setup_release.touch()
|
|
assert watcher.wait(timeout=15) == (0 if exit_code == 0 else 1)
|
|
receipt = json.loads((root / "update-result.json").read_text(encoding="utf-8"))
|
|
assert receipt["success"] is (exit_code == 0)
|
|
assert receipt["exit_code"] == exit_code
|
|
assert f"Installer-Exitcode: {exit_code}" in log.read_text(encoding="utf-8-sig")
|
|
finally:
|
|
for release in releases:
|
|
release.touch()
|
|
for child in children:
|
|
try:
|
|
child.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
child.kill()
|
|
child.wait(timeout=5)
|
|
|
|
|
|
def test_watcher_reports_timeout_without_starting_installer(
|
|
helper_exe: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
ready = tmp_path / "client.ready"
|
|
release = tmp_path / "client.release"
|
|
log = tmp_path / "bootstrap.log"
|
|
client = subprocess.Popen([str(helper_exe), str(ready), str(release), "0"])
|
|
watchers = []
|
|
|
|
def launch_with_short_deadline(executable: str, arguments: list[str]) -> None:
|
|
script = base64.b64decode(arguments[-1]).decode("utf-16-le")
|
|
script = script.replace("AddMinutes(2)", "AddMilliseconds(400)")
|
|
arguments[-1] = base64.b64encode(script.encode("utf-16-le")).decode("ascii")
|
|
watchers.append(subprocess.Popen([executable, *arguments], creationflags=subprocess.CREATE_NO_WINDOW))
|
|
|
|
try:
|
|
wait_for_file(ready)
|
|
monkeypatch.setattr(updater, "_start_detached", launch_with_short_deadline)
|
|
updater._start_installer_after_exit(tmp_path / "not-started.exe", ["/VERYSILENT"], [], log, helper_exe)
|
|
assert watchers[0].wait(timeout=15) == 1
|
|
receipt = json.loads((tmp_path / "update-result.json").read_text(encoding="utf-8"))
|
|
assert not receipt["success"]
|
|
assert str(client.pid) in receipt["message"]
|
|
assert "Beenden" in receipt["message"]
|
|
assert "Starte verifizierten Installer" not in log.read_text(encoding="utf-8-sig")
|
|
assert client.poll() is None
|
|
finally:
|
|
release.touch()
|
|
client.wait(timeout=10)
|
|
for watcher in watchers:
|
|
watcher.wait(timeout=10)
|