初始提交:飞球 ETL 系统全量代码
This commit is contained in:
165
tests/unit/test_audit_inventory_render.py
Normal file
165
tests/unit/test_audit_inventory_render.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
属性测试 — 清单渲染完整性与分类分组
|
||||
|
||||
Feature: repo-audit
|
||||
- Property 2: 清单渲染完整性
|
||||
- Property 8: 清单按分类分组
|
||||
|
||||
Validates: Requirements 1.4, 1.10
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import string
|
||||
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from scripts.audit import Category, Disposition, InventoryItem
|
||||
from scripts.audit.inventory_analyzer import render_inventory_report
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 生成器策略
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_PATH_CHARS = string.ascii_letters + string.digits + "_-."
|
||||
|
||||
_path_segment = st.text(
|
||||
alphabet=_PATH_CHARS,
|
||||
min_size=1,
|
||||
max_size=15,
|
||||
)
|
||||
|
||||
# 随机相对路径(1~3 层)
|
||||
_rel_path = st.lists(
|
||||
_path_segment,
|
||||
min_size=1,
|
||||
max_size=3,
|
||||
).map(lambda parts: "/".join(parts))
|
||||
|
||||
# 随机非空描述(不含管道符和换行符,避免破坏 Markdown 表格解析)
|
||||
_description = st.text(
|
||||
alphabet=st.characters(
|
||||
whitelist_categories=("L", "N", "P", "S", "Z"),
|
||||
blacklist_characters="|\n\r",
|
||||
),
|
||||
min_size=1,
|
||||
max_size=40,
|
||||
)
|
||||
|
||||
|
||||
def _inventory_item_strategy() -> st.SearchStrategy[InventoryItem]:
|
||||
"""生成随机 InventoryItem 的 hypothesis 策略。"""
|
||||
return st.builds(
|
||||
InventoryItem,
|
||||
rel_path=_rel_path,
|
||||
category=st.sampled_from(list(Category)),
|
||||
disposition=st.sampled_from(list(Disposition)),
|
||||
description=_description,
|
||||
)
|
||||
|
||||
|
||||
# 生成 0~20 个 InventoryItem 的列表
|
||||
_inventory_list = st.lists(
|
||||
_inventory_item_strategy(),
|
||||
min_size=0,
|
||||
max_size=20,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 2: 清单渲染完整性
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@given(items=_inventory_list)
|
||||
@settings(max_examples=100)
|
||||
def test_render_inventory_completeness(items: list[InventoryItem]) -> None:
|
||||
"""Property 2: 清单渲染完整性
|
||||
|
||||
Feature: repo-audit, Property 2: 清单渲染完整性
|
||||
Validates: Requirements 1.4
|
||||
|
||||
对于任意 InventoryItem 列表,render_inventory_report 生成的 Markdown 中,
|
||||
每个条目的 rel_path、category.value、disposition.value 和 description
|
||||
四个字段都应出现在输出文本中。
|
||||
"""
|
||||
report = render_inventory_report(items, "/tmp/test-repo")
|
||||
|
||||
for item in items:
|
||||
# rel_path 出现在表格行中
|
||||
assert item.rel_path in report, (
|
||||
f"rel_path '{item.rel_path}' 未出现在报告中"
|
||||
)
|
||||
# category.value 出现在分组标题中
|
||||
assert item.category.value in report, (
|
||||
f"category '{item.category.value}' 未出现在报告中"
|
||||
)
|
||||
# disposition.value 出现在表格行中
|
||||
assert item.disposition.value in report, (
|
||||
f"disposition '{item.disposition.value}' 未出现在报告中"
|
||||
)
|
||||
# description 出现在表格行中
|
||||
assert item.description in report, (
|
||||
f"description '{item.description}' 未出现在报告中"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 8: 清单按分类分组
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@given(items=_inventory_list)
|
||||
@settings(max_examples=100)
|
||||
def test_render_inventory_grouped_by_category(items: list[InventoryItem]) -> None:
|
||||
"""Property 8: 清单按分类分组
|
||||
|
||||
Feature: repo-audit, Property 8: 清单按分类分组
|
||||
Validates: Requirements 1.10
|
||||
|
||||
对于任意 InventoryItem 列表,render_inventory_report 生成的 Markdown 中,
|
||||
同一 Category 的条目应连续出现(不应被其他 Category 的条目打断)。
|
||||
"""
|
||||
report = render_inventory_report(items, "/tmp/test-repo")
|
||||
|
||||
if not items:
|
||||
return # 空列表无需验证
|
||||
|
||||
# 从报告中按行提取条目对应的 category 顺序
|
||||
# 表格行格式: | `{rel_path}` | {disposition} | {description} |
|
||||
# 分组标题格式: ## {category.value}
|
||||
lines = report.split("\n")
|
||||
|
||||
# 收集每个分组标题下的条目,按出现顺序记录 category
|
||||
categories_in_order: list[Category] = []
|
||||
current_category: Category | None = None
|
||||
|
||||
# 建立 category.value -> Category 的映射
|
||||
value_to_cat = {c.value: c for c in Category}
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
# 检测分组标题 "## {category.value}"
|
||||
if stripped.startswith("## ") and stripped[3:] in value_to_cat:
|
||||
current_category = value_to_cat[stripped[3:]]
|
||||
continue
|
||||
# 检测表格数据行(跳过表头和分隔行)
|
||||
if (
|
||||
current_category is not None
|
||||
and stripped.startswith("| `")
|
||||
and not stripped.startswith("| 相对路径")
|
||||
and not stripped.startswith("|---")
|
||||
):
|
||||
categories_in_order.append(current_category)
|
||||
|
||||
# 验证同一 Category 的条目连续出现
|
||||
seen: set[Category] = set()
|
||||
prev: Category | None = None
|
||||
for cat in categories_in_order:
|
||||
if cat != prev:
|
||||
assert cat not in seen, (
|
||||
f"Category '{cat.value}' 的条目不连续——"
|
||||
f"在其他分类条目之后再次出现"
|
||||
)
|
||||
seen.add(cat)
|
||||
prev = cat
|
||||
Reference in New Issue
Block a user