# -*- coding: utf-8 -*- """ run_audit 主入口的单元测试。 验证: - docs/audit/ 目录自动创建 - 三份报告文件正确生成 - 报告头部包含时间戳和仓库路径 - 目录创建失败时抛出 RuntimeError """ from __future__ import annotations import os import re from pathlib import Path import pytest class TestEnsureReportDir: """测试 _ensure_report_dir 目录创建逻辑。""" def test_creates_dir_when_missing(self, tmp_path: Path): from scripts.audit.run_audit import _ensure_report_dir result = _ensure_report_dir(tmp_path) expected = tmp_path / "docs" / "audit" / "repo" assert result == expected assert expected.is_dir() def test_returns_existing_dir(self, tmp_path: Path): from scripts.audit.run_audit import _ensure_report_dir audit_dir = tmp_path / "docs" / "audit" / "repo" audit_dir.mkdir(parents=True) result = _ensure_report_dir(tmp_path) assert result == audit_dir def test_raises_on_creation_failure(self, tmp_path: Path): from scripts.audit.run_audit import _ensure_report_dir # 在 docs/audit 位置放一个文件,使 mkdir 失败 docs = tmp_path / "docs" docs.mkdir() (docs / "audit").write_text("block", encoding="utf-8") with pytest.raises(RuntimeError, match="无法创建报告输出目录"): _ensure_report_dir(tmp_path) class TestInjectHeader: """测试 _inject_header 兜底注入逻辑。""" def test_skips_when_header_present(self): from scripts.audit.run_audit import _inject_header report = "# 标题\n\n- 生成时间: 2025-01-01T00:00:00Z\n- 仓库路径: `/repo`\n" result = _inject_header(report, "2025-06-01T00:00:00Z", "/other") # 不应修改已有头部 assert result == report def test_injects_when_header_missing(self): from scripts.audit.run_audit import _inject_header report = "# 无头部报告\n\n内容..." result = _inject_header(report, "2025-06-01T00:00:00Z", "/repo") assert "生成时间: 2025-06-01T00:00:00Z" in result assert "仓库路径: `/repo`" in result class TestRunAudit: """测试 run_audit 完整流程(使用最小仓库结构)。""" def _make_minimal_repo(self, tmp_path: Path) -> Path: """构造一个最小仓库结构,足以让 run_audit 跑通。""" repo = tmp_path / "repo" repo.mkdir() # 核心代码目录 cli_dir = repo / "cli" cli_dir.mkdir() (cli_dir / "__init__.py").write_text("", encoding="utf-8") (cli_dir / "main.py").write_text( "# -*- coding: utf-8 -*-\ndef main(): pass\n", encoding="utf-8", ) # config 目录 config_dir = repo / "config" config_dir.mkdir() (config_dir / "__init__.py").write_text("", encoding="utf-8") (config_dir / "defaults.py").write_text("DEFAULTS = {}\n", encoding="utf-8") # docs 目录 docs_dir = repo / "docs" docs_dir.mkdir() (docs_dir / "README.md").write_text("# 文档\n", encoding="utf-8") # 根目录文件 (repo / "README.md").write_text("# 项目\n", encoding="utf-8") return repo def test_creates_three_reports(self, tmp_path: Path): from scripts.audit.run_audit import run_audit repo = self._make_minimal_repo(tmp_path) run_audit(repo) audit_dir = repo / "docs" / "audit" / "repo" assert (audit_dir / "file_inventory.md").is_file() assert (audit_dir / "flow_tree.md").is_file() assert (audit_dir / "doc_alignment.md").is_file() def test_reports_contain_timestamp(self, tmp_path: Path): from scripts.audit.run_audit import run_audit repo = self._make_minimal_repo(tmp_path) run_audit(repo) audit_dir = repo / "docs" / "audit" / "repo" # ISO 时间戳格式 ts_pattern = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z") for name in ("file_inventory.md", "flow_tree.md", "doc_alignment.md"): content = (audit_dir / name).read_text(encoding="utf-8") assert ts_pattern.search(content), f"{name} 缺少时间戳" def test_reports_contain_repo_path(self, tmp_path: Path): from scripts.audit.run_audit import run_audit repo = self._make_minimal_repo(tmp_path) run_audit(repo) audit_dir = repo / "docs" / "audit" / "repo" repo_str = str(repo.resolve()) for name in ("file_inventory.md", "flow_tree.md", "doc_alignment.md"): content = (audit_dir / name).read_text(encoding="utf-8") assert repo_str in content, f"{name} 缺少仓库路径" def test_writes_only_to_docs_audit(self, tmp_path: Path): """验证所有写操作仅限 docs/audit/ 目录(Property 15)。""" from scripts.audit.run_audit import run_audit repo = self._make_minimal_repo(tmp_path) # 记录运行前的文件快照(排除 docs/audit/) before = set() for p in repo.rglob("*"): rel = p.relative_to(repo).as_posix() if not rel.startswith("docs/audit"): before.add((rel, p.stat().st_mtime if p.is_file() else None)) run_audit(repo) # 运行后检查:docs/audit/ 外的文件不应被修改 for p in repo.rglob("*"): rel = p.relative_to(repo).as_posix() if rel.startswith("docs/audit"): continue if p.is_file(): # 文件应在之前的快照中 found = any(r == rel for r, _ in before) assert found, f"意外创建了 docs/audit/ 外的文件: {rel}" def test_auto_creates_docs_audit_dir(self, tmp_path: Path): from scripts.audit.run_audit import run_audit repo = self._make_minimal_repo(tmp_path) # 确保 docs/audit/ 不存在 audit_dir = repo / "docs" / "audit" / "repo" assert not audit_dir.exists() run_audit(repo) assert audit_dir.is_dir()