mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Add tests for devtools client
This commit is contained in:
@@ -108,9 +108,7 @@ async def enqueue_server_info(outgoing_queue: Queue, width: int, height: int) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def consume_incoming(
|
async def consume_incoming(console: Console, incoming_queue: Queue[dict]) -> None:
|
||||||
console: Console, incoming_queue: Queue[dict], websocket: WebSocketResponse
|
|
||||||
) -> None:
|
|
||||||
while True:
|
while True:
|
||||||
message_json = await incoming_queue.get()
|
message_json = await incoming_queue.get()
|
||||||
type = message_json["type"]
|
type = message_json["type"]
|
||||||
@@ -140,7 +138,7 @@ async def consume_incoming(
|
|||||||
|
|
||||||
|
|
||||||
async def consume_outgoing(
|
async def consume_outgoing(
|
||||||
console: Console, outgoing_queue: Queue[dict], websocket: WebSocketResponse
|
outgoing_queue: Queue[dict], websocket: WebSocketResponse
|
||||||
) -> None:
|
) -> None:
|
||||||
while True:
|
while True:
|
||||||
message_json = await outgoing_queue.get()
|
message_json = await outgoing_queue.get()
|
||||||
@@ -161,9 +159,9 @@ async def websocket_handler(request: Request):
|
|||||||
|
|
||||||
request.app["tasks"].extend(
|
request.app["tasks"].extend(
|
||||||
(
|
(
|
||||||
asyncio.create_task(consume_outgoing(console, outgoing_queue, websocket)),
|
asyncio.create_task(consume_outgoing(outgoing_queue, websocket)),
|
||||||
asyncio.create_task(enqueue_size_changes(console, outgoing_queue)),
|
asyncio.create_task(enqueue_size_changes(console, outgoing_queue)),
|
||||||
asyncio.create_task(consume_incoming(console, incoming_queue, websocket)),
|
asyncio.create_task(consume_incoming(console, incoming_queue)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -86,20 +86,23 @@ class DevtoolsClient:
|
|||||||
self.update_console_task = asyncio.create_task(update_console())
|
self.update_console_task = asyncio.create_task(update_console())
|
||||||
|
|
||||||
async def cancel_tasks(self):
|
async def cancel_tasks(self):
|
||||||
|
await self.cancel_log_queue_processing()
|
||||||
|
await self.cancel_console_size_updates()
|
||||||
|
|
||||||
|
async def cancel_log_queue_processing(self) -> None:
|
||||||
if self.log_queue_task:
|
if self.log_queue_task:
|
||||||
self.log_queue_task.cancel()
|
self.log_queue_task.cancel()
|
||||||
with suppress(asyncio.CancelledError):
|
with suppress(asyncio.CancelledError):
|
||||||
await self.log_queue_task
|
await self.log_queue_task
|
||||||
|
|
||||||
|
async def cancel_console_size_updates(self) -> None:
|
||||||
if self.update_console_task:
|
if self.update_console_task:
|
||||||
self.update_console_task.cancel()
|
self.update_console_task.cancel()
|
||||||
with suppress(asyncio.CancelledError):
|
with suppress(asyncio.CancelledError):
|
||||||
await self.update_console_task
|
await self.update_console_task
|
||||||
|
|
||||||
async def disconnect(self) -> None:
|
async def disconnect(self) -> None:
|
||||||
"""Handle remaining log messages and then trigger disconnection
|
"""Disconnect from the devtools server by cancelling tasks and closing connections"""
|
||||||
process by placing `None` on the log queue.
|
|
||||||
"""
|
|
||||||
await self.cancel_tasks()
|
await self.cancel_tasks()
|
||||||
await self._close_connections()
|
await self._close_connections()
|
||||||
|
|
||||||
@@ -131,7 +134,7 @@ class DevtoolsClient:
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self.log_queue.put_nowait(message)
|
self.log_queue.put_nowait(message)
|
||||||
if self.spillover > 0:
|
if self.spillover > 0 and self.log_queue.qsize() < LOG_QUEUE_MAXSIZE:
|
||||||
# Tell the server how many messages we had to discard due
|
# Tell the server how many messages we had to discard due
|
||||||
# to the log queue filling to capacity on the client.
|
# to the log queue filling to capacity on the client.
|
||||||
spillover_message = json.dumps(
|
spillover_message = json.dumps(
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from asyncio import Queue
|
from asyncio import Queue
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import time_machine
|
import time_machine
|
||||||
|
from aiohttp.web_ws import WebSocketResponse
|
||||||
|
from rich.console import ConsoleDimensions
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
from textual.devtools import make_aiohttp_app
|
from textual.devtools import make_aiohttp_app
|
||||||
from textual.devtools_client import DevtoolsClient
|
from textual.devtools_client import DevtoolsClient
|
||||||
@@ -12,15 +16,20 @@ TIMESTAMP = 1649166819
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def devtools(aiohttp_client, aiohttp_server, unused_tcp_port):
|
async def server(aiohttp_server, unused_tcp_port):
|
||||||
server = await aiohttp_server(make_aiohttp_app(), port=unused_tcp_port)
|
server = await aiohttp_server(make_aiohttp_app(), port=unused_tcp_port)
|
||||||
|
yield server
|
||||||
|
await server.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def devtools(aiohttp_client, server):
|
||||||
client = await aiohttp_client(server)
|
client = await aiohttp_client(server)
|
||||||
devtools = DevtoolsClient(address=client.host, port=client.port)
|
devtools = DevtoolsClient(address=client.host, port=client.port)
|
||||||
await devtools.connect()
|
await devtools.connect()
|
||||||
yield devtools
|
yield devtools
|
||||||
await devtools.disconnect()
|
await devtools.disconnect()
|
||||||
await client.close()
|
await client.close()
|
||||||
await server.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_devtools_client_initialize_defaults():
|
def test_devtools_client_initialize_defaults():
|
||||||
@@ -34,8 +43,11 @@ async def test_devtools_client_is_connected(devtools):
|
|||||||
|
|
||||||
@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
|
@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
|
||||||
async def test_devtools_log_places_encodes_and_queues_message(devtools):
|
async def test_devtools_log_places_encodes_and_queues_message(devtools):
|
||||||
log = "Hello, world!"
|
await devtools.cancel_log_queue_processing()
|
||||||
expected_queued_log = {
|
devtools.log("Hello, world!")
|
||||||
|
queued_log = await devtools.log_queue.get()
|
||||||
|
queued_log_json = json.loads(queued_log)
|
||||||
|
assert queued_log_json == {
|
||||||
"payload": {
|
"payload": {
|
||||||
"encoded_segments": "gASVQgAAAAAAAABdlCiMDHJpY2guc2VnbWVudJSMB1NlZ"
|
"encoded_segments": "gASVQgAAAAAAAABdlCiMDHJpY2guc2VnbWVudJSMB1NlZ"
|
||||||
"21lbnSUk5SMDUhlbGxvLCB3b3JsZCGUTk6HlIGUaAOMAQqUTk6HlIGUZS4=",
|
"21lbnSUk5SMDUhlbGxvLCB3b3JsZCGUTk6HlIGUaAOMAQqUTk6HlIGUZS4=",
|
||||||
@@ -46,8 +58,67 @@ async def test_devtools_log_places_encodes_and_queues_message(devtools):
|
|||||||
"type": "client_log",
|
"type": "client_log",
|
||||||
}
|
}
|
||||||
|
|
||||||
devtools.log(log)
|
|
||||||
queued_log = await devtools.log_queue.get()
|
|
||||||
|
|
||||||
|
@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
|
||||||
|
async def test_devtools_log_places_encodes_and_queues_many_logs_as_string(devtools):
|
||||||
|
await devtools.cancel_log_queue_processing()
|
||||||
|
devtools.log("hello", "world")
|
||||||
|
queued_log = await devtools.log_queue.get()
|
||||||
queued_log_json = json.loads(queued_log)
|
queued_log_json = json.loads(queued_log)
|
||||||
assert queued_log_json == expected_queued_log
|
assert queued_log_json == {
|
||||||
|
"type": "client_log",
|
||||||
|
"payload": {
|
||||||
|
"timestamp": TIMESTAMP,
|
||||||
|
"path": "",
|
||||||
|
"line_number": 0,
|
||||||
|
"encoded_segments": "gASVQAAAAAAAAABdlCiMDHJpY2guc2VnbWVudJSMB1NlZ21lbnSUk5SMC2hlbGxvIHdvcmxklE5Oh5SBlGgDjAEKlE5Oh5SBlGUu",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_devtools_log_spillover(devtools):
|
||||||
|
# Give the devtools an intentionally small max queue size
|
||||||
|
devtools.log_queue = Queue(maxsize=2)
|
||||||
|
await devtools.cancel_log_queue_processing()
|
||||||
|
|
||||||
|
# Force spillover of 2
|
||||||
|
devtools.log(Panel("hello, world"))
|
||||||
|
devtools.log("second message")
|
||||||
|
devtools.log("third message") # Discarded by rate-limiting
|
||||||
|
devtools.log("fourth message") # Discarded by rate-limiting
|
||||||
|
|
||||||
|
assert devtools.spillover == 2
|
||||||
|
|
||||||
|
# Consume log queue
|
||||||
|
while not devtools.log_queue.empty():
|
||||||
|
await devtools.log_queue.get()
|
||||||
|
|
||||||
|
# Add another message now that we're under spillover threshold
|
||||||
|
devtools.log("another message")
|
||||||
|
await devtools.log_queue.get()
|
||||||
|
|
||||||
|
# Ensure we're informing the server of spillover rate-limiting
|
||||||
|
spillover_message = await devtools.log_queue.get()
|
||||||
|
assert json.loads(spillover_message) == {"type": "client_spillover", "payload": {"spillover": 2}}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_devtools_client_update_console_dimensions(devtools, server):
|
||||||
|
server_websocket: WebSocketResponse = next(iter(server.app["websockets"]))
|
||||||
|
# Send new server information from the server via the websocket
|
||||||
|
server_info = {
|
||||||
|
"type": "server_info",
|
||||||
|
"payload": {
|
||||||
|
"width": 123,
|
||||||
|
"height": 456,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await server_websocket.send_json(server_info)
|
||||||
|
timer = 0
|
||||||
|
poll_period = .1
|
||||||
|
while True:
|
||||||
|
if timer > 3:
|
||||||
|
pytest.fail("The devtools client dimensions did not update")
|
||||||
|
if devtools.console.size == ConsoleDimensions(123, 456):
|
||||||
|
break
|
||||||
|
await asyncio.sleep(.1)
|
||||||
|
timer += poll_period
|
||||||
|
|||||||
Reference in New Issue
Block a user