From 1067be927fe7b5fa2a4bb1d9dd66e8306ef19c57 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Apr 2022 13:31:37 +0100 Subject: [PATCH] Add tests for devtools client --- src/textual/devtools.py | 10 ++-- src/textual/devtools_client.py | 11 +++-- tests/test_devtools_client.py | 87 ++++++++++++++++++++++++++++++---- 3 files changed, 90 insertions(+), 18 deletions(-) diff --git a/src/textual/devtools.py b/src/textual/devtools.py index 1e737dfbb..39aaff51b 100644 --- a/src/textual/devtools.py +++ b/src/textual/devtools.py @@ -108,9 +108,7 @@ async def enqueue_server_info(outgoing_queue: Queue, width: int, height: int) -> ) -async def consume_incoming( - console: Console, incoming_queue: Queue[dict], websocket: WebSocketResponse -) -> None: +async def consume_incoming(console: Console, incoming_queue: Queue[dict]) -> None: while True: message_json = await incoming_queue.get() type = message_json["type"] @@ -140,7 +138,7 @@ async def consume_incoming( async def consume_outgoing( - console: Console, outgoing_queue: Queue[dict], websocket: WebSocketResponse + outgoing_queue: Queue[dict], websocket: WebSocketResponse ) -> None: while True: message_json = await outgoing_queue.get() @@ -161,9 +159,9 @@ async def websocket_handler(request: Request): 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(consume_incoming(console, incoming_queue, websocket)), + asyncio.create_task(consume_incoming(console, incoming_queue)), ) ) diff --git a/src/textual/devtools_client.py b/src/textual/devtools_client.py index 75e7904fe..fcb9581c2 100644 --- a/src/textual/devtools_client.py +++ b/src/textual/devtools_client.py @@ -86,20 +86,23 @@ class DevtoolsClient: self.update_console_task = asyncio.create_task(update_console()) 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: self.log_queue_task.cancel() with suppress(asyncio.CancelledError): await self.log_queue_task + async def cancel_console_size_updates(self) -> None: if self.update_console_task: self.update_console_task.cancel() with suppress(asyncio.CancelledError): await self.update_console_task async def disconnect(self) -> None: - """Handle remaining log messages and then trigger disconnection - process by placing `None` on the log queue. - """ + """Disconnect from the devtools server by cancelling tasks and closing connections""" await self.cancel_tasks() await self._close_connections() @@ -131,7 +134,7 @@ class DevtoolsClient: ) try: 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 # to the log queue filling to capacity on the client. spillover_message = json.dumps( diff --git a/tests/test_devtools_client.py b/tests/test_devtools_client.py index cb7116437..5b0f1759b 100644 --- a/tests/test_devtools_client.py +++ b/tests/test_devtools_client.py @@ -1,9 +1,13 @@ +import asyncio import json from asyncio import Queue from datetime import datetime import pytest 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_client import DevtoolsClient @@ -12,15 +16,20 @@ TIMESTAMP = 1649166819 @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) + yield server + await server.close() + + +@pytest.fixture +async def devtools(aiohttp_client, server): client = await aiohttp_client(server) devtools = DevtoolsClient(address=client.host, port=client.port) await devtools.connect() yield devtools await devtools.disconnect() await client.close() - await server.close() def test_devtools_client_initialize_defaults(): @@ -34,11 +43,14 @@ async def test_devtools_client_is_connected(devtools): @time_machine.travel(datetime.fromtimestamp(TIMESTAMP)) async def test_devtools_log_places_encodes_and_queues_message(devtools): - log = "Hello, world!" - expected_queued_log = { + await devtools.cancel_log_queue_processing() + devtools.log("Hello, world!") + queued_log = await devtools.log_queue.get() + queued_log_json = json.loads(queued_log) + assert queued_log_json == { "payload": { "encoded_segments": "gASVQgAAAAAAAABdlCiMDHJpY2guc2VnbWVudJSMB1NlZ" - "21lbnSUk5SMDUhlbGxvLCB3b3JsZCGUTk6HlIGUaAOMAQqUTk6HlIGUZS4=", + "21lbnSUk5SMDUhlbGxvLCB3b3JsZCGUTk6HlIGUaAOMAQqUTk6HlIGUZS4=", "line_number": 0, "path": "", "timestamp": TIMESTAMP, @@ -46,8 +58,67 @@ async def test_devtools_log_places_encodes_and_queues_message(devtools): "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) - 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