Skip to content

Commit 5390a38

Browse files
committed
Log but don't raise when publish fails
Closes #4
1 parent 4df1c20 commit 5390a38

File tree

3 files changed

+50
-37
lines changed

3 files changed

+50
-37
lines changed

mqlog/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
self.capacity = capacity
4040
self.buffer = []
4141
self.will_flush = asyncio.Event()
42+
self._logger = logging.getLogger("mqlog")
4243

4344
async def run(self):
4445
"""
@@ -69,8 +70,10 @@ def format(self, record):
6970
# Named with an underscore to avoid conflict with logging.Handler.flush
7071
async def _flush(self):
7172
if self.buffer:
72-
msg = ""
73-
while self.buffer:
74-
msg += self.buffer.pop(0) + "\n"
75-
await self.client.publish(self.topic, msg.strip(), qos=self.qos)
76-
self.will_flush.clear()
73+
try:
74+
msg = "\n".join([line for line in self.buffer])
75+
await self.client.publish(self.topic, msg, qos=self.qos)
76+
self.buffer.clear()
77+
except Exception as e:
78+
self._logger.error("Failed to publish logs", exc_info=e)
79+
self.will_flush.clear()

tests/test_handler.py

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@
44
from unittest import TestCase, skipIf
55

66
from mqlog import MqttHandler
7+
from tests.utils import AsyncMock, Mock, call
78

89

910
class FakeClient:
1011
"""A fake MQTT client for testing purposes."""
1112

12-
def __init__(self):
13-
self.calls = []
14-
15-
# Store the call like a mock so we can check it later
16-
async def publish(self, topic, payload, qos=0):
17-
self.calls.append((topic, payload, qos))
13+
publish: None
1814

1915

2016
class TestMqttHandler(TestCase):
2117
def setUp(self):
2218
self.client = FakeClient()
19+
self.client.publish = AsyncMock()
2320
self.handler = MqttHandler(self.client, "test_topic")
2421
self.handler.setFormatter(logging.Formatter("%(message)s"))
2522
self.logger = logging.getLogger("test")
@@ -50,32 +47,46 @@ def test_flush_full_buffer(self):
5047
self.logger.info(f"Test message {i}")
5148
self.assertTrue(self.handler.will_flush.is_set())
5249

53-
@skipIf(os.getenv("CI"), "Hangs in CI")
50+
def test_flush_fail(self):
51+
"""Flushing should log an error if publish fails"""
52+
self.handler._logger.error = Mock()
53+
self.client.publish = AsyncMock(side_effect=Exception("Publish failed"))
54+
55+
async def do_test(handler: logging.Handler, logger: logging.Logger):
56+
asyncio.create_task(handler.run())
57+
await asyncio.sleep(0.1) # Allow handler to start
58+
logger.error("Test message") # should trigger flush
59+
await asyncio.sleep(0.1)
60+
61+
asyncio.run(do_test(self.handler, self.logger))
62+
63+
self.handler._logger.error.assert_called()
64+
5465
def test_publish(self):
5566
"""Flushing should publish messages to MQTT topic"""
5667
self.handler.flush_level = logging.ERROR
5768

58-
async def do_test(logger):
69+
async def do_test(handler: logging.Handler, logger: logging.Logger):
70+
asyncio.create_task(handler.run())
71+
await asyncio.sleep(0.1)
5972
logger.info("Test message 1")
6073
await asyncio.sleep(0.1)
6174
logger.error("Test message 2")
6275
await asyncio.sleep(0.1)
6376

64-
async def main(handler, logger):
65-
await asyncio.gather(handler.run(), do_test(logger))
66-
67-
asyncio.run(main(self.handler, self.logger))
77+
asyncio.run(do_test(self.handler, self.logger))
6878

69-
self.assertEqual(
70-
self.client.calls, [("test_topic", "Test message 1\nTest message 2", 0)]
79+
self.client.publish.assert_called_with(
80+
"test_topic", "Test message 1\nTest message 2", qos=0
7181
)
7282

73-
@skipIf(os.getenv("CI"), "Hangs in CI")
7483
def test_flush_multiple(self):
7584
"""Flushing multiple times should publish separate messages"""
7685
self.handler.capacity = 2
7786

78-
async def do_test(logger):
87+
async def do_test(handler: logging.Handler, logger: logging.Logger):
88+
asyncio.create_task(handler.run())
89+
await asyncio.sleep(0.1)
7990
logger.info("Test message 1")
8091
await asyncio.sleep(0.1)
8192
logger.info("Test message 2")
@@ -85,15 +96,11 @@ async def do_test(logger):
8596
logger.info("Test message 4")
8697
await asyncio.sleep(0.1)
8798

88-
async def main(handler, logger):
89-
await asyncio.gather(handler.run(), do_test(logger))
90-
91-
asyncio.run(main(self.handler, self.logger))
99+
asyncio.run(do_test(self.handler, self.logger))
92100

93-
self.assertEqual(
94-
self.client.calls,
101+
self.client.publish.assert_has_calls(
95102
[
96-
("test_topic", "Test message 1\nTest message 2", 0),
97-
("test_topic", "Test message 3\nTest message 4", 0),
98-
],
103+
call("test_topic", "Test message 1\nTest message 2", qos=0),
104+
call("test_topic", "Test message 3\nTest message 4", qos=0),
105+
]
99106
)

tests/utils.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# from unittest import TestCase
2-
3-
41
def call(*args, **kwargs):
52
return (tuple(args), dict(kwargs))
63

@@ -14,7 +11,8 @@ def __init__(self, return_value=None, side_effect=None):
1411
self._calls = []
1512

1613
def __call__(self, *args, **kwargs):
17-
self._calls.append(call(*args, **kwargs))
14+
"""Call the mock and store the call details."""
15+
self._calls.append(call(*args[1:], **kwargs)) # Skip the first argument (self)
1816
if self._side_effect:
1917
raise self._side_effect
2018
return self._return_value
@@ -29,17 +27,22 @@ def assert_not_called(self):
2927

3028
def assert_called_with(self, *args, **kwargs):
3129
"""Assert that the mock was last called with the given arguments."""
32-
# First call should be self, so we prepend it
33-
expected_args = [self] + list(args)
34-
expectation = call(*expected_args, **kwargs)
30+
# Fail if no calls were made
31+
self.assert_called()
3532

3633
# Try to have a useful output for assertion failures
34+
expectation = call(*args, **kwargs)
3735
assert self._calls[-1] == expectation, "Expected call with {}, got {}".format(
3836
expectation, self._calls[-1]
3937
)
4038

4139
def assert_has_calls(self, calls):
4240
"""Assert that the mock has the expected calls with arguments."""
41+
assert calls, "Expected calls cannot be empty."
42+
43+
# Fail if no calls were made
44+
self.assert_called()
45+
4346
assert self._calls == calls, "Expected calls {}, got {}".format(
4447
calls, self._calls
4548
)

0 commit comments

Comments
 (0)