178 lines
6.0 KiB
Python
178 lines
6.0 KiB
Python
# -*- 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()
|