在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,186 @@
# -*- coding: utf-8 -*-
"""WebSocket 日志推送端点测试
测试 /ws/logs/{execution_id} 端点的连接、日志回放、实时推送和断开行为。
利用 TaskExecutor 已有的 subscribe/broadcast 机制进行验证。
"""
from __future__ import annotations
import asyncio
import pytest
from starlette.testclient import TestClient
from starlette.websockets import WebSocketDisconnect
from app.main import app
from app.services.task_executor import task_executor
@pytest.fixture(autouse=True)
def _cleanup_executor():
"""每个测试前后清理 TaskExecutor 内部状态。"""
yield
# 清理所有残留的缓冲区和订阅者
for eid in list(task_executor._log_buffers.keys()):
task_executor.cleanup(eid)
task_executor._subscribers.clear()
task_executor._log_buffers.clear()
class TestWebSocketConnection:
"""WebSocket 连接/断开基本行为"""
def test_connect_and_disconnect(self):
"""客户端能成功建立和关闭 WebSocket 连接。"""
client = TestClient(app)
with client.websocket_connect("/ws/logs/test-exec-001") as ws:
# 连接成功,直接关闭
pass # __exit__ 会关闭连接
def test_connect_registers_subscriber(self):
"""连接后 TaskExecutor 应注册订阅者。"""
client = TestClient(app)
# 预先初始化缓冲区(模拟有任务在运行)
task_executor._log_buffers["test-exec-002"] = []
with client.websocket_connect("/ws/logs/test-exec-002"):
# 连接期间应有订阅者
assert "test-exec-002" in task_executor._subscribers
assert len(task_executor._subscribers["test-exec-002"]) >= 1
class TestLogReplay:
"""历史日志回放"""
def test_replay_existing_logs(self):
"""连接时应先收到内存缓冲区中已有的日志行。"""
eid = "test-exec-replay"
# 预填充日志缓冲区
task_executor._log_buffers[eid] = [
"[stdout] 第一行",
"[stdout] 第二行",
"[stderr] 警告信息",
]
client = TestClient(app)
with client.websocket_connect(f"/ws/logs/{eid}") as ws:
# 应按顺序收到 3 条历史日志
msg1 = ws.receive_text()
msg2 = ws.receive_text()
msg3 = ws.receive_text()
assert msg1 == "[stdout] 第一行"
assert msg2 == "[stdout] 第二行"
assert msg3 == "[stderr] 警告信息"
def test_no_logs_no_replay(self):
"""没有历史日志时不应收到回放消息。"""
eid = "test-exec-empty"
task_executor._log_buffers[eid] = []
client = TestClient(app)
with client.websocket_connect(f"/ws/logs/{eid}") as ws:
# 发送结束信号让连接正常关闭
task_executor._broadcast_end(eid)
# 不应有回放消息,直接收到的是后续的结束信号处理
class TestBroadcastReceive:
"""实时日志广播接收"""
def test_receive_broadcast_messages(self):
"""连接后应能收到 TaskExecutor 广播的实时日志。"""
eid = "test-exec-broadcast"
task_executor._log_buffers[eid] = []
client = TestClient(app)
with client.websocket_connect(f"/ws/logs/{eid}") as ws:
# 模拟 TaskExecutor 广播日志
task_executor._broadcast(eid, "[stdout] 实时日志行1")
task_executor._broadcast(eid, "[stderr] 实时错误行")
msg1 = ws.receive_text()
msg2 = ws.receive_text()
assert msg1 == "[stdout] 实时日志行1"
assert msg2 == "[stderr] 实时错误行"
def test_end_signal_closes_connection(self):
"""收到 None 结束信号后 WebSocket 应正常关闭。"""
eid = "test-exec-end"
task_executor._log_buffers[eid] = []
client = TestClient(app)
with client.websocket_connect(f"/ws/logs/{eid}") as ws:
# 广播一条日志后发送结束信号
task_executor._broadcast(eid, "[stdout] 最后一行")
task_executor._broadcast_end(eid)
msg = ws.receive_text()
assert msg == "[stdout] 最后一行"
def test_replay_then_broadcast(self):
"""先回放历史日志,再接收实时广播。"""
eid = "test-exec-mixed"
task_executor._log_buffers[eid] = ["[stdout] 历史行"]
client = TestClient(app)
with client.websocket_connect(f"/ws/logs/{eid}") as ws:
# 先收到历史回放
replay = ws.receive_text()
assert replay == "[stdout] 历史行"
# 再收到实时广播
task_executor._broadcast(eid, "[stdout] 新行")
task_executor._broadcast_end(eid)
live = ws.receive_text()
assert live == "[stdout] 新行"
class TestLogBroadcasterUnit:
"""直接测试 TaskExecutor 的 subscribe/unsubscribe/broadcast 方法
(作为 LogBroadcaster 功能的单元测试)。
"""
def test_subscribe_creates_queue(self):
eid = "unit-sub"
q = task_executor.subscribe(eid)
assert isinstance(q, asyncio.Queue)
assert eid in task_executor._subscribers
task_executor.unsubscribe(eid, q)
def test_unsubscribe_removes_queue(self):
eid = "unit-unsub"
q = task_executor.subscribe(eid)
task_executor.unsubscribe(eid, q)
# 最后一个订阅者移除后key 也应被清理
assert eid not in task_executor._subscribers
def test_broadcast_delivers_to_all_subscribers(self):
eid = "unit-multi"
q1 = task_executor.subscribe(eid)
q2 = task_executor.subscribe(eid)
task_executor._broadcast(eid, "测试消息")
assert q1.get_nowait() == "测试消息"
assert q2.get_nowait() == "测试消息"
task_executor.unsubscribe(eid, q1)
task_executor.unsubscribe(eid, q2)
def test_broadcast_end_sends_none(self):
eid = "unit-end"
q = task_executor.subscribe(eid)
task_executor._broadcast_end(eid)
assert q.get_nowait() is None
task_executor.unsubscribe(eid, q)
def test_broadcast_no_subscribers_is_safe(self):
"""没有订阅者时广播不应报错。"""
task_executor._broadcast("nonexistent", "无人接收")
task_executor._broadcast_end("nonexistent")