"""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 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())