Files
gaokao-volunteer-system/data/crowd_db/tests/test_provenance_query.py
2026-06-12 16:25:05 +08:00

429 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""T3.2 溯源字段查询 + 验证 测试
覆盖:
1. 27省全部 validate_all 通过errors=0usable=1仅湖南
2. validate_province(hunan) — ok + is_usable=True + summary 7 字段齐全
3. validate_province(guizhou) — ok + is_usable=False + 1 warning (low_confidence)
4. validate_provenance(None) — 报 load_failed error
5. validate_provenance({}) — 报 7 missing_field
6. validate_provenance 边界confidence 越界、source_type 非法、data_year 类型错、date 格式错
7. filter_provinces 多维过滤source_type / min_confidence / max_confidence / data_year / updated_since / only_usable
8. filter_provinces 组合min + max confidence 区间
9. get_provenance_report: 统计字段total/usable_count/failed_count/by_source_type
10. 错误省份输入:未知省份返回 load_failed
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
from data.crowd_db.loader import (
CrowdDBLoader,
ProvenanceValidation,
)
# ------------------------------------------------------------------ #
# 1. validate_all 27省
# ------------------------------------------------------------------ #
def test_validate_all_27_passes():
"""27 省全部通过 schema 校验(无 errors"""
loader = CrowdDBLoader(warn_low_confidence=False)
results = loader.validate_all()
assert len(results) == 27, f"expected 27 entries, got {len(results)}"
failed = [(p, v.errors) for p, v in results.items() if not v.ok]
assert not failed, f"应全部 ok, 失败省份: {failed}"
def test_validate_all_usable_count():
"""usable 省份应当仅含湖南 (confidence 0.85)"""
loader = CrowdDBLoader(warn_low_confidence=False)
results = loader.validate_all()
usable = [p for p, v in results.items() if v.is_usable]
assert usable == ["湖南"], f"expected ['湖南'], got {usable}"
def test_validate_all_warnings_low_confidence():
"""低 confidence 的 26 省均应有 low_confidence warning"""
loader = CrowdDBLoader(warn_low_confidence=False)
results = loader.validate_all()
low_warn = [
p for p, v in results.items() if any("low_confidence" in w for w in v.warnings)
]
assert len(low_warn) == 26, (
f"expected 26 low_confidence warnings, got {len(low_warn)}"
)
# ------------------------------------------------------------------ #
# 2. validate_provincehunan — 高 confidence
# ------------------------------------------------------------------ #
def test_validate_province_hunan_usable():
"""湖南ok + is_usable + 7 字段 summary 齐全 + 0 warning"""
loader = CrowdDBLoader(warn_low_confidence=False)
v = loader.validate_province("湖南")
assert v.ok is True
assert v.is_usable is True
assert v.errors == []
assert v.warnings == []
expected_keys = {
"province",
"source",
"source_url",
"source_type",
"data_year",
"last_updated",
"confidence",
}
assert set(v.summary.keys()) == expected_keys
assert v.summary["province"] == "湖南"
assert v.summary["confidence"] >= 0.8
def test_validate_province_guizhou_low_confidence():
"""贵州ok + is_usable=False + 1 low_confidence warning"""
loader = CrowdDBLoader(warn_low_confidence=False)
v = loader.validate_province("贵州")
assert v.ok is True
assert v.is_usable is False
assert any("low_confidence" in w for w in v.warnings)
assert v.summary["confidence"] == 0.45
# ------------------------------------------------------------------ #
# 3. validate_provenance 边界class method 直接调用
# ------------------------------------------------------------------ #
def test_validate_provenance_none_load_failed():
"""None 数据 → 1 个 load_failed error"""
v = CrowdDBLoader.validate_provenance(None, province="X")
assert v.ok is False
assert v.is_usable is False
assert any("load_failed" in e for e in v.errors)
assert v.summary == {}
def test_validate_provenance_empty_dict():
"""空 dict → 7 个 missing_field 错误(无重复)"""
v = CrowdDBLoader.validate_provenance({}, province="X")
assert v.ok is False
expected = {
"province",
"last_updated",
"data_year",
"source",
"source_type",
"confidence",
"score_ranges",
}
reported = {e.split(": ", 1)[1] for e in v.errors if e.startswith("missing_field:")}
assert reported == expected, f"missing fields mismatch: {reported}"
def test_validate_provenance_confidence_out_of_range():
"""confidence=1.5 越界 → range_error"""
data = {
"province": "X",
"last_updated": "2025-01-01",
"data_year": 2025,
"source": "s",
"source_type": "manual_summary",
"confidence": 1.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is False
assert any("range_error" in e for e in v.errors)
def test_validate_provenance_invalid_source_type():
"""source_type='garbage' → enum_error"""
data = {
"province": "X",
"last_updated": "2025-01-01",
"data_year": 2025,
"source": "s",
"source_type": "garbage",
"confidence": 0.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is False
assert any("enum_error" in e for e in v.errors)
def test_validate_provenance_data_year_wrong_type():
"""data_year='2025' (str) → type_error"""
data = {
"province": "X",
"last_updated": "2025-01-01",
"data_year": "2025",
"source": "s",
"source_type": "manual_summary",
"confidence": 0.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is False
assert any("type_error" in e and "data_year" in e for e in v.errors)
def test_validate_provenance_bad_date_format():
"""last_updated='2025/01/01' → format_error"""
data = {
"province": "X",
"last_updated": "2025/01/01",
"data_year": 2025,
"source": "s",
"source_type": "manual_summary",
"confidence": 0.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is False
assert any("format_error" in e for e in v.errors)
def test_validate_provenance_empty_source_warning():
"""source='' → 0 错误 + 1 empty_source warning不影响 ok"""
data = {
"province": "X",
"last_updated": "2025-01-01",
"data_year": 2025,
"source": "",
"source_type": "manual_summary",
"confidence": 0.5,
"score_ranges": [],
}
v = CrowdDBLoader.validate_provenance(data, province="X")
assert v.ok is True
assert any("empty_source" in w for w in v.warnings)
# ------------------------------------------------------------------ #
# 4. filter_provinces 多维过滤
# ------------------------------------------------------------------ #
def test_filter_provinces_only_usable():
"""only_usable=True → 仅 湖南"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(only_usable=True)
assert result == ["湖南"]
def test_filter_provinces_source_type_manual():
"""source_type='manual_summary' → 27 省全匹配"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(source_type="manual_summary")
assert len(result) == 27
def test_filter_provinces_source_type_official():
"""source_type='official_release' → 当前无匹配 → 空列表"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(source_type="official_release")
assert result == []
def test_filter_provinces_data_year_match():
"""data_year=2025 → 27 省全匹配"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(data_year=2025)
assert len(result) == 27
def test_filter_provinces_data_year_no_match():
"""data_year=2024 → 当前无匹配 → 空列表"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(data_year=2024)
assert result == []
def test_filter_provinces_min_confidence():
"""min_confidence=0.5 → 仅 湖南"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(min_confidence=0.5)
assert result == ["湖南"]
def test_filter_provinces_max_confidence():
"""max_confidence=0.5 → 26 省0.45 全在范围内)"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(max_confidence=0.5)
assert len(result) == 26
assert "湖南" not in result
def test_filter_provinces_confidence_range():
"""min=0.4 max=0.5 → 26 省(仅 0.45 落在区间)"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(min_confidence=0.4, max_confidence=0.5)
assert len(result) == 26
assert "湖南" not in result
def test_filter_provinces_updated_since():
"""updated_since='2026-06-12' → 27 省(全在 2026-06-12"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(updated_since="2026-06-12")
assert len(result) == 27
def test_filter_provinces_updated_before_no_match():
"""updated_before='2026-06-11' → 0 省(全在 2026-06-12"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(updated_before="2026-06-11")
assert result == []
def test_filter_provinces_combined_usable_and_year():
"""组合only_usable=True + data_year=2025 → 仅 湖南"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(only_usable=True, data_year=2025)
assert result == ["湖南"]
def test_filter_provinces_preserves_map_order():
"""返回顺序按 PROVINCE_FILE_MAP"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(max_confidence=0.5)
expected_first = "北京" # map 中第一个低 confidence 的省
assert result[0] == expected_first
# 数量与 PROVINCE_FILE_MAP 一致(不含湖南)
assert len(result) == 26
# ------------------------------------------------------------------ #
# 5. get_provenance_report
# ------------------------------------------------------------------ #
def test_get_provenance_report_total():
"""report: total=27, usable_count=1, failed_count=0"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report()
assert r["total"] == 27
assert r["usable_count"] == 1
assert r["failed_count"] == 0
def test_get_provenance_report_by_source_type():
"""by_source_type 分布27 全在 manual_summary"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report()
assert r["by_source_type"] == {"manual_summary": 27}
def test_get_provenance_report_items_have_summary():
"""每个 item 含 province/ok/is_usable/errors/warnings/summary/file_name"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report()
required = {
"province",
"ok",
"is_usable",
"errors",
"warnings",
"summary",
"file_name",
}
for item in r["items"]:
assert required <= set(item.keys()), (
f"item 缺字段: {required - set(item.keys())}"
)
def test_get_provenance_report_filtered_only_usable():
"""only_usable=True 时 items 仅 1 个湖南"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report(only_usable=True)
assert r["total"] == 1
assert r["usable_count"] == 1
assert r["items"][0]["province"] == "湖南"
def test_get_provenance_report_filtered_source_type():
"""source_type 过滤时 by_source_type 只含该类型"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report(source_type="official_release")
assert r["total"] == 0
assert r["by_source_type"] == {}
# ------------------------------------------------------------------ #
# 6. 错误省份输入
# ------------------------------------------------------------------ #
def test_validate_province_unknown_load_failed():
"""未注册的省份 → load_failed error"""
loader = CrowdDBLoader(warn_low_confidence=False)
v = loader.validate_province("不存在的省")
assert v.ok is False
assert any("load_failed" in e for e in v.errors)
assert v.is_usable is False
def test_provenance_validation_to_dict_roundtrip():
"""ProvenanceValidation.to_dict 输出 6 字段且值一致"""
v = ProvenanceValidation(province="X", ok=True, is_usable=True)
v.errors = []
v.warnings = ["w1"]
v.summary = {"a": 1}
d = v.to_dict()
assert d["province"] == "X"
assert d["ok"] is True
assert d["is_usable"] is True
assert d["errors"] == []
assert d["warnings"] == ["w1"]
assert d["summary"] == {"a": 1}
# ------------------------------------------------------------------ #
# 7. 与 load_metadata 的契约对齐(确保未破坏既有 API
# ------------------------------------------------------------------ #
def test_load_metadata_unchanged():
"""既有 load_metadata 仍返回 8 字段"""
loader = CrowdDBLoader(warn_low_confidence=False)
meta = loader.load_metadata("湖南")
assert meta is not None
for k in (
"province",
"last_updated",
"data_year",
"source",
"source_url",
"source_type",
"confidence",
"record_count",
):
assert k in meta
assert meta["record_count"] > 0
def test_validate_provenance_does_not_mutate_caller_dict():
"""validate_provenance 不会修改入参 data"""
data = {
"province": "湖南",
"last_updated": "2026-06-12",
"data_year": 2025,
"source": "s",
"source_type": "manual_summary",
"confidence": 0.85,
"score_ranges": [],
}
import copy
before = copy.deepcopy(data)
CrowdDBLoader.validate_provenance(data, province="湖南")
assert data == before