Files
kefu/deploy/plaintext-chat-20260918/verify_frozen_plaintext_chat_notifications.py
T
2026-09-21 10:34:06 +08:00

368 lines
20 KiB
Python

"""Run finalized synthetic notification regressions against notification modules from EXE PYZ.
Both tested modules and their lazy reply_session_policy helper use this same EXE.
Run only after the final EXE and production test files have been synchronized:
C:\\wechat_rpa\\.build-venv\\Scripts\\python.exe <this-script>
No packages are installed. Only pure Python pytest dependencies may fall back to
Python 3.11 site-packages. All application state stays in a temporary directory;
unmocked external actions and skipped tests make the verification fail.
"""
from __future__ import annotations
import argparse
import contextlib
import hashlib
import importlib
import importlib.abc
import importlib.machinery
import importlib.util
import io
import json
import marshal
import os
from pathlib import Path
import socket
import sqlite3
import sys
import tempfile
import time
import traceback
import types
from unittest import mock
from urllib.parse import unquote, urlsplit
PROJECT = Path(r"C:\wechat_rpa")
FALLBACK = Path(r"C:\Users\isard\AppData\Local\Programs\Python\Python311\Lib\site-packages")
EXE_RELATIVE = "dist/ZhenAI-WeCom-Assistant-v1.4.29/ZhenAI-WeCom-Assistant-v1.4.29.exe"
PURE_FALLBACK = frozenset({"pytest", "_pytest", "pluggy", "iniconfig", "packaging", "pygments", "colorama"})
def inside(path, root):
try:
Path(path).resolve().relative_to(root.resolve())
return True
except (TypeError, ValueError, OSError):
return False
def sha256(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
for part in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(part)
return digest.hexdigest()
def allowed_fallback(name, origin, root):
if Path(origin).suffix.lower() not in (".py", ".pyc"):
return False
if name.split(".", 1)[0] in PURE_FALLBACK:
return True
# pytest 8.4 ships this shim in its own distribution RECORD. Reject the
# unrelated pylib package (py/__init__.py) if it has higher precedence.
return (name == "py" and Path(origin).resolve() == root / "py.py") or (
name in {"py.error", "py.path"} and inside(origin, root / "_pytest" / "_py"))
class PureFallbackOnly(importlib.abc.MetaPathFinder):
"""Prevent other CPython 3.11 packages/extensions entering a 3.12 process."""
def __init__(self, root):
self.root = root
def find_spec(self, fullname, path=None, target=None):
spec = importlib.machinery.PathFinder.find_spec(fullname, path, target)
if spec is None:
return None
locations = [spec.origin] if spec.origin not in (None, "built-in", "frozen") else []
locations += list(spec.submodule_search_locations or ())
if any(inside(location, self.root) for location in locations):
if not spec.origin or not allowed_fallback(fullname, spec.origin, self.root):
raise ImportError("Forbidden Python 3.11 fallback dependency: " + fullname)
return None
def fallback_inventory(root):
result = []
for name, module in tuple(sys.modules.items()):
origin = getattr(module, "__file__", None)
if origin and inside(origin, root):
if not allowed_fallback(name, origin, root):
raise RuntimeError("Unexpected Python 3.11 module: " + name)
result.append({"module": name, "path": str(origin)})
return sorted(result, key=lambda row: row["module"])
class FrozenCode(importlib.abc.MetaPathFinder, importlib.abc.Loader):
def __init__(self, code, exe, isolated_root):
self.code, self.exe, self.isolated_root = code, exe, isolated_root
def find_spec(self, fullname, path=None, target=None):
if fullname in self.code:
return importlib.util.spec_from_loader(fullname, self, origin=str(self.exe) + "!PYZ.pyz/" + fullname)
return None
def create_module(self, spec):
return None
def exec_module(self, module):
# Contacts includes __file__.parent in cache search paths. Keep it isolated
# while retaining the real EXE origin in __spec__ and this loader.
module.__file__ = str(self.isolated_root / (module.__name__ + ".py"))
exec(self.code[module.__name__], module.__dict__)
class Outcomes:
def __init__(self):
self.collected = []
self.reports = []
self.collection_errors = []
def pytest_collection_finish(self, session):
self.collected = [item.nodeid for item in session.items]
def pytest_collectreport(self, report):
if report.failed or report.skipped:
self.collection_errors.append({"test": report.nodeid, "outcome": report.outcome,
"detail": str(report.longrepr)})
def pytest_runtest_logreport(self, report):
self.reports.append({"test": report.nodeid, "phase": report.when, "outcome": report.outcome,
"xfail": str(getattr(report, "wasxfail", "")),
"detail": str(report.longrepr) if report.failed or report.skipped else ""})
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--project", type=Path, default=PROJECT)
parser.add_argument("--exe", type=Path)
parser.add_argument("--fallback", type=Path, default=FALLBACK)
parser.add_argument("--output", type=Path, default=Path(__file__).with_name("frozen-plaintext-chat-notifications.json"))
args = parser.parse_args()
plan = json.loads(Path(__file__).with_name('plaintext-chat-release-plan.json').read_text(encoding='utf-8-sig'))
if plan.get('finalized') is not True or plan.get('version') != '1.4.29':
raise RuntimeError('Root-finalized 1.4.29 plan required')
expected_tests = int(plan.get('notificationExpectedTests', 0))
frozen_modules = tuple(plan.get('notificationFrozenModules', ()))
test_names = tuple(plan.get('notificationTestFiles', ()))
if expected_tests <= 0 or not test_names:
raise RuntimeError('Confirm exact synthetic notification test files/count before verification')
if not {'review_assistant_contacts','review_assistant','reply_session_policy','review_state'} <= set(frozen_modules):
raise RuntimeError('Notification modules and review-state policy must come from EXE PYZ')
if not set(frozen_modules) <= set(plan['frozenModules']):
raise RuntimeError('Notification PYZ modules must also pass source fingerprint verification')
project, fallback = args.project.resolve(), args.fallback.resolve()
exe = (args.exe or project / EXE_RELATIVE).resolve()
output = args.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
log = io.StringIO()
started = time.monotonic()
attempts = []
report = {"ok": False, "expected_tests": expected_tests, "python": sys.version,
"executable": sys.executable, "project": str(project), "exe": str(exe),
"blocked_external_actions": attempts, "tests_run": 0, "passed": 0,
"failures": 0, "skipped": 0, "plugin_autoload_disabled": True}
state = {"active": False}
initial_cwd = Path.cwd()
def reject(name):
attempts.append(name)
raise RuntimeError("Frozen notification audit blocks external action: " + name)
def blocked(name):
def call(*a, **kw):
return reject(name)
return call
try:
with contextlib.redirect_stdout(log), contextlib.redirect_stderr(log):
if sys.version_info[:2] != (3, 12):
raise RuntimeError("Use the production Python 3.12 build venv")
if not inside(sys.executable, project / ".build-venv"):
raise RuntimeError("Interpreter is not the production build venv")
tests = [project / 'tests' / name for name in test_names]
if any(not inside(path, project / 'tests') or path.suffix != '.py' for path in tests):
raise RuntimeError('Only synthetic Python tests below project/tests may be selected')
for path in [exe, *tests, fallback / "pytest" / "__init__.py"]:
if not path.is_file():
raise FileNotFoundError("Required finalized artifact is missing: " + str(path))
report["test_files"] = [{"path": str(path), "sha256": sha256(path)} for path in tests]
report["exe_sha256"] = sha256(exe)
sys.dont_write_bytecode = True
os.environ["QT_QPA_PLATFORM"] = "offscreen"
os.environ["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] = "1"
os.environ.pop("PYTEST_PLUGINS", None)
os.environ.pop("PYTEST_ADDOPTS", None)
# Load native Qt BEFORE making any 3.11 dependencies discoverable.
from PySide6 import QtCore
import PySide6
import shiboken6
qt_modules = (PySide6, QtCore, shiboken6)
for module in qt_modules:
if not inside(module.__file__, project / ".build-venv"):
raise RuntimeError("Qt must come from the Python 3.12 build venv")
report["qt_dependencies"] = {module.__name__: module.__file__ for module in qt_modules}
fallback_guard = PureFallbackOnly(fallback)
sys.meta_path.insert(0, fallback_guard)
sys.path.append(str(fallback)) # No .pth execution, no installation.
dependencies = {name: importlib.import_module(name) for name in
("pytest", "pluggy", "iniconfig", "packaging", "pygments", "colorama")}
pytest = dependencies["pytest"]
report["pytest_dependencies"] = {name: {"path": module.__file__,
"version": str(getattr(module, "__version__", "")),
"from_311_fallback": inside(module.__file__, fallback)}
for name, module in dependencies.items()}
report["pytest_compatibility_shim"] = {"distribution": "pytest", "path": sys.modules["py"].__file__}
from PyInstaller.archive.readers import CArchiveReader
pyz = CArchiveReader(str(exe)).open_embedded_archive("PYZ.pyz")
code = {name: pyz.extract(name) for name in frozen_modules}
if any(not isinstance(value, types.CodeType) for value in code.values()):
raise RuntimeError("Required executable PYZ modules were not found")
report["frozen_modules"] = {name: {"origin": str(exe) + "!PYZ.pyz/" + name,
"code_sha256": hashlib.sha256(marshal.dumps(value)).hexdigest()} for name, value in code.items()}
with tempfile.TemporaryDirectory(prefix="frozen-customer-notify-", ignore_cleanup_errors=True) as temp, contextlib.ExitStack() as stack:
isolated = Path(temp).resolve()
os.chdir(isolated)
report["isolated_state"] = str(isolated)
stack.enter_context(mock.patch.dict(os.environ, {"LOCALAPPDATA": str(isolated),
"APPDATA": str(isolated), "TMP": str(isolated), "TEMP": str(isolated)}))
stack.enter_context(mock.patch.object(tempfile, "tempdir", str(isolated)))
# Runtime helpers are infrastructure, not either tested module.
sys.path.insert(0, str(project))
try:
runtime = importlib.import_module("runtime_paths")
finally:
sys.path.remove(str(project))
for name in ("application_data_dir", "resource_dir"):
stack.enter_context(mock.patch.object(runtime, name, return_value=isolated))
stack.enter_context(mock.patch.object(runtime, "resource_path", side_effect=lambda *parts: isolated.joinpath(*parts)))
for name in ("connect", "connect_ex", "send", "sendall", "sendto"):
stack.enter_context(mock.patch.object(socket.socket, name, blocked("socket." + name)))
stack.enter_context(mock.patch.object(socket, "getaddrinfo", blocked("socket.getaddrinfo")))
# Native modules are blocked underneath each test's explicit fakes.
native = types.ModuleType("wecom_native_sender")
native.NativeSender = blocked("native.NativeSender")
native.discover = blocked("native.discover")
native.readonly_build_validation_scope = contextlib.nullcontext
reply_db = types.ModuleType("reply_database")
reply_db.LiveReplyDatabase = type("BlockedLiveReplyDatabase", (),
{"_open_database": staticmethod(blocked("native.LiveReplyDatabase"))})
send_lock = types.ModuleType("send_lock")
send_lock.try_acquire = blocked("native.send_lock.acquire")
send_lock.release = blocked("native.send_lock.release")
substitutes = {"wecom_native_sender": native, "reply_database": reply_db, "send_lock": send_lock}
for module_name in ("pyautogui", "pyperclip", "win32gui", "win32api", "PIL.ImageGrab"):
module = types.ModuleType(module_name)
def get_attribute(name, prefix=module_name):
if name.startswith("__"):
raise AttributeError(name)
return blocked(prefix + "." + name)
module.__getattr__ = get_attribute
substitutes[module_name] = module
stack.enter_context(mock.patch.dict(sys.modules, substitutes))
def database_path(value):
text = os.fsdecode(value)
if text == ":memory:":
return isolated / "memory"
if text.startswith("file:"):
parsed = urlsplit(text)
if parsed.netloc:
return Path("//" + parsed.netloc + unquote(parsed.path))
text = unquote(parsed.path)
if len(text) > 2 and text[0] == "/" and text[2] == ":":
text = text[1:]
return Path(text)
real_connect = sqlite3.connect
def safe_connect(database, *a, **kw):
if not inside(database_path(database), isolated):
return reject("sqlite outside isolated state")
return real_connect(database, *a, **kw)
stack.enter_context(mock.patch.object(sqlite3, "connect", safe_connect))
private_roots = (project, Path(r"C:\kefu\wechat_rpa"),
Path(os.environ.get("USERPROFILE", str(isolated))) / "AppData/Local/ZhenYangTangRPA")
def audit(event, values):
if not state["active"]:
return
if event in {"subprocess.Popen", "os.system", "os.startfile", "os.startfile/2",
"ctypes.dlopen", "ctypes.dlsym", "socket.connect", "socket.bind", "socket.sendto"}:
reject(event)
if event == "sqlite3.connect" and not inside(database_path(values[0]), isolated):
reject("sqlite audit outside isolated state")
if event == "open" and isinstance(values[0], (str, bytes, os.PathLike)):
path = Path(os.fsdecode(values[0]))
if inside(path, isolated):
return
flags = values[2] if len(values) > 2 and isinstance(values[2], int) else 0
mode = values[1] if len(values) > 1 and isinstance(values[1], str) else ""
write = flags & (os.O_WRONLY | os.O_RDWR | os.O_CREAT | os.O_TRUNC | os.O_APPEND) or any(c in mode for c in "wax+")
if write:
reject("file write outside isolated state")
if (any(inside(path, root) for root in private_roots)
and not inside(path, project / ".build-venv")
and path.suffix.lower() not in {".py", ".pyc", ".pyd", ".dll"}):
reject("real application data read")
if event in {"os.remove", "os.rmdir", "os.mkdir", "os.rename"}:
paths = values[:2] if event == "os.rename" else values[:1]
if any(isinstance(path, (str, bytes, os.PathLike)) and not inside(os.fsdecode(path), isolated) for path in paths):
reject("filesystem mutation outside isolated state")
sys.addaudithook(audit)
state["active"] = True
frozen_loader = FrozenCode(code, exe, isolated)
if any(name in sys.modules for name in frozen_modules):
raise RuntimeError("Tested module was loaded before the frozen loader")
sys.meta_path.insert(0, frozen_loader)
frozen_loaded = {name: importlib.import_module(name) for name in frozen_modules}
for name, module in frozen_loaded.items():
if module.__loader__ is not frozen_loader:
raise RuntimeError("Source fallback detected: " + name)
ini = isolated / "pytest.ini"
ini.write_text("[pytest]\n", encoding="utf-8")
outcomes = Outcomes()
pytest_args = ["-q", "--strict-markers", "-p", "no:cacheprovider", "--noconftest",
"--import-mode=importlib", "--rootdir=" + str(isolated), "-c", str(ini),
"--basetemp=" + str(isolated / "pytest-tmp"),
"--log-file=" + str(isolated / "pytest.log"), *map(str, tests)]
report["pytest_args"] = pytest_args
report["pytest_exit_code"] = int(pytest.main(pytest_args, plugins=[outcomes]))
report["tests_run"] = len(outcomes.collected)
report["passed"] = sum(row["phase"] == "call" and row["outcome"] == "passed" for row in outcomes.reports)
report["skipped"] = sum(row["outcome"] == "skipped" or bool(row["xfail"]) for row in outcomes.reports)
report["failures"] = sum(row["outcome"] == "failed" for row in outcomes.reports)
report["collection_errors"] = outcomes.collection_errors
report["test_results"] = outcomes.reports
report["fallback_modules"] = fallback_inventory(fallback)
for name, module in frozen_loaded.items():
if sys.modules.get(name) is not module or module.__loader__ is not frozen_loader:
raise RuntimeError("Tested frozen module was not restored after test mocks: " + name)
report["ok"] = (report["pytest_exit_code"] == 0 and report["tests_run"] == expected_tests
and report["passed"] == expected_tests and report["skipped"] == 0
and report["failures"] == 0 and not outcomes.collection_errors and not attempts)
state["active"] = False
os.chdir(initial_cwd)
except BaseException:
state["active"] = False
report["error"] = traceback.format_exc()
log.write(report["error"])
report["ok"] = False
finally:
state["active"] = False
os.chdir(initial_cwd)
report["elapsed_seconds"] = round(time.monotonic() - started, 3)
output.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
output.with_suffix(".log").write_text(log.getvalue(), encoding="utf-8")
print(json.dumps({key: report[key] for key in
("ok", "tests_run", "passed", "failures", "skipped", "blocked_external_actions", "elapsed_seconds")}, ensure_ascii=False))
print("Report: " + str(output))
print("Log: " + str(output.with_suffix(".log")))
return 0 if report["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())