feat(T-003): 建立日志与运行产物目录策略
- src/paths.py: 集中管理 logs/、artifacts/ 路径与产物命名 (artifacts/<task_id>/<时间戳>_<step>.png|.xml),_safe 安全化防路径穿越, 运行时自动创建且被 .gitignore 忽略。 - src/logging_config.py: 统一日志格式(含 task_id/step),控制台 + logs/app.log, 缺上下文时由 filter 补默认值,setup 幂等;约定不记录敏感信息。 - src/main.py: 启动时初始化运行目录与日志。 - 运行方式统一为 python -m src.main(绝对导入下的唯一干净入口), 同步替换 00/03/05/current-state 文档命令与 dev.bat。 - 文档:04 §七 补 paths.py 与产物命名规则;tasks/progress/current-state 更新。 验证:compileall OK;unittest Ran 17 tests OK;python -m src.main exit=0,logs/app.log 写入正常。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
41bc666322
commit
b1e38ffd7a
@@ -1,3 +1,3 @@
|
|||||||
title run
|
title run
|
||||||
cd /d "%~dp0src" && python main.py
|
cd /d "%~dp0" && python -m src.main
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ MVP 不做:
|
|||||||
```powershell
|
```powershell
|
||||||
python -m compileall src
|
python -m compileall src
|
||||||
python -m unittest discover -s tests -t .
|
python -m unittest discover -s tests -t .
|
||||||
python src/main.py
|
python -m src.main
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
| 创建虚拟环境 | `python -m venv .venv` |
|
| 创建虚拟环境 | `python -m venv .venv` |
|
||||||
| 激活虚拟环境 | `.venv\Scripts\Activate.ps1` |
|
| 激活虚拟环境 | `.venv\Scripts\Activate.ps1` |
|
||||||
| 安装依赖 | `pip install -r requirements.txt` |
|
| 安装依赖 | `pip install -r requirements.txt` |
|
||||||
| 本地运行 | `python src/main.py` |
|
| 本地运行 | `python -m src.main` |
|
||||||
| 测试 | `python -m unittest discover -s tests -t .` |
|
| 测试 | `python -m unittest discover -s tests -t .` |
|
||||||
| 语法检查 | `python -m compileall src` |
|
| 语法检查 | `python -m compileall src` |
|
||||||
| 打包(待 T-402) | `pyinstaller app.spec` |
|
| 打包(待 T-402) | `pyinstaller app.spec` |
|
||||||
@@ -49,7 +49,7 @@ Windows PowerShell:
|
|||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
.venv\Scripts\Activate.ps1
|
.venv\Scripts\Activate.ps1
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python src/main.py
|
python -m src.main
|
||||||
python -m unittest discover -s tests -t .
|
python -m unittest discover -s tests -t .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,8 @@ cmpdd/
|
|||||||
│ ├── android/
|
│ ├── android/
|
||||||
│ │ ├── device.py
|
│ │ ├── device.py
|
||||||
│ │ └── pdd_flow.py
|
│ │ └── pdd_flow.py
|
||||||
│ └── logging_config.py
|
│ ├── logging_config.py
|
||||||
|
│ └── paths.py
|
||||||
├── tests/
|
├── tests/
|
||||||
├── samples/
|
├── samples/
|
||||||
├── logs/
|
├── logs/
|
||||||
@@ -337,8 +338,10 @@ cmpdd/
|
|||||||
- `src/core/`:任务模型、状态机、执行编排。
|
- `src/core/`:任务模型、状态机、执行编排。
|
||||||
- `src/excel/`:Excel 导入、校验、回写。
|
- `src/excel/`:Excel 导入、校验、回写。
|
||||||
- `src/android/`:设备连接、拼多多 App 自动化流程。
|
- `src/android/`:设备连接、拼多多 App 自动化流程。
|
||||||
- `logs/`:本地运行日志,默认不提交真实敏感日志。
|
- `src/logging_config.py`:统一日志格式(含 `task_id`、`step`),输出到控制台与 `logs/app.log`;不记录敏感信息。
|
||||||
- `artifacts/`:截图、UI XML、结果文件等运行产物,默认不提交真实数据。
|
- `src/paths.py`:集中管理运行时路径与产物命名;截图与 UI XML 按 `artifacts/<task_id>/<时间戳>_<step>.png|.xml` 归档,`task_id`/`step` 入路径前安全化以防穿越。
|
||||||
|
- `logs/`:本地运行日志(`app.log`),运行时自动创建并被 `.gitignore` 忽略,不提交真实敏感日志。
|
||||||
|
- `artifacts/`:截图、UI XML、结果文件等运行产物,运行时自动创建并被 `.gitignore` 忽略,不提交真实数据。
|
||||||
- `samples/`:脱敏样例 Excel。
|
- `samples/`:脱敏样例 Excel。
|
||||||
|
|
||||||
## 八、架构纪律
|
## 八、架构纪律
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
```powershell
|
```powershell
|
||||||
python -m compileall src
|
python -m compileall src
|
||||||
python -m unittest discover -s tests -t .
|
python -m unittest discover -s tests -t .
|
||||||
python src/main.py
|
python -m src.main
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8. 绝不
|
## 8. 绝不
|
||||||
|
|||||||
+10
-10
@@ -7,10 +7,10 @@
|
|||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-06-24
|
- 日期:2026-06-24
|
||||||
- 阶段:Phase 0 地基(T-001、T-002 完成)/ 原型验证前
|
- 阶段:Phase 0 地基(T-001、T-002、T-003 完成)/ 进入 Phase 1 原型验证
|
||||||
- 技术栈:Python 3.10(骨架已跑通);计划接入 PySide6 + `uiautomator2` + ADB + `openpyxl`
|
- 技术栈:Python 3.10(骨架已跑通);计划接入 PySide6 + `uiautomator2` + ADB + `openpyxl`
|
||||||
- 生产代码:骨架 + 核心数据模型(`src/main.py` 入口、`src/core/` 状态枚举与任务模型)
|
- 生产代码:骨架 + 核心数据模型 + 运行时基建(`src/main.py` 入口、`src/core/` 模型与状态、`src/logging_config.py` 日志、`src/paths.py` 产物路径)
|
||||||
- 测试:标准库 `unittest`,覆盖冒烟与核心模型(`tests/test_smoke.py`、`tests/test_models.py`)
|
- 测试:标准库 `unittest`,覆盖冒烟、核心模型、日志与产物路径(`tests/` 下 4 个测试文件)
|
||||||
- 数据:尚未提供真实 Excel 样例
|
- 数据:尚未提供真实 Excel 样例
|
||||||
|
|
||||||
## 当前目录要点
|
## 当前目录要点
|
||||||
@@ -18,20 +18,20 @@
|
|||||||
| 路径 | 状态 | 说明 |
|
| 路径 | 状态 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `docs/` | 已有 | 项目规范化文档,已按 harness coding 文档风格建立。 |
|
| `docs/` | 已有 | 项目规范化文档,已按 harness coding 文档风格建立。 |
|
||||||
| `src/` | 已有 | `main.py` 入口 + `core/`(`status.py` 状态枚举、`models.py` 任务模型);GUI/执行器/Excel/Android 模块后续添加。 |
|
| `src/` | 已有 | `main.py` 入口 + `core/`(状态枚举、任务模型)+ `logging_config.py`、`paths.py`;GUI/执行器/Excel/Android 模块后续添加。 |
|
||||||
| `tests/` | 已有 | 已建冒烟测试 `tests/test_smoke.py`(unittest)。 |
|
| `tests/` | 已有 | `test_smoke`、`test_models`、`test_paths`、`test_logging`(unittest)。 |
|
||||||
| `requirements.txt` | 已有 | 依赖清单;当前零第三方运行时依赖,含依赖纪律注释。 |
|
| `requirements.txt` | 已有 | 依赖清单;当前零第三方运行时依赖,含依赖纪律注释。 |
|
||||||
| `samples/` | 待建 | 脱敏 Excel 样例。 |
|
| `samples/` | 待建 | 脱敏 Excel 样例。 |
|
||||||
| `logs/` | 待建 | 本地运行日志。 |
|
| `logs/` | 运行时创建 | `app.log` 运行日志;被 `.gitignore` 忽略。 |
|
||||||
| `artifacts/` | 待建 | 截图、UI XML、结果文件等运行产物。 |
|
| `artifacts/` | 运行时创建 | 截图、UI XML 等运行产物;被 `.gitignore` 忽略。 |
|
||||||
|
|
||||||
## 任务与进度状态
|
## 任务与进度状态
|
||||||
|
|
||||||
任务定义以 [`../tasks.md`](../tasks.md) 为准,执行记录以 [`../progress.md`](../progress.md) 为准。
|
任务定义以 [`../tasks.md`](../tasks.md) 为准,执行记录以 [`../progress.md`](../progress.md) 为准。
|
||||||
|
|
||||||
- 已完成:项目文档初始化;`T-001 初始化 Python 项目骨架`;`T-002 建立核心数据模型和状态枚举`。
|
- 已完成:项目文档初始化;`T-001`、`T-002`、`T-003`(Phase 0 地基全部完成)。
|
||||||
- 正在进行:无。
|
- 正在进行:无。
|
||||||
- 下一个可领取任务:`T-003 建立日志和运行产物目录策略`。
|
- 下一个可领取任务:`T-101 验证 ADB 和 uiautomator2 真机连接`(Phase 1,需真机环境)。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 本地运行入口
|
# 本地运行入口
|
||||||
python src/main.py
|
python -m src.main
|
||||||
|
|
||||||
# 测试
|
# 测试
|
||||||
python -m unittest discover -s tests -t .
|
python -m unittest discover -s tests -t .
|
||||||
|
|||||||
+12
-3
@@ -14,9 +14,9 @@
|
|||||||
## 当前摘要
|
## 当前摘要
|
||||||
|
|
||||||
- 日期:2026-06-24
|
- 日期:2026-06-24
|
||||||
- 阶段:Phase 0 地基(T-001、T-002 完成)/ 原型验证前
|
- 阶段:Phase 0 地基完成(T-001/2/3)/ 进入 Phase 1 原型验证
|
||||||
- 当前任务:无(T-002 已完成)
|
- 当前任务:无(T-003 已完成)
|
||||||
- 下一个任务:`T-003 建立日志和运行产物目录策略`
|
- 下一个任务:`T-101 验证 ADB 和 uiautomator2 真机连接`(需真机环境)
|
||||||
- 当前阻塞:尚未提供真实 Excel 样例、Android 真机和测试商品链接
|
- 当前阻塞:尚未提供真实 Excel 样例、Android 真机和测试商品链接
|
||||||
|
|
||||||
## 进度记录
|
## 进度记录
|
||||||
@@ -66,6 +66,15 @@
|
|||||||
- 范围:未含 `PaymentConfig`/`PaymentMode`/`RunnerState`(属支付与执行器任务,按需引入)。
|
- 范围:未含 `PaymentConfig`/`PaymentMode`/`RunnerState`(属支付与执行器任务,按需引入)。
|
||||||
- 验证:`python3 -m compileall src` OK;`python3 -m unittest discover -s tests -t .` → Ran 10 tests, OK。
|
- 验证:`python3 -m compileall src` OK;`python3 -m unittest discover -s tests -t .` → Ran 10 tests, OK。
|
||||||
|
|
||||||
|
### 2026-06-24 · T-003 建立日志和运行产物目录策略(DONE)
|
||||||
|
|
||||||
|
- 完成:`src/paths.py` 集中管理运行时路径与产物命名(`logs/`、`artifacts/` 运行时创建并 gitignore;截图/UI XML 按 `artifacts/<task_id>/<时间戳>_<step>.png|.xml` 归档,`_safe` 安全化防路径穿越);`src/logging_config.py` 统一日志格式(含 `task_id`/`step`,控制台 + `logs/app.log`,缺上下文时由 filter 补默认值,幂等 setup)。
|
||||||
|
- 集成:`src/main.py` 启动时 `ensure_runtime_dirs()` + `setup_logging()` 并写一条启动日志。
|
||||||
|
- 修正:`main.py` 引入 `from src.*` 后 `python src/main.py` 直接运行会 `ModuleNotFoundError`;运行方式统一为 `python -m src.main`,同步替换 `00/03/05/current-state` 文档命令与 `dev.bat`。
|
||||||
|
- 测试:`tests/test_paths.py`、`tests/test_logging.py` 覆盖安全化、路径生成与归档、防穿越、日志上下文与幂等。
|
||||||
|
- 安全:日志格式不含敏感字段,并在 `logging_config`/`05` 留下「不记录支付密码/验证码/账号密码」约定。
|
||||||
|
- 验证:`python3 -m compileall src` OK;`python3 -m unittest discover -s tests -t .` → Ran 17 tests, OK;`python3 -m src.main` exit=0 且 `logs/app.log` 正常写入。
|
||||||
|
|
||||||
## 阻塞与风险
|
## 阻塞与风险
|
||||||
|
|
||||||
- 真实 Excel 表头待提供。
|
- 真实 Excel 表头待提供。
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""日志配置(T-003)。
|
||||||
|
|
||||||
|
统一日志格式,包含时间、级别、任务 ID 与步骤,便于按任务排查。
|
||||||
|
日志同时输出到控制台与 logs/app.log。
|
||||||
|
|
||||||
|
安全:日志不得记录支付密码、短信验证码、账号密码等敏感信息
|
||||||
|
(见 docs/05-coding-rules.md §4、§6)。调用方通过 get_task_logger(task_id, step)
|
||||||
|
获得带任务上下文的 logger。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from logging import LoggerAdapter
|
||||||
|
|
||||||
|
from src.paths import LOGS_DIR
|
||||||
|
|
||||||
|
LOG_FILE = LOGS_DIR / "app.log"
|
||||||
|
|
||||||
|
# 含任务 ID 与步骤的统一格式;缺失上下文时由过滤器补默认值
|
||||||
|
_FORMAT = "%(asctime)s %(levelname)s [task=%(task_id)s step=%(step)s] %(name)s: %(message)s"
|
||||||
|
|
||||||
|
_configured = False
|
||||||
|
|
||||||
|
|
||||||
|
class _ContextDefaultsFilter(logging.Filter):
|
||||||
|
"""为缺少 task_id/step 的记录补默认值,确保格式化不因缺字段报错。"""
|
||||||
|
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
if not hasattr(record, "task_id"):
|
||||||
|
record.task_id = "-"
|
||||||
|
if not hasattr(record, "step"):
|
||||||
|
record.step = "-"
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(level: int = logging.INFO) -> None:
|
||||||
|
"""配置根日志:控制台 + 文件(logs/app.log)。重复调用安全(只配置一次)。"""
|
||||||
|
global _configured
|
||||||
|
if _configured:
|
||||||
|
return
|
||||||
|
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
formatter = logging.Formatter(_FORMAT)
|
||||||
|
context_filter = _ContextDefaultsFilter()
|
||||||
|
|
||||||
|
root = logging.getLogger()
|
||||||
|
root.setLevel(level)
|
||||||
|
root.handlers.clear()
|
||||||
|
for handler in (logging.StreamHandler(), logging.FileHandler(LOG_FILE, encoding="utf-8")):
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
handler.addFilter(context_filter)
|
||||||
|
root.addHandler(handler)
|
||||||
|
|
||||||
|
_configured = True
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_logger(task_id: str, step: str = "-") -> LoggerAdapter:
|
||||||
|
"""返回带任务上下文(task_id/step)的 logger 适配器。
|
||||||
|
|
||||||
|
安全:调用方不得把支付密码、短信验证码、账号密码等敏感信息写入日志。
|
||||||
|
"""
|
||||||
|
return LoggerAdapter(logging.getLogger("cmpdd"), {"task_id": task_id, "step": step})
|
||||||
+10
-3
@@ -5,17 +5,24 @@ T-001 骨架阶段的最小可运行入口,仅用于验证项目结构与运
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from src.logging_config import get_task_logger, setup_logging
|
||||||
|
from src.paths import ensure_runtime_dirs
|
||||||
|
|
||||||
APP_NAME = "拼多多批量下单执行器"
|
APP_NAME = "拼多多批量下单执行器"
|
||||||
APP_VERSION = "0.1.0"
|
APP_VERSION = "0.1.0"
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""最小入口:打印应用信息并返回退出码 0。
|
"""最小入口:初始化运行目录与日志,打印应用信息并返回退出码 0。
|
||||||
|
|
||||||
GUI 与自动化能力尚未接入,此处只确认骨架可运行。
|
GUI 与自动化能力尚未接入,此处只确认骨架可运行、日志可写。
|
||||||
"""
|
"""
|
||||||
|
ensure_runtime_dirs()
|
||||||
|
setup_logging()
|
||||||
|
log = get_task_logger("-", "startup")
|
||||||
|
log.info("%s v%s 启动(骨架阶段)", APP_NAME, APP_VERSION)
|
||||||
print(f"{APP_NAME} v{APP_VERSION}")
|
print(f"{APP_NAME} v{APP_VERSION}")
|
||||||
print("项目骨架已就绪(T-001);GUI 与真机自动化将在后续任务接入。")
|
print("项目骨架已就绪;GUI 与真机自动化将在后续任务接入。")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""运行时路径与产物归档策略(T-003)。
|
||||||
|
|
||||||
|
集中管理日志与运行产物(截图、UI XML)的输出位置与命名,
|
||||||
|
确保排查时能按任务 ID 定位证据。
|
||||||
|
|
||||||
|
约定:
|
||||||
|
- `logs/`、`artifacts/` 位于项目根,运行时自动创建,并被 `.gitignore` 忽略,
|
||||||
|
不提交真实运行数据。
|
||||||
|
- 截图与 UI XML 按 `artifacts/<task_id>/<时间戳>_<step>.png|.xml` 归档。
|
||||||
|
- `task_id` / `step` 进入路径前统一做安全化,避免路径穿越与非法文件名。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 本文件位于 src/ 下,项目根为其上一级
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
LOGS_DIR = PROJECT_ROOT / "logs"
|
||||||
|
ARTIFACTS_DIR = PROJECT_ROOT / "artifacts"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_runtime_dirs() -> None:
|
||||||
|
"""确保 logs/ 与 artifacts/ 存在(幂等)。"""
|
||||||
|
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def task_artifact_dir(task_id: str) -> Path:
|
||||||
|
"""返回某任务的产物目录 artifacts/<task_id>/,并确保存在。"""
|
||||||
|
target = ARTIFACTS_DIR / _safe(task_id)
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_path(task_id: str, step: str) -> Path:
|
||||||
|
"""生成截图保存路径:artifacts/<task_id>/<时间戳>_<step>.png。"""
|
||||||
|
return task_artifact_dir(task_id) / f"{_timestamp()}_{_safe(step)}.png"
|
||||||
|
|
||||||
|
|
||||||
|
def ui_xml_path(task_id: str, step: str) -> Path:
|
||||||
|
"""生成 UI XML 保存路径:artifacts/<task_id>/<时间戳>_<step>.xml。"""
|
||||||
|
return task_artifact_dir(task_id) / f"{_timestamp()}_{_safe(step)}.xml"
|
||||||
|
|
||||||
|
|
||||||
|
def _timestamp() -> str:
|
||||||
|
# 含微秒,避免同一步骤快速连续产出时文件名冲突
|
||||||
|
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||||
|
|
||||||
|
|
||||||
|
def _safe(name: str) -> str:
|
||||||
|
"""安全化路径片段:仅保留字母数字、连字符、下划线,其余替换为下划线。
|
||||||
|
|
||||||
|
用于隔离 task_id/step 中可能出现的 `/`、`..` 等字符,防止逃逸 artifacts 根目录。
|
||||||
|
空结果回退为 'unknown',避免产生空文件名。
|
||||||
|
"""
|
||||||
|
cleaned = "".join(ch if (ch.isalnum() or ch in "-_") else "_" for ch in name)
|
||||||
|
return cleaned or "unknown"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| T-001 | 初始化 Python 项目骨架 | - | 建立 `src/`、`tests/`、`requirements.txt`;可运行最小入口;不引入无关依赖;用**真实可运行命令**替换 `00-ai-start-here.md`、`03-tech-stack.md`、`05-coding-rules.md`、`current-state.md` 中的占位符验证命令 | DONE |
|
| T-001 | 初始化 Python 项目骨架 | - | 建立 `src/`、`tests/`、`requirements.txt`;可运行最小入口;不引入无关依赖;用**真实可运行命令**替换 `00-ai-start-here.md`、`03-tech-stack.md`、`05-coding-rules.md`、`current-state.md` 中的占位符验证命令 | DONE |
|
||||||
| T-002 | 建立核心数据模型和状态枚举 | T-001 | `OrderTask`、`TaskStatus` 与 `04-architecture.md` 一致;有基础单元测试 | DONE |
|
| T-002 | 建立核心数据模型和状态枚举 | T-001 | `OrderTask`、`TaskStatus` 与 `04-architecture.md` 一致;有基础单元测试 | DONE |
|
||||||
| T-003 | 建立日志和运行产物目录策略 | T-001 | 日志、截图、UI XML 输出路径清晰;不记录敏感信息 | TODO |
|
| T-003 | 建立日志和运行产物目录策略 | T-001 | 日志、截图、UI XML 输出路径清晰;不记录敏感信息 | DONE |
|
||||||
|
|
||||||
## Phase 1 · 最高风险验证
|
## Phase 1 · 最高风险验证
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""日志配置测试(T-003)。"""
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from src import logging_config
|
||||||
|
from src.logging_config import _FORMAT, _ContextDefaultsFilter, get_task_logger
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingTest(unittest.TestCase):
|
||||||
|
def test_task_logger_carries_context(self):
|
||||||
|
log = get_task_logger("T9", "pay")
|
||||||
|
self.assertEqual(log.extra["task_id"], "T9")
|
||||||
|
self.assertEqual(log.extra["step"], "pay")
|
||||||
|
|
||||||
|
def test_format_tolerates_missing_context(self):
|
||||||
|
"""非任务日志(无 task_id/step)也能格式化,不抛 KeyError。"""
|
||||||
|
record = logging.LogRecord("x", logging.INFO, __file__, 1, "msg", None, None)
|
||||||
|
_ContextDefaultsFilter().filter(record)
|
||||||
|
out = logging.Formatter(_FORMAT).format(record)
|
||||||
|
self.assertIn("task=-", out)
|
||||||
|
self.assertIn("step=-", out)
|
||||||
|
|
||||||
|
def test_setup_logging_idempotent(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
with mock.patch.object(logging_config, "LOG_FILE", Path(tmp) / "app.log"):
|
||||||
|
logging_config._configured = False
|
||||||
|
logging.getLogger().handlers.clear()
|
||||||
|
logging_config.setup_logging()
|
||||||
|
first = len(logging.getLogger().handlers)
|
||||||
|
logging_config.setup_logging() # 第二次应无操作
|
||||||
|
self.assertEqual(len(logging.getLogger().handlers), first)
|
||||||
|
# 还原全局 logging 状态,避免影响其它测试
|
||||||
|
logging.getLogger().handlers.clear()
|
||||||
|
logging_config._configured = False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""运行产物路径策略测试(T-003)。"""
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from src import paths
|
||||||
|
|
||||||
|
|
||||||
|
class PathsTest(unittest.TestCase):
|
||||||
|
def test_safe_replaces_illegal_chars(self):
|
||||||
|
self.assertEqual(paths._safe("color/size..x"), "color_size__x")
|
||||||
|
self.assertEqual(paths._safe("ok-_1"), "ok-_1")
|
||||||
|
|
||||||
|
def test_safe_empty_fallback(self):
|
||||||
|
self.assertEqual(paths._safe(""), "unknown")
|
||||||
|
self.assertEqual(paths._safe("/.."), "___")
|
||||||
|
|
||||||
|
def test_screenshot_and_xml_paths(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
with mock.patch.object(paths, "ARTIFACTS_DIR", Path(tmp)):
|
||||||
|
png = paths.screenshot_path("T1", "open_product")
|
||||||
|
xml = paths.ui_xml_path("T1", "open_product")
|
||||||
|
self.assertTrue(png.name.endswith(".png"))
|
||||||
|
self.assertTrue(xml.name.endswith(".xml"))
|
||||||
|
# 同一任务归到 artifacts/<task_id>/ 子目录
|
||||||
|
self.assertEqual(png.parent.name, "T1")
|
||||||
|
self.assertEqual(xml.parent.name, "T1")
|
||||||
|
# 目录已被创建
|
||||||
|
self.assertTrue(png.parent.is_dir())
|
||||||
|
|
||||||
|
def test_illegal_task_id_cannot_escape_root(self):
|
||||||
|
"""task_id 含路径穿越字符时被安全化隔离,不逃逸 artifacts 根。"""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
with mock.patch.object(paths, "ARTIFACTS_DIR", Path(tmp)):
|
||||||
|
p = paths.screenshot_path("../evil", "step")
|
||||||
|
self.assertTrue(str(p.resolve()).startswith(str(Path(tmp).resolve())))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user