This commit is contained in:
Will McGugan
2022-09-01 15:44:26 +01:00
parent 4637747c57
commit 15d993d51d
10 changed files with 52 additions and 76 deletions

View File

@@ -2,6 +2,7 @@ from enum import Enum, auto
class LogGroup(Enum): class LogGroup(Enum):
UNDEFINED = 0
EVENT = auto() EVENT = auto()
DEBUG = auto() DEBUG = auto()
INFO = auto() INFO = auto()

View File

@@ -511,10 +511,10 @@ class App(Generic[ReturnType], DOMNode):
try: try:
if len(objects) == 1 and not kwargs: if len(objects) == 1 and not kwargs:
self.devtools.log( self.devtools.log(
DevtoolsLog(objects, caller=_textual_calling_frame),
group, group,
verbosity, verbosity,
severity, severity,
DevtoolsLog(objects, caller=_textual_calling_frame),
) )
else: else:
output = " ".join(str(arg) for arg in objects) output = " ".join(str(arg) for arg in objects)
@@ -524,10 +524,10 @@ class App(Generic[ReturnType], DOMNode):
) )
output = f"{output} {key_values}" if output else key_values output = f"{output} {key_values}" if output else key_values
self.devtools.log( self.devtools.log(
DevtoolsLog(output, caller=_textual_calling_frame),
group, group,
verbosity, verbosity,
severity, severity,
DevtoolsLog(output, caller=_textual_calling_frame),
) )
except Exception as error: except Exception as error:
self.on_exception(error) self.on_exception(error)

View File

@@ -139,7 +139,7 @@ class DevtoolsClient:
log_queue = self.log_queue log_queue = self.log_queue
websocket = self.websocket websocket = self.websocket
async def update_console(): async def update_console() -> None:
"""Coroutine function scheduled as a Task, which listens on """Coroutine function scheduled as a Task, which listens on
the websocket for updates from the server regarding any changes the websocket for updates from the server regarding any changes
in the server Console dimensions. When the client learns of this in the server Console dimensions. When the client learns of this
@@ -153,7 +153,7 @@ class DevtoolsClient:
payload = message_json["payload"] payload = message_json["payload"]
self.console.width = payload["width"] self.console.width = payload["width"]
self.console.height = payload["height"] self.console.height = payload["height"]
self.verbose = payload["verbose"] self.verbose = payload.get("verbose", False)
async def send_queued_logs(): async def send_queued_logs():
"""Coroutine function which is scheduled as a Task, which consumes """Coroutine function which is scheduled as a Task, which consumes
@@ -215,10 +215,10 @@ class DevtoolsClient:
def log( def log(
self, self,
group: LogGroup,
verbosity: LogVerbosity,
severity: LogSeverity,
log: DevtoolsLog, log: DevtoolsLog,
group: LogGroup = LogGroup.UNDEFINED,
verbosity: LogVerbosity = LogVerbosity.NORMAL,
severity: LogSeverity = LogSeverity.NORMAL,
) -> None: ) -> None:
"""Queue a log to be sent to the devtools server for display. """Queue a log to be sent to the devtools server for display.

View File

@@ -98,8 +98,8 @@ class StdoutRedirector:
batched_log = "".join(cast(str, log.objects_or_string) for log in log_batch) batched_log = "".join(cast(str, log.objects_or_string) for log in log_batch)
batched_log = batched_log.rstrip() batched_log = batched_log.rstrip()
self.devtools.log( self.devtools.log(
DevtoolsLog(batched_log, caller=log_batch[-1].caller),
LogGroup.PRINT, LogGroup.PRINT,
LogVerbosity.NORMAL, LogVerbosity.NORMAL,
LogSeverity.NORMAL, LogSeverity.NORMAL,
DevtoolsLog(batched_log, caller=log_batch[-1].caller),
) )

View File

