Add tests for devtools client

This commit is contained in:
Darren Burns
2022-04-06 13:31:37 +01:00
parent 832a3bd662
commit 1067be927f
3 changed files with 90 additions and 18 deletions

View File

@@ -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)),
) )
) )

View File

@@ -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(

View File

@@ -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,11 +43,14 @@ 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=",
"line_number": 0, "line_number": 0,
"path": "", "path": "",
"timestamp": TIMESTAMP, "timestamp": TIMESTAMP,
@@ -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