feat(provenance): URI 统一 + resolver + 强验证(T12 架构审查整改)
- T12 (OV3, P1): 新建 src/genesis/parsers/resolver.py
- parse_source_uri: 解析 file.xlsx#Sheet!CellRef → SourceRef(格式非法 raise URIError)
- provenance_to_uri: Provenance 还原 URI(与 build_source_uri 互逆)
- resolve_source_uri: StructuredSource 内定位真实 CellValue
- validate_source_uris: 批量强验证 → ValidationResult(resolved/unresolved)
格式错误或源中不存在一律 unresolved(防 QA#8 编造 URI 作弊)
- URI 唯一生成入口 build_source_uri(provenance.py),无散落不一致
- 新增 test_resolver.py(13 用例);同步 design.md §9.2 + §6.8 第五步
- TDD: RED(模块缺失)→ GREEN(聚焦 10 passed)→ 全量 231 passed / 100.00%(1191 stmts/298 br)
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""URI resolver + 强验证测试(T12,OV3)。
|
||||
|
||||
OV3 裁定:可追溯性标准未操作性定义(URI 格式不一致、无 resolver、QA#8 可作弊)
|
||||
→ 统一 URI + resolver + 强验证。
|
||||
本文件测试:URI 解析、provenance 往返、结构化源内定位、批量强验证(防 QA 作弊)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from genesis.data_models import (
|
||||
CellComment,
|
||||
CellValue,
|
||||
ExcelTable,
|
||||
ParsedTemplate,
|
||||
Provenance,
|
||||
RuleDocument,
|
||||
SheetType,
|
||||
StructuredSource,
|
||||
)
|
||||
from genesis.parsers.provenance import build_source_uri
|
||||
from genesis.parsers.resolver import (
|
||||
SourceRef,
|
||||
URIError,
|
||||
ValidationResult,
|
||||
parse_source_uri,
|
||||
provenance_to_uri,
|
||||
resolve_source_uri,
|
||||
validate_source_uris,
|
||||
)
|
||||
|
||||
|
||||
# ---------- 构造小源 ----------
|
||||
|
||||
def _cell(file_name: str, sheet: str, row: int, col: str, value: str) -> CellValue:
|
||||
return CellValue(
|
||||
value=value,
|
||||
provenance=Provenance(
|
||||
file_name=file_name, sheet_name=sheet, row=row, column=col, column_header="x"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _source_with(cells: list[CellValue]) -> StructuredSource:
|
||||
table = ExcelTable(
|
||||
name="機能一覧",
|
||||
detected_type=SheetType.FUNCTION,
|
||||
extraction_method="structured",
|
||||
headers=["v"],
|
||||
rows=[{"v": c} for c in cells],
|
||||
)
|
||||
return StructuredSource(
|
||||
tables=[table],
|
||||
template=ParsedTemplate(file_name="t.docx", sections=[], placeholders={}, styles={}),
|
||||
rule_docs=[],
|
||||
image_analyses=[],
|
||||
existing_system=None,
|
||||
comments=[],
|
||||
)
|
||||
|
||||
|
||||
# ---------- URI 解析 ----------
|
||||
|
||||
def test_parse_source_uri():
|
||||
ref = parse_source_uri("要求.xlsx#機能一覧!C3")
|
||||
assert ref.file_name == "要求.xlsx"
|
||||
assert ref.sheet_name == "機能一覧"
|
||||
assert ref.cell_ref == "C3"
|
||||
|
||||
|
||||
def test_parse_source_uri_rejects_missing_hash():
|
||||
with pytest.raises(URIError):
|
||||
parse_source_uri("要求.xlsx機能一覧!C3")
|
||||
|
||||
|
||||
def test_parse_source_uri_rejects_missing_bang():
|
||||
with pytest.raises(URIError):
|
||||
parse_source_uri("要求.xlsx#機能一覧")
|
||||
|
||||
|
||||
def test_parse_source_uri_rejects_empty_file():
|
||||
with pytest.raises(URIError):
|
||||
parse_source_uri("#機能一覧!C3")
|
||||
|
||||
|
||||
def test_parse_source_uri_rejects_empty_sheet_or_cell():
|
||||
with pytest.raises(URIError):
|
||||
parse_source_uri("f.xlsx#!C3")
|
||||
with pytest.raises(URIError):
|
||||
parse_source_uri("f.xlsx#Sheet!")
|
||||
|
||||
|
||||
def test_resolve_skips_cell_without_provenance():
|
||||
"""CellValue.provenance 为 None 时不匹配任何 URI(_matches 防御分支)。"""
|
||||
orphan = CellValue(value="x", provenance=None)
|
||||
source = _source_with([orphan])
|
||||
assert resolve_source_uri("f.xlsx#機能一覧!C3", source) is None
|
||||
|
||||
|
||||
# ---------- provenance 往返 ----------
|
||||
|
||||
def test_provenance_to_uri_roundtrip():
|
||||
prov = Provenance(file_name="f.xlsx", sheet_name="S", row=3, column="C", column_header="h")
|
||||
uri = provenance_to_uri(prov)
|
||||
assert uri == "f.xlsx#S!C3"
|
||||
ref = parse_source_uri(uri)
|
||||
assert ref == SourceRef(file_name="f.xlsx", sheet_name="S", cell_ref="C3")
|
||||
|
||||
|
||||
def test_build_and_parse_roundtrip():
|
||||
uri = build_source_uri("要求.xlsx", "機能一覧", "A5")
|
||||
ref = parse_source_uri(uri)
|
||||
assert ref == SourceRef(file_name="要求.xlsx", sheet_name="機能一覧", cell_ref="A5")
|
||||
|
||||
|
||||
# ---------- 结构化源内定位 ----------
|
||||
|
||||
def test_resolve_finds_existing_cell():
|
||||
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
|
||||
source = _source_with([cell])
|
||||
found = resolve_source_uri("f.xlsx#機能一覧!C3", source)
|
||||
assert found is not None
|
||||
assert found.value == "登録"
|
||||
|
||||
|
||||
def test_resolve_returns_none_for_missing():
|
||||
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
|
||||
source = _source_with([cell])
|
||||
assert resolve_source_uri("f.xlsx#機能一覧!D9", source) is None
|
||||
|
||||
|
||||
def test_resolve_distinguishes_sheet():
|
||||
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
|
||||
source = _source_with([cell])
|
||||
# 不同 Sheet 的同 cell_ref 不应命中
|
||||
assert resolve_source_uri("f.xlsx#画面一覧!C3", source) is None
|
||||
|
||||
|
||||
# ---------- 批量强验证(防 QA 作弊) ----------
|
||||
|
||||
def test_validate_source_uris_split():
|
||||
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
|
||||
source = _source_with([cell])
|
||||
uris = ["f.xlsx#機能一覧!C3", "f.xlsx#機能一覧!D9", "invalid-uri"]
|
||||
result = validate_source_uris(uris, source)
|
||||
assert isinstance(result, ValidationResult)
|
||||
assert result.resolved == ["f.xlsx#機能一覧!C3"]
|
||||
assert result.unresolved == ["f.xlsx#機能一覧!D9", "invalid-uri"]
|
||||
|
||||
|
||||
def test_validate_rejects_malformed_separately():
|
||||
"""格式错误的 URI 应归入 unresolved(QA#8 不可靠 URI 无效,防作弊)。"""
|
||||
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
|
||||
source = _source_with([cell])
|
||||
result = validate_source_uris(["garbage-without-hash"], source)
|
||||
assert result.resolved == []
|
||||
assert result.unresolved == ["garbage-without-hash"]
|
||||
Reference in New Issue
Block a user