"""测试依赖与执行证据的逐例行为验证。 保护:用例选择、结构化报告、离线依赖阻断、超时与失败区分、 空收集与缺环境不静默、源身份与执行证据的拒绝语义。 环境:离线;受限子进程和临时测试项目(复制仓库真实 conftest 和身份模块后在 临时项目中运行 pytest,验证同一套隔离与守卫语义,不是旧文件迁移)。 """ from __future__ import annotations import ast import shutil import subprocess import sys import textwrap import xml.etree.ElementTree as ET from pathlib import Path import pytest 仓库根 = Path(__file__).resolve().parents[2] 真实conftest = 仓库根 / "tests" / "conftest.py" class 临时项目: """受限临时 pytest 项目:复制真实 conftest,写入指定用例文件。""" def __init__(self, 根: Path) -> None: self.根 = 根 shutil.copyfile(真实conftest, 根 / "conftest.py") shutil.copytree( 仓库根 / "tests/支持", 根 / "支持", ignore=shutil.ignore_patterns("__pycache__") ) (根 / "工具").mkdir(exist_ok=True) shutil.copyfile(仓库根 / "工具/用例身份.py", 根 / "工具/用例身份.py") def 写(self, 相对: str, 内容: str, *, 登记身份: bool = True) -> Path: """合成用例使用显式源码 marker;不生产或读取外部位置清单。""" 目标 = self.根 / 相对 目标.parent.mkdir(parents=True, exist_ok=True) 文本 = textwrap.dedent(内容) if 目标.name.startswith("test_") and 目标.suffix == ".py" and 登记身份: 树 = ast.parse(文本) 行 = 文本.splitlines(keepends=True) 插入 = [] for 节点 in ast.walk(树): if not isinstance(节点, ast.FunctionDef) or not 节点.name.startswith("test_"): continue if any("pytest.mark.case_id" in ast.unparse(标) for 标 in 节点.decorator_list): continue 后缀 = 节点.name.rsplit("__", 1)[-1] 起始 = min([节点.lineno, *[标.lineno for 标 in 节点.decorator_list]]) - 1 插入.append( (起始, " " * 节点.col_offset + f"@pytest.mark.case_id('TC-x-{后缀}000000')\n") ) for 位置, 代码 in sorted(插入, reverse=True): 行.insert(位置, 代码) 文本 = "".join(行) if 插入 and not any( isinstance(节, ast.Import) and any(别名.name == "pytest" for 别名 in 节.names) for 节 in 树.body ): 文本 = "import pytest\n" + 文本 目标.write_text(文本, encoding="utf-8") return 目标 def 运行(self, *参数: str, 环境: dict[str, str] | None = None) -> tuple[int, dict]: 报告 = self.根 / "报告.xml" 完整 = [sys.executable, "-m", "pytest", "-q", f"--junitxml={报告}", *参数] 进程 = subprocess.run( 完整, cwd=self.根, capture_output=True, text=True, timeout=180, env={**_清理环境(), **(环境 or {})}, ) 结果: dict = {"退出码": 进程.returncode, "stdout": 进程.stdout, "stderr": 进程.stderr} if 报告.exists(): 结果["报告"] = 解析junit(报告) return 进程.returncode, 结果 def 收集(self, *路径: str) -> list[dict]: return 收集用例(self.根, list(路径) or None) def _清理环境() -> dict[str, str]: """子进程环境:剔除数据库变量,避免宿主环境泄漏进隔离语义。""" import os 环境 = dict(os.environ) 环境.pop("MUSE_TEST_DATABASE_URL", None) return 环境 def 解析junit(路径: Path) -> dict: 树 = ET.parse(路径) 根 = 树.getroot() 用例 = [] for 节点 in 根.iter("testcase"): 条目: dict = {"名称": 节点.get("name", ""), "类": 节点.get("classname", "")} for 子 in 节点: if 子.tag not in {"failure", "error", "skipped"}: continue 条目["结果"] = 子.tag 条目["消息"] = (子.get("message") or "")[:300] if 子.text: 条目["正文"] = 子.text[:300] 用例.append(条目) 汇总 = 根.find(".//testsuite") return { "用例": 用例, "数量": int(汇总.get("tests", "0")) if 汇总 is not None else len(用例), "失败": int(汇总.get("failures", "0")) if 汇总 is not None else 0, "跳过": int(汇总.get("skipped", "0")) if 汇总 is not None else 0, } @pytest.fixture def 项目(tmp_path: Path) -> 临时项目: return 临时项目(tmp_path) # --------------------------------------------------------------------------- # 用例选择与结构化报告 # --------------------------------------------------------------------------- @pytest.mark.case_id( "TC-a5c7d4adafe3", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_selector_alias_and_intersection_are_reported_as_json__a5c7d4(项目: 临时项目) -> None: """--case 与标记取交集只选中目标用例;结构化报告精确到单个用例。""" 项目.写( "test_样本.py", """ import pytest def test_目标行为__aa0001(): assert 1 + 1 == 2 @pytest.mark.skip(reason="标记交集应在执行前排除") def test_另一行为__bb0002(): assert 2 + 2 == 4 """, ) 退出码, 结果 = 项目.运行( "--case", "TC-x-aa0001000000", "--case", "TC-x-bb0002000000", "-m", "not skip" ) assert 退出码 == 0, 结果["stderr"] 报告 = 结果["报告"] assert 报告["数量"] == 1, 报告 assert 报告["用例"][0]["名称"] == "test_目标行为__aa0001" assert "结果" not in 报告["用例"][0] # 通过:无 failure/skipped 节点 @pytest.mark.case_id( "TC-09d22d646494", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_offline_dependency_is_blocked_without_running_child__09d22d(项目: 临时项目) -> None: """数据库标记用例在缺环境时明确失败,业务子进程不执行。""" 项目.写( "test_数据库用例.py", """ from pathlib import Path import pytest @pytest.mark.数据库 def test_需要数据库__cc0003(隔离数据库URL): Path("child_ran").write_text("ran", encoding="utf-8") assert 隔离数据库URL """, ) 退出码, 结果 = 项目.运行("--case", "TC-x-cc0003000000") # 缺配置在setup阶段失败,不以skip计绿;业务体不执行。 assert 退出码 == 1, (退出码, 结果["stderr"]) 报告 = 结果["报告"] assert 报告["用例"][0]["结果"] == "error" assert "MUSE_TEST_DATABASE_URL" in 报告["用例"][0]["消息"] # 子进程没有执行:没有任何副作用文件 assert not (项目.根 / "child_ran").exists() @pytest.mark.case_id( "TC-08eb1b30bf46", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_allow_requires_runs_and_still_reports_dependency__08eb1b(项目: 临时项目) -> None: """提供隔离库引用后数据库用例真实执行;依赖来源在结果中可见。""" 项目.写( "test_数据库用例.py", """ import pytest @pytest.mark.数据库 def test_获得隔离引用__cc0003(隔离数据库URL): assert 隔离数据库URL == "postgresql://synthetic-test" """, ) 退出码, 结果 = 项目.运行( "--case", "TC-x-cc0003000000", 环境={"MUSE_TEST_DATABASE_URL": "postgresql://synthetic-test"}, ) assert 退出码 == 0, (退出码, 结果["stderr"]) assert 结果["报告"]["数量"] == 1 @pytest.mark.case_id( "TC-306973cf24b1", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_nonzero_and_timeout_have_distinct_structured_statuses__306973(项目: 临时项目) -> None: """断言失败与超时都以非零退出,且失败消息可区分。""" 项目.写( "test_失败.py", """ def test_断言失败__dd0004(): assert 1 == 2, "合成失败" """, ) 项目.写( "test_睡眠.py", """ import time def test_超时睡眠__ee0005(): time.sleep(30) """, ) 码1, 结果1 = 项目.运行("--case", "TC-x-dd0004000000") assert 码1 == 1 断言消息 = 结果1["报告"]["用例"][0]["消息"] assert "合成失败" in 断言消息 码2, 结果2 = 项目.运行("--case", "TC-x-ee0005000000", "--timeout=2") assert 码2 == 1 超时正文 = 结果2["报告"]["用例"][0].get("正文", "") + 结果2["报告"]["用例"][0].get("消息", "") assert "Timeout" in 超时正文 or "timeout" in 超时正文 # 两者消息可区分:超时消息不含合成失败标记 assert "合成失败" not in 超时正文 @pytest.mark.case_id( "TC-ae03b535241c", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_no_matches_and_missing_path_are_not_silent__ae03b5(项目: 临时项目) -> None: """未知 case 选择与非空选择的空收集都以非零退出并说明原因。""" 项目.写( "test_存在.py", """ def test_存在用例__ff0006(): assert True """, ) 码1, 结果1 = 项目.运行("--case", "TC-x-0000zz9999") assert 码1 != 0 assert "--case" in 结果1["stderr"] or "--case" in 结果1["stdout"] 码2, 结果2 = 项目.运行("tests/不存在的路径") assert 码2 in (4, 5) # 缺失路径或空收集:非零且不静默 assert "no tests" in (结果2["stdout"] + 结果2["stderr"]).lower() @pytest.mark.case_id( "TC-f7150be204a9", environment="离线,受限子进程和临时测试项目", given="损坏的 JSON 用例清单,以及合法空清单中的未登记完整 ID", when="通过原生 pytest --case 选择", then=["非法清单与未登记选择分别退出 4,错误原因明确,不执行测试"], contract=".agent/rules/测试隔离.md", ) def test_invalid_manifest_and_selector_requirement_are_structured__f7150b(项目: 临时项目) -> None: """旧导出损坏不干扰源身份;无效源 marker 给出准确错误。""" 项目.写("test_任意.py", "def test_任意用例__000007():\n assert True\n") (项目.根 / "用例清单.json").write_text("{broken", encoding="utf-8") 码, 结果 = 项目.运行("--case", "TC-x-000007000000") assert 码 == 0, 结果 项目.写( "test_任意.py", "import pytest\n@pytest.mark.case_id('wrong')\ndef test_任意(): assert True\n", ) 码, 结果 = 项目.运行() assert 码 == 4 assert "case_id 无效" in 结果["stderr"] @pytest.mark.case_id( "TC-265060590b97", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_all_offline_is_explicit_opt_in_and_output_is_summary_only__265060(项目: 临时项目) -> None: """默认运行按标记剔除外部环境用例;报告只含离线用例。""" 项目.写( "test_离线.py", """ def test_离线用例__110008(): assert True """, ) 项目.写( "test_外部.py", """ import pytest @pytest.mark.数据库 def test_外部用例__220009(隔离数据库URL): assert 隔离数据库URL """, ) 码, 结果 = 项目.运行("-m", "not 数据库") assert 码 == 0, (码, 结果["stderr"]) 名称 = [u["名称"] for u in 结果["报告"]["用例"]] assert 名称 == ["test_离线用例__110008"], 名称 @pytest.mark.case_id( "TC-50f80b27c79f", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_zero_exit_without_execution_evidence_fails_closed__50f80b(项目: 临时项目) -> None: """空用例文件与纯跳过都不允许零退出冒充通过。""" 项目.写( "test_空.py", """ 说明 = "本文件没有任何测试函数" """, ) 项目.写( "test_跳过.py", """ import pytest def test_跳过用例__33000a(): pytest.skip("合成跳过") """, ) # 只选空文件:无收集 → 退出码 5 码1, 结果1 = 项目.运行("test_空.py") assert 码1 == 5 # 纯跳过:守卫改写退出码为 5,不计通过 码2, 结果2 = 项目.运行("test_跳过.py") assert 码2 == 5, (码2, 结果2["stdout"]) assert "零实际执行" in 结果2["stderr"] # --------------------------------------------------------------------------- # 登记范围(生成范围)检查:工具/维护索引.py 的同一语义 # --------------------------------------------------------------------------- sys.path.insert(0, str(仓库根 / "工具")) from 用例身份 import 收集用例 # noqa: E402 from 维护索引 import 检查用例身份 # noqa: E402 @pytest.mark.case_id( "NC-case-parameter-binding", environment="离线子进程", given="同一个原生参数化函数的两行分别登记不同 case_id", when="仅选择第一条完整 ID", then=["只运行对应参数行,JUnit 记录该 case_id"], contract=".agent/rules/测试隔离.md", ) def test_独立用例身份可绑定原生参数行__e2000b(tmp_path: Path) -> None: 项目 = 临时项目(tmp_path) 项目.写( "test_参数.py", """ import pytest @pytest.mark.parametrize("输入", [ pytest.param(1, id="first", marks=pytest.mark.case_id("NC-first", given="第一输入", then=["等于1"])), pytest.param(2, id="second", marks=pytest.mark.case_id("NC-second", when="读取第二输入")), ]) def test_参数行为__abcdef(输入): assert 输入 in (1, 2) """, ) for 身份, 参数 in [("NC-first", "first"), ("NC-second[second]", "second")]: 退出, 结果 = 项目.运行("--case", 身份) assert 退出 == 0, 结果 行 = ET.parse(tmp_path / "报告.xml").findall(".//testcase") assert len(行) == 1 and 行[0].attrib["name"].endswith(f"[{参数}]") 身份属性 = 行[0].find(".//property[@name='case_id']") 参数属性 = 行[0].find(".//property[@name='parameter_id']") assert 身份属性 is not None and 身份属性.attrib["value"] == 身份.split("[")[0] assert 参数属性 is not None and 参数属性.attrib["value"] == 参数 导出 = {条["case_id"]: 条 for 条 in 项目.收集()} assert 导出["NC-first"]["then"] == ["等于1"] assert "then" not in 导出["NC-second"] @pytest.mark.case_id( "TC-69440b7b4439", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_generated_scope_rejects_unregistered_disk_asset__69440b(tmp_path: Path) -> None: """未标记的测试在实际收集时拒绝,不依赖旧清单。""" 项目 = 临时项目(tmp_path) 项目.写("test_登记.py", "def test_已登记__44000b(): assert True\n") 项目.写("test_未登记.py", "def test_未登记__55000c(): assert True\n", 登记身份=False) with pytest.raises(ValueError, match="未标记 case_id.*test_未登记__55000c"): 项目.收集() @pytest.mark.case_id( "TC-20f4fa31f97b", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_generated_scope_uses_manifest_scan_roots__20f4fa(tmp_path: Path) -> None: """自定义收集路径直接来自 pytest,不需登记扫描根。""" 项目 = 临时项目(tmp_path) 项目.写("custom-tests/test_自定义.py", "def test_自定义用例__66000d(): assert True\n") 条 = 项目.收集("custom-tests")[0] assert 条["file"] == "custom-tests/test_自定义.py" assert 条["symbol"] == "test_自定义用例__66000d" @pytest.mark.case_id( "TC-b4901fe62743", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_generated_scope_rejects_manifest_extra_asset__b4901f(tmp_path: Path) -> None: """旧导出不能令已删除的用例仍被选中。""" 项目 = 临时项目(tmp_path) 项目.写("test_存在.py", "def test_存在__44000b(): assert True\n") 项目.写("用例清单.json", '{"cases":[{"case_id":"TC-removed","file":"test_已删除.py"}]}') 码, 结果 = 项目.运行("--case", "TC-removed") assert 码 == 4 assert "未收集" in 结果["stderr"] @pytest.mark.case_id( "TC-5b8bb297e8c9", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_generated_scope_accepts_existing_behavior_eval_entry__5b8bb2(tmp_path: Path) -> None: """仅收集真实模型层的身份和环境,不执行模型测试体。""" 项目 = 临时项目(tmp_path) 项目.写( "tests/evals/test_评测.py", "import pytest\n@pytest.mark.真实模型\ndef test_评测用例__88000f():\n" " raise AssertionError('不能执行')\n", ) 条 = 项目.收集()[0] assert 条["case_id"] == "TC-x-88000f000000" assert "真实模型" in 条["markers"] assert 条["file"] == "tests/evals/test_评测.py" @pytest.mark.case_id( "TC-fe3fba0485ff", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_generated_scope_rejects_missing_behavior_eval_entry__fe3fba(tmp_path: Path) -> None: """实际删除的模型用例从当前收集退出,不能由历史导出复活。""" 项目 = 临时项目(tmp_path) 文件 = 项目.写( "test_评测.py", "import pytest\n@pytest.mark.真实模型\ndef test_评测__990010(): assert True\n", ) assert len(项目.收集()) == 1 文件.unlink() assert 项目.收集() == [] 码, 结果 = 项目.运行("--case", "TC-x-990010000000") assert 码 == 4 assert "未收集" in 结果["stderr"] @pytest.mark.case_id( "TC-14f941957440", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_generated_scope_ignores_non_test_helpers_under_test_roots__14f941(tmp_path: Path) -> None: """普通辅助函数不产生测试身份要求。""" 项目 = 临时项目(tmp_path) 项目.写( "tests/helpers/test_辅助.py", "def 辅助函数(): return 1\ndef test_真用例__a10011(): assert 辅助函数() == 1\n", ) assert [条["symbol"] for 条 in 项目.收集()] == ["test_真用例__a10011"] @pytest.mark.case_id( "TC-36f79ee6e164", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_generated_scope_blocks_empty_and_print_only_scripts_before_child__36f79e( tmp_path: Path, ) -> None: """print 型用例不能靠登记身份取得有断言的验证地位。""" 项目 = 临时项目(tmp_path) 项目.写("tests/单元/test_空断言.py", "def test_空用例__b20012(): print('只有输出')\n") 问题 = 检查用例身份(tmp_path) assert any("无可判定语句" in p for p in 问题), 问题 @pytest.mark.case_id( "TC-7c13823a1ef2", environment="离线,受限子进程和临时测试项目", when="收集并执行选定用例,模拟超时、缺依赖或零执行。", contract=".agent/rules/测试隔离.md", ) def test_generated_scope_accepts_supported_python_shapes__7c1382(tmp_path: Path) -> None: """函数式与类式身份及当前位置由真实 pytest 收集导出。""" 项目 = 临时项目(tmp_path) 项目.写( "test_形状.py", "class Test类式:\n def test_类方法__c30013(self): assert True\n" "def test_函数式__d30014(): assert True\n", ) 条目 = 项目.收集() assert {条["symbol"] for 条 in 条目} == {"test_类方法__c30013", "test_函数式__d30014"} assert any("::Test类式::" in 节点 for 条 in 条目 for 节点 in 条["node_ids"]) @pytest.mark.case_id( "NC-case-exact-id", environment="离线,临时 pytest 项目", given="临时项目内已登记的完整 ID、路径及测试符号", when="选择用例或核对执行身份", then=["未登记 ID 退出 4,不执行已登记用例"], contract=".agent/rules/测试隔离.md", ) def test_完整身份不接受冒用后缀__a02001(项目: 临时项目) -> None: """未登记 ID 与真实符号后缀相同,也不能选择执行。""" 项目.写("test_样本.py", "def test_目标__aa0001():\n assert True\n") 码, 结果 = 项目.运行("--case", "XX-forged-aa0001000000") assert 码 == 4 assert "未收集" in 结果["stderr"] @pytest.mark.case_id( "NC-case-exact-node", environment="离线,临时 pytest 项目", given="临时项目内已登记的完整 ID、路径及测试符号", when="选择用例或核对执行身份", then=["文件及符号必须与登记一致,不能冒用后缀"], contract=".agent/rules/测试隔离.md", ) def test_完整身份绑定文件与符号__a02002(tmp_path: Path) -> None: """稳定身份可随文件/函数移动;同一身份绑定两函数则明确拒绝。""" 项目 = 临时项目(tmp_path) 项目.写( "test_原例.py", "import pytest\n@pytest.mark.case_id('NC-stable')\ndef test_原例(): assert True\n", ) 原 = 项目.收集()[0] (tmp_path / "test_原例.py").unlink() 项目.写( "test_改名.py", "import pytest\n@pytest.mark.case_id('NC-stable')\ndef test_改名(): assert True\n", ) 新 = 项目.收集()[0] assert 新["case_id"] == 原["case_id"] assert 新["file"] == "test_改名.py" and 新["symbol"] == "test_改名" 项目.写( "test_冒用.py", "import pytest\n@pytest.mark.case_id('NC-stable')\ndef test_冒用(): assert True\n", ) with pytest.raises(ValueError, match="同一 case_id 绑定不同函数"): 项目.收集() @pytest.mark.case_id( "NC-offline-libpq-process", environment="隔离 PostgreSQL,内层离线进程", given="真实可达的隔离 PostgreSQL 与无外部标记的临时测试", when="分别从收集、测试体及子进程经 psycopg 连接", then=["外部父测试 SELECT 1 成功", "三个离线子进程路径均由 OS 拒绝连接并非零退出"], contract=".agent/rules/测试隔离.md", ) @pytest.mark.数据库 @pytest.mark.parametrize("阶段", ["收集", "测试体", "子进程"], ids=["collection", "call", "child"]) def test_离线进程拒绝真实数据库连接__a02005(项目: 临时项目, 隔离数据库URL: str, 阶段: str) -> None: """先确认隔离库可达,再由未标记的子测试验证 OS 阻断 libpq。""" import psycopg with psycopg.connect(隔离数据库URL) as 连: assert 连.execute("SELECT 1").fetchone() == (1,) 连接代码 = "import os, psycopg; psycopg.connect(os.environ['MUSE_TEST_DATABASE_URL'])" if 阶段 == "收集": 内容 = 连接代码 + "\ndef test_试连接__aa0001():\n assert True\n" elif 阶段 == "子进程": 内容 = ( "import subprocess, sys\n" "def test_试连接__aa0001():\n" f" subprocess.run([sys.executable, '-c', {连接代码!r}], check=True)\n" ) else: 内容 = "def test_试连接__aa0001():\n " + 连接代码 + "\n" 项目.写("test_连接.py", 内容) 码, 结果 = 项目.运行(环境={"MUSE_TEST_DATABASE_URL": 隔离数据库URL}) assert 码 in (1, 2) assert "Operation not permitted" in 结果["stdout"] + 结果["stderr"] @pytest.mark.case_id( "NC-case-implemented-target", environment="离线", given="已声明执行过的登记目标已消失", when="执行默认索引检查", then=["明确报告目标缺失"], contract=".agent/rules/测试隔离.md", ) @pytest.mark.parametrize("目录", ["架构", "迁移"]) def test_已执行目标丢失不被过渡期忽略__a02006(tmp_path: Path, 目录: str) -> None: """实际节点丢失就不能继续选择;恢复相同身份可继续精确选例。""" 项目 = 临时项目(tmp_path) 路径 = f"tests/{目录}/test_已执行.py" 文件 = 项目.写( 路径, "import pytest\n@pytest.mark.case_id('NC-existing-test')\n" "def test_已执行(): assert 1 == 1\n", ) assert 项目.收集()[0]["case_id"] == "NC-existing-test" 文件.unlink() 码, 结果 = 项目.运行("--case", "NC-existing-test") assert 码 == 4 and "未收集" in 结果["stderr"] 项目.写( 路径, "import pytest\n@pytest.mark.case_id('NC-existing-test')\n" "def test_已执行(): assert 1 == 1\n", ) 码, 结果 = 项目.运行("--case", "NC-existing-test") assert 码 == 0, 结果