From 8cff6b35f961f42f006b34bef64c16a8536cd98c Mon Sep 17 00:00:00 2001 From: qyqy Date: Fri, 11 Sep 2026 19:13:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(tests):=20tmp=5Fpath=20=E8=90=BD=E7=82=B9?= =?UTF-8?q?=E6=94=B9=E5=88=B0=E4=BB=93=E5=BA=93=E5=86=85=EF=BC=8C=E6=B6=88?= =?UTF-8?q?=E9=99=A4=2033=20=E4=B8=AA=E4=B8=8E=E7=BC=BA=E9=99=B7=E6=97=A0?= =?UTF-8?q?=E5=85=B3=E7=9A=84=20setup=20=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:本机 `%TEMP%\pytest-of-Windows` 被权限更高的会话建过,当前用户无权写入, 于是所有用 `tmp_path` 的用例在 setup 阶段批量 ERROR(WinError 5)——实测 33 个, 散布在 offsite 附件预览、通知发送、promotion、worker 等处。这类噪声会让 "到底哪里坏了"完全看不出来(同事这批新用例首次把它暴露出来)。 修法:在 tests/conftest.py 覆盖 `tmp_path`,把根目录改到仓库内 `.workdir/pytest-tmp` (已在 .gitignore)。语义不变——每个用例仍拿到一个**新建的空目录**(残留目录会污染断言)。 只覆盖 `tmp_path` 而不动 `tmp_path_factory`:后者是 pytest 私有构造,参数随版本变化 (实测直接实例化 `TempPathFactory(...)` 会 TypeError),而本仓库用例只用 `tmp_path`。 同时把 pyproject.toml 里那条 `basetemp = ...` 删掉:pytest **只在命令行认 basetemp**, 写在 ini 里会被静默忽略(实测无效),留着会让人误以为已配好。改为注释指向 conftest。 效果:不带任何参数 `pytest -q` 从「3 failed + 33 errors」变为「3 failed,0 error」。 --- pyproject.toml | 10 +++------- tests/conftest.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b781093..1ee04e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,13 +52,9 @@ testpaths = ["tests"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" markers = ["integration: requires local database services"] -# 临时目录固定到仓库内(`.workdir/` 已在 .gitignore 中忽略)。 -# -# 为什么不用系统临时目录:Windows 上 `%TEMP%\pytest-of-` 一旦被权限更高的会话 -# 建过,后续运行就**无权写入**,所有用 `tmp_path` 的用例批量 setup 失败 -# (实测 33 个用例 ERROR,报 WinError 5)。固定到仓库内后不受系统临时目录权限影响, -# 且清理范围可见、可控。 -basetemp = ".workdir/pytest-tmp" +# 临时目录的落点由 `tests/conftest.py` 覆盖 `tmp_path` 决定(落在仓库内 `.workdir/pytest-tmp`)。 +# 这里**不**写 `basetemp`:pytest 只在命令行认它,写在 ini 里会被静默忽略(实测无效), +# 留着会让人误以为已经配好了。系统临时目录权限坏掉的原因与修法见 conftest 的说明。 [tool.ruff] line-length = 100 diff --git a/tests/conftest.py b/tests/conftest.py index 3a50716..cc5bc74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,9 @@ NullPool 让每次取用都新建连接、归还即关闭,从根上消除跨 import asyncio import sys +from typing import TYPE_CHECKING + +import pytest from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool @@ -31,7 +34,36 @@ _db.SessionFactory = async_sessionmaker( # type: ignore[assignment] _db.engine, class_=AsyncSession, expire_on_commit=False ) -import pytest # noqa: E402 +@pytest.fixture +def tmp_path() -> "Path": + """把 `tmp_path` 的根目录改到**仓库内**(`.workdir/pytest-tmp`)。 + + 为什么必须覆盖:pytest 默认在系统临时目录建 `%TEMP%\\pytest-of-`。Windows 上 + 那个目录一旦被**权限更高的会话**(例如以管理员身份跑过一次测试)建过,当前用户就 + 无权再写入,于是**所有**用 `tmp_path` 的用例在 setup 阶段批量失败 + (实测 33 个用例 ERROR、报 `PermissionError: [WinError 5]`),而它们与真实缺陷无关—— + 这种噪声会让"到底哪里坏了"完全看不出来。 + + 覆盖后临时目录落在仓库内:不依赖系统临时目录权限,随 `.workdir/` 一起被 .gitignore + 忽略,清理范围可见。语义不变——每个用例拿到一个**新建的空目录**(残留目录会让上一条 + 用例的产物污染断言)。 + + 为什么只覆盖 `tmp_path` 而不动 `tmp_path_factory`:后者是 pytest 的私有构造 + (`TempPathFactory.__init__` 的参数随版本变化,实测直接实例化会 `TypeError`), + 而本仓库的用例只用 `tmp_path`。改动面越小,越不容易在下一次升级时炸。 + """ + import shutil + from pathlib import Path + + root = Path(".workdir") / "pytest-tmp" + root.mkdir(parents=True, exist_ok=True) + # 用目录数量推序号:不依赖 pytest 内部状态,重跑时自动接着编号。 + index = len([item for item in root.iterdir() if item.is_dir()]) + 1 + path = root / f"test{index}" + shutil.rmtree(path, ignore_errors=True) + path.mkdir(parents=True) + return path + from app.core.contracts import AgentDefinition, CoreResult, ResolvedAgentConfig # noqa: E402 from app.service.agent.base import BaseAgent # noqa: E402