WechatOnCloud/bridge/tests/test_ui_action_scheduler.py
Kris ed2ee086de refactor: 重构UI操作调度,引入优先级调度与监控
主要变更:
1. 新增UIActionScheduler实现优先级UI任务调度,支持CRITICAL/HIGH/NORMAL/LOW四级优先级
2. 替换原有SendQueue为UIActionScheduler,统一所有UI操作的调度逻辑
3. 为截图、登录、重启等API添加调度器封装,支持超时、限流与fast-fail机制
4. 新增Prometheus监控指标,统计UI任务执行、等待耗时、超时、限流与快速失败情况
5. 为QrCapture添加命令执行超时保护,避免X11操作卡死
6. 扩展StatusResponse与状态接口,暴露调度器指标
7. 新增完整的单元测试覆盖调度器功能
8. 为看门狗注入调度器,实现kill前队列清空与快速失败窗口
2026-07-18 03:43:28 +08:00

252 lines
8.8 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.

"""UIActionScheduler 单元测试。
覆盖:
- 向后兼容 enqueue
- 优先级排序CRITICAL 优先于 NORMAL
- drain空队列 / 有 pending / 超时)
- 队列满抛 RATE_LIMITED
- 等待超时抛 TIMEOUT
- metrics 准确性
- fast-fail 机制mark_wechat_dead 后非 CRITICAL 抛 WECHAT_NOT_READY
- CRITICAL 任务在 fast-fail 窗口内正常执行
- CRITICAL 任务自动 delay_ms=0
"""
from __future__ import annotations
import asyncio
import os
import sys
import time
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from woc_bridge.messaging.ui_action_scheduler import UIActionScheduler, Priority
from woc_bridge.models import BridgeError
pytestmark = pytest.mark.asyncio
async def test_backward_compat_enqueue():
"""向后兼容 enqueue以 NORMAL 优先级入队并返回结果。"""
scheduler = UIActionScheduler(send_delay_ms=10, max_calls_per_sec=100)
await scheduler.start()
try:
result = await scheduler.enqueue(lambda: asyncio.sleep(0.01, result="ok"))
assert result == "ok"
metrics = scheduler.get_metrics()
assert metrics["executed_total"] == 1
assert metrics["executed_by_priority"]["NORMAL"] == 1
finally:
await scheduler.stop()
async def test_priority_ordering():
"""CRITICAL 任务应先于后入队的 NORMAL 任务开始执行。"""
scheduler = UIActionScheduler(send_delay_ms=0, max_calls_per_sec=100)
await scheduler.start()
try:
start_times: list[tuple[str, str]] = []
async def _track(result: str, delay: float = 0.01) -> str:
start_times.append(("start", result))
await asyncio.sleep(delay)
return result
# 第一个 NORMAL 立即开始执行
normal_task = asyncio.create_task(
scheduler.enqueue(lambda: _track("normal", 0.1))
)
await asyncio.sleep(0.02) # 等第一个开始
critical_task = asyncio.create_task(
scheduler.execute(
lambda: _track("critical", 0.01), priority=Priority.CRITICAL
)
)
normal2_task = asyncio.create_task(
scheduler.execute(
lambda: _track("normal2", 0.01), priority=Priority.NORMAL
)
)
critical_result = await critical_task
normal2_result = await normal2_task
normal_result = await normal_task
assert critical_result == "critical"
assert normal2_result == "normal2"
assert normal_result == "normal"
# CRITICAL 应在 normal2 之前开始
critical_idx = next(i for i, (_, r) in enumerate(start_times) if r == "critical")
normal2_idx = next(i for i, (_, r) in enumerate(start_times) if r == "normal2")
assert critical_idx < normal2_idx, (
f"CRITICAL 应在 NORMAL2 之前执行,实际顺序: {start_times}"
)
finally:
await scheduler.stop()
async def test_drain_empty_queue():
"""空队列 drain 应立即返回 True。"""
scheduler = UIActionScheduler()
result = await scheduler.drain(timeout=1.0)
assert result is True
async def test_drain_with_pending():
"""有 pending 任务的队列 drain 应等待任务完成。"""
scheduler = UIActionScheduler(send_delay_ms=10)
await scheduler.start()
try:
await scheduler.enqueue(lambda: asyncio.sleep(0.05, result="ok"))
result = await scheduler.drain(timeout=2.0)
assert result is True
await asyncio.sleep(0.01) # 让 worker 完全进入 idle
finally:
await scheduler.stop()
async def test_drain_timeout():
"""长任务未完成时 drain 应超时返回 False。"""
scheduler = UIActionScheduler(send_delay_ms=0)
await scheduler.start()
try:
# 入队长任务但不 await避免阻塞测试
asyncio.create_task(
scheduler.enqueue(lambda: asyncio.sleep(1.0, result="slow"))
)
await asyncio.sleep(0.05) # 让任务开始执行
result = await scheduler.drain(timeout=0.1)
assert result is False
finally:
await scheduler.stop()
async def test_queue_full_rate_limited():
"""队列满时入队应抛 RATE_LIMITED。"""
scheduler = UIActionScheduler(send_delay_ms=1000, max_queue_size=2)
await scheduler.start()
try:
# 第一个任务开始执行worker 取走后队列腾出空位)
asyncio.create_task(scheduler.enqueue(lambda: asyncio.sleep(0.5)))
await asyncio.sleep(0.05) # 让 worker 取走第一个任务并开始执行
# 再入队 2 个排队任务队列满2 个 pending
asyncio.create_task(scheduler.enqueue(lambda: asyncio.sleep(0.01)))
asyncio.create_task(scheduler.enqueue(lambda: asyncio.sleep(0.01)))
await asyncio.sleep(0.02) # 确保两个任务都入队
# 第 3 个排队任务应被拒绝(队列已满)
with pytest.raises(BridgeError) as exc_info:
await scheduler.enqueue(lambda: asyncio.sleep(0.01))
assert exc_info.value.code == "RATE_LIMITED"
finally:
await scheduler.stop()
async def test_wait_timeout():
"""等待结果超时应抛 TIMEOUT。"""
scheduler = UIActionScheduler(send_delay_ms=1000, max_calls_per_sec=100)
await scheduler.start()
try:
# 第一个任务慢(执行 + 默认延时会让第二个等待很久)
asyncio.create_task(scheduler.enqueue(lambda: asyncio.sleep(0.5)))
await asyncio.sleep(0.02) # 让第一个开始
with pytest.raises(BridgeError) as exc_info:
await scheduler.enqueue(
lambda: asyncio.sleep(0.01), wait_timeout_ms=100
)
assert exc_info.value.code == "TIMEOUT"
finally:
await scheduler.stop()
async def test_metrics_accuracy():
"""metrics 应准确统计各优先级执行次数。"""
scheduler = UIActionScheduler(send_delay_ms=0, max_calls_per_sec=100)
await scheduler.start()
try:
async def _ret(v):
return v
await scheduler.execute(lambda: _ret("a"), priority=Priority.CRITICAL)
await scheduler.execute(lambda: _ret("b"), priority=Priority.HIGH)
await scheduler.execute(lambda: _ret("c"), priority=Priority.NORMAL)
await scheduler.execute(lambda: _ret("d"), priority=Priority.LOW)
metrics = scheduler.get_metrics()
assert metrics["executed_total"] == 4
assert metrics["executed_by_priority"]["CRITICAL"] == 1
assert metrics["executed_by_priority"]["HIGH"] == 1
assert metrics["executed_by_priority"]["NORMAL"] == 1
assert metrics["executed_by_priority"]["LOW"] == 1
assert metrics["pending_count"] == 0
finally:
await scheduler.stop()
async def test_fast_fail_wechat_dead():
"""mark_wechat_dead 后非 CRITICAL 任务应 fast-fail 抛 WECHAT_NOT_READY。"""
scheduler = UIActionScheduler(send_delay_ms=0, max_calls_per_sec=100)
await scheduler.start()
try:
scheduler.mark_wechat_dead(2.0) # 启动 2 秒 fast-fail 窗口
with pytest.raises(BridgeError) as exc_info:
await scheduler.execute(
lambda: asyncio.sleep(0.01, result="ok"),
priority=Priority.NORMAL,
)
assert exc_info.value.code == "WECHAT_NOT_READY"
metrics = scheduler.get_metrics()
assert metrics["fast_fail_total"] == 1
finally:
await scheduler.stop()
async def test_critical_bypasses_fast_fail():
"""CRITICAL 任务在 fast-fail 窗口内应正常执行。"""
scheduler = UIActionScheduler(send_delay_ms=0, max_calls_per_sec=100)
await scheduler.start()
try:
scheduler.mark_wechat_dead(2.0) # 启动 fast-fail 窗口
result = await scheduler.execute(
lambda: asyncio.sleep(0.01, result="critical_ok"),
priority=Priority.CRITICAL,
)
assert result == "critical_ok"
metrics = scheduler.get_metrics()
assert metrics["executed_total"] == 1
assert metrics["executed_by_priority"]["CRITICAL"] == 1
assert metrics["fast_fail_total"] == 0
finally:
await scheduler.stop()
async def test_critical_auto_delay_zero():
"""CRITICAL 任务应自动 delay_ms=0不受默认 send_delay_ms 影响。"""
scheduler = UIActionScheduler(send_delay_ms=5000, max_calls_per_sec=100)
await scheduler.start()
try:
async def _ret(v):
return v
t0 = time.monotonic()
await scheduler.execute(lambda: _ret("first"), priority=Priority.CRITICAL)
await scheduler.execute(lambda: _ret("second"), priority=Priority.CRITICAL)
elapsed = time.monotonic() - t0
# 若 delay_ms=5000 生效,第二个任务会等 5s。自动 delay=0 后应在 1s 内完成
assert elapsed < 1.5, f"CRITICAL 自动 delay=0 失效,耗时 {elapsed:.2f}s"
finally:
await scheduler.stop()