# -*- 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