@@ -36,7 +36,7 @@ async def _on_startup(app: Application) -> None:
await service.start() await service.start()
def _run_devtools(verbose: bool, exclude: list[str]) -> None: def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None:
app = _make_devtools_aiohttp_app(verbose=verbose, exclude=exclude) app = _make_devtools_aiohttp_app(verbose=verbose, exclude=exclude)
def noop_print(_: str): def noop_print(_: str):
@@ -46,9 +46,9 @@ def _run_devtools(verbose: bool, exclude: list[str]) -> None:
def _make_devtools_aiohttp_app( def _make_devtools_aiohttp_app(
exclude: list[str],
size_change_poll_delay_secs: float = DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS, size_change_poll_delay_secs: float = DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS,
verbose: bool = False, verbose: bool = False,
exclude: list[str] | None = None,
) -> Application: ) -> Application:
app = Application() app = Application()

View File

@@ -100,7 +100,7 @@ class DevtoolsService:
{ {
"type": "server_info", "type": "server_info",
"payload": { "payload": {
"width": self.console.width - 4, "width": self.console.width,
"height": self.console.height, "height": self.console.height,
"verbose": self.verbose, "verbose": self.verbose,
}, },
@@ -181,7 +181,7 @@ class ClientHandler:
type = message["type"] type = message["type"]
if type == "client_log": if type == "client_log":
payload = message["payload"] payload = message["payload"]
if LogGroup(payload["group"]).name in self.service.exclude: if LogGroup(payload.get("group", 0)).name in self.service.exclude:
continue continue
encoded_segments = payload["segments"] encoded_segments = payload["segments"]
segments = pickle.loads(encoded_segments) segments = pickle.loads(encoded_segments)
@@ -198,9 +198,9 @@ class ClientHandler:
path=payload["path"], path=payload["path"],
line_number=payload["line_number"], line_number=payload["line_number"],
unix_timestamp=payload["timestamp"], unix_timestamp=payload["timestamp"],
group=payload["group"], group=payload.get("group", 0),
verbosity=payload["verbosity"], verbosity=payload.get("verbosity", 0),
severity=payload["severity"], severity=payload.get("severity", 0),
) )
) )
last_message_time = message_time last_message_time = message_time

View File

@@ -37,6 +37,9 @@ def test_log_message_render(console):
path="abc/hello.py", path="abc/hello.py",
line_number=123, line_number=123,
unix_timestamp=TIMESTAMP, unix_timestamp=TIMESTAMP,
group=0,
verbosity=0,
severity=0,
) )
table = next(iter(message.__rich_console__(console, console.options))) table = next(iter(message.__rich_console__(console, console.options)))
@@ -52,7 +55,7 @@ def test_log_message_render(console):
local_time = datetime.fromtimestamp(TIMESTAMP) local_time = datetime.fromtimestamp(TIMESTAMP)
string_timestamp = local_time.time() string_timestamp = local_time.time()
assert left == f"[dim]{string_timestamp}" assert left.plain == f"[{string_timestamp}] UNDEFINED"
assert right.align == "right" assert right.align == "right"
assert "hello.py:123" in right.renderable assert "hello.py:123" in right.renderable

View File

