|
10 | 10 | from types import EllipsisType |
11 | 11 | import pytest |
12 | 12 | from uuid import UUID, uuid4 |
| 13 | +from fastapi.testclient import TestClient |
13 | 14 | from labthings_fastapi import logs |
| 15 | +from labthings_fastapi.invocations import LogRecordModel |
14 | 16 | from labthings_fastapi.invocation_contexts import ( |
15 | 17 | fake_invocation_context, |
16 | 18 | set_invocation_id, |
|
19 | 21 | from labthings_fastapi.exceptions import LogConfigurationError |
20 | 22 | from labthings_fastapi.testing import create_thing_without_server |
21 | 23 |
|
| 24 | +from .temp_client import poll_task |
| 25 | + |
22 | 26 |
|
23 | 27 | class ThingThatLogs(lt.Thing): |
24 | 28 | @lt.action |
25 | 29 | def log_a_message(self, msg: str): |
26 | 30 | """Log a message to the thing's logger.""" |
27 | 31 | self.logger.info(msg) |
28 | 32 |
|
| 33 | + @lt.action |
| 34 | + def log_and_capture(self, msg: str) -> str: |
| 35 | + """Log a message to the thing's logger and retrieve it as a string.""" |
| 36 | + self.logger.info(msg) |
| 37 | + self.logger.warning(msg) |
| 38 | + self.logger.error(msg) |
| 39 | + logs = self.get_current_invocation_logs() |
| 40 | + logging_str = "" |
| 41 | + for record in logs: |
| 42 | + level = record.levelname |
| 43 | + msg = record.getMessage() |
| 44 | + logging_str += f"[{level}] {msg}\n" |
| 45 | + return logging_str |
| 46 | + |
29 | 47 |
|
30 | 48 | def reset_thing_logger(): |
31 | 49 | """Remove all handlers from the THING_LOGGER to reset it.""" |
@@ -176,3 +194,38 @@ def test_add_thing_log_destination(): |
176 | 194 | thing.log_a_message("Test Message.") |
177 | 195 | assert len(dest) == 1 |
178 | 196 | assert dest[0].getMessage() == "Test Message." |
| 197 | + |
| 198 | + |
| 199 | +def _call_action_can_get_logs(): |
| 200 | + """Run `log_and_capture` as an action, Return the final HTTP response.""" |
| 201 | + server = lt.ThingServer({"logging_thing": ThingThatLogs}) |
| 202 | + with TestClient(server.app) as client: |
| 203 | + response = client.post("/logging_thing/log_and_capture", json={"msg": "foobar"}) |
| 204 | + response.raise_for_status() |
| 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() |
| 211 | + assert invocation["status"] == "completed" |
| 212 | + # Check the logs are returned by the action itself. |
| 213 | + expected_message = "[INFO] foobar\n[WARNING] foobar\n[ERROR] foobar\n" |
| 214 | + 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