98 lines
6.0 KiB
Python
98 lines
6.0 KiB
Python
"""Exercise the real offline installer with unusable external tools and proxies."""
|
|
import ctypes
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
ARTIFACTS = ROOT / "test-artifacts"
|
|
ARTIFACTS.mkdir(exist_ok=True)
|
|
INSTALLER = ROOT / "dist/YouTubeDL-Helper-Setup.exe"
|
|
environment = {**os.environ, "PATH": str(Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32"),
|
|
"PYTHONHOME": "C:/NONEXISTENT", "PYTHONPATH": "C:/NONEXISTENT", "PYTHONNOUSERSITE": "1",
|
|
"HTTP_PROXY": "http://127.0.0.1:9", "HTTPS_PROXY": "http://127.0.0.1:9", "ALL_PROXY": "http://127.0.0.1:9"}
|
|
|
|
|
|
def run(*args):
|
|
result = subprocess.run([str(item) for item in args], env=environment, capture_output=True,
|
|
encoding="utf-8", errors="replace", timeout=180)
|
|
if result.returncode:
|
|
raise RuntimeError(json.dumps({"code": result.returncode, "stdout": result.stdout[-500:], "stderr": result.stderr[-2500:]}))
|
|
return result
|
|
|
|
|
|
def picker(manager, folder):
|
|
import base64
|
|
encoded = base64.b64encode(json.dumps(str(folder)).encode()).decode()
|
|
process = subprocess.Popen([str(manager), "--pick-folder", "--initial-folder", encoded], env=environment,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
user32 = ctypes.windll.user32
|
|
callback_type = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
|
|
dialogs = []
|
|
def callback(window, unused):
|
|
pid = ctypes.c_ulong(); user32.GetWindowThreadProcessId(window, ctypes.byref(pid))
|
|
if pid.value == process.pid:
|
|
name = ctypes.create_unicode_buffer(256); user32.GetClassNameW(window, name, 256)
|
|
if name.value == "#32770" and user32.IsWindowVisible(window): dialogs.append(window)
|
|
return True
|
|
deadline = time.monotonic() + 15
|
|
handler = callback_type(callback)
|
|
while not dialogs and time.monotonic() < deadline:
|
|
user32.EnumWindows(handler, 0); time.sleep(0.1)
|
|
assert dialogs, "The bundled Windows folder picker must open"
|
|
# Select its preselected existing folder, including Unicode in the path.
|
|
user32.PostMessageW(ctypes.c_void_p(dialogs[0]), 0x0111, 1, 0) # WM_COMMAND / IDOK
|
|
output, errors = process.communicate(timeout=15)
|
|
assert process.returncode == 0, errors.decode("utf-8", errors="replace")
|
|
assert Path(json.loads(output)) == folder
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="offline-bundle-", dir=ARTIFACTS) as temporary:
|
|
parent = Path(temporary)
|
|
application = parent / "Helper"
|
|
legacy = application / ".runtime"; legacy.mkdir(parents=True)
|
|
(legacy / "completed-jobs.json").write_text("[]")
|
|
first = run(INSTALLER, "--silent", "--no-register", "--target-root", application)
|
|
assert (application / "data/completed-jobs.json").read_text() == "[]"
|
|
marker = json.loads((application / "installation.json").read_text(encoding="utf-8-sig"))
|
|
release = Path(marker["current"])
|
|
python = release / "runtime/python/python.exe"
|
|
measured = json.loads(run(python, "-I", "-B", "-c",
|
|
"import json,sys; from host.media import capabilities,tool; print(json.dumps({'python':sys.executable,'path':sys.path,'hardware':capabilities(),'tools':{k:tool(k) for k in ('node','ffmpeg','ffprobe')}}))").stdout)
|
|
assert measured["hardware"]["ready"]
|
|
assert measured["hardware"]["bundle"]["mode"] == "autonomous"
|
|
assert {"h264", "hevc", "av1"} <= set(measured["hardware"]["nvenc"]), measured["hardware"]
|
|
assert Path(measured["python"]).is_relative_to(release)
|
|
assert all(Path(path).is_relative_to(release) for path in measured["tools"].values())
|
|
assert all(Path(path).is_relative_to(release) for path in measured["path"])
|
|
picker(application / "YouTubeDL-Manager.exe", ARTIFACTS)
|
|
preserved = release / "user-kept.mp4"; preserved.write_bytes(b"user file")
|
|
history = application / "data/completed-jobs.json"; history.parent.mkdir(exist_ok=True); history.write_text("[]")
|
|
# Reinstall beside the running copy; an old DLL handle must not prevent repair.
|
|
handle = (release / "runtime/python/python313.dll").open("rb")
|
|
try: run(INSTALLER, "--silent", "--no-register", "--target-root", application)
|
|
finally: handle.close()
|
|
current = Path(json.loads((application / "installation.json").read_text(encoding="utf-8-sig"))["current"])
|
|
assert current != release and current.is_dir()
|
|
manager = application / "YouTubeDL-Manager.exe"
|
|
run(manager, "--uninstall", "--silent", "--target-root", application)
|
|
assert preserved.read_bytes() == b"user file" and history.read_text() == "[]"
|
|
assert not current.exists() and not python.exists()
|
|
deadline = time.monotonic() + 15
|
|
while manager.exists() and time.monotonic() < deadline: time.sleep(0.1)
|
|
assert not manager.exists(), "The uninstaller must remove its own installed executable"
|
|
# Payload damage must be rejected before registering or installing any tools.
|
|
damaged = parent / "damaged.exe"
|
|
payload = bytearray(INSTALLER.read_bytes()); offset = payload.index(b"PK\x03\x04"); payload[offset + 100] ^= 1
|
|
damaged.write_bytes(payload)
|
|
invalid = subprocess.run([str(damaged), "--silent", "--no-register", "--target-root", str(parent / "Invalid")],
|
|
env=environment, capture_output=True, encoding="utf-8", timeout=60)
|
|
assert invalid.returncode != 0 and "Контрольная сумма" in invalid.stderr, invalid.stderr
|
|
(ARTIFACTS / "offline-bundle.json").write_text(json.dumps({"offlineInstallation": "passed", "isolatedRuntime": "passed",
|
|
"folderPicker": "passed", "repairWhileOldFilesOpen": "passed", "uninstallPreservesUserMedia": "passed", "corruptedPayload": "rejected",
|
|
"versions": measured["hardware"]["dependencies"], "nvenc": measured["hardware"]["nvenc"]}, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
print("Offline bundle passed: isolated runtime, blocked proxies, Windows picker, NVENC, repair, scoped uninstall and damaged-payload rejection.")
|