@@ -36,15 +36,20 @@ async def test_devtools_log_places_encodes_and_queues_message(devtools):
queued_log = await devtools.log_queue.get() queued_log = await devtools.log_queue.get()
queued_log_data = msgpack.unpackb(queued_log) queued_log_data = msgpack.unpackb(queued_log)
print(repr(queued_log_data)) print(repr(queued_log_data))
assert queued_log_data == {
"type": "client_log",
"payload": { {
"timestamp": 1649166819, "type": "client_log",
"path": "a/b/c.py", "payload": {
"line_number": 123, "group": 0,
"segments": b"\x80\x04\x95B\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x0crich.segment\x94\x8c\x07Segment\x94\x93\x94\x8c\rHello, world!\x94NN\x87\x94\x81\x94h\x03\x8c\x01\n\x94NN\x87\x94\x81\x94e.", "verbosity": 0,
}, "severity": 0,
} "timestamp": 1649166819,
"path": "a/b/c.py",
"line_number": 123,
"segments": b"\x80\x04\x95@\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x0crich.segment\x94\x8c\x07Segment\x94\x93\x94\x8c\x0bhello world\x94NN\x87\x94\x81\x94h\x03\x8c\x01\n\x94NN\x87\x94\x81\x94e.",
},
}
@time_machine.travel(datetime.utcfromtimestamp(TIMESTAMP)) @time_machine.travel(datetime.utcfromtimestamp(TIMESTAMP))
@@ -57,6 +62,9 @@ async def test_devtools_log_places_encodes_and_queues_many_logs_as_string(devtoo
assert queued_log_data == { assert queued_log_data == {
"type": "client_log", "type": "client_log",
"payload": { "payload": {
"group": 0,
"verbosity": 0,
"severity": 0,
"timestamp": 1649166819, "timestamp": 1649166819,
"path": "a/b/c.py", "path": "a/b/c.py",
"line_number": 123, "line_number": 123,

View File

@@ -14,7 +14,7 @@ TIMESTAMP = 1649166819
async def test_print_redirect_to_devtools_only(devtools): async def test_print_redirect_to_devtools_only(devtools):
await devtools._stop_log_queue_processing() await devtools._stop_log_queue_processing()
with redirect_stdout(StdoutRedirector(devtools, None)): # type: ignore with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
print("Hello, world!") print("Hello, world!")
assert devtools.log_queue.qsize() == 1 assert devtools.log_queue.qsize() == 1
@@ -32,28 +32,26 @@ async def test_print_redirect_to_devtools_only(devtools):
) )
async def test_print_redirect_to_logfile_only(devtools, in_memory_logfile): async def test_print_redirect_to_logfile_only(devtools):
await devtools.disconnect() await devtools.disconnect()
with redirect_stdout(StdoutRedirector(devtools, in_memory_logfile)): # type: ignore with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
print("Hello, world!") print("Hello, world!")
assert in_memory_logfile.getvalue() == "Hello, world!\n"
async def test_print_redirect_to_devtools_and_logfile(devtools, in_memory_logfile): async def test_print_redirect_to_devtools_and_logfile(devtools):
await devtools._stop_log_queue_processing() await devtools._stop_log_queue_processing()
with redirect_stdout(StdoutRedirector(devtools, in_memory_logfile)): # type: ignore with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
print("Hello, world!") print("Hello, world!")
assert devtools.log_queue.qsize() == 1 assert devtools.log_queue.qsize() == 1
assert in_memory_logfile.getvalue() == "Hello, world!\n"
@time_machine.travel(datetime.fromtimestamp(TIMESTAMP)) @time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
async def test_print_without_flush_not_sent_to_devtools(devtools, in_memory_logfile): async def test_print_without_flush_not_sent_to_devtools(devtools):
await devtools._stop_log_queue_processing() await devtools._stop_log_queue_processing()
with redirect_stdout(StdoutRedirector(devtools, in_memory_logfile)): # type: ignore with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
# End is no longer newline character, so print will no longer # End is no longer newline character, so print will no longer
# flush the output buffer by default. # flush the output buffer by default.
print("Hello, world!", end="") print("Hello, world!", end="")
@@ -62,44 +60,19 @@ async def test_print_without_flush_not_sent_to_devtools(devtools, in_memory_logf
@time_machine.travel(datetime.fromtimestamp(TIMESTAMP)) @time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
async def test_print_forced_flush_sent_to_devtools(devtools, in_memory_logfile): async def test_print_forced_flush_sent_to_devtools(devtools):
await devtools._stop_log_queue_processing() await devtools._stop_log_queue_processing()
with redirect_stdout(StdoutRedirector(devtools, in_memory_logfile)): # type: ignore with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
print("Hello, world!", end="", flush=True) print("Hello, world!", end="", flush=True)
assert devtools.log_queue.qsize() == 1 assert devtools.log_queue.qsize() == 1
@time_machine.travel(datetime.fromtimestamp(TIMESTAMP)) @time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
async def test_print_multiple_args_batched_as_one_log(devtools, in_memory_logfile): async def test_print_multiple_args_batched_as_one_log(devtools):
await devtools._stop_log_queue_processing() await devtools._stop_log_queue_processing()
redirector = StdoutRedirector(devtools)
with redirect_stdout(StdoutRedirector(devtools, in_memory_logfile)): # type: ignore
# We call print with multiple arguments here, but it
# results in a single log added to the log queue.
print("Hello", "world", "multiple")
assert devtools.log_queue.qsize() == 1
queued_log = await devtools.log_queue.get()
queued_log_json = json.loads(queued_log)
payload = queued_log_json["payload"]
assert queued_log_json["type"] == "client_log"
assert payload["timestamp"] == TIMESTAMP
assert (
payload["encoded_segments"]
== "gANdcQAoY3JpY2guc2VnbWVudApTZWdtZW50CnEBWBQAAABIZWxsbyB3b3JsZCBtdWx0aXBsZXECTk6HcQOBcQRoAVgBAAAACnEFTk6HcQaBcQdlLg=="
)
assert len(payload["path"]) > 0
assert payload["line_number"] != 0
@time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
async def test_print_multiple_args_batched_as_one_log(devtools, in_memory_logfile):
await devtools._stop_log_queue_processing()
redirector = StdoutRedirector(devtools, in_memory_logfile)
with redirect_stdout(redirector): # type: ignore with redirect_stdout(redirector): # type: ignore
# This print adds 3 messages to the buffer that can be batched # This print adds 3 messages to the buffer that can be batched
print("The first", "batch", "of logs", end="") print("The first", "batch", "of logs", end="")
@@ -111,10 +84,10 @@ async def test_print_multiple_args_batched_as_one_log(devtools, in_memory_logfil
@time_machine.travel(datetime.fromtimestamp(TIMESTAMP)) @time_machine.travel(datetime.fromtimestamp(TIMESTAMP))
async def test_print_strings_containing_newline_flushed(devtools, in_memory_logfile): async def test_print_strings_containing_newline_flushed(devtools):
await devtools._stop_log_queue_processing() await devtools._stop_log_queue_processing()
with redirect_stdout(StdoutRedirector(devtools, in_memory_logfile)): # type: ignore with redirect_stdout(StdoutRedirector(devtools)): # type: ignore
# Flushing is disabled since end="", but the first # Flushing is disabled since end="", but the first
# string will be flushed since it contains a newline # string will be flushed since it contains a newline
print("Hel\nlo", end="") print("Hel\nlo", end="")
@@ -127,7 +100,7 @@ async def test_print_strings_containing_newline_flushed(devtools, in_memory_logf
async def test_flush_flushes_buffered_logs(devtools, in_memory_logfile): async def test_flush_flushes_buffered_logs(devtools, in_memory_logfile):
await devtools._stop_log_queue_processing() await devtools._stop_log_queue_processing()
redirector = StdoutRedirector(devtools, in_memory_logfile) redirector = StdoutRedirector(devtools)
with redirect_stdout(redirector): # type: ignore with redirect_stdout(redirector): # type: ignore
print("x", end="") print("x", end="")

View File

@@ -25,20 +25,11 @@ from textual.geometry import Size, Region
class AppTest(App): class AppTest(App):
def __init__( def __init__(self, *, test_name: str, size: Size):
self,
*,
test_name: str,
size: Size,
log_verbosity: int = 2,
):
# Tests will log in "/tests/test.[test name].log": # Tests will log in "/tests/test.[test name].log":
log_path = Path(__file__).parent.parent / f"test.{test_name}.log" log_path = Path(__file__).parent.parent / f"test.{test_name}.log"
super().__init__( super().__init__(
driver_class=DriverTest, driver_class=DriverTest,
log_path=log_path,
log_verbosity=log_verbosity,
log_color_system="256",
) )
# Let's disable all features by default # Let's disable all features by default