mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 18:10:37 +08:00
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from astrbot._internal.protocols.lsp.client import AstrbotLspClient
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lsp_reader_task_failure_marks_client_disconnected_and_logs():
|
|
"""Test reader task failures update connection state and are logged."""
|
|
client = AstrbotLspClient()
|
|
fake_process = SimpleNamespace(stdout=MagicMock(), stdin=MagicMock())
|
|
|
|
with (
|
|
patch(
|
|
"astrbot._internal.protocols.lsp.client.anyio.open_process",
|
|
AsyncMock(return_value=fake_process),
|
|
),
|
|
patch.object(
|
|
client,
|
|
"_read_responses",
|
|
AsyncMock(side_effect=RuntimeError("reader crashed")),
|
|
),
|
|
patch.object(client, "send_request", AsyncMock(return_value={})),
|
|
patch.object(client, "send_notification", AsyncMock()),
|
|
patch("astrbot._internal.protocols.lsp.client.log") as mock_log,
|
|
):
|
|
await client.connect_to_server(["python", "fake_lsp.py"], "file:///tmp")
|
|
await asyncio.sleep(0)
|
|
|
|
reader_task = client._reader_task
|
|
assert reader_task is not None
|
|
_ = reader_task.exception()
|
|
await asyncio.sleep(0)
|
|
|
|
assert client.connected is False
|
|
mock_log.error.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lsp_connect_to_server_cancels_previous_reader_task_before_restart():
|
|
"""Test reconnect tears down an existing reader task before replacing it."""
|
|
client = AstrbotLspClient()
|
|
fake_process = SimpleNamespace(stdout=MagicMock(), stdin=MagicMock())
|
|
first_reader_cancelled = asyncio.Event()
|
|
first_reader_started = asyncio.Event()
|
|
|
|
async def first_reader() -> None:
|
|
first_reader_started.set()
|
|
try:
|
|
await asyncio.Event().wait()
|
|
except asyncio.CancelledError:
|
|
first_reader_cancelled.set()
|
|
raise
|
|
|
|
with (
|
|
patch(
|
|
"astrbot._internal.protocols.lsp.client.anyio.open_process",
|
|
AsyncMock(return_value=fake_process),
|
|
),
|
|
patch.object(client, "send_request", AsyncMock(return_value={})),
|
|
patch.object(client, "send_notification", AsyncMock()),
|
|
):
|
|
client._read_responses = first_reader # type: ignore[method-assign]
|
|
await client.connect_to_server(["python", "first_lsp.py"], "file:///tmp")
|
|
await asyncio.wait_for(first_reader_started.wait(), timeout=1)
|
|
assert client.connected is True
|
|
|
|
second_reader = AsyncMock(return_value=None)
|
|
client._read_responses = second_reader # type: ignore[method-assign]
|
|
await client.connect_to_server(["python", "second_lsp.py"], "file:///tmp")
|
|
await asyncio.sleep(0)
|
|
|
|
assert first_reader_cancelled.is_set() is True
|
|
assert client.connected is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lsp_stop_reader_task_does_not_await_current_task():
|
|
"""Test stopping the reader from within itself does not self-await."""
|
|
client = AstrbotLspClient()
|
|
done = asyncio.Event()
|
|
|
|
async def stop_self() -> None:
|
|
client._reader_task = asyncio.current_task()
|
|
await client._stop_reader_task()
|
|
done.set()
|
|
|
|
task = asyncio.create_task(stop_self())
|
|
await asyncio.wait_for(done.wait(), timeout=1)
|
|
await task
|