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

115 lines
5.3 KiB
Python

"""Run selected regression modules with isolated state and no external effects."""
from __future__ import annotations
import argparse
import contextlib
import io
import json
import os
import socket
import sys
import tempfile
import time
import unittest
from pathlib import Path
from unittest import mock
parser = argparse.ArgumentParser()
parser.add_argument("project")
parser.add_argument("output")
parser.add_argument("modules", nargs="*")
args = parser.parse_args()
project = Path(args.project).resolve()
output = Path(args.output).resolve()
output.parent.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(project))
sys.dont_write_bytecode = True
os.environ["QT_QPA_PLATFORM"] = "offscreen"
modules = args.modules or [
"test_visual_pipeline", "test_vision_fallback", "test_message_navigation",
"test_self_reply_loop", "test_vision", "test_session_name",
"test_unread_cross_machine", "test_reply_safety",
]
attempts = []
def blocked(name):
def reject(*a, **kw):
attempts.append(name)
raise RuntimeError("Audit blocks real external action: " + name)
return reject
original_connect = socket.socket.connect
original_connect_ex = socket.socket.connect_ex
def loopback_connect(self, address):
if isinstance(address, tuple) and address[0] in ("127.0.0.1", "localhost", "::1"):
return blocked("socket.connect")()
return blocked("socket.connect")()
def loopback_connect_ex(self, address):
if isinstance(address, tuple) and address[0] in ("127.0.0.1", "localhost", "::1"):
return blocked("socket.connect_ex")()
return blocked("socket.connect_ex")()
with tempfile.TemporaryDirectory(prefix="wechat-review-audit-", ignore_cleanup_errors=True) as temporary, contextlib.ExitStack() as stack:
os.chdir(temporary)
import runtime_paths
stack.enter_context(mock.patch.object(runtime_paths, "application_data_dir", return_value=Path(temporary)))
stack.enter_context(mock.patch.object(socket.socket, "connect", loopback_connect))
stack.enter_context(mock.patch.object(socket.socket, "connect_ex", loopback_connect_ex))
import requests
stack.enter_context(mock.patch.object(requests.adapters.HTTPAdapter, "send", side_effect=blocked("HTTP transport")))
import pyautogui
for name in ("click", "doubleClick", "rightClick", "moveTo", "moveRel", "dragTo", "dragRel", "scroll", "hscroll", "vscroll", "press", "hotkey", "write", "typewrite", "keyDown", "keyUp", "mouseDown", "mouseUp", "screenshot"):
if hasattr(pyautogui, name):
stack.enter_context(mock.patch.object(pyautogui, name, side_effect=blocked("pyautogui." + name)))
import pyperclip
for name in ("copy", "paste"):
stack.enter_context(mock.patch.object(pyperclip, name, side_effect=blocked("pyperclip." + name)))
import win32gui
for name in ("SetForegroundWindow", "SetWindowPos", "MoveWindow", "ShowWindow", "PostMessage", "SendMessage"):
if hasattr(win32gui, name):
stack.enter_context(mock.patch.object(win32gui, name, side_effect=blocked("win32gui." + name)))
from PIL import ImageGrab
stack.enter_context(mock.patch.object(ImageGrab, "grab", side_effect=blocked("ImageGrab.grab")))
# Guards stay below per-test mocks, so expected synthetic operations remain testable.
import wechat_bot
stack.enter_context(mock.patch.object(wechat_bot.ctypes.windll.user32, "PrintWindow", side_effect=blocked("PrintWindow")))
import send_lock
stack.enter_context(mock.patch.object(send_lock, "_MUTEX_NAME", "Local\\VisualAuditSendLock-" + str(os.getpid())))
log = io.StringIO()
started = time.monotonic()
suite = unittest.defaultTestLoader.loadTestsFromNames(modules)
with contextlib.redirect_stdout(log), contextlib.redirect_stderr(log):
result = unittest.TextTestRunner(stream=log, verbosity=2).run(suite)
report = {
"project": str(project), "modules": modules, "tests_run": result.testsRun,
"failures": len(result.failures), "errors": len(result.errors),
"skipped": len(result.skipped), "elapsed_seconds": round(time.monotonic() - started, 3),
"blocked_external_actions": attempts,
"failure_details": [{"test": str(test), "trace": trace} for test, trace in result.failures + result.errors],
"skip_details": [{"test": str(test), "reason": reason} for test, reason in result.skipped],
}
# Dispose native widgets and log handlers before removing isolated temp data.
import logging
try:
from PySide6.QtCore import QCoreApplication, QEvent
from PySide6.QtWidgets import QApplication
app = QApplication.instance()
if app:
for widget in app.topLevelWidgets():
widget.close()
widget.deleteLater()
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
app.processEvents()
finally:
logging.shutdown()
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: value for key, value in report.items() if key not in ("failure_details", "skip_details")}, ensure_ascii=False))
for item in report["failure_details"]:
print(item["test"] + "\n" + item["trace"])
exit_code = 0 if result.wasSuccessful() else 1
os.chdir(project)
raise SystemExit(exit_code)