From 482904b591da6a43003b95c4404e1a123c2fddb2 Mon Sep 17 00:00:00 2001 From: im47cn <67424112+im47cn@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:52:49 +0800 Subject: [PATCH] =?UTF-8?q?test(panelcache):=20=E5=AD=90=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E5=8A=A0=E5=9B=BA=20=E2=80=94=20=E5=8E=BB=20?= =?UTF-8?q?flaky=20+=20=E8=A1=A5=E9=81=A5=E6=B5=8B=E5=B7=AE=E5=80=BC?= =?UTF-8?q?=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三处审查驱动的测试改进: - 并发计数测试放宽 reuse_count==4 为 reuse+hit==4 (保留"只扫盘1次"核心不变量), 消除慢调度下 leader 已写缓存致某线程走 hit 分支的 flaky。 - _FakeEngine 零值 dict 改从 PanelCache().stats() 取键 —— 字段重命名时桩自动跟随, 杜绝 test 绿而生产 cache_stats KeyError 的契约漂移。 - 新增 test_walkforward_cache_telemetry_computes_deltas: 递增桩验证 scans/hits/ reuses/秒数 = after-before, 覆盖此前 _FakeEngine 恒返 0 掩盖的差值/顺序逻辑。 --- backend/tests/backtest/test_walkforward.py | 34 ++++++++++++++++++++-- backend/tests/test_backtest_etf.py | 5 +++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/backend/tests/backtest/test_walkforward.py b/backend/tests/backtest/test_walkforward.py index 8fbc95f..df49071 100644 --- a/backend/tests/backtest/test_walkforward.py +++ b/backend/tests/backtest/test_walkforward.py @@ -136,11 +136,14 @@ class _FakeOptimizer: return {"best_params": {"p": cfg.start.month}, "best_score": 2.0, "results": [], "n_completed": 1} -_ZERO_CACHE_STATS = {"compute_seconds": 0.0, "compute_count": 0, "hit_count": 0, "reuse_count": 0} +# 从真实 PanelCache 取字段模板 —— 字段被重命名时本桩自动跟随, 避免 test 绿而生产 KeyError。 +from app.backtest.engine import PanelCache + +_ZERO_CACHE_STATS = {k: type(v)() for k, v in PanelCache().stats().items()} class _FakeEngine: - """最小引擎桩: 仅提供 WF 遥测所需的 cache_stats。""" + """最小引擎桩: 仅提供 WF 遥测所需的 cache_stats (字段同源自 PanelCache.stats)。""" def cache_stats(self): return dict(_ZERO_CACHE_STATS) @@ -182,6 +185,33 @@ def test_walkforward_optimizes_train_applies_oos(): assert svc.calls[0]["start"] >= opt.train_ranges[0][1] +class _CountingEngine: + """首尾两次 cache_stats 返回不同值, 用于验证 WF 遥测差值/顺序计算 (非全零掩盖)。""" + def __init__(self): + self._n = 0 + + def cache_stats(self): + self._n += 1 + if self._n == 1: # run 开头快照 (before) + return {"compute_seconds": 1.0, "compute_count": 2, "hit_count": 0, "reuse_count": 0} + return {"compute_seconds": 3.5, "compute_count": 7, "hit_count": 4, "reuse_count": 3} # after + + +def test_walkforward_cache_telemetry_computes_deltas(): + """cache_telemetry 用首尾快照差值: scans/hits/reuses/秒数 = after - before, 且方向正确。""" + opt, svc = _FakeOptimizer(), _FakeService() + svc.engine = _CountingEngine() + wf = WalkForwardService(opt, svc, strategy_engine=None) + out = wf.run(_wf_cfg()) + + tel = out["cache_telemetry"] + assert tel["scans"] == 5 # 7 - 2, 顺序写反会得 -5 + assert tel["hits"] == 4 # 4 - 0 + assert tel["single_flight_reuses"] == 3 # 3 - 0 + assert abs(tel["load_panel_seconds"] - 2.5) < 1e-9 # 3.5 - 1.0 + assert tel["load_panel_pct"] >= 0.0 # 扫盘耗时 / WF总耗时, 非负 + + def test_walkforward_reports_degradation(): opt, svc = _FakeOptimizer(), _FakeService() wf = WalkForwardService(opt, svc, strategy_engine=None) diff --git a/backend/tests/test_backtest_etf.py b/backend/tests/test_backtest_etf.py index f95e25e..3253e1a 100644 --- a/backend/tests/test_backtest_etf.py +++ b/backend/tests/test_backtest_etf.py @@ -187,8 +187,11 @@ def test_panel_cache_stats_counts_scans_hits_reuses(): t.join() s = cache.stats() + # 核心不变量: 5 线程并发同 key 只扫盘 1 次 (args1 首次 + args2 一次 = 2)。 assert s["compute_count"] == 2, "并发同 key 应只扫盘 1 次" - assert s["reuse_count"] == 4, "其余 4 个跟随者应计为复用" + # 其余 4 线程要么 single-flight 复用, 要么(慢调度下 leader 已写缓存)命中 —— + # 二者之和恒为 4。不锁定 reuse/hit 具体分配, 避免时序 flaky。 + assert s["reuse_count"] + (s["hit_count"] - 1) == 4, "4 个非 leader 线程应复用或命中" def test_job_key_includes_asset_type_and_is_consistent():