2026-09-14 10:16:46 +08:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
"""观察读数包 (2026-09-14 方案第三节, 台账 013)。全部离线, 不连库。
|
|
|
|
|
|
|
|
|
|
把每轮扫描里合议相关的判定落进 pms_consensus_stat, 让台账 006 到 011 的复核有据。
|
|
|
|
|
A 动作引擎打标签: 合议判跳过分 fund/bear、判观察打 wait_tech、增持门打 gate_block;
|
|
|
|
|
开关关掉时标签不出现 (逐字回旧); tag 值与 consensus_stats 认的 kind 对齐。
|
|
|
|
|
B 归类落表 record_round: 检查点写路由与装配两类、转空实弹随时写、试算不写、被闸拒不记、
|
|
|
|
|
确认与否据动作名; daily_summary 取数失败返回空不抛。
|
|
|
|
|
C 映射覆盖 record_map_cover: 记 map_cover 与逐只 tech_noread。
|
|
|
|
|
D 仓库 upsert: 更新子句只用 VALUES 列不带绑定参数 (pymysql 批量陷阱), SQL 单表合规。
|
|
|
|
|
E 参数登记与检查点窗口。
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import traceback
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
|
|
|
|
from app.core import action_engine as ae # noqa: E402
|
|
|
|
|
from app.services import consensus_stats as cst # noqa: E402
|
|
|
|
|
from app.repo import consensus_stat_repo as csr # noqa: E402
|
|
|
|
|
|
|
|
|
|
RESULTS = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def case(name):
|
|
|
|
|
def deco(fn):
|
|
|
|
|
RESULTS.append((name, fn))
|
|
|
|
|
return fn
|
|
|
|
|
return deco
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================ 公共桩
|
|
|
|
|
def _copen(code, route, rr, direction="中性"):
|
|
|
|
|
"""一条挂了合议块的候选 (scan_open 用)。"""
|
|
|
|
|
return {"ts_code": code, "score": 100, "rank": 1,
|
|
|
|
|
"consensus": {"route": route, "route_reason": rr, "direction": direction}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _caps():
|
|
|
|
|
return {"max_names": 20, "sector_source_ready": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pos_con(code, direction):
|
|
|
|
|
"""一条挂了合议块的持仓行 (scan 增持门用)。"""
|
|
|
|
|
return {"ts_code": code, "total_qty": 6000, "avail_qty": 6000, "price": 10.0,
|
|
|
|
|
"avg_cost": 10.0, "base_qty": 6000, "add_qty": 0, "dca_qty": 0,
|
|
|
|
|
"cushion_pct": 0.0, "cushion_peak": 0.0, "frozen_reason": "NONE",
|
|
|
|
|
"price_ok": True, "target_pct": 0.06,
|
|
|
|
|
"consensus_blocks": {"consensus": {"direction": direction},
|
|
|
|
|
"tech": {"stance": direction}, "fund": {"stance": direction}}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sp(**kw):
|
|
|
|
|
p = {"cushion_solid": 0.03, "trim_peak": 0.06, "trim_giveback": 0.5,
|
|
|
|
|
"dca_triggers": (-0.08, -0.15), "dca_deep_confirm": -0.15, "dca_max_ratio": 0.5,
|
|
|
|
|
"no_chase_ma5": 0.06, "build_window_tdays": 10, "fill_max_loss": -0.03,
|
|
|
|
|
"batch_split": (0.5, 0.25, 0.25), "stock_target_default": 0.06, "scale": 2_000_000,
|
|
|
|
|
"logic_state_route": False, "tech_gate_increase": True}
|
|
|
|
|
p.update(kw)
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _patched:
|
|
|
|
|
"""把 record_round 会碰的两处外部依赖换成桩: param_store.get 返回固定检查点串,
|
|
|
|
|
consensus_stat_repo.upsert_many 捕获落表行 (不连库)。with 结束自动还原。"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, times="0935,1030,1330,1445"):
|
|
|
|
|
self.times = times
|
|
|
|
|
self.captured = []
|
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
self._o_get = cst.param_store.get
|
|
|
|
|
self._o_up = cst.consensus_stat_repo.upsert_many
|
2026-09-14 11:07:39 +08:00
|
|
|
self._o_del = cst.consensus_stat_repo.delete_day_kind
|
2026-09-14 10:16:46 +08:00
|
|
|
cst.param_store.get = lambda k, d=None: (self.times if k == cst.STAT_TIMES_KEY
|
|
|
|
|
else self._o_get(k, d))
|
|
|
|
|
cst.consensus_stat_repo.upsert_many = lambda rows: (self.captured.extend(rows)
|
|
|
|
|
or len(rows))
|
2026-09-14 11:07:39 +08:00
|
|
|
cst.consensus_stat_repo.delete_day_kind = lambda d, k: 0 # 离线不打库
|
2026-09-14 10:16:46 +08:00
|
|
|
return self.captured
|
|
|
|
|
|
|
|
|
|
def __exit__(self, *a):
|
|
|
|
|
cst.param_store.get = self._o_get
|
|
|
|
|
cst.consensus_stat_repo.upsert_many = self._o_up
|
2026-09-14 11:07:39 +08:00
|
|
|
cst.consensus_stat_repo.delete_day_kind = self._o_del
|
2026-09-14 10:16:46 +08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _by_kind(rows):
|
|
|
|
|
out = {}
|
|
|
|
|
for r in rows:
|
|
|
|
|
out.setdefault(r["kind"], []).append(r)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sample_out():
|
|
|
|
|
return {"dry_run": False, "rejected": [],
|
|
|
|
|
"consensus_seen": [
|
|
|
|
|
{"ts_code": "600000.SH", "fund": "看多", "tech": "看多", "timing": "看多",
|
|
|
|
|
"direction": "看多", "route": "放行", "phase": "趋势多"},
|
|
|
|
|
{"ts_code": "600001.SH", "fund": "无读数", "tech": "中性", "timing": "看多",
|
|
|
|
|
"direction": "看多", "route": "交人", "phase": None},
|
|
|
|
|
{"ts_code": "600010.SH", "fund": "看多", "tech": "看多", "timing": "无读数",
|
|
|
|
|
"direction": "看多", "route": "放行", "phase": "趋势多"}],
|
|
|
|
|
"skipped": [
|
|
|
|
|
{"ts_code": "600002.SH", "action": "OPEN", "why": "没有买方评析",
|
|
|
|
|
"tag": "open_skip_fund"},
|
|
|
|
|
{"ts_code": "600003.SH", "action": "OPEN", "why": "方向看空(…)",
|
|
|
|
|
"tag": "open_skip_bear"},
|
|
|
|
|
{"ts_code": "600004.SH", "action": "OPEN", "why": "等技术面开口",
|
|
|
|
|
"tag": "open_wait_tech", "disp": "wait_tech"},
|
|
|
|
|
{"ts_code": "600005.SH", "action": "FILL", "why": "技术面看空",
|
|
|
|
|
"tag": "gate_block", "detail": {"gate_kind": "fill"}},
|
|
|
|
|
{"ts_code": "600009.SH", "why": "宏观偏热闸生效"}]} # 无 tag, 应被忽略
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sample_scanned():
|
|
|
|
|
return {"candidates": [
|
|
|
|
|
{"ts_code": "600006.SH", "action": "EXIT", "source": "tech_exit", "qty": 1000,
|
|
|
|
|
"reason": "确认转空清仓"},
|
|
|
|
|
{"ts_code": "600007.SH", "action": "TRIM", "source": "tech_exit", "qty": 300,
|
|
|
|
|
"reason": "未确认减三分之一"},
|
|
|
|
|
{"ts_code": "600008.SH", "action": "FILL", "source": "engine", "qty": 500}]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================ A 动作引擎打标签
|
|
|
|
|
@case("A scan_open·合议判跳过·没有买方评析 → open_skip_fund")
|
|
|
|
|
def _():
|
|
|
|
|
res = ae.scan_open(candidates=[_copen("600000.SH", "跳过", "没有买方评析")],
|
|
|
|
|
params={"consensus_route": True}, caps=_caps(),
|
|
|
|
|
room_amt=1e6, slots=10, skip={})
|
|
|
|
|
assert len(res["skipped"]) == 1
|
|
|
|
|
assert res["skipped"][0]["tag"] == "open_skip_fund"
|
|
|
|
|
assert res["skipped"][0]["ts_code"] == "600000.SH"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("A scan_open·合议判跳过·方向看空 → open_skip_bear")
|
|
|
|
|
def _():
|
|
|
|
|
res = ae.scan_open(candidates=[_copen("600000.SH", "跳过", "方向看空(基本面看空、技术面看空、择时中性)")],
|
|
|
|
|
params={"consensus_route": True}, caps=_caps(),
|
|
|
|
|
room_amt=1e6, slots=10, skip={})
|
|
|
|
|
assert res["skipped"][0]["tag"] == "open_skip_bear"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("A scan_open·合议判观察 → open_wait_tech 且 disp wait_tech")
|
|
|
|
|
def _():
|
|
|
|
|
res = ae.scan_open(candidates=[_copen("600000.SH", "观察", "等技术面开口")],
|
|
|
|
|
params={"consensus_route": True}, caps=_caps(),
|
|
|
|
|
room_amt=1e6, slots=10, skip={})
|
|
|
|
|
assert res["skipped"][0]["tag"] == "open_wait_tech"
|
|
|
|
|
assert res["skipped"][0]["disp"] == ae.DISP_WAIT_TECH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("A scan_open·合议开关关掉·跳过项不带 tag (开着才有, 逐字回旧)")
|
|
|
|
|
def _():
|
|
|
|
|
cand = _copen("600000.SH", "跳过", "没有买方评析")
|
|
|
|
|
on = ae.scan_open(candidates=[dict(cand)], params={"consensus_route": True},
|
|
|
|
|
caps=_caps(), room_amt=1e6, slots=10, skip={})
|
|
|
|
|
# 关掉合议: con 为 None, 那只候选不再被合议分流; 用 skip 在 eval_open 之前接住它, 免起重设备
|
|
|
|
|
off = ae.scan_open(candidates=[dict(cand)], params={"consensus_route": False},
|
|
|
|
|
caps=_caps(), room_amt=1e6, slots=10,
|
|
|
|
|
skip={("600000.SH", "OPEN"): "已在途"})
|
|
|
|
|
assert on["skipped"][0].get("tag") == "open_skip_fund"
|
|
|
|
|
assert all("tag" not in s for s in off["skipped"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("A scan·增持门拦下 → gate_block 且 detail.gate_kind ∈ {fill,add,dca}")
|
|
|
|
|
def _():
|
|
|
|
|
res = ae.scan(positions=[_pos_con("600000.SH", "看空")], params=_sp(tech_gate_increase=True),
|
|
|
|
|
market={"600000.SH": {}}, skip={})
|
|
|
|
|
gb = [s for s in res["skipped"] if s.get("tag") == "gate_block"]
|
|
|
|
|
assert gb, "增持门看空应打 gate_block 标签"
|
|
|
|
|
assert all((s.get("detail") or {}).get("gate_kind") in ("fill", "add", "dca") for s in gb)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("A scan·增持门开关关掉 → 无 gate_block tag (逐字回旧)")
|
|
|
|
|
def _():
|
|
|
|
|
res = ae.scan(positions=[_pos_con("600000.SH", "看空")], params=_sp(tech_gate_increase=False),
|
|
|
|
|
market={"600000.SH": {}}, skip={})
|
|
|
|
|
assert all(s.get("tag") != "gate_block" for s in res["skipped"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("A 哨兵·动作引擎 tag 与 consensus_stats 认的 kind 对齐")
|
|
|
|
|
def _():
|
|
|
|
|
assert cst.K_OPEN_SKIP_FUND == ae.TAG_OPEN_SKIP_FUND == "open_skip_fund"
|
|
|
|
|
assert cst.K_OPEN_SKIP_BEAR == ae.TAG_OPEN_SKIP_BEAR == "open_skip_bear"
|
|
|
|
|
assert cst.K_OPEN_WAIT_TECH == ae.TAG_OPEN_WAIT_TECH == "open_wait_tech"
|
|
|
|
|
assert cst.K_GATE_BLOCK == ae.TAG_GATE_BLOCK == "gate_block"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================ B 归类落表 record_round
|
|
|
|
|
@case("B record_round·检查点写全部类别 (放行/交人/无读数/跳过/观察/增持门/转空实弹)")
|
|
|
|
|
def _():
|
|
|
|
|
with _patched() as cap:
|
|
|
|
|
r = cst.record_round(_sample_out(), _sample_scanned(), {}, datetime(2026, 9, 15, 9, 35))
|
|
|
|
|
assert r["ok"]
|
|
|
|
|
bk = _by_kind(cap)
|
|
|
|
|
assert {x["ts_code"] for x in bk["open_pass"]} == {"600000.SH", "600010.SH"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["open_confirm"]} == {"600001.SH"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["fund_noread"]} == {"600001.SH"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["timing_noread"]} == {"600010.SH"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["open_skip_fund"]} == {"600002.SH"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["open_skip_bear"]} == {"600003.SH"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["open_wait_tech"]} == {"600004.SH"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["gate_block"]} == {"600005.SH"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["tech_exit"]} == {"600006.SH", "600007.SH"}
|
|
|
|
|
assert all(x["stat_date"] == 20260915 for x in cap) # 北京日期
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("B record_round·gate_block 带上 detail.gate_kind; 无 tag 的跳过项被忽略")
|
|
|
|
|
def _():
|
|
|
|
|
with _patched() as cap:
|
|
|
|
|
cst.record_round(_sample_out(), _sample_scanned(), {}, datetime(2026, 9, 15, 10, 30))
|
|
|
|
|
bk = _by_kind(cap)
|
|
|
|
|
assert json.loads(bk["gate_block"][0]["detail"])["gate_kind"] == "fill"
|
|
|
|
|
assert "600009.SH" not in {x["ts_code"] for x in cap} # 无 tag 的宏观闸跳过不落
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("B record_round·转空实弹据动作名记 confirm (EXIT 真 / TRIM 假)")
|
|
|
|
|
def _():
|
|
|
|
|
with _patched() as cap:
|
|
|
|
|
cst.record_round(_sample_out(), _sample_scanned(), {}, datetime(2026, 9, 15, 9, 35))
|
|
|
|
|
ex = {x["ts_code"]: json.loads(x["detail"]) for x in _by_kind(cap)["tech_exit"]}
|
|
|
|
|
assert ex["600006.SH"]["confirm"] is True and ex["600006.SH"]["qty"] == 1000
|
|
|
|
|
assert ex["600007.SH"]["confirm"] is False and ex["600007.SH"]["qty"] == 300
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("B record_round·非检查点只写转空实弹 (路由与装配两类不写)")
|
|
|
|
|
def _():
|
|
|
|
|
with _patched() as cap:
|
|
|
|
|
cst.record_round(_sample_out(), _sample_scanned(), {}, datetime(2026, 9, 15, 10, 0))
|
|
|
|
|
bk = _by_kind(cap)
|
|
|
|
|
assert set(bk) == {"tech_exit"}
|
|
|
|
|
assert {x["ts_code"] for x in bk["tech_exit"]} == {"600006.SH", "600007.SH"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("B record_round·试算 (dry_run) 一律不写")
|
|
|
|
|
def _():
|
|
|
|
|
out = {**_sample_out(), "dry_run": True}
|
|
|
|
|
with _patched() as cap:
|
|
|
|
|
r = cst.record_round(out, _sample_scanned(), {}, datetime(2026, 9, 15, 9, 35))
|
|
|
|
|
assert cap == [] and r["rows"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("B record_round·转空离场被闸拒的不记 tech_exit")
|
|
|
|
|
def _():
|
|
|
|
|
out = {"dry_run": False, "consensus_seen": [], "skipped": [],
|
|
|
|
|
"rejected": [{"ts_code": "600006.SH", "action": "EXIT"}]}
|
|
|
|
|
with _patched() as cap:
|
|
|
|
|
cst.record_round(out, _sample_scanned(), {}, datetime(2026, 9, 15, 10, 0))
|
|
|
|
|
assert {x["ts_code"] for x in _by_kind(cap).get("tech_exit", [])} == {"600007.SH"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("B record_round·空串检查点 → 只写转空实弹")
|
|
|
|
|
def _():
|
|
|
|
|
with _patched(times="") as cap:
|
|
|
|
|
cst.record_round(_sample_out(), _sample_scanned(), {}, datetime(2026, 9, 15, 9, 35))
|
|
|
|
|
assert set(_by_kind(cap)) == {"tech_exit"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("B daily_summary·取数失败返回空 dict 不抛")
|
|
|
|
|
def _():
|
|
|
|
|
o = cst.consensus_stat_repo.count_by_kind
|
|
|
|
|
|
|
|
|
|
def _boom(a, b):
|
|
|
|
|
raise RuntimeError("db down")
|
|
|
|
|
cst.consensus_stat_repo.count_by_kind = _boom
|
|
|
|
|
try:
|
|
|
|
|
assert cst.daily_summary(20260915) == {}
|
|
|
|
|
finally:
|
|
|
|
|
cst.consensus_stat_repo.count_by_kind = o
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================ C 映射覆盖
|
|
|
|
|
@case("C record_map_cover·记 map_cover (ts_code=*) 与逐只 tech_noread")
|
|
|
|
|
def _():
|
|
|
|
|
with _patched() as cap:
|
|
|
|
|
cst.record_map_cover(relevant_codes=["600000.SH", "600001.SH", "600002.SH"],
|
|
|
|
|
state_codes={"600000.SH", "600001.SH"},
|
|
|
|
|
now=datetime(2026, 9, 15, 8, 40))
|
|
|
|
|
bk = _by_kind(cap)
|
|
|
|
|
mc = bk["map_cover"][0]
|
|
|
|
|
assert mc["ts_code"] == "*"
|
|
|
|
|
assert json.loads(mc["detail"]) == {"codes": 3, "states": 2}
|
|
|
|
|
assert {x["ts_code"] for x in bk["tech_noread"]} == {"600002.SH"}
|
|
|
|
|
|
|
|
|
|
|
2026-09-14 11:07:39 +08:00
|
|
|
@case("C record_map_cover·重建先清当日 tech_noread (以最新一次为准, 评审第三条)")
|
|
|
|
|
def _():
|
|
|
|
|
deleted, upserted = [], []
|
|
|
|
|
o_del, o_up = csr.delete_day_kind, csr.upsert_many
|
|
|
|
|
csr.delete_day_kind = lambda d, k: (deleted.append((d, k)), 0)[1]
|
|
|
|
|
csr.upsert_many = lambda rows: (upserted.extend(rows), len(rows))[1]
|
|
|
|
|
try:
|
|
|
|
|
cst.record_map_cover(relevant_codes=["600000.SH"], state_codes=set(),
|
|
|
|
|
now=datetime(2026, 9, 15, 8, 40))
|
|
|
|
|
finally:
|
|
|
|
|
csr.delete_day_kind, csr.upsert_many = o_del, o_up
|
|
|
|
|
assert (20260915, cst.K_TECH_NOREAD) in deleted # 先清当日 tech_noread
|
|
|
|
|
assert any(r["kind"] == cst.K_MAP_COVER for r in upserted) # 再写覆盖行
|
|
|
|
|
|
|
|
|
|
|
2026-09-14 10:16:46 +08:00
|
|
|
# ================================================================ D 仓库 upsert
|
|
|
|
|
@case("D upsert_many·更新子句只用 VALUES 列不带绑定参数; SQL 单表合规")
|
|
|
|
|
def _():
|
|
|
|
|
from app.db.session import assert_single_table
|
|
|
|
|
grabbed = {}
|
|
|
|
|
o = csr.execute_many
|
|
|
|
|
csr.execute_many = lambda sql, payload, **kw: (grabbed.update(sql=sql, payload=payload)
|
|
|
|
|
or len(payload))
|
|
|
|
|
try:
|
|
|
|
|
n = csr.upsert_many([{"stat_date": 20260915, "ts_code": "600000.SH", "kind": "open_pass",
|
|
|
|
|
"reason": "看多", "detail": None,
|
|
|
|
|
"first_at": datetime.now(), "last_at": datetime.now()}])
|
|
|
|
|
finally:
|
|
|
|
|
csr.execute_many = o
|
|
|
|
|
sql = grabbed["sql"]
|
|
|
|
|
upd = sql.split("ON DUPLICATE KEY UPDATE", 1)[1]
|
|
|
|
|
assert ":" not in upd, "更新子句不许有绑定参数 (:col) —— 2026-09-11 pymysql 批量陷阱"
|
|
|
|
|
assert "VALUES(reason)" in upd and "rounds = rounds + 1" in upd
|
|
|
|
|
assert_single_table(sql) # 不抛即单表合规 (守卫会摘掉 ON DUPLICATE 尾巴)
|
|
|
|
|
assert n == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("D upsert_many·缺主键三件套的行被丢掉")
|
|
|
|
|
def _():
|
|
|
|
|
grabbed = {}
|
|
|
|
|
o = csr.execute_many
|
|
|
|
|
csr.execute_many = lambda sql, payload, **kw: (grabbed.update(payload=payload)
|
|
|
|
|
or len(payload))
|
|
|
|
|
try:
|
|
|
|
|
# 只有一行齐全, 另两行分别缺 kind 与 ts_code
|
|
|
|
|
n = csr.upsert_many([
|
|
|
|
|
{"stat_date": 20260915, "ts_code": "600000.SH", "kind": "open_pass",
|
|
|
|
|
"first_at": datetime.now(), "last_at": datetime.now()},
|
|
|
|
|
{"stat_date": 20260915, "ts_code": "600001.SH"},
|
|
|
|
|
{"stat_date": 20260915, "kind": "open_pass"}])
|
|
|
|
|
finally:
|
|
|
|
|
csr.execute_many = o
|
|
|
|
|
assert n == 1 and len(grabbed["payload"]) == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================ E 参数与检查点窗口
|
|
|
|
|
@case("E 参数·PMS_CONSENSUS_STAT_TIMES 登记, 默认四检查点, 类型 str")
|
|
|
|
|
def _():
|
|
|
|
|
from app.services import param_store as ps
|
|
|
|
|
assert cst.STAT_TIMES_KEY == "PMS_CONSENSUS_STAT_TIMES"
|
|
|
|
|
dv, typ, _desc = ps.RUNTIME_EXTRA[cst.STAT_TIMES_KEY]
|
|
|
|
|
assert dv == "0935,1030,1330,1445" and typ is str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@case("E _at_checkpoint·命中检查点及其下一分钟, 其余与空串不命中")
|
|
|
|
|
def _():
|
|
|
|
|
t = "0935,1030,1330,1445"
|
|
|
|
|
assert cst._at_checkpoint(datetime(2026, 9, 15, 9, 35), t) is True
|
|
|
|
|
assert cst._at_checkpoint(datetime(2026, 9, 15, 9, 36), t) is True # 下一分钟
|
|
|
|
|
assert cst._at_checkpoint(datetime(2026, 9, 15, 9, 37), t) is False
|
|
|
|
|
assert cst._at_checkpoint(datetime(2026, 9, 15, 14, 45), t) is True
|
|
|
|
|
assert cst._at_checkpoint(datetime(2026, 9, 15, 9, 35), "") is False # 空串无检查点
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
ok = 0
|
|
|
|
|
for name, fn in RESULTS:
|
|
|
|
|
try:
|
|
|
|
|
fn()
|
|
|
|
|
ok += 1
|
|
|
|
|
print(" ok " + name)
|
|
|
|
|
except Exception:
|
|
|
|
|
print(" FAIL " + name)
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
print("-" * 60)
|
|
|
|
|
if ok == len(RESULTS):
|
|
|
|
|
print("ALL PASS (%d cases)" % ok)
|
|
|
|
|
return 0
|
|
|
|
|
print("FAILED %d/%d" % (len(RESULTS) - ok, len(RESULTS)))
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|