Files
sanguo_vnpy_v2/tests/data_platform/test_circuit_breaker.py
T
claude_dev e3b688354f fix(data): data_platform硬化(增量merge/verify+raw_redownload/run_daily_update)+测试
merge_increment/verify_increment 增量staging→验证→合并工具; raw_redownload/run_daily_update/import_vnpy_daily 强化; 补 data_platform 与 index_downloader 测试.
2026-07-15 07:12:46 +08:00

156 lines
5.5 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.
"""断路器单元测试(task: Phase 2 可靠性增强)。
测试 check_circuit_breaker 纯函数——不启动整个下载流程,
直接构造 recent_results 列表验证触发逻辑。
覆盖场景:
1. 35% 失败率 → 触发
2. 25% 失败率 → 不触发
3. 北交所 920xxx 的 fail 不计入 → 不触发
4. 不足窗口 → 不触发
5. 恰好 30% → 不触发(阈值是 > 0.30,不含等于)
"""
import os
import sys
import pytest
_SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "scripts", "data_platform")
_SCRIPT_DIR = os.path.abspath(_SCRIPT_DIR)
if _SCRIPT_DIR not in sys.path:
sys.path.insert(0, _SCRIPT_DIR)
from raw_redownload import ( # noqa: E402
CIRCUIT_BREAKER_FAIL_RATE,
CIRCUIT_BREAKER_WINDOW,
KNOWN_UNSUPPORTED_PREFIX,
check_circuit_breaker,
)
def _make_results(n_ok: int, n_fail: int, fail_prefix: str = "600") -> list:
"""构造 recent_results 列表:n_ok 个 ok + n_fail 个 fail。
fail_prefix 控制失败码的前缀(用于测试北交所排除)。
"""
ok_list = [("600001", True)] * n_ok
fail_list = [(f"{fail_prefix}9999", False)] * n_fail
return ok_list + fail_list
class TestCircuitBreakerTrigger:
"""断路器触发阈值测试。"""
def test_35_percent_fail_triggers(self):
"""200 只里 70 只 fail35%)→ 触发。"""
results = _make_results(n_ok=130, n_fail=70)
assert len(results) == 200
assert check_circuit_breaker(results) is True
def test_25_percent_fail_no_trigger(self):
"""200 只里 50 只 fail25%)→ 不触发。"""
results = _make_results(n_ok=150, n_fail=50)
assert len(results) == 200
assert check_circuit_breaker(results) is False
def test_exactly_30_percent_no_trigger(self):
"""恰好 30%(60/200)→ 不触发(阈值是 > 0.30,不含等于)。"""
results = _make_results(n_ok=140, n_fail=60)
assert len(results) == 200
fail_rate = 60 / 200
assert fail_rate == pytest.approx(CIRCUIT_BREAKER_FAIL_RATE)
assert check_circuit_breaker(results) is False
def test_31_percent_triggers(self):
"""31% 失败率 → 触发(刚过阈值)。"""
results = _make_results(n_ok=138, n_fail=62)
assert len(results) == 200
assert check_circuit_breaker(results) is True
class TestCircuitBreakerBjExclusion:
"""北交所码不计入断路器分母测试。"""
def test_bj_fails_excluded_from_denominator(self):
"""北交所 920xxx 的 fail 不计入 → 不触发。
200 只非北交所全部 ok + 100 只北交所全部 fail
有效分母=200,有效失败=0 → 0% → 不触发。
"""
ok_list = [("600001", True)] * 200
bj_fail_list = [("920001", False)] * 100
results = ok_list + bj_fail_list
assert check_circuit_breaker(results) is False
def test_bj_fails_do_not_inflate_rate(self):
"""北交所 fail 混在 200 窗口内不抬高失败率。
150 只非北交所 ok + 50 只北交所 fail = 200 只总数:
有效分母=150(< 窗口 200)→ 不足窗口,不触发。
"""
ok_list = [("600001", True)] * 150
bj_fail_list = [("920001", False)] * 50
results = ok_list + bj_fail_list
assert check_circuit_breaker(results) is False
def test_mixed_bj_and_normal_fails(self):
"""混合场景:200 非北交所(60 fail=30%+ 50 北交所 fail。
有效:200 非北交所,60 fail = 30%,恰好阈值(> 0.30 不含等于)→ 不触发。
北交所 50 fail 被排除,不影响计算。
"""
ok_normal = [("600001", True)] * 140
fail_normal = [("600002", False)] * 60
fail_bj = [("920001", False)] * 50
results = ok_normal + fail_normal + fail_bj
assert check_circuit_breaker(results) is False
def test_all_bj_prefixes_excluded(self):
"""所有已知不支持前缀都排除:920/921/83/87。"""
ok_list = [("600001", True)] * 200
bj_fails = (
[("920001", False)] * 30
+ [("921002", False)] * 30
+ [("830003", False)] * 30
+ [("870004", False)] * 30
)
results = ok_list + bj_fails
assert check_circuit_breaker(results) is False
class TestCircuitBreakerWindow:
"""窗口大小边界测试。"""
def test_insufficient_window_no_trigger(self):
"""不足 200 只 → 不触发(即使全部 fail)。"""
results = [("600001", False)] * 199
assert check_circuit_breaker(results) is False
def test_exactly_window_triggers_if_high_fail(self):
"""恰好 200 只且失败率超阈值 → 触发。"""
results = _make_results(n_ok=100, n_fail=100)
assert len(results) == 200
assert check_circuit_breaker(results) is True
def test_empty_results_no_trigger(self):
"""空列表 → 不触发。"""
assert check_circuit_breaker([]) is False
def test_all_ok_no_trigger(self):
"""200 只全部 ok → 不触发。"""
results = [("600001", True)] * 200
assert check_circuit_breaker(results) is False
class TestCircuitBreakerConstants:
"""常量值校验(防止意外修改)。"""
def test_window_is_200(self):
assert CIRCUIT_BREAKER_WINDOW == 200
def test_fail_rate_is_030(self):
assert CIRCUIT_BREAKER_FAIL_RATE == 0.30
def test_known_unsupported_prefixes(self):
assert KNOWN_UNSUPPORTED_PREFIX == ("920", "921", "83", "87")