v2.8.1: Fix version parsing crash, stabilize Nuitka lite mode, and refine CLI flags
- Fixed `IndexError: tuple index out of range` in Python version parsing for backport exclusion rules and environment analysis - Stabilized Nuitka Lite Mode by adjusting aggressive optimization flags (`--lto=auto`, `--python-flag=-O`) to prevent compilation OOM and linker errors - Restored essential base packages in Nuitka dev-module exclusions to prevent breaking downstream third-party libraries - Fixed PyInstaller exclude parameter syntax (`--exclude-module`) incorrectly using Nuitka's `--nofollow-import-to` - Improved console pause code injection to preserve PEP 263 file encoding declarations and Shebang headers - Enhanced package name parsing regex to properly handle complex requirements specifications (e.g., `pkg[extras]`, `@ git+https://`) - Added defensive validation when querying target environment interpreter versions and parsing backport rules - Updated application version metadata and locale dictionaries to v2.8.1 Update test_check.py
This commit is contained in:
1 parent
448dbdbbf3
commit
e91b729463
5 files changed
+390
-530
No files matched your search
@@ -3,6 +3,18 @@
|
||||
QPyPack 项目的所有重要变更都将记录在此文件中。
|
||||
|
||||
[English Version](./CHANGELOG_EN.md) | **中文**
|
||||
这里为你准备的 **QPyPack v2.8.1** 精简版 Release/Changelog(分为**中文版**和**英文版**,可直接复制到 GitHub Release 或发布公告中):
|
||||
|
||||
---
|
||||
|
||||
## [2.8.1] - 2026-09-03
|
||||
|
||||
### 修复
|
||||
- 修复废弃兼容包规则及环境检查时偶发的报错。
|
||||
- 调整激进的优化参数(`--lto=auto` / `-O`),移除了对底层基础库的误拦截,解决精简模式编译中断/内存溢出问题。
|
||||
- 纠正排除参数语法,解决包含排除包时 PyInstaller 报未知参数的 Bug。
|
||||
- 改进控制台暂停注入机制,避免破坏脚本开头的 PEP 263 编码声明与 Shebang。
|
||||
- 支持带有 `@`、`extras[...]` 等复合规范依赖包的准确识别与安装。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,6 +6,17 @@ All notable changes to the **QPyPack** project will be documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [2.8.1] - 2026-09-03
|
||||
|
||||
### Fixed
|
||||
- Resolved the exception during backport rule evaluation and environment analysis.
|
||||
- Adjusted aggressive flags (`--lto=auto`, `-O`) and removed unintended blocking of foundational modules to prevent build failures.
|
||||
- Corrected parameter syntax for module exclusions under PyInstaller engine.
|
||||
- Protected PEP 263 file encodings and Shebang lines when injecting console pause routines.
|
||||
- Improved extraction for complex package specifications (e.g. `@`, `[extras]`).
|
||||
|
||||
---
|
||||
|
||||
## [2.8.0] - 2026-08-31
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "qpypack"
|
||||
version = "2.8.0"
|
||||
version = "2.8.1"
|
||||
description = "A cross-platform GUI packaging and compilation suite based on PyInstaller and Nuitka."
|
||||
readme = "README_EN.md"
|
||||
authors = [
|
||||
|
||||
+30
-32
@@ -125,9 +125,9 @@ except ImportError:
|
||||
HAS_QT_AUDIO = False
|
||||
|
||||
__app_name__ = "QPyPack"
|
||||
__version__ = "2.8.0"
|
||||
__version__ = "2.8.1"
|
||||
__author__ = "QwejayHuang"
|
||||
__company__ = "QwejayHuang"
|
||||
__company__ = "Qwesoft"
|
||||
__description__ = "Modern Cross-Platform Python Packaging GUI Powered by PyInstaller & Nuitka"
|
||||
|
||||
DISK_SPACE_MIN_GB = 0.5
|
||||
@@ -4099,8 +4099,8 @@ class SettingsPanel(QWidget):
|
||||
kw["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
res = subprocess.run([raw_py, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"], **kw)
|
||||
if res.returncode == 0 and res.stdout.strip():
|
||||
parts = res.stdout.strip().split(".")
|
||||
return raw_py, (int(parts[0]), int(parts[1]))
|
||||
parts = [int(x) for x in re.findall(r"\d+", res.stdout.strip())]
|
||||
return raw_py, (parts[0], parts[1]) if len(parts) >= 2 else ((parts[0], 0) if parts else (3, 8))
|
||||
except Exception:
|
||||
pass
|
||||
return raw_py, (3, 8)
|
||||
@@ -5204,6 +5204,12 @@ class PackingThread(QThread):
|
||||
)
|
||||
|
||||
try:
|
||||
lines = code.splitlines(keepends=True)
|
||||
header_line_count = 0
|
||||
for i, l in enumerate(lines[:3]):
|
||||
if l.startswith("#!") or "coding" in l:
|
||||
header_line_count = i + 1
|
||||
|
||||
tree = ast.parse(code, filename=orig_path.as_posix())
|
||||
last_future_lineno = 0
|
||||
for node in tree.body:
|
||||
@@ -5214,9 +5220,9 @@ class PackingThread(QThread):
|
||||
else:
|
||||
break
|
||||
|
||||
lines = code.splitlines(keepends=True)
|
||||
if last_future_lineno > 0:
|
||||
injected_code = "".join(lines[:last_future_lineno]) + "\n" + pause_code + "".join(lines[last_future_lineno:])
|
||||
insert_idx = max(header_line_count, last_future_lineno)
|
||||
if insert_idx > 0:
|
||||
injected_code = "".join(lines[:insert_idx]) + "\n" + pause_code + "".join(lines[insert_idx:])
|
||||
else:
|
||||
injected_code = pause_code + code
|
||||
except Exception:
|
||||
@@ -5906,8 +5912,8 @@ class PackingThread(QThread):
|
||||
[system_python_exe, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"], **kw_v
|
||||
)
|
||||
if res_v.returncode == 0 and res_v.stdout.strip():
|
||||
pv = res_v.stdout.strip().split(".")
|
||||
target_py_ver = (int(pv[0]), int(pv[1]))
|
||||
pv = [int(x) for x in re.findall(r"\d+", res_v.stdout.strip())]
|
||||
target_py_ver = (pv[0], pv[1]) if len(pv) >= 2 else (pv[0] if pv else 3, 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -5918,8 +5924,13 @@ class PackingThread(QThread):
|
||||
min_ver_tup = (int(min_ver_raw[0]), int(min_ver_raw[1])) if len(min_ver_raw) >= 2 else (int(min_ver_raw[0]), 0)
|
||||
elif isinstance(min_ver_raw, str):
|
||||
try:
|
||||
parts = [int(x) for x in min_ver_raw.strip().split(".")]
|
||||
min_ver_tup = (parts[0], parts[1]) if len(parts) >= 2 else (parts[0], 0)
|
||||
parts = [int(x) for x in re.findall(r"\d+", str(min_ver_raw))]
|
||||
if len(parts) >= 2:
|
||||
min_ver_tup = (parts[0], parts[1])
|
||||
elif len(parts) == 1:
|
||||
min_ver_tup = (parts[0], 0)
|
||||
else:
|
||||
min_ver_tup = (3, 4)
|
||||
except Exception:
|
||||
min_ver_tup = (3, 4)
|
||||
else:
|
||||
@@ -5971,7 +5982,11 @@ class PackingThread(QThread):
|
||||
_("[WARN] Specified versions failed to install. Stripping version constraints for automatic compatibility match...")
|
||||
)
|
||||
|
||||
flex_install_list = [re.split(r"[=><!~]", pkg)[0].strip() for pkg in dedup_install_list]
|
||||
def extract_pure_pkg_name(pkg_str):
|
||||
m = re.match(r"^([a-zA-Z0-9_\-\.]+)", pkg_str.strip())
|
||||
return m.group(1).lower() if m else pkg_str.strip().lower()
|
||||
|
||||
flex_install_list = [re.split(r"[=><!~@;\[]", pkg)[0].strip() for pkg in dedup_install_list if re.split(r"[=><!~@;\[]", pkg)[0].strip()]
|
||||
|
||||
installed_any = False
|
||||
for pkg in flex_install_list:
|
||||
@@ -6511,27 +6526,10 @@ class PackingThread(QThread):
|
||||
for ex in safe_dev_excludes:
|
||||
cmd.append(f"--exclude-module={ex}")
|
||||
elif engine == "Nuitka":
|
||||
cmd.append("--python-flag=-OO")
|
||||
cmd.append("--lto=yes")
|
||||
|
||||
cmd.append("--python-flag=-O")
|
||||
cmd.append("--lto=auto")
|
||||
nuitka_dev_excludes = [
|
||||
"binder",
|
||||
"tkinter.test",
|
||||
"pip",
|
||||
"wheel",
|
||||
"distutils",
|
||||
"pkg_resources",
|
||||
"jupyter",
|
||||
"notebook",
|
||||
"IPython",
|
||||
"pytest",
|
||||
"_pytest",
|
||||
"unittest",
|
||||
"setuptools",
|
||||
"pydoc",
|
||||
"doctest",
|
||||
"test",
|
||||
"tests",
|
||||
"unittest", "doctest", "pdb", "pydoc", "test", "pytest", "IPython", "jupyter"
|
||||
]
|
||||
|
||||
if any(q in imports_lower for q in ("pyside6", "pyqt6", "pyside2", "pyqt5")):
|
||||
|
||||
+336
-497
@@ -1,57 +1,40 @@
|
||||
# -*- 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 sys
|
||||
import time
|
||||
import stat
|
||||
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"
|
||||
import pytest
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
current_path = Path(__file__).resolve()
|
||||
if current_path.parent.name == "tests":
|
||||
ROOT_DIR = current_path.parent.parent
|
||||
else:
|
||||
ROOT_DIR = current_path.parent
|
||||
# 智能搜索并添加项目路径
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, PROJECT_ROOT.as_posix())
|
||||
if (PROJECT_ROOT / "src").exists():
|
||||
sys.path.insert(0, (PROJECT_ROOT / "src").as_posix())
|
||||
if (PROJECT_ROOT / "qpypack").exists():
|
||||
sys.path.insert(0, (PROJECT_ROOT / "qpypack").as_posix())
|
||||
|
||||
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))
|
||||
try:
|
||||
import main
|
||||
except ModuleNotFoundError:
|
||||
try:
|
||||
from qpypack import main
|
||||
except ModuleNotFoundError:
|
||||
import src.qpypack.main as main
|
||||
|
||||
|
||||
class Colors:
|
||||
CYAN = "\033[96m"
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
RED = "\033[91m"
|
||||
RESET = "\033[0m"
|
||||
YELLOW = "\033[93m"
|
||||
CYAN = "\033[96m"
|
||||
BOLD = "\033[1m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
def log_pass(msg):
|
||||
@@ -66,481 +49,337 @@ 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 safe_cleanup_dir(path: Path, retries=15, delay=0.5):
|
||||
"""Windows 下鲁棒删除临时目录,解决句柄延迟释放导致的 WinError 5 PermissionError"""
|
||||
if not path or not path.exists():
|
||||
return True
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 垃圾自净机制 (自动清理根目录残留)
|
||||
# =====================================================================
|
||||
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:
|
||||
def remove_readonly(func, p, exc_info):
|
||||
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)
|
||||
os.chmod(p, stat.S_IWRITE)
|
||||
func(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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}"
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
shutil.rmtree(path, onerror=remove_readonly)
|
||||
if not path.exists():
|
||||
return True
|
||||
except Exception:
|
||||
time.sleep(delay)
|
||||
|
||||
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
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
return not path.exists()
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
app = QApplication.instance() or QApplication(["qp_test", "-platform", "offscreen"])
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def init_qapp():
|
||||
app = QApplication.instance()
|
||||
if not app:
|
||||
app = QApplication(["pytest_qpypack", "-platform", "offscreen"])
|
||||
yield app
|
||||
|
||||
|
||||
def test_01_config_and_i18n():
|
||||
"""测试 1: 配置加载、保存及多语言系统"""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 1/8] 偏好设置及国际化引擎校验...{Colors.RESET}")
|
||||
config = main.load_config()
|
||||
assert "Settings" in config
|
||||
assert "Mappings" in config
|
||||
assert "BackportRules" in config
|
||||
|
||||
# 验证翻译引擎
|
||||
zh_text = main.ZH_CN_DICT.get("Start Build", "开始构建")
|
||||
assert zh_text == "开始构建"
|
||||
log_pass("配置加载与多语言引擎校验通过")
|
||||
|
||||
|
||||
def test_02_ast_dependency_scanner():
|
||||
"""测试 2: AST 依赖深度扫描与本地白名单识别"""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 2/8] AST 依赖引擎与白名单扫描...{Colors.RESET}")
|
||||
tmp_dir = Path(tempfile.mkdtemp(prefix="qp_test_ast_"))
|
||||
try:
|
||||
script = tmp_dir / "sample.py"
|
||||
script.write_text("import requests\nfrom PIL import Image\nimport local_dummy\n", encoding="utf-8")
|
||||
|
||||
local_mod = tmp_dir / "local_dummy.py"
|
||||
local_mod.write_text("def hello(): pass\n", encoding="utf-8")
|
||||
|
||||
discovered = main.extract_project_imports_via_ast(script, scan_dir=False)
|
||||
assert "requests" in discovered
|
||||
assert "PIL" in discovered
|
||||
assert "local_dummy" in discovered
|
||||
|
||||
local_modules = main.get_all_local_modules(tmp_dir, tmp_dir)
|
||||
assert "local_dummy" in local_modules
|
||||
|
||||
log_pass(f"成功识别 AST 依赖: {discovered},本地模块排除列表: {local_modules}")
|
||||
finally:
|
||||
safe_cleanup_dir(tmp_dir)
|
||||
|
||||
|
||||
def test_03_backport_version_parsing_safety():
|
||||
"""测试 3: 废弃兼容包版本解析安全性 (防御 tuple index out of range)"""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 3/8] 版本解析防护机制 (防止 IndexError)...{Colors.RESET}")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="qp_e2e_pyi_") as tmp_dir:
|
||||
test_cases = [
|
||||
("3.11", (3, 11)),
|
||||
("3.9.13", (3, 9)),
|
||||
("3", (3, 0)),
|
||||
("", (3, 4)),
|
||||
("invalid", (3, 4)),
|
||||
((3, 10), (3, 10)),
|
||||
([3, 12], (3, 12)),
|
||||
]
|
||||
|
||||
for val, expected in test_cases:
|
||||
if isinstance(val, (tuple, list)):
|
||||
res = (int(val[0]), int(val[1])) if len(val) >= 2 else (int(val[0]), 0)
|
||||
elif isinstance(val, str):
|
||||
import re
|
||||
parts = [int(x) for x in re.findall(r"\d+", str(val))]
|
||||
if len(parts) >= 2:
|
||||
res = (parts[0], parts[1])
|
||||
elif len(parts) == 1:
|
||||
res = (parts[0], 0)
|
||||
else:
|
||||
res = (3, 4)
|
||||
else:
|
||||
res = (3, 4)
|
||||
|
||||
assert res == expected, f"解析 {val} 失败,得到 {res}"
|
||||
|
||||
log_pass("版本解析异常防御测试完全通过")
|
||||
|
||||
|
||||
def test_04_sanitize_script_pep263_injection():
|
||||
"""测试 4: 脚本净化与控制台暂停代码注入 (保护 PEP 263 编码头与 Shebang)"""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 4/8] 脚本安全净化与控制台暂停代码注入...{Colors.RESET}")
|
||||
tmp_dir = Path(tempfile.mkdtemp(prefix="qp_test_sanitize_"))
|
||||
try:
|
||||
script = tmp_dir / "app_with_encoding.py"
|
||||
raw_code = (
|
||||
"#!/usr/bin/env python\n"
|
||||
"# -*- coding: utf-8 -*-\n"
|
||||
"from __future__ import annotations\n"
|
||||
"print('Hello World')\n"
|
||||
)
|
||||
script.write_text(raw_code, encoding="utf-8")
|
||||
|
||||
mock_params = {
|
||||
"noconsole": False,
|
||||
"script_path": script.as_posix()
|
||||
}
|
||||
thread = main.PackingThread(mock_params)
|
||||
sanitized_path, is_temp, err = thread.sanitize_script(script)
|
||||
|
||||
assert sanitized_path.exists()
|
||||
new_content = sanitized_path.read_text(encoding="utf-8")
|
||||
|
||||
# 验证前部关键声明全部保留,且注入了 _qpypack_pause
|
||||
assert "#!/usr/bin/env python" in new_content
|
||||
assert "coding: utf-8" in new_content
|
||||
assert "from __future__ import annotations" in new_content
|
||||
assert "_qpypack_pause" in new_content
|
||||
|
||||
# 验证 pause 代码位于头部声明之后
|
||||
pos_coding = new_content.find("coding: utf-8")
|
||||
pos_pause = new_content.find("_qpypack_pause")
|
||||
assert pos_coding < pos_pause, "pause 代码不应插在编码声明之前"
|
||||
|
||||
if is_temp:
|
||||
sanitized_path.unlink(missing_ok=True)
|
||||
|
||||
log_pass("控制台暂停注入成功保留 Shebang 与 PEP 263 编码头声明")
|
||||
finally:
|
||||
safe_cleanup_dir(tmp_dir)
|
||||
|
||||
|
||||
def test_05_complex_dependency_spec_parsing():
|
||||
"""测试 5: 复合依赖规范字符串解析 (extras, @, 版本符号切分)"""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 5/8] 复合依赖包名正则表达式提取...{Colors.RESET}")
|
||||
import re
|
||||
|
||||
test_specs = [
|
||||
"uvicorn[standard]>=0.20.0",
|
||||
"fastapi==0.100.0",
|
||||
"requests~=2.31.0",
|
||||
"my-pkg @ git+https://github.com/org/repo.git",
|
||||
"certifi ; python_version >= '3.8'",
|
||||
]
|
||||
|
||||
extracted = []
|
||||
for pkg in test_specs:
|
||||
cleaned = re.split(r"[=><!~@;\[]", pkg)[0].strip()
|
||||
if cleaned:
|
||||
extracted.append(cleaned)
|
||||
|
||||
assert extracted == ["uvicorn", "fastapi", "requests", "my-pkg", "certifi"]
|
||||
log_pass(f"复合依赖包名提取完全符合预期: {extracted}")
|
||||
|
||||
|
||||
def test_06_ui_settings_panel(init_qapp):
|
||||
"""测试 6: 构建设置面板组件与参数状态双向绑定"""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 6/8] UI 设置面板及交互流转...{Colors.RESET}")
|
||||
win = main.MainWindow()
|
||||
panel = win.settings_panel
|
||||
|
||||
panel.rb_lite_mode.setChecked(True)
|
||||
assert panel.rb_lite_mode.isChecked()
|
||||
|
||||
panel.engine_combo.setCurrentText("Nuitka")
|
||||
assert panel.engine_combo.currentText() == "Nuitka"
|
||||
|
||||
panel._add_resource_item("file", "C:/dummy.txt", ".")
|
||||
assert panel.add_data_list.count() == 1
|
||||
panel.clear_resource()
|
||||
assert panel.add_data_list.count() == 0
|
||||
|
||||
win.close()
|
||||
log_pass("UI 核心交互与状态绑定测试通过")
|
||||
|
||||
|
||||
def run_e2e_mock_build(engine_name: str, tmp_dir: Path, is_onefile: bool = True):
|
||||
"""辅助函数: 创建模拟项目并执行全自动构建与拉起验证"""
|
||||
app_dir = tmp_dir / "mock_app"
|
||||
app_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
entry_py = app_dir / "app_main.py"
|
||||
entry_py.write_text(
|
||||
"import sys, os, time\n"
|
||||
"if __name__ == '__main__':\n"
|
||||
" print('BOOT_SUCCESS_OK')\n"
|
||||
" time.sleep(0.1)\n"
|
||||
" sys.exit(0)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
reqs = app_dir / "requirements.txt"
|
||||
reqs.write_text("# simple requirements\n", encoding="utf-8")
|
||||
|
||||
python_exe = main.get_python_executable()
|
||||
assert python_exe and os.path.exists(python_exe), "未检测到可用的测试 Python 解释器"
|
||||
|
||||
params = {
|
||||
"engine": engine_name,
|
||||
"python_exe": python_exe,
|
||||
"script_path": entry_py.as_posix(),
|
||||
"project_folder": app_dir.as_posix(),
|
||||
"enable_sign": False,
|
||||
"cert_path": "",
|
||||
"cert_pass": "",
|
||||
"app_name": "app_main",
|
||||
"onefile": is_onefile,
|
||||
"contents_dir": "_internal",
|
||||
"noconsole": True,
|
||||
"icon": "",
|
||||
"use_reqs": True,
|
||||
"use_pipreqs": False,
|
||||
"use_pipreqs_dir": False,
|
||||
"reqs_file": reqs.as_posix(),
|
||||
"hidden_imports": "",
|
||||
"add_data_list": [],
|
||||
"upx": False,
|
||||
"upx_path": "",
|
||||
"cpu_cores": 2,
|
||||
"exclude_modules": "",
|
||||
"out_mode": 0,
|
||||
"custom_out_dir": "",
|
||||
"temp_sandbox_mode": 0,
|
||||
"use_venv": True,
|
||||
"keep_venv": False,
|
||||
"venv_mode": "isolated",
|
||||
"shared_venv_dir": "",
|
||||
"clean_all": True,
|
||||
"version_file": None,
|
||||
"temp_icon_file": None,
|
||||
"ver_comp": "Qwesoft",
|
||||
"ver_desc": "E2E Test App",
|
||||
"ver_ver": "1.0.0",
|
||||
"pip_index_url": "https://pypi.org/simple",
|
||||
"pip_index_backup": "",
|
||||
"concise_log": True,
|
||||
"auto_save_log": False,
|
||||
"lite_mode": True,
|
||||
"pyi_version": "",
|
||||
"nuitka_version": "",
|
||||
"mappings": main.DEFAULT_MAPPINGS.copy(),
|
||||
"enable_backport_shield": True,
|
||||
"backport_rules": main.DEFAULT_BACKPORT_RULES.copy(),
|
||||
}
|
||||
|
||||
thread = main.PackingThread(params)
|
||||
results = []
|
||||
|
||||
def on_finished(success, msg, failed_pkgs):
|
||||
results.append((success, msg))
|
||||
|
||||
thread.build_finished.connect(on_finished)
|
||||
thread.run()
|
||||
|
||||
assert len(results) > 0, "构建线程未返回结果"
|
||||
success, msg = results[0]
|
||||
assert success, f"{engine_name} 构建失败: {msg}"
|
||||
|
||||
ext = ".exe" if os.name == "nt" else ""
|
||||
if is_onefile:
|
||||
built_exe = app_dir / f"app_main{ext}"
|
||||
else:
|
||||
# 针对 文件夹模式 (onedir / standalone) 全面兼容查找路径
|
||||
candidates = [
|
||||
app_dir / "app_main" / f"app_main{ext}",
|
||||
app_dir / "app_main.dist" / f"app_main{ext}",
|
||||
app_dir / f"app_main{ext}",
|
||||
]
|
||||
built_exe = next((p for p in candidates if p.exists()), None)
|
||||
|
||||
assert built_exe is not None and built_exe.exists(), f"找不到构建产物,搜索路径: {candidates if not is_onefile else built_exe}"
|
||||
|
||||
# 实机拉起产物验证
|
||||
proc = subprocess.run([built_exe.as_posix()], capture_output=True, text=True, timeout=10)
|
||||
assert proc.returncode == 0
|
||||
assert "BOOT_SUCCESS_OK" in proc.stdout
|
||||
log_pass(f"{engine_name} (Lite Mode) 产物实机拉起验证成功!")
|
||||
|
||||
|
||||
def test_07_pyinstaller_real_build(init_qapp):
|
||||
"""测试 7: PyInstaller 真实构建与产物执行测试"""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 7/8] PyInstaller Lite 模式真实端到端打包...{Colors.RESET}")
|
||||
tmp_dir = Path(tempfile.mkdtemp(prefix="qp_e2e_pyi_"))
|
||||
try:
|
||||
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()
|
||||
run_e2e_mock_build("PyInstaller", tmp_dir, is_onefile=True)
|
||||
log_pass(f"PyInstaller E2E 测试通过,耗时: {time.time() - start_t:.2f}s")
|
||||
finally:
|
||||
time.sleep(0.5)
|
||||
init_qapp.processEvents()
|
||||
safe_cleanup_dir(tmp_dir)
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 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}")
|
||||
def test_08_nuitka_real_build(init_qapp):
|
||||
"""测试 8: Nuitka 真实编译与产物执行测试 (防 WinError 5)"""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}>>> [测试 8/8] Nuitka Lite 模式原生编译与实机验证...{Colors.RESET}")
|
||||
try:
|
||||
import nuitka
|
||||
except ImportError:
|
||||
log_warn("缺少 Nuitka,跳过测试")
|
||||
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:
|
||||
tmp_dir = Path(tempfile.mkdtemp(prefix="qp_e2e_nuitka_"))
|
||||
try:
|
||||
start_t = time.time()
|
||||
# 测试 Nuitka 的文件夹模式 (Standalone)
|
||||
run_e2e_via_mainwindow("Nuitka", Path(tmp_dir), is_onefile=False)
|
||||
# 测试 Nuitka 文件夹独立模式 (Standalone)
|
||||
run_e2e_mock_build("Nuitka", 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)
|
||||
finally:
|
||||
time.sleep(0.5)
|
||||
init_qapp.processEvents()
|
||||
safe_cleanup_dir(tmp_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
pytest.main(["-s", "-vv", __file__])
|
||||
Reference in new issue
Block a user