Files
kefu/wechat_rpa/tmp/test_engine_b_modes.py
T
2026-08-27 14:04:28 +08:00

152 lines
4.9 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.
# -*- coding: utf-8 -*-
"""引擎 B 数据源模式单测:parallel / db / json 三模式调度 + DB 故障隔离。
验证点:
1. json 模式:每轮只跑 conversations.jsonpoll_once),不碰 DB
2. db 模式(有 db_source):只跑 DB 直读,不碰 JSON;
3. db 模式(无 db_source):自动退化为 JSON,避免静默无检测;
4. parallel 模式:两路每轮都跑,互为兜底;
5. DB 故障隔离:get_new_messages 抛异常 → db_active=False
JSON 路径下一轮仍独立检测投递;DB 恢复后 db_active 回 True。
"""
from __future__ import annotations
import json
import os
import sys
import tempfile
import threading
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from engine_b import DataEngine # noqa: E402
FAILED: list[str] = []
def check(name: str, cond: bool, detail: str = "") -> None:
tag = "PASS" if cond else "FAIL"
print(f"[{tag}] {name}" + (f" ({detail})" if detail else ""))
if not cond:
FAILED.append(name)
class CountingEngine(DataEngine):
"""重写两路检测方法为计数器,验证 _run 的调度分支。"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.poll_once_calls = 0
self.db_once_calls = 0
self.poll_lock = threading.Lock()
def poll_once(self):
with self.poll_lock:
self.poll_once_calls += 1
def _poll_db_once(self):
with self.poll_lock:
self.db_once_calls += 1
class StubDB:
def __init__(self, rows=None, fail=False):
self.rows = rows or []
self.fail = fail
self.calls = 0
def get_new_messages(self, since_ts):
self.calls += 1
if self.fail:
raise RuntimeError("DB 解密失败")
return self.rows
def run_rounds(engine: DataEngine, seconds: float = 1.5) -> None:
engine.start()
time.sleep(seconds)
engine.stop()
# ---------- 1/2/4. 三模式调度 ----------
mode_cases = [
("json", {}, {"poll": 2, "db": 0}),
("db", {"db_source": StubDB()}, {"poll": 0, "db": 2}),
("parallel", {"db_source": StubDB()}, {"poll": 2, "db": 2}),
]
for mode, kw, expect in mode_cases:
eng = CountingEngine(bot=object(), conversations_path="/nonexistent.json",
poll_interval=0.4, **kw)
eng.data_source_mode = mode
run_rounds(eng, seconds=1.3)
check(f"[{mode}] 每轮调用 poll_once 次数符合预期",
eng.poll_once_calls >= expect["poll"],
f"got {eng.poll_once_calls}")
check(f"[{mode}] 每轮调用 _poll_db_once 次数符合预期",
eng.db_once_calls >= expect["db"],
f"got {eng.db_once_calls}")
# ---------- 3. db 模式无数据源 → 退化为 JSON ----------
eng = CountingEngine(bot=object(), conversations_path="/nonexistent.json",
poll_interval=0.4, db_source=None)
eng.data_source_mode = "db"
run_rounds(eng, seconds=1.3)
check("[db-无数据源] 退化为 JSON 检测", eng.poll_once_calls >= 2,
f"poll_once={eng.poll_once_calls}")
# ---------- 5. DB 故障隔离与恢复 ----------
now = time.time()
fp = "f" * 40
conversations = {
fp: {
"display_name": "隔离测试客户",
"history": [{"role": "user", "content": "DB 挂了不影响我", "ts": now - 3}],
"last_lines": ["DB 挂了不影响我"],
}
}
tmp_json = os.path.join(tempfile.gettempdir(), "engine_b_mode_test.json")
with open(tmp_json, "w", encoding="utf-8") as handle:
json.dump(conversations, handle, ensure_ascii=False)
class StubBot:
def __init__(self):
self.enqueued = []
def has_active_pending(self, fp_hex):
return False
def enqueue_detected(self, **kwargs):
self.enqueued.append(kwargs)
return True, "ok"
bot = StubBot()
db = StubDB(fail=True)
eng = DataEngine(bot=bot, conversations_path=tmp_json,
poll_interval=0.4, db_source=db, data_source_mode="parallel")
eng._poll_db_once()
check("DB 异常 → db_active=False", eng.db_active is False,
f"db_active={eng.db_active}")
check("DB 异常 → last_error 记录", "DB 直读失败" in eng.last_error,
eng.last_error)
eng.poll_once()
check("DB 异常后 JSON 路径仍独立投递", bot.enqueued and eng.enqueued_count == 1,
f"enqueued={len(bot.enqueued)}")
check("DB 异常不传播到 _run(线程安全)",
True) # 上面的直接调用已证明 _poll_db_once 自吞异常
# DB 恢复
db.fail = False
eng._poll_db_once()
check("DB 恢复 → db_active 回 True", eng.db_active is True,
f"db_active={eng.db_active}")
# 非法模式回落
eng2 = DataEngine(bot=bot, conversations_path=tmp_json,
poll_interval=0.5, data_source_mode="garbage")
check("非法模式回落 parallel", eng2.data_source_mode == "parallel",
eng2.data_source_mode)
print()
if FAILED:
print(f"RESULT: FAIL ({len(FAILED)}) -> {FAILED}")
sys.exit(1)
print("RESULT: ALL PASS")