feat(data): sanguo-bs-daily单入口串行编排(bs_eod→bs_fundamentals)——2026-08-20数据任务重组阶段1:根治当日撞点(bs_eod加指数段后ETA 23:00+与旧23:05 bs-fund重叠=同IP双baostock连接封禁红线),合并进同一schtask两腿先后子进程,leg1进程退出(logout+消亡)连接才释放leg2才login;bs_daily_main只做顺序+退出码透传({0,3}=OK,3=按预算graceful不算失败)+[BS-DAILY]腿起止时间线日志(验收'eod logout后fund才login'直接证据);leg2无条件跑(leg1致命不连坐,连接已释放fund独立作业);wrapper传BS_DAILY_LIMIT=15000收紧eod病态重试硬顶(常态~11.1k不受影响,与fund30000合计≤45.1k<48k/IP/天);--smoke零配额链路自检;bs_fundamentals._bs_eod_running补注释:旧任务名已删探测必失败fail-open=设计行为勿修复(探新名会探到父任务永久让路);+15测试(顺序铁律/leg2无条件/退出码聚合9组合/smoke零子进程) [vps]
CI/CD / test (push) Successful in 18s
CI/CD / nas-deploy (push) Successful in 39s
CI/CD / nas-verify (push) Successful in 14s

This commit is contained in:
2026-08-20 19:49:00 +08:00
parent d54f648efc
commit 952cc5adf8
4 changed files with 178 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
"""TDD for bs_daily_main.py — sanguo-bs-daily 单入口串行编排 (2026-08-20 重组).
核心契约:
1. 顺序铁律: bs_eod 腿必先于 bs_fund 腿(subprocess.run 阻塞=天然串行, 同 IP 永不
双 baostock 连接 —— 08-20 撞点(eod 加指数段 ETA 23:00+ 撞 fund 23:05)的根治)
2. leg2 无条件跑: leg1 致命(1)/登录失败(2)不连坐 —— 连接已随进程释放, fund 独立作业
3. 退出码聚合: {0,3}=OK(3=按预算 graceful stop 不算失败), 任务码=第一个非 OK 腿
4. --smoke: 零配额链路自检, 不起任何子进程
"""
import sys
from unittest.mock import MagicMock, patch
import pytest
if "baostock" not in sys.modules: # bs_fundamentals import 链上需要
sys.modules["baostock"] = MagicMock()
from scripts.data_platform import bs_daily_main as bd # noqa: E402
def _mock_run(rc1, rc2):
"""subprocess.run 按调用次序返回 returncode=rc1/rc2 的假结果, 记录调用脚本名。"""
calls = []
def fake_run(cmd, **kw):
calls.append(cmd[-1])
return MagicMock(returncode=rc1 if len(calls) == 1 else rc2)
return calls, fake_run
# ---------- smoke ----------
def test_smoke_exits_0_without_subprocess():
with patch.object(bd.subprocess, "run",
side_effect=AssertionError("smoke 不应起子进程")):
assert bd.main(["--smoke"]) == 0
# ---------- 顺序铁律 ----------
def test_runs_eod_then_fund_in_order():
calls, fake = _mock_run(0, 0)
with patch.object(bd.subprocess, "run", side_effect=fake):
assert bd.main([]) == 0
assert [c.rsplit("\\", 1)[-1].rsplit("/", 1)[-1] for c in calls] == \
["bs_eod.py", "bs_fundamentals.py"]
def test_leg_scripts_resolve_to_real_files():
for _name, script in bd.LEGS:
assert script.exists(), script
# ---------- leg2 无条件跑(leg1 异常不连坐) ----------
@pytest.mark.parametrize("rc1", [1, 2])
def test_leg2_runs_even_when_leg1_fatal(rc1):
calls, fake = _mock_run(rc1, 0)
with patch.object(bd.subprocess, "run", side_effect=fake):
bd.main([])
assert len(calls) == 2 # fund 腿照跑
# ---------- 退出码聚合 ----------
@pytest.mark.parametrize("rc1,rc2,expected", [
(0, 0, 0),
(3, 0, 0), # eod 按预算 graceful stop = 正常
(0, 3, 0), # fund 按日预算收工 = 正常
(3, 3, 0),
(1, 0, 1), # leg1 致命 → 任务码如实上报(不被 leg2 的 0 掩盖)
(2, 0, 2),
(0, 1, 1),
(0, 2, 2),
(1, 2, 1), # 双异常取第一个(先修 leg1)
])
def test_exit_aggregation(rc1, rc2, expected):
_calls, fake = _mock_run(rc1, rc2)
with patch.object(bd.subprocess, "run", side_effect=fake):
assert bd.main([]) == expected
def test_ok_codes_contract():
"""{0,3}=OK 契约钉死: 两脚本退出码语义变更时这里必须同步评审。"""
assert bd.OK_CODES == {0, 3}