- Added direct project directory drag-and-drop support with automatic root workspace and requirements.txt detection - Added smart entry script prioritization and interactive selection dialog for multi-script projects - Expanded AST dependency analysis across the entire project workspace to capture sub-package imports - Implemented local module and resource whitelisting to prevent unintended pip installations of local packages - Expanded PyPI mappings and Auto-Hooks rules for C extensions and dynamic libraries (PyCryptodome, NaCl, pynput, etc.) - Added automatic search path injection (`--paths` for PyInstaller and `PYTHONPATH` for Nuitka) for sub-directory entries - Improved `requirements.txt` parser to safely handle inline comments and environment markers - Added cross-hierarchy import detection and workspace configuration tips for standalone scripts - Fixed SyntaxError caused by console pause code injection preceding `from __future__` statements - Fixed TypeError crash caused by `os.walk` iteration variable shadowing the global translation function Update QUICKSTART.md、README.MD...
547 lines
22 KiB
Python
547 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
=============================================================================
|
||
QPyPack 全功能终极一体化自检与双引擎真实打包验证系统 (UI 深度集成完整版)
|
||
=============================================================================
|
||
特性:
|
||
- 深度集成:直接实例化 MainWindow 模拟用户拖拽与点击,完整测试 UI->Engine 链路
|
||
- 前瞻兼容:不写死参数字典,直接读取 UI 界面控件生成的配置,随软件升级自动适配
|
||
- 自动自净:测试开始前与结束后自动抹除一切 build/、spec、egg-info 残留
|
||
- 全量覆盖:包含 AST 分析、I18N 多语言引擎、状态机逻辑等 8 大核心维度
|
||
=============================================================================
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import re
|
||
import json
|
||
import time
|
||
import shutil
|
||
import zipfile
|
||
import tempfile
|
||
import subprocess
|
||
import traceback
|
||
from pathlib import Path
|
||
|
||
# 全局开启 Qt 无头模式与 Windows 字体 (屏蔽无关警告)
|
||
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
||
if sys.platform == "win32":
|
||
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||
if os.path.exists(r"C:\Windows\Fonts"):
|
||
os.environ["QT_QPA_FONTDIR"] = r"C:\Windows\Fonts"
|
||
os.environ["QT_LOGGING_RULES"] = "qt.qpa.fonts=false;qt.qpa.*=false;qt.multimedia*=false"
|
||
|
||
current_path = Path(__file__).resolve()
|
||
if current_path.parent.name == "tests":
|
||
ROOT_DIR = current_path.parent.parent
|
||
else:
|
||
ROOT_DIR = current_path.parent
|
||
|
||
PACKAGE_DIR = ROOT_DIR / "qpypack"
|
||
LOCALES_DIR = PACKAGE_DIR / "locales"
|
||
ICON_PATH = PACKAGE_DIR / "icon.ico"
|
||
|
||
if str(ROOT_DIR) not in sys.path:
|
||
sys.path.insert(0, str(ROOT_DIR))
|
||
|
||
|
||
class Colors:
|
||
CYAN = "\033[96m"
|
||
GREEN = "\033[92m"
|
||
YELLOW = "\033[93m"
|
||
RED = "\033[91m"
|
||
RESET = "\033[0m"
|
||
BOLD = "\033[1m"
|
||
|
||
|
||
def log_pass(msg):
|
||
print(f" {Colors.GREEN}✔ [PASS]{Colors.RESET} {msg}")
|
||
|
||
|
||
def log_warn(msg):
|
||
print(f" {Colors.YELLOW}⚠ [WARN]{Colors.RESET} {msg}")
|
||
|
||
|
||
def log_fail(msg):
|
||
print(f" {Colors.RED}✖ [FAIL]{Colors.RESET} {msg}")
|
||
|
||
|
||
def log_info(msg):
|
||
print(f" {Colors.CYAN}ℹ [INFO]{Colors.RESET} {msg}")
|
||
|
||
|
||
# =====================================================================
|
||
# 垃圾自净机制 (自动清理根目录残留)
|
||
# =====================================================================
|
||
def clean_project_junk():
|
||
"""扫描并强制清除项目根目录下的所有测试与构建残留"""
|
||
junk_patterns_dir = ["build", "dist", "*.egg-info", "__pycache__", ".pytest_cache"]
|
||
junk_patterns_file = ["*.spec", ".coverage"]
|
||
|
||
# 清理目录
|
||
for pattern in junk_patterns_dir:
|
||
for p in ROOT_DIR.glob(pattern):
|
||
if p.is_dir() and p != PACKAGE_DIR:
|
||
try:
|
||
shutil.rmtree(p, ignore_errors=True)
|
||
except Exception:
|
||
pass
|
||
for p in (ROOT_DIR / "tests").glob(pattern):
|
||
if p.is_dir():
|
||
try:
|
||
shutil.rmtree(p, ignore_errors=True)
|
||
except Exception:
|
||
pass
|
||
|
||
# 清理文件
|
||
for pattern in junk_patterns_file:
|
||
for p in ROOT_DIR.glob(pattern):
|
||
if p.is_file():
|
||
try:
|
||
p.unlink(missing_ok=True)
|
||
except Exception:
|
||
pass
|
||
for p in (ROOT_DIR / "tests").glob(pattern):
|
||
if p.is_file():
|
||
try:
|
||
p.unlink(missing_ok=True)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
# 测试前先自净一次历史垃圾
|
||
clean_project_junk()
|
||
|
||
|
||
# =====================================================================
|
||
# 1. 静态资源与配置深度检测
|
||
# =====================================================================
|
||
def test_01_static_assets():
|
||
"""测试 1: 静态资源与配置物理合法性"""
|
||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 1/8] 静态资源与工程配置完整性检查...{Colors.RESET}")
|
||
|
||
assert ICON_PATH.exists(), f"关键图标缺失: {ICON_PATH}"
|
||
assert ICON_PATH.stat().st_size > 1024, "icon.ico 文件大小异常"
|
||
with open(ICON_PATH, "rb") as f:
|
||
header = f.read(4)
|
||
assert header == b"\x00\x00\x01\x00", "icon.ico 不是有效的 Windows ICO 格式"
|
||
log_pass(f"图标格式与二进制签名正常 (大小: {ICON_PATH.stat().st_size} bytes)")
|
||
|
||
toml_path = ROOT_DIR / "pyproject.toml"
|
||
assert toml_path.exists(), "pyproject.toml 缺失"
|
||
assert "qpypack" in toml_path.read_text(encoding="utf-8").lower()
|
||
log_pass("pyproject.toml 配置文件解析正常")
|
||
|
||
|
||
# =====================================================================
|
||
# 2. 16 种多语言语法、Key 一致性与占位符检测
|
||
# =====================================================================
|
||
def test_02_locales_and_placeholders():
|
||
"""测试 2: 16 国多语言深度校验"""
|
||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 2/8] 16 国国际化语言包与语法占位符检测...{Colors.RESET}")
|
||
|
||
assert LOCALES_DIR.is_dir(), "locales 目录不存在"
|
||
json_files = list(LOCALES_DIR.glob("*.json"))
|
||
assert len(json_files) == 16, f"应包含 16 种语言包,实际发现 {len(json_files)} 个"
|
||
log_pass(f"成功定位到全部 {len(json_files)} 个语言包文件")
|
||
|
||
parsed = {}
|
||
for jf in json_files:
|
||
with open(jf, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
assert isinstance(data, dict), f"{jf.name} 格式非合法字典"
|
||
parsed[jf.name] = data
|
||
log_pass("所有语言包 JSON 语法解析正常")
|
||
|
||
base_data = parsed.get("zh_CN.json", {})
|
||
assert base_data, "基准语言包 zh_CN.json 缺失或为空"
|
||
base_keys = set(base_data.keys())
|
||
|
||
missing_map = {}
|
||
for name, data in parsed.items():
|
||
if name == "zh_CN.json":
|
||
continue
|
||
missing = base_keys - set(data.keys())
|
||
if missing:
|
||
missing_map[name] = missing
|
||
|
||
if missing_map:
|
||
for fname, keys in missing_map.items():
|
||
log_warn(f"{fname} 缺少 {len(keys)} 个词条: {list(keys)[:3]}...")
|
||
else:
|
||
log_pass("16 国语言包与 zh_CN.json 词条保持 100% 完全对齐!")
|
||
|
||
|
||
# =====================================================================
|
||
# 3. AST 语法分析与依赖提取算法测试
|
||
# =====================================================================
|
||
def test_03_ast_engine():
|
||
"""测试 3: AST 源码静态语法分析与依赖侦测 (QPyPack 原生算法)"""
|
||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 3/8] AST 静态分析与依赖侦测 (QPyPack 原生算法)...{Colors.RESET}")
|
||
|
||
from qpypack.main import extract_project_imports_via_ast
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
mock_file = Path(tmpdir) / "test_ast.py"
|
||
mock_file.write_text("import os, sys\nimport requests.auth\nfrom PyQt5.QtWidgets import QMainWindow", encoding="utf-8")
|
||
|
||
modules = extract_project_imports_via_ast(mock_file, scan_dir=False)
|
||
assert {"requests", "PyQt5", "os", "sys"}.issubset(modules), f"QPyPack 原生提取异常: {modules}"
|
||
log_pass(f"QPyPack AST 引擎成功提取模块依赖: {modules}")
|
||
|
||
|
||
# =====================================================================
|
||
# 4. 核心模块与入口健全性 (集成配置存储及多语言引擎联调)
|
||
# =====================================================================
|
||
def test_04_core_logic():
|
||
"""测试 4: 核心配置管理与翻译引擎联调测试"""
|
||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 4/8] QPyPack 核心配置读取与多语言翻译引擎测试...{Colors.RESET}")
|
||
|
||
from qpypack.main import load_config, I18N
|
||
|
||
config = load_config(retry=True)
|
||
assert "Settings" in config, "配置生成或加载失败"
|
||
assert "Mappings" in config, "包名映射字典缺失"
|
||
|
||
# 动态切换并断言多语言系统
|
||
I18N.set_language("en_US")
|
||
assert I18N.t("Build Settings") == "Build Settings", "英文翻译错误"
|
||
I18N.set_language("zh_CN")
|
||
assert I18N.t("Build Settings") == "构建设置", "中文翻译未命中映射"
|
||
|
||
log_pass("配置 I/O 系统与 I18N 多语言引擎运行正常")
|
||
|
||
|
||
# =====================================================================
|
||
# 5. UI 控件与主窗口状态机独立交互测试
|
||
# =====================================================================
|
||
def test_05_ui_interaction():
|
||
"""测试 5: Qt UI 界面与控件状态机逻辑交互测试 (独立无打包)"""
|
||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 5/8] Qt UI 控件与状态机模拟交互测试...{Colors.RESET}")
|
||
|
||
try:
|
||
from PySide6.QtWidgets import QApplication
|
||
app = QApplication.instance() or QApplication(["qp_test", "-platform", "offscreen"])
|
||
except ImportError:
|
||
log_warn("未安装 PySide6,跳过 GUI 交互测试")
|
||
return
|
||
|
||
from PySide6.QtCore import QEventLoop, QTimer
|
||
from qpypack.main import MainWindow
|
||
window = MainWindow()
|
||
|
||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||
mock_py = Path(tmp_dir) / "mock_app.py"
|
||
mock_py.write_text("print('hello')", encoding="utf-8")
|
||
|
||
# 测试拖入脚本
|
||
window.on_script_selected(str(mock_py))
|
||
|
||
# 等待后台 AST 解析子线程完成
|
||
loop = QEventLoop()
|
||
timer = QTimer()
|
||
timer.timeout.connect(loop.quit)
|
||
timer.start(1000)
|
||
loop.exec()
|
||
timer.stop()
|
||
|
||
assert window.current_state == "ready", "脚本载入后状态机未变为 ready"
|
||
assert window.settings_panel.name_edit.text() == "mock_app", "未成功解析推导出默认程序名"
|
||
|
||
# 测试修改引擎配置
|
||
window.settings_panel.engine_combo.setCurrentIndex(1)
|
||
assert window.settings_panel.engine_combo.currentText() == "Nuitka"
|
||
|
||
log_pass("主窗口完成内存初始化,状态机推导与 UI 数据绑定响应正常")
|
||
|
||
|
||
# =====================================================================
|
||
# 6. Wheel 分发包真实打包与解压内嵌资产验证 (沙箱环境零残留)
|
||
# =====================================================================
|
||
def test_06_wheel_packaging():
|
||
"""测试 6: Wheel 包构建与资源嵌入"""
|
||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 6/8] Wheel (.whl) 分发包构建与资源嵌入深度检查...{Colors.RESET}")
|
||
|
||
with tempfile.TemporaryDirectory(prefix="qp_whl_") as tmp_dist:
|
||
try:
|
||
build_cmd = [sys.executable, "-m", "build", "--wheel", "--outdir", tmp_dist, str(ROOT_DIR)]
|
||
result = subprocess.run(build_cmd, capture_output=True, text=True, cwd=tmp_dist)
|
||
|
||
if result.returncode != 0:
|
||
result = subprocess.run(
|
||
[sys.executable, "-m", "pip", "wheel", "--no-deps", "-w", tmp_dist, str(ROOT_DIR)],
|
||
capture_output=True, text=True, cwd=tmp_dist
|
||
)
|
||
assert result.returncode == 0, f"构建 Wheel 失败:\n{result.stderr}"
|
||
|
||
wheels = list(Path(tmp_dist).glob("*.whl"))
|
||
assert wheels, "未找到生成的 .whl 文件"
|
||
wheel_file = wheels[0]
|
||
log_pass(f"成功构建 Wheel: {wheel_file.name}")
|
||
|
||
with zipfile.ZipFile(wheel_file, "r") as zf:
|
||
names = zf.namelist()
|
||
assert any("icon.ico" in n for n in names), "icon.ico 漏打包进 Wheel!"
|
||
json_in_wheel = [n for n in names if "locales/" in n and n.endswith(".json")]
|
||
assert len(json_in_wheel) >= 16, f"语言包未全部嵌入 Wheel (仅 {len(json_in_wheel)} 个)!"
|
||
log_pass(f"icon.ico 及 16 个语言包已 100% 嵌入 Wheel 压缩包中")
|
||
finally:
|
||
clean_project_junk()
|
||
|
||
|
||
# =====================================================================
|
||
# 辅助函数: 创建微型真实测试工程 & 产物实机拉起断言
|
||
# =====================================================================
|
||
def _create_mock_project(base_dir: Path) -> Path:
|
||
proj_dir = base_dir / "mock_app"
|
||
proj_dir.mkdir(parents=True, exist_ok=True)
|
||
assets_dir = proj_dir / "assets"
|
||
assets_dir.mkdir(exist_ok=True)
|
||
|
||
with open(assets_dir / "data.json", "w", encoding="utf-8") as f:
|
||
json.dump({"engine_test": "QPYPACK_REAL_OK"}, f)
|
||
|
||
with open(proj_dir / "calc.py", "w", encoding="utf-8") as f:
|
||
f.write("def multiply(a, b): return a * b\n")
|
||
|
||
entry_file = proj_dir / "app_main.py"
|
||
with open(entry_file, "w", encoding="utf-8") as f:
|
||
f.write('''# -*- coding: utf-8 -*-
|
||
__version__ = "1.0.0"
|
||
__app_name__ = "MockTestApp"
|
||
|
||
import sys, os, json, multiprocessing
|
||
from calc import multiply
|
||
|
||
def get_res(rel):
|
||
if hasattr(sys, '_MEIPASS'):
|
||
return os.path.join(sys._MEIPASS, rel)
|
||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), rel)
|
||
|
||
if __name__ == '__main__':
|
||
multiprocessing.freeze_support()
|
||
calc_res = multiply(6, 7)
|
||
res_path = get_res(os.path.join("assets", "data.json"))
|
||
val = "FAIL"
|
||
if os.path.exists(res_path):
|
||
with open(res_path, "r", encoding="utf-8") as f:
|
||
val = json.load(f).get("engine_test", "CORRUPTED")
|
||
else:
|
||
val = f"NOT_FOUND:{res_path}"
|
||
print(f"__QP_ASSERT__|{calc_res}|{val}|__QP_ASSERT_END__")
|
||
sys.exit(0)
|
||
''')
|
||
return entry_file
|
||
|
||
|
||
def _get_size(path: Path):
|
||
if path.is_file():
|
||
return path.stat().st_size
|
||
return sum(f.stat().st_size for f in path.rglob('*') if f.is_file())
|
||
|
||
|
||
def _assert_executable_execution(exe_path: Path):
|
||
if sys.platform == "darwin" and exe_path.is_dir() and exe_path.name.endswith(".app"):
|
||
exe_path = exe_path / "Contents" / "MacOS" / exe_path.stem
|
||
|
||
assert exe_path.exists(), f"未找到生成的 EXE/二进制 文件: {exe_path}"
|
||
start_t = time.time()
|
||
proc = subprocess.run([str(exe_path)], capture_output=True, text=True, timeout=20, cwd=str(exe_path.parent))
|
||
|
||
assert proc.returncode == 0, f"程序运行报错 (Code {proc.returncode}):\n{proc.stderr}"
|
||
assert "__QP_ASSERT__|42|QPYPACK_REAL_OK|__QP_ASSERT_END__" in proc.stdout, f"输出不符,资源提取失败:\n{proc.stdout}"
|
||
|
||
size_mb = _get_size(exe_path) / (1024 * 1024)
|
||
log_pass(f"产物实机拉起验证通过!耗时: {time.time() - start_t:.2f}s | 体积: {size_mb:.2f} MB | 静态资源捆绑读取正常")
|
||
|
||
|
||
# =====================================================================
|
||
# E2E 测试核心引擎: 完整模拟 MainWindow 界面交互与打包
|
||
# =====================================================================
|
||
def run_e2e_via_mainwindow(engine_name: str, tmp_dir: Path, is_onefile: bool):
|
||
"""
|
||
通过直接实例化 MainWindow 并触发界面事件,模拟真实用户的打包全流程。
|
||
这能保证测试紧跟 UI 升级,覆盖完整的 "界面配置 -> 参数生成 -> 线程打包" 链路。
|
||
"""
|
||
from PySide6.QtCore import QEventLoop, QTimer
|
||
from qpypack.main import MainWindow
|
||
|
||
sandbox = Path(tmp_dir)
|
||
entry_file = _create_mock_project(sandbox)
|
||
|
||
# 1. 实例化主窗口 (触发所有的初始化和配置读取)
|
||
window = MainWindow()
|
||
|
||
# 2. 模拟用户拖拽源码文件到界面
|
||
window.on_script_selected(str(entry_file))
|
||
|
||
# 等待分析线程(ScriptAnalysisThread)完成
|
||
loop_wait = QEventLoop()
|
||
timer_wait = QTimer()
|
||
timer_wait.timeout.connect(loop_wait.quit)
|
||
timer_wait.start(1000) # 等待1秒确保分析完毕
|
||
loop_wait.exec()
|
||
timer_wait.stop()
|
||
|
||
assert window.current_state == "ready", "脚本载入后状态机未变为 ready"
|
||
|
||
# 3. 模拟用户配置界面面板 (SettingsPanel)
|
||
sp = window.settings_panel
|
||
sp.engine_combo.setCurrentText(engine_name)
|
||
sp.rb_onefile.setChecked(is_onefile)
|
||
sp.rb_onedir.setChecked(not is_onefile)
|
||
sp.noconsole_check.setChecked(True)
|
||
sp.enable_sign_check.setChecked(False) # 测试环境关闭签名防阻断
|
||
|
||
# 设置输出目录到沙箱中
|
||
sp.out_mode_combo.setCurrentIndex(1)
|
||
sp.out_dir_edit.setText(str(sandbox / "dist"))
|
||
|
||
# 4. 模拟用户拖入附加资源 (assets 文件夹)
|
||
window.on_main_resources_dropped([str(entry_file.parent / "assets")])
|
||
|
||
# 5. 模拟点击【开始构建】
|
||
window.start_pack()
|
||
assert window.current_state == "building", "启动打包后,主窗口未进入 building 状态"
|
||
|
||
# 6. 使用事件循环等待 PackingThread 后台线程完成
|
||
loop = QEventLoop()
|
||
def check_status():
|
||
# 轮询状态机是否退出 building
|
||
if window.current_state in ("done", "failed"):
|
||
loop.quit()
|
||
|
||
timer = QTimer()
|
||
timer.timeout.connect(check_status)
|
||
timer.start(500)
|
||
loop.exec()
|
||
timer.stop()
|
||
|
||
assert window.current_state == "done", f"【{engine_name}】打包失败!详细日志如下:\n{window.log_detailed.toPlainText()}"
|
||
|
||
# 7. 提取生成的程序名,并定位产物
|
||
app_name = sp.name_edit.text().strip() or "MockTestApp"
|
||
ext = ".exe" if sys.platform == "win32" else ""
|
||
dist_dir = sandbox / "dist"
|
||
|
||
if sys.platform == "darwin" and sp.noconsole_check.isChecked():
|
||
exe_path = dist_dir / f"{app_name}.app"
|
||
else:
|
||
if is_onefile:
|
||
exe_path = dist_dir / f"{app_name}{ext}"
|
||
else:
|
||
# 文件夹模式:产物为 dist_dir/app_name/app_name.exe
|
||
exe_path = dist_dir / app_name / f"{app_name}{ext}"
|
||
|
||
# 容错:Nuitka 在极少数情况下可能未重命名 .dist 文件夹,或者文件名带版本号
|
||
if not exe_path.exists():
|
||
fallback_dist = dist_dir / f"{app_name}.dist" / f"{app_name}{ext}"
|
||
if fallback_dist.exists():
|
||
exe_path = fallback_dist
|
||
else:
|
||
# 终极兜底:直接在目录里搜索可执行文件
|
||
possible_exes = list((dist_dir / app_name).glob(f"*{ext}"))
|
||
if possible_exes:
|
||
exe_path = possible_exes[0]
|
||
|
||
# 8. 校验实机拉起
|
||
_assert_executable_execution(exe_path)
|
||
|
||
|
||
# =====================================================================
|
||
# 7. PyInstaller E2E 测试
|
||
# =====================================================================
|
||
def test_07_pyinstaller_real_build():
|
||
"""测试 7: 深度集成 UI 的 PyInstaller 真实打包与拉起测试"""
|
||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 7/8] 深度集成测试: UI 操作 -> PyInstaller 构建 -> 实机拉起...{Colors.RESET}")
|
||
try:
|
||
import PyInstaller
|
||
except ImportError:
|
||
log_warn("缺少 PyInstaller,跳过测试")
|
||
return
|
||
|
||
from PySide6.QtWidgets import QApplication
|
||
app = QApplication.instance() or QApplication(["qp_test", "-platform", "offscreen"])
|
||
|
||
with tempfile.TemporaryDirectory(prefix="qp_e2e_pyi_") as tmp_dir:
|
||
start_t = time.time()
|
||
# 测试 PyInstaller 的单文件模式
|
||
run_e2e_via_mainwindow("PyInstaller", Path(tmp_dir), is_onefile=True)
|
||
log_pass(f"PyInstaller E2E 完整链路测试通过,耗时: {time.time() - start_t:.2f}s")
|
||
|
||
clean_project_junk()
|
||
|
||
|
||
# =====================================================================
|
||
# 8. Nuitka E2E 测试
|
||
# =====================================================================
|
||
def test_08_nuitka_real_build():
|
||
"""测试 8: 深度集成 UI 的 Nuitka 原生编译与拉起测试"""
|
||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 8/8] 深度集成测试: UI 操作 -> Nuitka 原生编译 -> 实机拉起...{Colors.RESET}")
|
||
try:
|
||
import nuitka
|
||
except ImportError:
|
||
log_warn("缺少 Nuitka,跳过测试")
|
||
return
|
||
|
||
from PySide6.QtWidgets import QApplication
|
||
app = QApplication.instance() or QApplication(["qp_test", "-platform", "offscreen"])
|
||
|
||
with tempfile.TemporaryDirectory(prefix="qp_e2e_nuitka_") as tmp_dir:
|
||
start_t = time.time()
|
||
# 测试 Nuitka 的文件夹模式 (Standalone)
|
||
run_e2e_via_mainwindow("Nuitka", Path(tmp_dir), is_onefile=False)
|
||
log_pass(f"Nuitka E2E 完整链路测试通过,耗时: {time.time() - start_t:.2f}s")
|
||
|
||
clean_project_junk()
|
||
|
||
|
||
# =====================================================================
|
||
# 主调度入口
|
||
# =====================================================================
|
||
def main():
|
||
print(f"""
|
||
{Colors.CYAN}{Colors.BOLD}======================================================================
|
||
🚀 QPyPack 全功能终极一体化自检与双引擎真实打包验证系统 (UI深度集成版)
|
||
======================================================================{Colors.RESET}""")
|
||
|
||
tests = [
|
||
("静态资源与工程配置", test_01_static_assets),
|
||
("多语言包格式对齐与校验", test_02_locales_and_placeholders),
|
||
("QPyPack 原生 AST 分析引擎", test_03_ast_engine),
|
||
("QPyPack 核心配置与翻译引擎逻辑", test_04_core_logic),
|
||
("Qt UI 主窗口状态机独立交互测试", test_05_ui_interaction),
|
||
("Wheel 分发包沙箱构建测试", test_06_wheel_packaging),
|
||
("PyInstaller E2E 链路验证 (单文件)", test_07_pyinstaller_real_build),
|
||
("Nuitka E2E 链路验证 (文件夹)", test_08_nuitka_real_build),
|
||
]
|
||
|
||
failed = False
|
||
for title, func in tests:
|
||
try:
|
||
func()
|
||
except Exception as e:
|
||
failed = True
|
||
log_fail(f"【{title}】未通过!原因: {e}")
|
||
print(f"\n{Colors.RED}详细错误回溯:{Colors.RESET}")
|
||
traceback.print_exc()
|
||
break
|
||
|
||
# 无论成功失败,强制执行纯净清理
|
||
clean_project_junk()
|
||
|
||
print(f"\n{Colors.BOLD}======================================================================{Colors.RESET}")
|
||
if not failed:
|
||
print(f"{Colors.GREEN}{Colors.BOLD} 🎉 [CONGRATULATIONS] 全部端到端深度集成测试 100% 顺利通过!{Colors.RESET}")
|
||
print(f"{Colors.GREEN} 💡 你的所有 UI 修改、功能迭代、参数注入均完美运行,且一切残留已被销毁!{Colors.RESET}")
|
||
print(f"{Colors.BOLD}======================================================================{Colors.RESET}\n")
|
||
sys.exit(0)
|
||
else:
|
||
print(f"{Colors.RED}{Colors.BOLD} 💥 [TEST FAILED] 发现程序缺陷!测试已中断,请根据上方回溯修复代码。{Colors.RESET}")
|
||
print(f"{Colors.BOLD}======================================================================{Colors.RESET}\n")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|