272 lines
8.8 KiB
Python
272 lines
8.8 KiB
Python
"""进度监控测试。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
import pyflowx as px
|
|
from pyflowx.progress import ProgressCallback, RichProgressMonitor
|
|
from pyflowx.task import TaskEvent, TaskStatus
|
|
|
|
|
|
# ---------------------------------------------------------------------- #
|
|
# 测试用监控器
|
|
# ---------------------------------------------------------------------- #
|
|
class _RecordingMonitor:
|
|
"""记录所有事件的测试用监控器。"""
|
|
|
|
def __init__(self) -> None:
|
|
self.events: list[TaskEvent] = []
|
|
self.started_total: int | None = None
|
|
self.finished: bool = False
|
|
|
|
def start(self, total: int) -> None:
|
|
self.started_total = total
|
|
|
|
def on_event(self, event: TaskEvent) -> None:
|
|
self.events.append(event)
|
|
|
|
def finish(self) -> None:
|
|
self.finished = True
|
|
|
|
|
|
# ---------------------------------------------------------------------- #
|
|
# ProgressCallback Protocol
|
|
# ---------------------------------------------------------------------- #
|
|
def test_progress_callback_protocol_satisfied() -> None:
|
|
"""_RecordingMonitor 满足 ProgressCallback 协议。"""
|
|
monitor = _RecordingMonitor()
|
|
assert isinstance(monitor, ProgressCallback)
|
|
|
|
|
|
def test_rich_progress_monitor_satisfies_protocol() -> None:
|
|
"""RichProgressMonitor 满足 ProgressCallback 协议。"""
|
|
assert isinstance(RichProgressMonitor(), ProgressCallback)
|
|
|
|
|
|
# ---------------------------------------------------------------------- #
|
|
# RichProgressMonitor
|
|
# ---------------------------------------------------------------------- #
|
|
def test_rich_monitor_start_finish_lifecycle() -> None:
|
|
"""start/finish 生命周期正常。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.start(5)
|
|
assert monitor._total == 5
|
|
assert monitor._task_id is not None
|
|
monitor.finish()
|
|
|
|
|
|
def test_rich_monitor_on_event_running() -> None:
|
|
"""RUNNING 事件将任务加入 running 集合。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.start(3)
|
|
monitor.on_event(TaskEvent(task="a", status=TaskStatus.RUNNING))
|
|
assert "a" in monitor._running
|
|
monitor.finish()
|
|
|
|
|
|
def test_rich_monitor_on_event_success_advances() -> None:
|
|
"""SUCCESS 事件推进进度并从 running 移除。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.start(2)
|
|
monitor.on_event(TaskEvent(task="a", status=TaskStatus.RUNNING))
|
|
monitor.on_event(TaskEvent(task="a", status=TaskStatus.SUCCESS, duration=0.1))
|
|
assert "a" not in monitor._running
|
|
assert monitor._completed == 1
|
|
monitor.finish()
|
|
|
|
|
|
def test_rich_monitor_on_event_failed_advances() -> None:
|
|
"""FAILED 事件也推进进度。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.start(1)
|
|
monitor.on_event(TaskEvent(task="a", status=TaskStatus.FAILED, error="boom"))
|
|
assert monitor._completed == 1
|
|
monitor.finish()
|
|
|
|
|
|
def test_rich_monitor_on_event_skipped_advances() -> None:
|
|
"""SKIPPED 事件也推进进度。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.start(1)
|
|
monitor.on_event(TaskEvent(task="a", status=TaskStatus.SKIPPED, reason="缓存命中"))
|
|
assert monitor._completed == 1
|
|
monitor.finish()
|
|
|
|
|
|
def test_rich_monitor_context_manager() -> None:
|
|
"""__enter__/__exit__ 正常工作。"""
|
|
with RichProgressMonitor() as monitor:
|
|
monitor.start(1)
|
|
monitor.on_event(TaskEvent(task="a", status=TaskStatus.SUCCESS))
|
|
# 退出后不再报错
|
|
|
|
|
|
def test_rich_monitor_on_event_before_start() -> None:
|
|
"""start 前调用 on_event 不报错(task_id 为 None 时早返回)。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.on_event(TaskEvent(task="a", status=TaskStatus.SUCCESS))
|
|
# 未 start 时 advance 不执行,_update_description 早返回
|
|
assert monitor._completed == 1
|
|
assert monitor._task_id is None
|
|
|
|
|
|
def test_rich_monitor_pending_event_noop() -> None:
|
|
"""PENDING 事件不是 RUNNING 也不是终态,应被忽略。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.start(2)
|
|
monitor.on_event(TaskEvent(task="a", status=TaskStatus.PENDING))
|
|
assert monitor._completed == 0
|
|
assert len(monitor._running) == 0
|
|
monitor.finish()
|
|
|
|
|
|
def test_rich_monitor_update_description_running() -> None:
|
|
"""运行中时描述应包含任务名。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.start(5)
|
|
monitor.on_event(TaskEvent(task="task_a", status=TaskStatus.RUNNING))
|
|
monitor.on_event(TaskEvent(task="task_b", status=TaskStatus.RUNNING))
|
|
# 无法直接断言 rich 内部状态,但 _running 应包含两个任务
|
|
assert len(monitor._running) == 2
|
|
monitor.finish()
|
|
|
|
|
|
def test_rich_monitor_update_description_many_running_folds() -> None:
|
|
"""超过 3 个运行中任务时描述应折叠。"""
|
|
monitor = RichProgressMonitor()
|
|
monitor.start(10)
|
|
for i in range(5):
|
|
monitor.on_event(TaskEvent(task=f"task_{i}", status=TaskStatus.RUNNING))
|
|
assert len(monitor._running) == 5
|
|
# 不报错即可(描述更新逻辑内部处理折叠)
|
|
monitor.finish()
|
|
|
|
|
|
# ---------------------------------------------------------------------- #
|
|
# run() 集成测试
|
|
# ---------------------------------------------------------------------- #
|
|
def _make_simple_graph() -> px.Graph:
|
|
"""构建简单测试图。"""
|
|
|
|
def task_a() -> int:
|
|
return 1
|
|
|
|
def task_b(task_a: int) -> int:
|
|
return task_a + 1
|
|
|
|
return px.Graph.from_specs(
|
|
[
|
|
px.TaskSpec("task_a", task_a),
|
|
px.TaskSpec("task_b", task_b, depends_on=("task_a",)),
|
|
]
|
|
)
|
|
|
|
|
|
def test_run_with_progress_true() -> None:
|
|
"""progress=True 使用 RichProgressMonitor 不报错。"""
|
|
graph = _make_simple_graph()
|
|
report = px.run(graph, strategy="sequential", progress=True)
|
|
assert report.success
|
|
assert report["task_b"] == 2
|
|
|
|
|
|
def test_run_with_custom_monitor() -> None:
|
|
"""progress=自定义监控器,事件被正确捕获。"""
|
|
graph = _make_simple_graph()
|
|
monitor = _RecordingMonitor()
|
|
report = px.run(graph, strategy="sequential", progress=monitor)
|
|
assert report.success
|
|
assert monitor.started_total == 2
|
|
assert monitor.finished
|
|
# 至少有 RUNNING + SUCCESS 事件
|
|
statuses = [e.status for e in monitor.events]
|
|
assert TaskStatus.RUNNING in statuses
|
|
assert TaskStatus.SUCCESS in statuses
|
|
|
|
|
|
def test_run_with_monitor_and_on_event_coexist() -> None:
|
|
"""progress 监控器与 on_event 回调可共存。"""
|
|
graph = _make_simple_graph()
|
|
monitor = _RecordingMonitor()
|
|
on_event_events: list[TaskEvent] = []
|
|
|
|
def capture(event: TaskEvent) -> None:
|
|
on_event_events.append(event)
|
|
|
|
report = px.run(
|
|
graph,
|
|
strategy="sequential",
|
|
progress=monitor,
|
|
on_event=capture,
|
|
)
|
|
assert report.success
|
|
# 两者都应收到事件
|
|
assert len(monitor.events) > 0
|
|
assert len(on_event_events) > 0
|
|
assert len(monitor.events) == len(on_event_events)
|
|
|
|
|
|
def test_run_with_monitor_finish_called_on_failure() -> None:
|
|
"""任务失败时 monitor.finish 仍被调用。"""
|
|
|
|
def fail_task() -> Any:
|
|
raise RuntimeError("boom")
|
|
|
|
graph = px.Graph.from_specs([px.TaskSpec("fail", fail_task)])
|
|
monitor = _RecordingMonitor()
|
|
with pytest.raises(px.TaskFailedError):
|
|
px.run(graph, strategy="sequential", progress=monitor)
|
|
assert monitor.finished
|
|
assert monitor.started_total == 1
|
|
|
|
|
|
def test_run_progress_false_no_monitor() -> None:
|
|
"""progress=False 不创建监控器,行为不变。"""
|
|
graph = _make_simple_graph()
|
|
report = px.run(graph, strategy="sequential", progress=False)
|
|
assert report.success
|
|
|
|
|
|
def test_run_progress_with_verbose_coexist() -> None:
|
|
"""progress 与 verbose 可同时使用。"""
|
|
graph = _make_simple_graph()
|
|
monitor = _RecordingMonitor()
|
|
report = px.run(graph, strategy="sequential", progress=monitor, verbose=True)
|
|
assert report.success
|
|
assert monitor.finished
|
|
|
|
|
|
def test_run_progress_with_thread_strategy() -> None:
|
|
"""progress 在 thread 策略下正常工作。"""
|
|
graph = _make_simple_graph()
|
|
monitor = _RecordingMonitor()
|
|
report = px.run(graph, strategy="thread", progress=monitor)
|
|
assert report.success
|
|
assert monitor.finished
|
|
|
|
|
|
def test_run_progress_with_dependency_strategy() -> None:
|
|
"""progress 在 dependency 策略下正常工作。"""
|
|
graph = _make_simple_graph()
|
|
monitor = _RecordingMonitor()
|
|
report = px.run(graph, strategy="dependency", progress=monitor)
|
|
assert report.success
|
|
assert monitor.finished
|
|
|
|
|
|
def test_run_progress_with_async_strategy() -> None:
|
|
"""progress 在 async 策略下正常工作。"""
|
|
|
|
async def atask() -> int:
|
|
return 42
|
|
|
|
graph = px.Graph.from_specs([px.TaskSpec("atask", atask)])
|
|
monitor = _RecordingMonitor()
|
|
report = px.run(graph, strategy="async", progress=monitor)
|
|
assert report.success
|
|
assert monitor.finished
|
|
assert monitor.started_total == 1
|