Skip to content

Commit 4c91c61

Browse files
Add tests that logs from actions serialise as LogRecordModels
1 parent e3296b5 commit 4c91c61

File tree

2 files changed

+43
-16
lines changed

2 files changed

+43
-16
lines changed

src/labthings_fastapi/invocations.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,23 +54,26 @@ class LogRecordModel(BaseModel):
5454
def generate_message(cls, data: Any) -> Any:
5555
"""Ensure LogRecord objects have constructed their message.
5656
57-
:param data: The LogRecord to process.
57+
:param data: The LogRecord or serialised log record data to process.
5858
5959
:return: The LogRecord, with a message constructed.
6060
"""
61+
if not isinstance(data, logging.LogRecord):
62+
return data
63+
6164
if not hasattr(data, "message"):
62-
if isinstance(data, logging.LogRecord):
63-
try:
64-
data.message = data.getMessage()
65-
except (ValueError, TypeError) as e:
66-
# too many args causes an error - but errors
67-
# in validation can be a problem for us:
68-
# it will cause 500 errors when retrieving
69-
# the invocation.
70-
# This way, you can find and fix the source.
71-
data.message = f"Error constructing message ({e}) from {data!r}."
72-
73-
if data.exc_info:
65+
try:
66+
data.message = data.getMessage()
67+
except (ValueError, TypeError) as e:
68+
# too many args causes an error - but errors
69+
# in validation can be a problem for us:
70+
# it will cause 500 errors when retrieving
71+
# the invocation.
72+
# This way, you can find and fix the source.
73+
data.message = f"Error constructing message ({e}) from {data!r}."
74+
75+
# Also check data.exc_info[0] as sys.exc_info() can return (None, None, None).
76+
if data.exc_info and data.exc_info[0] is not None:
7477
data.exception_type = data.exc_info[0].__name__
7578
data.traceback = "\n".join(traceback.format_exception(*data.exc_info))
7679

tests/test_logs.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from uuid import UUID, uuid4
1313
from fastapi.testclient import TestClient
1414
from labthings_fastapi import logs
15+
from labthings_fastapi.invocations import LogRecordModel
1516
from labthings_fastapi.invocation_contexts import (
1617
fake_invocation_context,
1718
set_invocation_id,
@@ -195,13 +196,36 @@ def test_add_thing_log_destination():
195196
assert dest[0].getMessage() == "Test Message."
196197

197198

198-
def test_action_can_get_logs():
199-
"""Check that an action can get a copy of its own logs."""
199+
def _call_action_can_get_logs():
200+
"""Run `log_and_capture` as an action, Return the final HTTP response."""
200201
server = lt.ThingServer({"logging_thing": ThingThatLogs})
201202
with TestClient(server.app) as client:
202203
response = client.post("/logging_thing/log_and_capture", json={"msg": "foobar"})
203204
response.raise_for_status()
204-
invocation = poll_task(client, response.json())
205+
return poll_task(client, response.json())
206+
207+
208+
def test_action_can_get_logs():
209+
"""Check that an action can get a copy of its own logs."""
210+
invocation = _call_action_can_get_logs()
205211
assert invocation["status"] == "completed"
212+
# Check the logs are returned by the action itself.
206213
expected_message = "[INFO] foobar\n[WARNING] foobar\n[ERROR] foobar\n"
207214
assert invocation["output"] == expected_message
215+
216+
217+
def test_action_logs_over_http():
218+
"""Check that the action logs are sent over HTTP in JSON."""
219+
invocation = _call_action_can_get_logs()
220+
logs = invocation["log"]
221+
assert isinstance(logs, list)
222+
assert len(logs) == 3
223+
assert logs[0]["levelname"] == "INFO"
224+
assert logs[0]["levelno"] == 20
225+
assert logs[1]["levelname"] == "WARNING"
226+
assert logs[1]["levelno"] == 30
227+
assert logs[2]["levelname"] == "ERROR"
228+
assert logs[2]["levelno"] == 40
229+
for log in logs:
230+
log_as_model = LogRecordModel(**log)
231+
assert log_as_model.message == "foobar"

0 commit comments

Comments
 (0)