Additional testing for devtools client/server

This commit is contained in:
Darren Burns
2022-04-06 18:00:32 +01:00
parent 1067be927f
commit 5648894560
7 changed files with 207 additions and 60 deletions

24
tests/conftest.py Normal file
View File

@@ -0,0 +1,24 @@
import pytest
from textual.devtools import _make_devtools_aiohttp_app
from textual.devtools_client import DevtoolsClient
@pytest.fixture
async def server(aiohttp_server, unused_tcp_port):
app = _make_devtools_aiohttp_app(
size_change_poll_delay_secs=0.001,
)
server = await aiohttp_server(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()

88
tests/test_devtools.py Normal file
View File

@@ -0,0 +1,88 @@
import asyncio
import pytest
import time_machine
from rich.align import Align
from rich.console import Console
from rich.segment import Segment
from tests.utilities.render import wait_for_predicate
from textual.devtools import DevtoolsLogMessage, DevtoolsInternalMessage
TIMESTAMP = 1649166819
WIDTH = 40
# The string "Hello, world!" is encoded in the payload below
EXAMPLE_LOG = {
"type": "client_log",
"payload": {
"encoded_segments": "gASVQgAAAAAAAABdlCiMDHJpY2guc2VnbWVudJSMB1NlZ"
"21lbnSUk5SMDUhlbGxvLCB3b3JsZCGUTk6HlIGUaAOMAQqUTk6HlIGUZS4=",
"line_number": 123,
"path": "abc/hello.py",
"timestamp": TIMESTAMP,
},
}
@pytest.fixture(scope="module")
def console():
return Console(width=WIDTH)
@time_machine.travel(TIMESTAMP)
def test_log_message_render(console):
message = DevtoolsLogMessage(
[Segment("content")],
path="abc/hello.py",
line_number=123,
unix_timestamp=TIMESTAMP,
)
table = next(iter(message.__rich_console__(console, console.options)))
assert len(table.rows) == 1
columns = list(table.columns)
left_cells = list(columns[0].cells)
left = left_cells[0]
right_cells = list(columns[1].cells)
right: Align = right_cells[0]
assert left == " [#888177]15:53:39 [dim]BST[/]"
assert right.align == "right"
assert "hello.py:123" in right.renderable
def test_internal_message_render(console):
message = DevtoolsInternalMessage("hello")
rule = next(iter(message.__rich_console__(console, console.options)))
assert rule.title == "hello"
assert rule.characters == ""
async def test_devtools_valid_client_log(devtools):
await devtools.websocket.send_json(EXAMPLE_LOG)
assert devtools.is_connected
async def test_devtools_string_not_json_message(devtools):
await devtools.websocket.send_str("ABCDEFG")
assert devtools.is_connected
async def test_devtools_invalid_json_message(devtools):
await devtools.websocket.send_json({"invalid": "json"})
assert devtools.is_connected
async def test_devtools_spillover_message(devtools):
await devtools.websocket.send_json(
{"type": "client_spillover", "payload": {"spillover": 123}}
)
assert devtools.is_connected
async def test_devtools_console_size_change(server, devtools):
# Update the width of the console on the server-side
server.app["console"].width = 124
# Wait for the client side to update the console on their end
await wait_for_predicate(lambda: devtools.console.width == 124)

View File

@@ -9,29 +9,12 @@ from aiohttp.web_ws import WebSocketResponse
from rich.console import ConsoleDimensions
from rich.panel import Panel
from textual.devtools import make_aiohttp_app
from tests.utilities.render import wait_for_predicate
from textual.devtools_client import DevtoolsClient
TIMESTAMP = 1649166819
@pytest.fixture
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()
def test_devtools_client_initialize_defaults():
devtools = DevtoolsClient()
assert devtools.url == "ws://127.0.0.1:8081"
@@ -50,7 +33,7 @@ async def test_devtools_log_places_encodes_and_queues_message(devtools):
assert queued_log_json == {
"payload": {
"encoded_segments": "gASVQgAAAAAAAABdlCiMDHJpY2guc2VnbWVudJSMB1NlZ"
"21lbnSUk5SMDUhlbGxvLCB3b3JsZCGUTk6HlIGUaAOMAQqUTk6HlIGUZS4=",
"21lbnSUk5SMDUhlbGxvLCB3b3JsZCGUTk6HlIGUaAOMAQqUTk6HlIGUZS4=",
"line_number": 0,
"path": "",
"timestamp": TIMESTAMP,
@@ -99,12 +82,17 @@ async def test_devtools_log_spillover(devtools):
# 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}}
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
"""Sending new server info through websocket from server to client should (eventually)
result in the dimensions of the devtools client console being updated to match.
"""
server_to_client: WebSocketResponse = next(iter(server.app["websockets"]))
server_info = {
"type": "server_info",
"payload": {
@@ -112,13 +100,7 @@ async def test_devtools_client_update_console_dimensions(devtools, server):
"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
await server_to_client.send_json(server_info)
await wait_for_predicate(
lambda: devtools.console.size == ConsoleDimensions(123, 456)
)

View File

@@ -1,9 +1,11 @@
import asyncio
import io
import re
from typing import Callable
import pytest
from rich.console import Console, RenderableType
re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b")
@@ -23,3 +25,32 @@ def render(renderable: RenderableType, no_wrap: bool = False) -> str:
console.print(renderable, no_wrap=no_wrap, end="")
output = replace_link_ids(capture.get())
return output
async def wait_for_predicate(
predicate: Callable[[], bool],
timeout_secs: float = 2,
poll_delay_secs: float = 0.001,
) -> None:
"""Wait for the given predicate to become True by evaluating it every `poll_delay_secs`
seconds. Fail the pytest test if the predicate does not become True after `timeout_secs`
seconds.
Args:
predicate (Callable[[], bool]): The predicate function which will be called repeatedly.
timeout_secs (float): If the predicate doesn't evaluate to True after this number of
seconds, the test will fail.
poll_delay_secs (float): The number of seconds to wait between each call to the
predicate function.
"""
time_taken = 0
while True:
result = predicate()
if result:
return
await asyncio.sleep(poll_delay_secs)
time_taken += poll_delay_secs
if time_taken > timeout_secs:
pytest.fail(
f"Predicate {predicate} did not return True after {timeout_secs} seconds."
)