# ek-event 测试方案(基于 enkeep_trade 实际使用方式)
> 来源:`enkeep_trade/trader/gateway.py`、`trader/engine.py`(OmsEngine/LockTraderApiEngine/RecordEngine2)、
> `app/paperaccount/engine.py`、`app/rpc_server/engine.py`、`app/optionmaster/incubator`、`trader/ui/widget.py` 等。
> 方针:**测试不 import enkeep_trade**,而是在测试文件里用测试本地结构体复刻这些使用姿势,
> 以此验证 ek-event 的公共 API 是否足以支撑真实平台生态。
## 0. 提取的使用模式 → 测试映射总表
| P1 | Gateway 标准投递:聚合 + 逐品种/逐订单 双投(gateway.py on_tick/on_order/on_trade/on_position/on_account) | tests/usage_gateway.rs::G1,G2 | 高 |
| P2 | 多引擎共享 + 各自注册(OmsEngine / paperaccount / UI 全部共享同一 EventEngine) | tests/usage_shared.rs::S1,S2 | 高 |
| P3 | UI 桥接生命周期:open 注册 / close 注销 / 再次 open 重新注册(widget.py、kline_widget.py) | tests/usage_lifecycle.rs::L1,L2 | 高 |
| P4 | 全事件转发器:register_general 收一切,rpc_server 按 gateway_name 过滤、空载荷跳过(engine.py process_event) | tests/usage_rpc.rs::R1,R2 | 高 |
| P5 | 定时驱动:EVENT_TIMER 计数 N 次触发任务(paperaccount process_timer_event;xt_gateway timer) | tests/usage_timer.rs::T1,T2 | 高 |
| P6 | 双实例并存互不串扰:LockTraderApiEngine 自建第二个 EventEngine(trader/engine.py:2034) | tests/usage_shared.rs::S3 | 中 |
| P7 | 阶段哨兵事件:CONTRACT_FINISH→CONTRACTS_READY、POSITION_QUERY_DONE(data=gateway_name)、下载 progress→complete 成对事件 | tests/usage_sentinel.rs::E1,E2 | 中 |
| P8 | 批量事件(data 为 list):eTickStockBatch 等,BatchBase/_process 分支 | tests/usage_batch.rs::B1 | 中 |
| P9 | 逐品种订阅:register(EVENT_TICK + en_symbol)(optionmaster/easyalgotrading) | tests/usage_gateway.rs::G2(与 P1 合并) | 中 |
| P10 | 极限吞吐 + 多生产者:行情洪峰(多线程 × 大量 tick)不丢不重 | tests/usage_stress.rs::X1,X2 | 中(压测在 benchmarks 落地前先用冒烟阈值) |
## 1. 分测试文件与用例明细
约定:全部放 `ek-event/tests/`(integration test,只用公共 API,顺带验证导出面);
测试内定义 `TickMsg/OrderMsg/ContractMsg` 等本地结构体模拟 `TickData/OrderData/ContractData`。
### tests/usage_gateway.rs —— Gateway 投递模式(P1/P9)
- **G1 聚合 + 逐品种双投**:
模拟 `on_tick`:先 `put(Event("eTick.", tick))`,再 `put(Event("eTick.{symbol}", tick))`。
OMS 角色 handler 注册 `"eTick."`,Panel 角色 handler 注册 `"eTick.600000"`。
断言:两个 handler 都收到同一条 tick;只换品种时逐品种 handler 不触发其它品种。
- **G2 逐订单/逐账户 + 生命周期注销**:
`on_order` 双投到 `"eOrder."` 与 `"eOrder.{orderid}"`;逐 id handler 用后 `unregister(id)`,
后续同 id 的订单不再到达(对齐 UI 关闭即注销)。
- **G3 put-before-register 顺序**:
MainEngine 顺序是 `start()` → 之后各组件 `register()`(trader/engine.py:128-168)。
断言:先 start、后注册,注册之后发出的事件必达;注册之前已在队列中的事件不被新 handler 收到(公平性,非强求,断言"不崩溃+新事件可达"即可)。
### tests/usage_shared.rs —— 多引擎共享(P2/P6)
- **S1 四组件共享一个 Arc<EventEngine>**:
gateway 线程(生产)、oms handler、risk handler、rpc 转发器 handler 同时工作 2 秒;
断言:各组件各自收齐自己该看的全部事件,总数守恒。
- **S2 并发注册/注销风暴**:
4 线程 × 循环 register/unregister 不同 type 与 general,同时另一线程洪峰 put;
无死锁、无 panic、分发线程存活。结束条件:所有线程 join + 排空。
- **S3 双实例隔离**:
两个 EventEngine 实例(模拟 LockTraderApiEngine 自建 ee),
A 注册的事件 B 收不到;各自 stop 互不影响。
### tests/usage_lifecycle.rs —— 注册/注销生命周期(P3)
- **L1 open→close→open**:
UI 组件:注册 handler(id 保存)→ 泄 3 个事件(全部到达)→ unregister → 再泄 3 个(零到达)
→ 重新注册(新 id)→ 再泄 3 个(全部到达)。
- **L2 general 注册同理**:
rpc 服务重启语义:unregister_general 后事件停止转发,重注册后恢复。
- **L3 handler 自注销不许死锁**:
handler 内部调用 unregister(注销自己)→ 后续事件不再调用它,且引擎继续正常分发其余类型(原版 vn.py 常见"条件注销"写法)。
### tests/usage_rpc.rs —— 全事件转发器(P4)
- **R1 general 转发顺序 + 过滤**:
转发器 register_general;交替泄 tick/order/trade/log(混杂不同 type);
断言:收到顺序 == put 顺序(跨 type 也保序,这是 single-dispatcher 的核心卖点);
payload 的 `gateway_name != "hedge"` 的跳过(复刻 process_event 过滤);
`data 为空(无载荷事件)`跳过(复刻 `if not data: return`)。
- **R2 泄 ORDER 也通知 general**:
同一事件 type handler 与 general handler 均达(达序:先 type 后 general,补一条断言)。
### tests/usage_timer.rs —— 定时驱动(P5)
- **T1 计数器节流**(paperaccount 模式):
handler 内 `count+=1; if count < N: return; N 次才真正干活`;
interval=50ms 跑 400ms,N=3:干活次数 ≈ floor(收到次数/3),断言 ≥ 2 且 ≤ 预期上限。
- **T2 timer 与业务事件交织保序**:
交替泄 tick 与等 timer;快照 handler 记录到达次序,断言"timer 与 tick 到达序列非乱序、无丢"。
### tests/usage_sentinel.rs —— 阶段哨兵事件(P7)
- **E1 成对事件流水线**:
模拟下载流程:每类资产 progress×n → complete;汇总 handler 等 `all complete` 后发 "all done"。
断言:所有 complete 都在 all-done 之前(顺序)。
- **E2 无载荷哨兵**:
`Event("eContractFinish")`(data=())与 `Event("ePositionDone", gateway_name 字符串)`
两种形态都能被对应 handler 收到且 data 可正确 downcast。
### tests/usage_batch.rs —— 批量事件(P8)
- **B1 list 载荷分发**:
`Event("eTickStockBatch", Arc::new(vec![tick;500]))`,handler 遍历逐条入库计数;
断言 500 条全收、顺序与入队一致(一个事件内的迭代顺序)。
### tests/usage_stress.rs —— 洪峰冒烟(P10)
- **X1 单线程洪峰**:10 万 tick/双投(聚合+逐品种)== 20 万 put,wait 排空 ≤ 5s(阈值放宽为环境噪声,严目标是 ≥100 万/秒,后续 criterion);零丢失。
- **X2 多线程洪峰**:8 线程 × 各 5 万 put,总数 40 万,排空后计数精确等于 40 万(不丢不重);
同时并发 register/unregister 若干次(与 S2 叠加)。
## 2. 通过标准与验收
| 全组 | `cargo test` 无 fail、无 flaky(连跑 10 次)。定时类测试用宽阈值,避免 CI 计时抖动 |
| 功能组(G/S/L/R/E/B) | 语义级精确断言(次数、顺序、正确去重) |
| 压测组(X) | 仅守"不丢不重+排空",性能阈值留给 criterion 基准(DESIGN.md §7) |
| 工程卫生 | `cargo clippy -- -D warnings` 干净;`cargo test -- --nocapture` 无多余输出 |
## 3. 实施顺序(建议两轮)
1. **第一轮(语义正确性)**:G1/G2、S1/S3、L1/L2、R1、E1/E2、B1、T1 —— 覆盖全部 10 类模式 без 压测,~12 个用例。
2. **第二轮(压力与鲁棒)**:S2(注册风暴)、X1/X2(洪峰)、L3(自注销)、T2(交织保序)、G3(先 start 后注册)。
## 4. 风险与说明
- **本轮测试专属语义测试**(本地结构体)只写作练习,禁止把业务结构体沉入 ek-event —— 边界承诺见 DESIGN.md §1。
- 定时类用例在 Windows 计时器精度波动下可能慢于 interval,统一用 `wait_for` 宽阈值轮询,避免 sleep-to-assert。
- 压测若显现性能不足(例如 general 快照克隆开销),只对 `HandlerRegistry::snapshot` 做预克隆缓存优化,不改公共 API。
## 5. 执行结果(已全部落地)
- **19 个测试函数 / 27 个断言组,9 个测试二进制全部通过;cargo clippy --all-targets -- -D warnings 零告警;10 连跑零失败。**
- X1 实测:debug 构建下 20 万事件(含 10 万逐品种注册 type 查询)分发 < 1s,冒烟达标。
### 执行中发现并修正的问题
| 1 | src/handler.rs | `Table` derive Debug,但 `Registered` 含 `Arc<dyn Fn>` 不可 derive | 去掉 Table/HandlerRegistry 的 Debug derive |
| 2 | src/handler.rs/engine.rs | `register(event_type: &str)` 使生态层 `format!("e…")` 注册别扭 | 签名放宽为 `impl Into<String>`(对齐原版 str 语义) |
| 3 | lib.rs T3/T5 | 我误把"聚合事件"理解成前缀匹配:`"eTick."` 是**精确 type 字符串**(对齐 enkeep gateway `on_event(EVENT_TICK, tick)`),put `"eTick.600000"` 属于另一个 type | 测试改为 put `"eTick."`;此语义已在 gateway G1/S3 用例中按正确形态覆盖 |
| 4 | handler.rs T2 用例 | 断言"空表回收"时第二 handler 尚未注销,顺序错误 | 补 `unregister(id2)` |
| 5 | usage_timer T1 | `fires` 变量身兼"总量"与"节流计数"两职,总量被 store(0) 重置 | 拆分为独立 total/throttle 两个计数器 |
| 6 | usage_timer T2 | `for i in 0..10` 推断为 i32,与 downcast::<usize> 不匹配报 panic | 显式 `0..10usize` |
| 7 | usage_shared S3 | 与 #3 同根因;且跨引擎隔离验证不够强 | 改为 put `"eTick."` + 载荷引擎编号,断言"自家计数+跨泄漏为 0" |
| 8 | 告警清理 | dead_code / unused import / 未用变量等 10 处 | allow(dead_code) 语义注释 + 删除死代码,clippy --all-targets -- -D warnings 零告警 |
### 关键语义结论(来自真实使用模式验证)
1. **聚合事件 type 就是 `"eTick."` 字符串本身**;后缀 type(`"eTick.<symbol>"`)是完全独立的 key,不存在前缀匹配 —— OmsEngine 收聚合、面板收具体,两者并行不悖。
2. 未启动时 `put()` 合法入队(start 后分发);已注册已消费的事件不回放给后来注册的 handler(G3)。
3. handler 内部自注销安全(L3);跨 engine 实例零串扰(S3);20k 事件并发注册风暴下 general 转发零丢失(S2);40 万事件多线程洪峰零丢零重(X2)。