pytrunner 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytrunner-1.0.0/LICENSE +21 -0
- pytrunner-1.0.0/MANIFEST.in +10 -0
- pytrunner-1.0.0/PKG-INFO +96 -0
- pytrunner-1.0.0/README.md +81 -0
- pytrunner-1.0.0/__init__.py +21 -0
- pytrunner-1.0.0/console/__init__.py +1 -0
- pytrunner-1.0.0/console/console_ui.py +67 -0
- pytrunner-1.0.0/core/__init__.py +1 -0
- pytrunner-1.0.0/core/error_types.py +149 -0
- pytrunner-1.0.0/core/log_manager.py +177 -0
- pytrunner-1.0.0/core/test_probe.py +200 -0
- pytrunner-1.0.0/core/test_runner.py +173 -0
- pytrunner-1.0.0/gui/__init__.py +1 -0
- pytrunner-1.0.0/gui/canvas_view.py +664 -0
- pytrunner-1.0.0/gui/config.py +60 -0
- pytrunner-1.0.0/main.py +852 -0
- pytrunner-1.0.0/probes/__init__.py +0 -0
- pytrunner-1.0.0/probes/draw_probes.py +612 -0
- pytrunner-1.0.0/probes/ok_probes.py +172 -0
- pytrunner-1.0.0/probes/probe_cases.py +255 -0
- pytrunner-1.0.0/pyproject.toml +26 -0
- pytrunner-1.0.0/pytrunner.egg-info/PKG-INFO +96 -0
- pytrunner-1.0.0/pytrunner.egg-info/SOURCES.txt +53 -0
- pytrunner-1.0.0/pytrunner.egg-info/dependency_links.txt +1 -0
- pytrunner-1.0.0/pytrunner.egg-info/entry_points.txt +2 -0
- pytrunner-1.0.0/pytrunner.egg-info/top_level.txt +1 -0
- pytrunner-1.0.0/scanners/__init__.py +0 -0
- pytrunner-1.0.0/scanners/allin_scanner.py +478 -0
- pytrunner-1.0.0/scanners/architecture_detector.py +968 -0
- pytrunner-1.0.0/scanners/dynamic_tester.py +620 -0
- pytrunner-1.0.0/setup.cfg +4 -0
- pytrunner-1.0.0/tests/__init__.py +0 -0
- pytrunner-1.0.0/tests/test_allin_scanner.py +84 -0
- pytrunner-1.0.0/tests/test_architecture_detector.py +52 -0
- pytrunner-1.0.0/tests/test_cases.py +226 -0
- pytrunner-1.0.0/tests/test_merged_gui_dynamic.py +170 -0
- pytrunner-1.0.0/tests/test_merged_gui_dynamic_v2.py +267 -0
- pytrunner-1.0.0/tests/test_merged_gui_static.py +278 -0
pytrunner-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 siwpan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pytrunner-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytrunner
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python 项目全覆盖测试框架:静态扫描 / 架构检测 / 动态运行
|
|
5
|
+
Author: siwpan
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitee.com/siwpan/py_test_runner
|
|
8
|
+
Project-URL: Repository, https://gitee.com/siwpan/py_test_runner
|
|
9
|
+
Project-URL: Changelog, https://gitee.com/siwpan/py_test_runner/blob/master/CHANGELOG.md
|
|
10
|
+
Keywords: test,testing,static-analysis,architecture,qa
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# py_test_runner — Python 项目全覆盖测试框架
|
|
17
|
+
|
|
18
|
+
> 仓库:<https://gitee.com/siwpan/py_test_runner> | 许可:MIT(见 [LICENSE](LICENSE))
|
|
19
|
+
> 注:文中 <PATH> 为路径占位符,请替换为你本机实际的目录 / 可执行文件路径。
|
|
20
|
+
|
|
21
|
+
> 多功能 Python 测试框架,支持静态扫描、架构检测、动态运行三大类测试。
|
|
22
|
+
> 由全哥创建,用于 G 盘项目(10,700+ Python 文件)的日常质量保障。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd <PATH>
|
|
30
|
+
|
|
31
|
+
python main.py -c # 控制台模式(最常用)
|
|
32
|
+
python main.py --smoke -c # 探针冒烟测试
|
|
33
|
+
python main.py --allin -c # 全项目扫描(P1 快速)
|
|
34
|
+
python main.py --arch -c # 架构模式检测
|
|
35
|
+
python main.py --dynamic -c # 动态测试(尝试运行)
|
|
36
|
+
python main.py --simple -c # 简单测试:编译 + JSON 校验
|
|
37
|
+
python main.py --simple -c --simple-target <PATH>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 功能模式一览
|
|
43
|
+
|
|
44
|
+
| 模式 | 命令 | 说明 |
|
|
45
|
+
|------|------|------|
|
|
46
|
+
| 控制台模式 | `python main.py -c` | 运行内置测试用例 |
|
|
47
|
+
| GUI 模式 | `python main.py` | Tkinter Canvas 可视化界面 |
|
|
48
|
+
| 探针模式 | `python main.py --smoke -c` | 冒烟测试(P1 关键探针) |
|
|
49
|
+
| OK 库扫描 | `python main.py --ok -c` | 扫描 `not/OK` 下所有项目 |
|
|
50
|
+
| draw 项目 | `python main.py --draw -c` | 深度扫描 ECS 架构项目 |
|
|
51
|
+
| 全项目扫描 | `python main.py --allin -c` | 递归扫描 `<PATH>` 全部目录 |
|
|
52
|
+
| 架构检测 | `python main.py --arch -c` | 识别 13 种架构模式 |
|
|
53
|
+
| 动态测试 | `python main.py --dynamic -c` | 尝试导入并运行 Python 文件 |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 文档
|
|
58
|
+
|
|
59
|
+
| 文档 | 说明 |
|
|
60
|
+
|------|------|
|
|
61
|
+
| [docs/user_manual.md](docs/user_manual.md) | 完整使用手册(全参数说明、各模式详解、常见工作流、故障排查) |
|
|
62
|
+
| [docs/architecture_patterns.md](docs/architecture_patterns.md) | 架构模式详解(13 种模式原理、适用场景、检测依据) |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 项目结构
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
py_testrunner/
|
|
70
|
+
├── main.py # 入口
|
|
71
|
+
├── README.md # 本文件
|
|
72
|
+
├── requirements.txt
|
|
73
|
+
├── core/ # 核心引擎(日志、错误类型、测试运行器、探针系统)
|
|
74
|
+
├── gui/ # Tkinter Canvas 图形界面
|
|
75
|
+
├── console/ # 控制台界面
|
|
76
|
+
├── logs/ # 日志 & 测试报告输出
|
|
77
|
+
├── scanners/ # 扫描器(全项目、架构检测、动态测试)
|
|
78
|
+
├── probes/ # 探针定义(通用、OK 库、draw 项目)
|
|
79
|
+
├── reports/ # 报告生成器(HTML/JSON、coverage)
|
|
80
|
+
├── tests/ # 测试脚本(自用)
|
|
81
|
+
└── docs/ # 文档(使用手册、架构模式详解)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 依赖
|
|
87
|
+
|
|
88
|
+
**必需(标准库)**:`tkinter` · `ast` · `subprocess` · `dataclasses`
|
|
89
|
+
|
|
90
|
+
> ⚠️ `tkinter` 在 WorkBuddy managed Python 中不含,动态测试会自动回退到系统 Python。
|
|
91
|
+
|
|
92
|
+
**可选**:`coverage`(覆盖率)、`matplotlib`(图表)
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
*内部项目,全哥创建。*
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# py_test_runner — Python 项目全覆盖测试框架
|
|
2
|
+
|
|
3
|
+
> 仓库:<https://gitee.com/siwpan/py_test_runner> | 许可:MIT(见 [LICENSE](LICENSE))
|
|
4
|
+
> 注:文中 <PATH> 为路径占位符,请替换为你本机实际的目录 / 可执行文件路径。
|
|
5
|
+
|
|
6
|
+
> 多功能 Python 测试框架,支持静态扫描、架构检测、动态运行三大类测试。
|
|
7
|
+
> 由全哥创建,用于 G 盘项目(10,700+ Python 文件)的日常质量保障。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd <PATH>
|
|
15
|
+
|
|
16
|
+
python main.py -c # 控制台模式(最常用)
|
|
17
|
+
python main.py --smoke -c # 探针冒烟测试
|
|
18
|
+
python main.py --allin -c # 全项目扫描(P1 快速)
|
|
19
|
+
python main.py --arch -c # 架构模式检测
|
|
20
|
+
python main.py --dynamic -c # 动态测试(尝试运行)
|
|
21
|
+
python main.py --simple -c # 简单测试:编译 + JSON 校验
|
|
22
|
+
python main.py --simple -c --simple-target <PATH>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 功能模式一览
|
|
28
|
+
|
|
29
|
+
| 模式 | 命令 | 说明 |
|
|
30
|
+
|------|------|------|
|
|
31
|
+
| 控制台模式 | `python main.py -c` | 运行内置测试用例 |
|
|
32
|
+
| GUI 模式 | `python main.py` | Tkinter Canvas 可视化界面 |
|
|
33
|
+
| 探针模式 | `python main.py --smoke -c` | 冒烟测试(P1 关键探针) |
|
|
34
|
+
| OK 库扫描 | `python main.py --ok -c` | 扫描 `not/OK` 下所有项目 |
|
|
35
|
+
| draw 项目 | `python main.py --draw -c` | 深度扫描 ECS 架构项目 |
|
|
36
|
+
| 全项目扫描 | `python main.py --allin -c` | 递归扫描 `<PATH>` 全部目录 |
|
|
37
|
+
| 架构检测 | `python main.py --arch -c` | 识别 13 种架构模式 |
|
|
38
|
+
| 动态测试 | `python main.py --dynamic -c` | 尝试导入并运行 Python 文件 |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 文档
|
|
43
|
+
|
|
44
|
+
| 文档 | 说明 |
|
|
45
|
+
|------|------|
|
|
46
|
+
| [docs/user_manual.md](docs/user_manual.md) | 完整使用手册(全参数说明、各模式详解、常见工作流、故障排查) |
|
|
47
|
+
| [docs/architecture_patterns.md](docs/architecture_patterns.md) | 架构模式详解(13 种模式原理、适用场景、检测依据) |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 项目结构
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
py_testrunner/
|
|
55
|
+
├── main.py # 入口
|
|
56
|
+
├── README.md # 本文件
|
|
57
|
+
├── requirements.txt
|
|
58
|
+
├── core/ # 核心引擎(日志、错误类型、测试运行器、探针系统)
|
|
59
|
+
├── gui/ # Tkinter Canvas 图形界面
|
|
60
|
+
├── console/ # 控制台界面
|
|
61
|
+
├── logs/ # 日志 & 测试报告输出
|
|
62
|
+
├── scanners/ # 扫描器(全项目、架构检测、动态测试)
|
|
63
|
+
├── probes/ # 探针定义(通用、OK 库、draw 项目)
|
|
64
|
+
├── reports/ # 报告生成器(HTML/JSON、coverage)
|
|
65
|
+
├── tests/ # 测试脚本(自用)
|
|
66
|
+
└── docs/ # 文档(使用手册、架构模式详解)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 依赖
|
|
72
|
+
|
|
73
|
+
**必需(标准库)**:`tkinter` · `ast` · `subprocess` · `dataclasses`
|
|
74
|
+
|
|
75
|
+
> ⚠️ `tkinter` 在 WorkBuddy managed Python 中不含,动态测试会自动回退到系统 Python。
|
|
76
|
+
|
|
77
|
+
**可选**:`coverage`(覆盖率)、`matplotlib`(图表)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
*内部项目,全哥创建。*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""pytrunner —— Python 项目全覆盖测试框架(命名空间根包)。"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# 兼容仓库「脚本模式」(根目录在 sys.path 上时 `from core.x` / `from tests.x` 等可用)。
|
|
6
|
+
# 打包为命名空间包后顶层不再有这些子包,这里把别名指回本包子模块,
|
|
7
|
+
# 使既有绝对导入在包模式下仍能解析。若已有同名顶层包则不动(setdefault)。
|
|
8
|
+
for _alias, _sub in (
|
|
9
|
+
("core", "pytrunner.core"),
|
|
10
|
+
("console", "pytrunner.console"),
|
|
11
|
+
("gui", "pytrunner.gui"),
|
|
12
|
+
("tests", "pytrunner.tests"),
|
|
13
|
+
("probes", "pytrunner.probes"),
|
|
14
|
+
("scanners", "pytrunner.scanners"),
|
|
15
|
+
):
|
|
16
|
+
try:
|
|
17
|
+
__import__(_sub)
|
|
18
|
+
sys.modules.setdefault(_alias, sys.modules[_sub])
|
|
19
|
+
except ImportError:
|
|
20
|
+
pass
|
|
21
|
+
del _alias, _sub
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# console — 控制台模式界面
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
控制台测试界面 — 纯终端交互模式。
|
|
3
|
+
|
|
4
|
+
适用场景:
|
|
5
|
+
- CI/CD 流水线
|
|
6
|
+
- 无 GUI 环境
|
|
7
|
+
- 脚本自动化
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
from typing import List, Callable
|
|
13
|
+
|
|
14
|
+
# 强制 stdout 使用 UTF-8,避免 emoji 等字符在 GBK 下报错
|
|
15
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
16
|
+
|
|
17
|
+
from core.log_manager import LogManager
|
|
18
|
+
from core.test_runner import TestRunner
|
|
19
|
+
from core.error_types import ErrorCode
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConsoleUI:
|
|
23
|
+
"""控制台测试界面"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, log_manager: LogManager):
|
|
26
|
+
self.log = log_manager
|
|
27
|
+
self.runner = TestRunner(log_manager)
|
|
28
|
+
|
|
29
|
+
def print_banner(self):
|
|
30
|
+
"""打印启动横幅"""
|
|
31
|
+
banner = r"""
|
|
32
|
+
╔══════════════════════════════════════════════════╗
|
|
33
|
+
║ 🧪 测试框架 v1.0 ║
|
|
34
|
+
║ Console Mode ║
|
|
35
|
+
╚══════════════════════════════════════════════════╝
|
|
36
|
+
"""
|
|
37
|
+
self.log.print_colored(banner, bold=True)
|
|
38
|
+
|
|
39
|
+
def run(self, test_fns: List[Callable], timeout: float = 30.0):
|
|
40
|
+
"""执行测试套件"""
|
|
41
|
+
self.print_banner()
|
|
42
|
+
self.runner.timeout = timeout
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
results = self.runner.run_tests(test_fns)
|
|
46
|
+
self.log.print_summary()
|
|
47
|
+
|
|
48
|
+
# 返回退出码
|
|
49
|
+
has_failure = any(
|
|
50
|
+
r.error_code.category.value in ("失败", "错误")
|
|
51
|
+
for r in results
|
|
52
|
+
)
|
|
53
|
+
if has_failure:
|
|
54
|
+
self.log.print_colored("⚠️ 有测试未通过!", bold=True)
|
|
55
|
+
return 1
|
|
56
|
+
else:
|
|
57
|
+
self.log.print_colored("✅ 全部测试通过!", bold=True)
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
except KeyboardInterrupt:
|
|
61
|
+
self.log.print_colored("\n⚠️ 测试被用户中断", bold=True)
|
|
62
|
+
return 2
|
|
63
|
+
except Exception as e:
|
|
64
|
+
self.log.print_colored(f"\n💥 测试框架崩溃: {e}", bold=True)
|
|
65
|
+
import traceback
|
|
66
|
+
traceback.print_exc()
|
|
67
|
+
return 3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# core — 测试框架核心引擎
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
错误类型定义 — 明确分类测试中可能出现的所有错误。
|
|
3
|
+
|
|
4
|
+
设计原则:
|
|
5
|
+
每种错误类型都有唯一的 name/code/emoji,方便日志筛选和 GUI 渲染。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ErrorCategory(Enum):
|
|
15
|
+
"""错误大类"""
|
|
16
|
+
PASS = "通过"
|
|
17
|
+
FAIL = "失败"
|
|
18
|
+
ERROR = "错误"
|
|
19
|
+
SKIP = "跳过"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ErrorCode(Enum):
|
|
23
|
+
"""错误细类 — 每种错误有唯一标识码"""
|
|
24
|
+
|
|
25
|
+
# ── 通过 ──
|
|
26
|
+
TEST_PASS = ("PASS-000", "测试通过", ErrorCategory.PASS, "✅")
|
|
27
|
+
|
|
28
|
+
# ── 断言失败 ──
|
|
29
|
+
ASSERT_EQUAL = ("FAIL-001", "断言相等失败:期望值与实际值不符", ErrorCategory.FAIL, "❌")
|
|
30
|
+
ASSERT_TRUE = ("FAIL-002", "断言为真失败:表达式求值为 False", ErrorCategory.FAIL, "❌")
|
|
31
|
+
ASSERT_FALSE = ("FAIL-003", "断言为假失败:表达式求值为 True", ErrorCategory.FAIL, "❌")
|
|
32
|
+
ASSERT_IN = ("FAIL-004", "断言包含失败:值不在集合中", ErrorCategory.FAIL, "❌")
|
|
33
|
+
ASSERT_RAISES = ("FAIL-005", "断言异常失败:未抛出预期异常", ErrorCategory.FAIL, "❌")
|
|
34
|
+
UNEXPECTED_RESULT = ("FAIL-006", "意外结果:返回值与预期不符", ErrorCategory.FAIL, "❌")
|
|
35
|
+
|
|
36
|
+
# ── 运行时错误 ──
|
|
37
|
+
RUNTIME_ERROR = ("ERR-001", "运行时错误", ErrorCategory.ERROR, "💥")
|
|
38
|
+
ATTRIBUTE_ERROR = ("ERR-002", "属性错误:对象没有该属性或方法", ErrorCategory.ERROR, "💥")
|
|
39
|
+
TYPE_ERROR = ("ERR-003", "类型错误:操作数类型不匹配", ErrorCategory.ERROR, "💥")
|
|
40
|
+
VALUE_ERROR = ("ERR-004", "值错误:参数值不合法", ErrorCategory.ERROR, "💥")
|
|
41
|
+
KEY_ERROR = ("ERR-005", "键错误:字典中不存在该键", ErrorCategory.ERROR, "💥")
|
|
42
|
+
INDEX_ERROR = ("ERR-006", "索引错误:序列索引越界", ErrorCategory.ERROR, "💥")
|
|
43
|
+
ZERO_DIVISION = ("ERR-007", "零除错误:除数为零", ErrorCategory.ERROR, "💥")
|
|
44
|
+
NAME_ERROR = ("ERR-008", "名称错误:变量未定义", ErrorCategory.ERROR, "💥")
|
|
45
|
+
OS_ERROR = ("ERR-009", "系统错误:操作系统相关异常", ErrorCategory.ERROR, "💥")
|
|
46
|
+
IO_ERROR = ("ERR-010", "IO 错误:文件读写失败", ErrorCategory.ERROR, "💥")
|
|
47
|
+
RECURSION_ERROR = ("ERR-011", "递归错误:递归深度超限", ErrorCategory.ERROR, "💥")
|
|
48
|
+
MEMORY_ERROR = ("ERR-012", "内存错误:内存不足", ErrorCategory.ERROR, "💥")
|
|
49
|
+
|
|
50
|
+
# ── 语法/导入错误 ──
|
|
51
|
+
SYNTAX_ERROR = ("ERR-101", "语法错误:代码无法解析", ErrorCategory.ERROR, "📝")
|
|
52
|
+
IMPORT_ERROR = ("ERR-102", "导入错误:模块不存在或无法加载", ErrorCategory.ERROR, "📝")
|
|
53
|
+
INDENTATION_ERROR = ("ERR-103", "缩进错误:代码缩进不正确", ErrorCategory.ERROR, "📝")
|
|
54
|
+
MODULE_NOT_FOUND = ("ERR-104", "模块未找到:依赖缺失", ErrorCategory.ERROR, "📝")
|
|
55
|
+
|
|
56
|
+
# ── 超时错误 ──
|
|
57
|
+
TIMEOUT_ERROR = ("ERR-201", "超时错误:执行超过时间限制", ErrorCategory.ERROR, "⏱️")
|
|
58
|
+
|
|
59
|
+
# ── 测试框架错误 ──
|
|
60
|
+
SETUP_ERROR = ("ERR-301", "准备阶段错误:setUp 方法异常", ErrorCategory.ERROR, "🔧")
|
|
61
|
+
TEARDOWN_ERROR = ("ERR-302", "清理阶段错误:tearDown 方法异常", ErrorCategory.ERROR, "🔧")
|
|
62
|
+
FIXTURE_ERROR = ("ERR-303", "夹具错误:测试数据准备失败", ErrorCategory.ERROR, "🔧")
|
|
63
|
+
|
|
64
|
+
# ── 跳过 ──
|
|
65
|
+
TEST_SKIP = ("SKIP-000", "测试跳过", ErrorCategory.SKIP, "⏭️")
|
|
66
|
+
SKIP_CONDITION = ("SKIP-001", "条件跳过:前置条件不满足", ErrorCategory.SKIP, "⏭️")
|
|
67
|
+
SKIP_PLATFORM = ("SKIP-002", "平台跳过:当前平台不支持", ErrorCategory.SKIP, "⏭️")
|
|
68
|
+
|
|
69
|
+
# ── 测试探针 ──(快速冒烟检测)
|
|
70
|
+
PROBE_PASS = ("PROBE-PASS", "探针通过:检测点正常", ErrorCategory.PASS, "📍")
|
|
71
|
+
PROBE_FAIL = ("PROBE-FAIL", "探针失败:检测点异常", ErrorCategory.FAIL, "📍")
|
|
72
|
+
PROBE_SKIP = ("PROBE-SKIP", "探针跳过:条件不满足", ErrorCategory.SKIP, "📍")
|
|
73
|
+
|
|
74
|
+
# ── 未知 ──
|
|
75
|
+
UNKNOWN_ERROR = ("ERR-999", "未知错误:无法归类的异常", ErrorCategory.ERROR, "❓")
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_exception(cls, exc: Exception) -> "ErrorCode":
|
|
79
|
+
"""根据异常类型自动匹配 ErrorCode"""
|
|
80
|
+
mapping = {
|
|
81
|
+
AssertionError: cls.ASSERT_EQUAL,
|
|
82
|
+
AttributeError: cls.ATTRIBUTE_ERROR,
|
|
83
|
+
TypeError: cls.TYPE_ERROR,
|
|
84
|
+
ValueError: cls.VALUE_ERROR,
|
|
85
|
+
KeyError: cls.KEY_ERROR,
|
|
86
|
+
IndexError: cls.INDEX_ERROR,
|
|
87
|
+
ZeroDivisionError: cls.ZERO_DIVISION,
|
|
88
|
+
NameError: cls.NAME_ERROR,
|
|
89
|
+
OSError: cls.OS_ERROR,
|
|
90
|
+
IOError: cls.IO_ERROR,
|
|
91
|
+
RecursionError: cls.RECURSION_ERROR,
|
|
92
|
+
MemoryError: cls.MEMORY_ERROR,
|
|
93
|
+
SyntaxError: cls.SYNTAX_ERROR,
|
|
94
|
+
ImportError: cls.IMPORT_ERROR,
|
|
95
|
+
IndentationError: cls.INDENTATION_ERROR,
|
|
96
|
+
ModuleNotFoundError: cls.MODULE_NOT_FOUND,
|
|
97
|
+
TimeoutError: cls.TIMEOUT_ERROR,
|
|
98
|
+
}
|
|
99
|
+
return mapping.get(type(exc), cls.UNKNOWN_ERROR)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def code(self) -> str:
|
|
103
|
+
return self.value[0]
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def description(self) -> str:
|
|
107
|
+
return self.value[1]
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def category(self) -> ErrorCategory:
|
|
111
|
+
return self.value[2]
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def emoji(self) -> str:
|
|
115
|
+
return self.value[3]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class TestError:
|
|
120
|
+
"""单条测试错误记录"""
|
|
121
|
+
error_code: ErrorCode
|
|
122
|
+
message: str
|
|
123
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
124
|
+
traceback: Optional[str] = None
|
|
125
|
+
test_name: str = ""
|
|
126
|
+
file_path: str = ""
|
|
127
|
+
line_number: int = 0
|
|
128
|
+
|
|
129
|
+
def brief(self) -> str:
|
|
130
|
+
"""一行摘要"""
|
|
131
|
+
loc = f"{self.file_path}:{self.line_number}" if self.file_path else ""
|
|
132
|
+
return f"[{self.error_code.code}] {self.error_code.emoji} {self.message} {loc}"
|
|
133
|
+
|
|
134
|
+
def detail(self) -> str:
|
|
135
|
+
"""完整详情"""
|
|
136
|
+
lines = [
|
|
137
|
+
f"{'='*60}",
|
|
138
|
+
f" 错误码 : {self.error_code.code}",
|
|
139
|
+
f" 类型 : {self.error_code.description}",
|
|
140
|
+
f" 时间 : {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}",
|
|
141
|
+
f" 测试 : {self.test_name}",
|
|
142
|
+
]
|
|
143
|
+
if self.file_path:
|
|
144
|
+
lines.append(f" 位置 : {self.file_path}:{self.line_number}")
|
|
145
|
+
lines.append(f" 消息 : {self.message}")
|
|
146
|
+
if self.traceback:
|
|
147
|
+
lines.append(f" 堆栈 :\n{self.traceback}")
|
|
148
|
+
lines.append(f"{'='*60}")
|
|
149
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
日志管理器 — 统一管理控制台输出、GUI 回调和文件写入。
|
|
3
|
+
|
|
4
|
+
功能:
|
|
5
|
+
- 同时输出到多个目标(控制台/GUI 回调/文件)
|
|
6
|
+
- 按错误级别自动着色
|
|
7
|
+
- 自动创建带时间戳的日志文件
|
|
8
|
+
- 线程安全
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Optional, Callable, List
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .error_types import ErrorCode, TestError, ErrorCategory
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LogManager:
|
|
20
|
+
"""多路日志管理器"""
|
|
21
|
+
|
|
22
|
+
# 控制台颜色(Windows 10+ 支持 ANSI)
|
|
23
|
+
COLORS = {
|
|
24
|
+
ErrorCategory.PASS: "\033[32m",
|
|
25
|
+
ErrorCategory.FAIL: "\033[31m",
|
|
26
|
+
ErrorCategory.ERROR: "\033[35m",
|
|
27
|
+
ErrorCategory.SKIP: "\033[33m",
|
|
28
|
+
"reset": "\033[0m",
|
|
29
|
+
"bold": "\033[1m",
|
|
30
|
+
"dim": "\033[2m",
|
|
31
|
+
"header": "\033[1;36m",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def __init__(self, log_dir: str = "logs", use_color: bool = True):
|
|
35
|
+
self.log_dir = Path(log_dir)
|
|
36
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
39
|
+
self.log_path = self.log_dir / f"test_run_{timestamp}.log"
|
|
40
|
+
self.error_path = self.log_dir / f"test_errors_{timestamp}.log"
|
|
41
|
+
|
|
42
|
+
self._lock = threading.RLock()
|
|
43
|
+
self._gui_callbacks: List[Callable[[str], None]] = []
|
|
44
|
+
self._error_callbacks: List[Callable[[TestError], None]] = []
|
|
45
|
+
self._errors: List[TestError] = []
|
|
46
|
+
self._log_entries: List[str] = []
|
|
47
|
+
self._use_color = use_color
|
|
48
|
+
|
|
49
|
+
self._write_to_file(self.log_path, f"=== 测试日志 === 开始时间: {datetime.now()}\n", mode='w')
|
|
50
|
+
self._write_to_file(self.error_path, f"=== 错误日志 === 开始时间: {datetime.now()}\n\n", mode='w')
|
|
51
|
+
|
|
52
|
+
# ── 回调注册 ──
|
|
53
|
+
def on_log(self, callback: Callable[[str], None]):
|
|
54
|
+
self._gui_callbacks.append(callback)
|
|
55
|
+
|
|
56
|
+
def on_error(self, callback: Callable[[TestError], None]):
|
|
57
|
+
self._error_callbacks.append(callback)
|
|
58
|
+
|
|
59
|
+
# ── 写入 ──
|
|
60
|
+
def _write_to_file(self, path: Path, text: str, mode: str = 'a'):
|
|
61
|
+
try:
|
|
62
|
+
with open(path, mode, encoding='utf-8') as f:
|
|
63
|
+
f.write(text)
|
|
64
|
+
except OSError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def _notify_gui(self, line: str):
|
|
68
|
+
for cb in self._gui_callbacks:
|
|
69
|
+
try:
|
|
70
|
+
cb(line)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def _notify_error(self, error: TestError):
|
|
75
|
+
for cb in self._error_callbacks:
|
|
76
|
+
try:
|
|
77
|
+
cb(error)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# ── 日志方法 ──
|
|
82
|
+
def log(self, msg: str, level: str = "INFO"):
|
|
83
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
84
|
+
line = f"[{timestamp}] [{level}] {msg}"
|
|
85
|
+
with self._lock:
|
|
86
|
+
self._log_entries.append(line)
|
|
87
|
+
self._write_to_file(self.log_path, line + "\n")
|
|
88
|
+
self._notify_gui(line)
|
|
89
|
+
|
|
90
|
+
def info(self, msg: str):
|
|
91
|
+
self.log(msg, "INFO")
|
|
92
|
+
|
|
93
|
+
def warning(self, msg: str):
|
|
94
|
+
self.log(msg, "WARN")
|
|
95
|
+
|
|
96
|
+
def debug(self, msg: str):
|
|
97
|
+
self.log(msg, "DEBUG")
|
|
98
|
+
|
|
99
|
+
def error(self, test_error: TestError):
|
|
100
|
+
"""记录结构化错误"""
|
|
101
|
+
with self._lock:
|
|
102
|
+
self._errors.append(test_error)
|
|
103
|
+
self._write_to_file(self.error_path, test_error.detail() + "\n")
|
|
104
|
+
line = f"[{datetime.now().strftime('%H:%M:%S')}] [ERROR] {test_error.brief()}"
|
|
105
|
+
self._log_entries.append(line)
|
|
106
|
+
self._write_to_file(self.log_path, line + "\n")
|
|
107
|
+
self._notify_gui(line)
|
|
108
|
+
self._notify_error(test_error)
|
|
109
|
+
|
|
110
|
+
def section(self, title: str):
|
|
111
|
+
"""打印分隔标题"""
|
|
112
|
+
line = f"\n{'━'*50}\n {title}\n{'━'*50}"
|
|
113
|
+
with self._lock:
|
|
114
|
+
self._log_entries.append(line)
|
|
115
|
+
self._write_to_file(self.log_path, line + "\n")
|
|
116
|
+
self._notify_gui(line)
|
|
117
|
+
|
|
118
|
+
# ── 控制台输出 ──
|
|
119
|
+
def print_colored(self, msg: str, category: Optional[ErrorCategory] = None, bold: bool = False):
|
|
120
|
+
"""带颜色的控制台输出"""
|
|
121
|
+
if self._use_color:
|
|
122
|
+
color = self.COLORS.get(category, "")
|
|
123
|
+
style = self.COLORS["bold"] if bold else ""
|
|
124
|
+
reset = self.COLORS["reset"]
|
|
125
|
+
print(f"{style}{color}{msg}{reset}", flush=True)
|
|
126
|
+
else:
|
|
127
|
+
print(msg, flush=True)
|
|
128
|
+
|
|
129
|
+
def print_result(self, error_code: ErrorCode, test_name: str, duration: float = 0):
|
|
130
|
+
"""打印单条测试结果"""
|
|
131
|
+
emoji = error_code.emoji
|
|
132
|
+
duration_str = f"({duration:.3f}s)" if duration > 0 else ""
|
|
133
|
+
msg = f" {emoji} {test_name:<40} {error_code.code:<10} {duration_str}"
|
|
134
|
+
self.print_colored(msg, category=error_code.category)
|
|
135
|
+
|
|
136
|
+
# ── 汇总报告 ──
|
|
137
|
+
def print_summary(self):
|
|
138
|
+
"""控制台打印汇总报告"""
|
|
139
|
+
total = len(self._errors)
|
|
140
|
+
passed = sum(1 for e in self._errors if e.error_code.category == ErrorCategory.PASS)
|
|
141
|
+
failed = sum(1 for e in self._errors if e.error_code.category == ErrorCategory.FAIL)
|
|
142
|
+
errored = sum(1 for e in self._errors if e.error_code.category == ErrorCategory.ERROR)
|
|
143
|
+
skipped = sum(1 for e in self._errors if e.error_code.category == ErrorCategory.SKIP)
|
|
144
|
+
|
|
145
|
+
self.section("测试汇总")
|
|
146
|
+
print()
|
|
147
|
+
self.print_colored(f" 总计 : {total} 个测试", bold=True)
|
|
148
|
+
self.print_colored(f" 通过 : {passed} ✅", ErrorCategory.PASS)
|
|
149
|
+
self.print_colored(f" 失败 : {failed} ❌", ErrorCategory.FAIL)
|
|
150
|
+
self.print_colored(f" 错误 : {errored} 💥", ErrorCategory.ERROR)
|
|
151
|
+
self.print_colored(f" 跳过 : {skipped} ⏭️", ErrorCategory.SKIP)
|
|
152
|
+
print()
|
|
153
|
+
self.print_colored(f" 日志文件: {self.log_path}", bold=False)
|
|
154
|
+
self.print_colored(f" 错误文件: {self.error_path}", bold=False)
|
|
155
|
+
print()
|
|
156
|
+
|
|
157
|
+
error_list = [e for e in self._errors if e.error_code.category != ErrorCategory.PASS]
|
|
158
|
+
if error_list:
|
|
159
|
+
self.print_colored("失败/错误详情:", ErrorCategory.ERROR, bold=True)
|
|
160
|
+
for e in error_list:
|
|
161
|
+
self.print_colored(f" {e.brief()}", e.error_code.category)
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
def get_summary(self) -> dict:
|
|
165
|
+
"""返回汇总数据(GUI 用)"""
|
|
166
|
+
total = len(self._errors)
|
|
167
|
+
return {
|
|
168
|
+
"total": total,
|
|
169
|
+
"passed": sum(1 for e in self._errors if e.error_code.category == ErrorCategory.PASS),
|
|
170
|
+
"failed": sum(1 for e in self._errors if e.error_code.category == ErrorCategory.FAIL),
|
|
171
|
+
"errored": sum(1 for e in self._errors if e.error_code.category == ErrorCategory.ERROR),
|
|
172
|
+
"skipped": sum(1 for e in self._errors if e.error_code.category == ErrorCategory.SKIP),
|
|
173
|
+
"log_path": str(self.log_path),
|
|
174
|
+
"error_path": str(self.error_path),
|
|
175
|
+
"errors": self._errors,
|
|
176
|
+
"entries": self._log_entries,
|
|
177
|
+
}
|