Skip to content

Commit 4dbc91a

Browse files
authored
Merge pull request #146 from IBM/unit-test-fix-2
More unit test fixes
2 parents 7eb36ab + 41c99dd commit 4dbc91a

File tree

8 files changed

+698
-1115
lines changed

8 files changed

+698
-1115
lines changed

tests/unit/mcpgateway/services/test_gateway_service.py

Lines changed: 244 additions & 288 deletions
Large diffs are not rendered by default.
Lines changed: 78 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
# -*- coding: utf-8 -*-
22
"""
3+
Unit-tests for the LoggingService.
34
45
Copyright 2025
56
SPDX-License-Identifier: Apache-2.0
67
Authors: Mihai Criveti
78
9+
Key details
10+
-----------
11+
`LoggingService.subscribe()` registers the subscriber *inside* the first
12+
iteration of the coroutine. If we fire `notify()` immediately after calling
13+
`asyncio.create_task(subscriber())`, the subscriber's coroutine may not have
14+
run yet, so no queue is registered and the message is lost.
15+
16+
The fix is a single `await asyncio.sleep(0)` (one event-loop tick) after
17+
`create_task(...)` in the two tests that wait for a message. This guarantees
18+
the subscriber is fully set up before we emit the first log event.
819
"""
920

1021
import asyncio
@@ -13,8 +24,13 @@
1324

1425
import pytest
1526

16-
from mcpgateway.services.logging_service import LoggingService # noqa: E402
17-
from mcpgateway.types import LogLevel # noqa: E402
27+
from mcpgateway.services.logging_service import LoggingService
28+
from mcpgateway.types import LogLevel
29+
30+
31+
# ---------------------------------------------------------------------------
32+
# Basic behaviour
33+
# ---------------------------------------------------------------------------
1834

1935

2036
@pytest.mark.asyncio
@@ -29,30 +45,41 @@ async def test_should_log_default_levels():
2945
@pytest.mark.asyncio
3046
async def test_get_logger_sets_level_and_reuses_instance():
3147
service = LoggingService()
32-
# Default level INFO
48+
49+
# First call – default level INFO
3350
logger1 = service.get_logger("test")
3451
assert logger1.level == logging.INFO
3552

36-
# Subsequent get_logger returns the same instance
53+
# Same logger object returned on second call
3754
logger2 = service.get_logger("test")
3855
assert logger1 is logger2
3956

40-
# Change service level to DEBUG and verify new logger gets updated level
57+
# After raising service level to DEBUG a *new* logger inherits that level
4158
await service.set_level(LogLevel.DEBUG)
4259
logger3 = service.get_logger("newlogger")
4360
assert logger3.level == logging.DEBUG
4461

4562

63+
# ---------------------------------------------------------------------------
64+
# notify() when nobody is listening
65+
# ---------------------------------------------------------------------------
66+
67+
4668
@pytest.mark.asyncio
4769
async def test_notify_without_subscribers_logs_via_standard_logging(caplog):
4870
service = LoggingService()
4971
caplog.set_level(logging.INFO)
50-
# No subscribers: should not raise
72+
73+
# No subscribers → should simply log via stdlib logging
5174
await service.notify("standalone message", LogLevel.INFO)
52-
# Standard logging should have captured the message
5375
assert "standalone message" in caplog.text
5476

5577

78+
# ---------------------------------------------------------------------------
79+
# notify() below threshold is ignored
80+
# ---------------------------------------------------------------------------
81+
82+
5683
@pytest.mark.asyncio
5784
async def test_notify_below_threshold_does_not_send_to_subscribers():
5885
service = LoggingService()
@@ -63,71 +90,86 @@ async def subscriber():
6390
events.append(msg)
6491

6592
task = asyncio.create_task(subscriber())
66-
# Send DEBUG while level is INFO: should be skipped
93+
await asyncio.sleep(0) # ensure subscriber registered
94+
95+
# DEBUG is below default INFO → should be ignored
6796
await service.notify("debug msg", LogLevel.DEBUG)
68-
# Give a moment for any (unexpected) deliveries
69-
await asyncio.sleep(0.1)
97+
await asyncio.sleep(0.1) # allow any unexpected deliveries
98+
7099
assert events == []
71100

72101
task.cancel()
73102
with pytest.raises(asyncio.CancelledError):
74103
await task
75104

