101 lines
4.8 KiB
Python
101 lines
4.8 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 original_connect(self, address)
|
|
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 original_connect_ex(self, address)
|
|
return blocked("socket.connect_ex")()
|
|
|
|
with tempfile.TemporaryDirectory(prefix="wechat-visual-audit-") 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.sessions.Session, "request", side_effect=blocked("HTTP request")))
|
|
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],
|
|
}
|
|
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)
|