Skip to content

Commit 33ac92f

Browse files
authored
Merge pull request #149 from IBM/coverage
Improve test coverage
2 parents 7b6cf8c + 0665d3a commit 33ac92f

17 files changed

+2350
-12
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
docs/docs/test/
12
tmp
23
*.tgz
34
*.gz

Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,11 @@ clean:
178178
# help: smoketest - Run smoketest.py --verbose (build container, add MCP server, test endpoints)
179179
# help: test - Run unit tests with pytest
180180
# help: coverage - Run tests with coverage, emit md/HTML/XML + badge
181+
# help: htmlcov - (re)build just the HTML coverage report into docs
181182
# help: test-curl - Smoke-test API endpoints with curl script
182183
# help: pytest-examples - Run README / examples through pytest-examples
183184

184-
.PHONY: smoketest test coverage pytest-examples test-curl
185+
.PHONY: smoketest test coverage pytest-examples test-curl htmlcov
185186

186187
## --- Automated checks --------------------------------------------------------
187188
smoketest:
@@ -215,6 +216,18 @@ coverage:
215216
@/bin/bash -c "source $(VENV_DIR)/bin/activate && coverage-badge -fo $(DOCS_DIR)/docs/images/coverage.svg"
216217
@echo "✅ Coverage artefacts: md, HTML in $(COVERAGE_DIR), XML & badge ✔"
217218

219+
htmlcov:
220+
@echo "📊 Generating HTML coverage report…"
221+
@test -d "$(VENV_DIR)" || $(MAKE) venv
222+
@mkdir -p $(COVERAGE_DIR)
223+
# If there's no existing coverage data, fall back to the full test-run
224+
@if [ ! -f .coverage ]; then \
225+
echo "ℹ️ No .coverage file found – running full coverage first…"; \
226+
$(MAKE) --no-print-directory coverage; \
227+
fi
228+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && coverage html -i -d $(COVERAGE_DIR)"
229+
@echo "✅ HTML coverage report ready → $(COVERAGE_DIR)/index.html"
230+
218231
pytest-examples:
219232
@echo "🧪 Testing README examples…"
220233
@test -d "$(VENV_DIR)" || $(MAKE) venv

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[![CodeQL](https://github.com/IBM/mcp-context-forge/actions/workflows/codeql.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/codeql.yml) 
1010
[![Bandit Security](https://github.com/IBM/mcp-context-forge/actions/workflows/bandit.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/bandit.yml) 
1111
[![Dependency Review](https://github.com/IBM/mcp-context-forge/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/dependency-review.yml) 
12+
[![Tests & Coverage](https://github.com/IBM/mcp-context-forge/actions/workflows/pytest.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/pytest.yml) 
1213

1314
<!-- === Container Build & Deploy === -->
1415
[![Secure Docker Build](https://github.com/IBM/mcp-context-forge/actions/workflows/docker-image.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/docker-image.yml)&nbsp;

mcpgateway/translate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ async def event_gen() -> AsyncIterator[dict]:
175175
yield {
176176
"event": "endpoint",
177177
"data": endpoint_url,
178-
"retry": keep_alive * 1000,
178+
"retry": int(keep_alive * 1000),
179179
}
180180

181181
# 2️⃣ Immediate keepalive so clients know the stream is alive
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# -*- coding: utf-8 -*-
2+
"""Memory-backend unit tests for `session_registry.py`.
3+
4+
Copyright 2025
5+
SPDX-License-Identifier: Apache-2.0
6+
Author: Mihai Criveti
7+
8+
These tests cover the essential public behaviours of SessionRegistry when
9+
configured with backend="memory":
10+
11+
* add_session / get_session / get_session_sync
12+
* remove_session (disconnects transport & clears cache)
13+
* broadcast + respond (with generate_response monkey-patched)
14+
15+
No Redis or SQLAlchemy fixtures are required, making the suite fast and
16+
portable.
17+
"""
18+
19+
import asyncio
20+
21+
import pytest
22+
23+
# Import SessionRegistry – works whether the file lives inside the package or beside it
24+
try:
25+
from mcpgateway.cache.session_registry import SessionRegistry
26+
except (ModuleNotFoundError, ImportError): # pragma: no cover
27+
from session_registry import SessionRegistry # type: ignore
28+
29+
30+
class FakeSSETransport:
31+
"""Minimal stub implementing only the methods SessionRegistry uses."""
32+
33+
def __init__(self, session_id: str):
34+
self.session_id = session_id
35+
self._connected = True
36+
self.sent_messages = []
37+
38+
async def disconnect(self):
39+
self._connected = False
40+
41+
async def is_connected(self):
42+
return self._connected
43+
44+
async def send_message(self, msg):
45+
self.sent_messages.append(msg)
46+
47+
48+
# ---------------------------------------------------------------------------
49+
# Pytest fixtures
50+
# ---------------------------------------------------------------------------
51+
52+
53+
@pytest.fixture(name="event_loop")
54+
def _event_loop_fixture():
55+
loop = asyncio.new_event_loop()
56+
yield loop
57+
loop.close()
58+
59+
60+
@pytest.fixture()
61+
async def registry():
62+
reg = SessionRegistry(backend="memory")
63+
await reg.initialize()
64+
yield reg
65+
await reg.shutdown()
66+
67+
68+
# ---------------------------------------------------------------------------
69+
# Tests
70+
# ---------------------------------------------------------------------------
71+
72+
73+
@pytest.mark.asyncio
74+
async def test_add_and_get_session(registry):
75+
tr = FakeSSETransport("abc")
76+
await registry.add_session("abc", tr)
77+
78+
assert await registry.get_session("abc") is tr
79+
assert registry.get_session_sync("abc") is tr
80+
81+
82+
@pytest.mark.asyncio
83+
async def test_remove_session(registry):
84+
tr = FakeSSETransport("dead")
85+
await registry.add_session("dead", tr)
86+
87+
await registry.remove_session("dead")
88+
89+
assert not await tr.is_connected()
90+
assert registry.get_session_sync("dead") is None
91+
92+
93+
@pytest.mark.asyncio
94+
async def test_broadcast_and_respond(monkeypatch, registry):
95+
"""Ensure broadcast stores the message and respond delivers it via generate_response."""
96+
tr = FakeSSETransport("xyz")
97+
await registry.add_session("xyz", tr)
98+
99+
captured = {}
100+
101+
async def fake_generate_response(*, message, transport, **_):
102+
captured["transport"] = transport
103+
captured["message"] = message
104+
105+
monkeypatch.setattr(registry, "generate_response", fake_generate_response)
106+
107+
ping_msg = {"method": "ping", "id": 1, "params": {}}
108+
await registry.broadcast("xyz", ping_msg)
109+
110+
# respond should call our fake_generate_response exactly once
111+
await registry.respond(
112+
server_id=None,
113+
user={},
114+
session_id="xyz",
115+
base_url="http://localhost",
116+
)
117+
118+
assert captured["transport"] is tr
119+
assert captured["message"] == ping_msg

0 commit comments

Comments
 (0)