- 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)
159 lines
4.9 KiB
Python
159 lines
4.9 KiB
Python
"""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"]
|