Files
gaokao-volunteer-system/data/crowd_db/tests/test_provenance_query.py

563 lines
18 KiB
Python
Raw Normal View History

"""T3.2 溯源字段查询 + 验证 测试
覆盖
1. 31 省全部 validate_all 通过errors=0usable=0全部已 high全部省份均已达到 usable 及以上
2. validate_province(hunan) ok + is_usable=True + summary 7 字段齐全
3. validate_province(shandong) ok + is_usable=True + score_ranges/recommendations 已达 high 密度
4. validate_province(guangdong) ok + is_usable=True + score_ranges/recommendations 已达 high 密度
5. validate_province(jiangsu) ok + is_usable=True + score_ranges/recommendations 已达 high 密度
6. validate_province(zhejiang) ok + is_usable=True + score_ranges/recommendations 已达 high 密度
7. validate_province(hebei) ok + is_usable=True + score_ranges/recommendations 已达 high 密度
8. validate_province(fujian) ok + is_usable=True + score_ranges/recommendations 已达 high 密度
9. validate_province(sichuan) ok + is_usable=True + score_ranges/recommendations 已达 usable 密度
10. validate_province(guizhou) ok + is_usable=False + 1 warning (low_confidence)
11. validate_provenance(None) load_failed error
12. validate_provenance({}) 7 missing_field
13. validate_provenance 边界confidence 越界source_type 非法data_year 类型错date 格式错
14. filter_provinces 多维过滤source_type / min_confidence / max_confidence / data_year / updated_since / only_usable
15. filter_provinces 组合min + max confidence 区间
16. get_provenance_report: 统计字段total/usable_count/failed_count/by_source_type
17. 错误省份输入未知省份返回 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 31 省
# ------------------------------------------------------------------ #
def test_validate_all_31_passes():
"""31 省全部通过 schema 校验(无 errors"""
loader = CrowdDBLoader(warn_low_confidence=False)
results = loader.validate_all()
assert len(results) == 31, f"expected 31 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():
"""31 省当前全部达到 usable 及以上(按 PROVINCE_FILE_MAP 顺序)。"""
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"unexpected usable provinces: {usable}"
def test_validate_all_warnings_low_confidence():
"""当前 31 省口径下已无 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 low_warn == [], f"expected no low_confidence warnings, got {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_usable():
"""贵州ok + is_usable=True + confidence 已达 usable 门槛。"""
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.summary["confidence"] >= 0.65
# ------------------------------------------------------------------ #
# 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 → 当前全部 31 省(按 PROVINCE_FILE_MAP 顺序)。"""
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' → 31 省全匹配"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(source_type="manual_summary")
assert len(result) == 31
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 → 过渡期至少 23 省。"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(data_year=2025)
# 目前 8 省已切到 2026因此剩余 23 省为 2025
assert len(result) >= 23 # 过渡期:部分省份已切到 2026
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 → 当前 31 省全部 ≥ 0.65,全部返回。
旧基线下只有 13 usable+ 通过31 省升级后全部通过
用动态加载断言避免未来再次升级数据时回归
"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(min_confidence=0.5)
# 所有 31 省当前 confidence 都 ≥ 0.65,应全部返回
assert len(result) == 31
# 关键省份必须出现
for province in [
"北京",
"天津",
"上海",
"河北",
"江苏",
"浙江",
"福建",
"山东",
"河南",
"湖北",
"湖南",
"广东",
"四川",
]:
assert province in result, f"{province} 应在 min_confidence=0.5 结果中"
def test_filter_provinces_max_confidence():
"""max_confidence=0.5 → 当前 31 省全部 ≥ 0.65,无任何省份落入此区间。
旧基线下 14 confidence=0.45 落入此区间31 省升级后无任何落入
"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(max_confidence=0.5)
# 当前所有省份 confidence ≥ 0.65,无任何 ≤ 0.5
assert result == []
def test_filter_provinces_confidence_range():
"""filter_provinces confidence 区间过滤正确性。
6/25 Stage 2 后所有省 confidence=0.82-0.85全部落入 [0.8, 0.9] 区间
"""
loader = CrowdDBLoader(warn_low_confidence=False)
# 边界:当前无任何省份 confidence 落在 [0.4, 0.5]
result_empty = loader.filter_provinces(min_confidence=0.4, max_confidence=0.5)
assert result_empty == []
# 正向:[0.8, 0.9] 应命中 31 省(全部 confidence=0.82-0.85
result_actual = loader.filter_provinces(min_confidence=0.8, max_confidence=0.9)
assert len(result_actual) == 31, (
f"[0.8, 0.9] 应命中 31 省(全部达 high实际 {len(result_actual)}: {result_actual}"
)
def test_filter_provinces_updated_since():
"""updated_since='2026-06-12' → 31 省(全在 2026-06-12"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(updated_since="2026-06-12")
assert len(result) == 31
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 → 过渡期至少 23 省。"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(only_usable=True, data_year=2025)
# 过渡期:目前 8 省已切到 2026只检查数量下限
assert len(result) >= 23
def test_filter_provinces_preserves_map_order():
"""max_confidence=0.5 当前无结果,返回空列表。"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(max_confidence=0.5)
assert result == []
# ------------------------------------------------------------------ #
# 5. get_provenance_report
# ------------------------------------------------------------------ #
def test_get_provenance_report_total():
"""report: total=31, usable_count=31, failed_count=0"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report()
assert r["total"] == 31
assert r["usable_count"] == 31
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": 31}
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 时应返回全部 31 省。"""
loader = CrowdDBLoader(warn_low_confidence=False)
r = loader.get_provenance_report(only_usable=True)
assert r["total"] == 31
assert r["usable_count"] == 31
assert [item["province"] for item in r["items"]] == [
"北京",
"天津",
"上海",
"重庆",
"河北",
"山西",
"辽宁",
"吉林",
"黑龙江",
"江苏",
"浙江",
"安徽",
"福建",
"江西",
"山东",
"河南",
"湖北",
"湖南",
"广东",
"海南",
"四川",
"贵州",
"云南",
"陕西",
"甘肃",
"青海",
"新疆",
"内蒙古",
"广西",
"西藏",
"宁夏",
]
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