Files
Haocode/tests/test_wv2_guard.py
T
sorrow404null a7412824e0 chore: import original project baseline
Import the pre-repair source tree as the history baseline.
Runtime data (data/), virtualenvs, bytecode caches and logs are
gitignored so local secrets and user state stay out of the repo.
2026-09-17 16:40:01 +08:00

130 lines
5.4 KiB
Python
Raw 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.
# -*- coding: utf-8 -*-
"""
WebView2 守卫单测 —— 锁死 T0 级事故不再复发
事故链(已发生):
每个 MainWindow()(含 offscreen 测试)→ get_environment()
→ 无条件 `taskkill /F /IM msedgewebview2.exe`
→ 把【正在运行的生产 app】的 WebView2 浏览器进程一并杀掉
→ 它的 controller 变 disposedset_bounds 报 0x8007139F
→ DOM 照渲染但视觉层永久空白(“选中会话不渲染核心内容”)
覆盖:
T1 无头环境(QT_QPA_PLATFORM=offscreen)→ 不启用 WebView2
T2 HAOCODE_FORCE_QTWEBENGINE=1 → 不启用
T3 桌面平台(QT_QPA_PLATFORM=windows)→ 允许
T4 单实例锁:持有者独占;【另一个进程】拿不到(跨进程互斥,是真守卫)
T5 offscreen 下 get_environment() 直接返回 None(根本不碰共享 profile
T6 同一进程重复 acquire 幂等返回 True(不会把自己锁死)
运行: PYTHONIOENCODING=utf-8 python tests/test_wv2_guard.py
"""
import os
import sys
import tempfile
import subprocess
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 铁律:测试用独立锁文件,不得与正在运行的 app 争 data/app_instance.lock
# (否则断言会依赖“app 是否在跑”)
_LOCK_TMP = os.path.join(tempfile.gettempdir(), "haocode_wv2guard_%d.lock" % os.getpid())
os.environ["HAOCODE_INSTANCE_LOCK_FILE"] = _LOCK_TMP
if os.path.exists(_LOCK_TMP):
try:
os.remove(_LOCK_TMP)
except Exception:
pass
RESULTS = []
def check(name, cond, extra=""):
RESULTS.append((name, bool(cond)))
print(f"{'PASS' if cond else 'FAIL'} {name} {extra if not cond else ''}", flush=True)
import core.webview2 as W # noqa: E402
_ORIG = dict(os.environ)
try:
# ---------------- T1/T2/T3: 环境守卫 ----------------
os.environ["QT_QPA_PLATFORM"] = "offscreen"
os.environ.pop("HAOCODE_FORCE_QTWEBENGINE", None)
check("T1.1 offscreen → 不允许 WebView2", W._wv2_allowed_here() is False)
os.environ["QT_QPA_PLATFORM"] = "windows"
check("T1.2 windows → 允许 WebView2", W._wv2_allowed_here() is True)
for p in ("minimal", "minimalegl", "vnc", "wayland", "embedded", "offscreen"):
os.environ["QT_QPA_PLATFORM"] = p
if W._wv2_allowed_here() is not False:
check(f"T1.3 平台 {p} 应被拒绝", False, p)
break
else:
check("T1.3 非 windows 平台全部拒绝", True)
os.environ["QT_QPA_PLATFORM"] = "windows"
os.environ["HAOCODE_FORCE_QTWEBENGINE"] = "1"
check("T2.1 强制回退开关生效", W._wv2_allowed_here() is False)
os.environ.pop("HAOCODE_FORCE_QTWEBENGINE", None)
# ---------------- T4: 跨进程单实例互斥(核心) ----------------
os.environ["QT_QPA_PLATFORM"] = "windows"
W._INSTANCE_LOCK["fh"] = None # 从头开始,避免受本进程历史影响
first = W.acquire_instance_lock()
check("T4.1 首个 acquires 成功", first is True, str(first))
check("T4.2 同进程重复 acquire 幂等为 True",
W.acquire_instance_lock() is True)
code = ("import sys; sys.path.insert(0, r'%s');"
"import core.webview2 as W; print(W.acquire_instance_lock())"
% os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
r = subprocess.run([sys.executable, "-c", code], capture_output=True,
text=True, encoding="utf-8", errors="replace", timeout=60)
got = (r.stdout or "").strip().splitlines()
got = got[-1] if got else ""
check("T4.3 另一进程拿不到锁(互斥生效)", got == "False", f"stdout={got!r} err={(r.stderr or '')[:120]}")
# ---------------- T5: offscreen 下 get_environment 直接 None ----------------
os.environ["QT_QPA_PLATFORM"] = "offscreen"
W._env = None
env = W.get_environment(None)
check("T5.1 offscreen 下 get_environment → None(不碰共享 profile", env is None)
check("T5.2 被守卫拦下后 _env 仍为空", W._env is None)
# ---------------- T6: 已有实例在跑时 → 不启用 WebView2 ----------------
# 此时本进程仍持有锁;用子进程模拟“后来的实例”
code2 = ("import sys; sys.path.insert(0, r'%s');"
"import core.webview2 as W; print(W.get_environment(None) is None)"
% os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
r2 = subprocess.run([sys.executable, "-c", code2], capture_output=True,
text=True, encoding="utf-8", errors="replace",
env={**os.environ, "QT_QPA_PLATFORM": "windows"},
timeout=60)
out2 = (r2.stdout or "").strip().splitlines()
out2 = out2[-1] if out2 else ""
check("T6.1 已有实例持锁 → 后来的实例拿到 None(回落 QtWebEngine,不 taskkill",
out2 == "True", f"stdout={out2!r} err={(r2.stderr or '')[:160]}")
finally:
try:
fh = W._INSTANCE_LOCK.get("fh")
if fh is not None:
fh.close()
except Exception:
pass
try:
if os.path.exists(_LOCK_TMP):
os.remove(_LOCK_TMP)
except Exception:
pass
os.environ.clear()
os.environ.update(_ORIG)
failed = [n for n, ok in RESULTS if not ok]
print(f"\n===== {len(RESULTS) - len(failed)}/{len(RESULTS)} PASS =====", flush=True)
print("ALL PASS" if not failed else f"FAILED: {failed}", flush=True)
sys.exit(0 if not failed else 1)