Files
gaokao-volunteer-system/data/crowd_db/tests/test_provenance_query.py
Hermes Agent 97ea07c231
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
feat(crowd_db): Phase 2 - 2026分数线接入(6省真实数据)
P2级改进:基于真实核实的2026官方分数线更新crowd_db

真实数据来源:
- 湖南:搜狐教育(本科历史类446/物理类400)
- 江苏:微博/教育在线(本科历史类484/物理类456,特控历史类532/物理类513)
- 广东:搜狐/新京报(本科历史类440/物理类425,特控历史类546/物理类539)
- 山东:高考100(一段441,特控521)
- 河北:教育在线(本科历史类485/物理类443,特控历史类542/物理类510)
- 河南:微博(本科历史类459/物理类419,特殊类型历史类534/物理类513)

实现内容:
1. 6省data_year: 2025 -> 2026
2. 更新source_url指向官方公布链接
3. 增加quality_note标注2026官方分数线已接入
4. 调整check_crowd_db_consistency.py:允许多年份共存
5. 修复测试以适应过渡期

验证: pytest 155 passed, 3 skipped; consistency check通过
2026-06-25 12:43:14 +08:00

562 lines
18 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. 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 → 过渡期至少 25 省"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(data_year=2025)
assert len(result) >= 25 # 过渡期:部分省份已切到 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 → 过渡期至少 25 省。"""
loader = CrowdDBLoader(warn_low_confidence=False)
result = loader.filter_provinces(only_usable=True, data_year=2025)
# 过渡期:部分省份已切到 2026只检查数量下限
assert len(result) >= 25
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