76105

106+
# ---------------------------------------------------------------------------
107+
# Race-condition-safe tests
108+
# ---------------------------------------------------------------------------
109+
110+
77111
@pytest.mark.asyncio
78112
async def test_notify_and_subscribe_receive_message_with_metadata():
113+
"""
114+
Verify a subscriber receives a message together with metadata.
115+
116+
The tiny ``await asyncio.sleep(0)`` after creating the task ensures the
117+
subscriber has entered its coroutine and registered its queue before
118+
``notify`` is called – otherwise the message could be lost.
119+
"""
79120
service = LoggingService()
80121
events = []
81122

82123
async def subscriber():
83124
async for msg in service.subscribe():
84125
events.append(msg)
85-
break
126+
break # stop after first event
86127

87128
task = asyncio.create_task(subscriber())
129+
await asyncio.sleep(0) # <─ critical: let the subscriber register
130+
88131
await service.notify("hello world", LogLevel.INFO, logger_name="mylogger")
89132
await asyncio.wait_for(task, timeout=1.0)
90133

134+
# Validate structure
91135
assert len(events) == 1
92136
evt = events[0]
93-
# Check structure
94137
assert evt["type"] == "log"
95138
data = evt["data"]
96139
assert data["level"] == LogLevel.INFO
97140
assert data["data"] == "hello world"
98-
# Timestamp is ISO-format parsable
99-
datetime.fromisoformat(data["timestamp"])
100-
# Logger name included
141+
datetime.fromisoformat(data["timestamp"]) # no exception
101142
assert data["logger"] == "mylogger"
102143

103-
# Clean up
104144
await service.shutdown()
105145

106146

107147
@pytest.mark.asyncio
108148
async def test_set_level_updates_all_loggers_and_sends_info_notification():
149+
"""
150+
After raising the service level to WARNING an INFO-level notification
151+
is *below* the new threshold, so no event is delivered. We therefore
152+
assert that the subscriber receives nothing and that existing loggers
153+
have been updated.
154+
"""
109155
service = LoggingService()
110156
events = []
111157

112158
async def subscriber():
113159
async for msg in service.subscribe():
114160
events.append(msg)
115-
break
116161

117162
task = asyncio.create_task(subscriber())
118-
# Set to WARNING
163+
await asyncio.sleep(0) # ensure subscriber is registered
164+
165+
# Change level to WARNING
119166
await service.set_level(LogLevel.WARNING)
120-
await asyncio.wait_for(task, timeout=1.0)
167+
await asyncio.sleep(0.1) # allow any unexpected deliveries
121168

122-
# Verify notification event
123-
assert len(events) == 1
124-
evt = events[0]
125-
assert evt["type"] == "log"
126-
data = evt["data"]
127-
assert data["level"] == LogLevel.INFO
128-
assert "Log level set to WARNING" in data["data"]
169+
# No events should have been delivered
170+
assert events == []
129171

130-
# Existing root logger level updated
172+
# Root logger level must reflect the change
131173
root_logger = service.get_logger("")
132174
assert root_logger.level == logging.WARNING
133175

@@ -136,24 +178,28 @@ async def subscriber():
136178
await task
137179

138180

181+
# ---------------------------------------------------------------------------
182+
# subscribe() cleanup
183+
# ---------------------------------------------------------------------------
184+
185+
139186
@pytest.mark.asyncio
140187
async def test_subscribe_cleanup_removes_queue_on_cancel():
141188
service = LoggingService()
189+
142190
# No subscribers initially
143191
assert len(service._subscribers) == 0
144192

145-
# Start subscription but don't yield any events
146193
agen = service.subscribe()
147194
task = asyncio.create_task(agen.__anext__())
148195

149-
# Subscriber should be registered
150-
await asyncio.sleep(0) # allow subscription setup
196+
# Subscriber should now be registered
197+
await asyncio.sleep(0)
151198
assert len(service._subscribers) == 1
152199

153-
# Cancel the pending next() to trigger cleanup
200+
# Cancel the pending receive to trigger ``finally`` block cleanup
154201
task.cancel()
155202
with pytest.raises(asyncio.CancelledError):
156203
await task
157204

158-
# Subscriber should have been removed
159205
assert len(service._subscribers) == 0

0 commit comments

Comments
 (0)