Skip to content

Commit c9aaa42

Browse files
HanSur94claude
andcommitted
test: increase code coverage from 63% to 90%
Add 11 new test files with 385 tests covering previously untested modules: tools/jobs, tools/discovery, tools/custom, tools/files, tools/admin, tools/core, server, monitoring/dashboard, monitoring/store, monitoring/collector, jobs/executor, output/formatter, output/thumbnail, session/manager, pool/engine, and monitoring/health. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f12a205 commit c9aaa42

11 files changed

Lines changed: 5182 additions & 0 deletions

tests/test_coverage_gaps.py

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
"""Targeted tests for remaining coverage gaps across modules.
2+
3+
Covers:
4+
- tools/core.py: check_code JSON parsing, non-completed result, cleanup exception
5+
- session/manager.py: config=None defaults, destroy temp dir cleanup failure
6+
- monitoring/health.py: degraded health check failure counter
7+
- pool/engine.py: is_alive property, stop exception, addpath exception
8+
- pool/manager.py: scale up with collector
9+
"""
10+
from __future__ import annotations
11+
12+
import json
13+
import shutil
14+
import time
15+
from pathlib import Path
16+
from unittest.mock import MagicMock, patch
17+
18+
import pytest
19+
20+
from matlab_mcp.config import AppConfig, ExecutionConfig, PoolConfig, SecurityConfig, WorkspaceConfig
21+
from matlab_mcp.jobs.executor import JobExecutor
22+
from matlab_mcp.jobs.tracker import JobTracker
23+
from matlab_mcp.pool.engine import EngineState, MatlabEngineWrapper
24+
from matlab_mcp.security.validator import SecurityValidator
25+
from tests.mocks.matlab_engine_mock import MockMatlabEngine
26+
27+
28+
# ---------------------------------------------------------------------------
29+
# Helpers
30+
# ---------------------------------------------------------------------------
31+
32+
def _make_mock_pool():
33+
mock_engine_inner = MockMatlabEngine()
34+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
35+
workspace_cfg = WorkspaceConfig()
36+
wrapper = MatlabEngineWrapper("engine-0", pool_cfg, workspace_cfg)
37+
wrapper._engine = mock_engine_inner
38+
wrapper._state = EngineState.IDLE
39+
40+
class MockPool:
41+
async def acquire(self):
42+
wrapper.mark_busy()
43+
return wrapper
44+
async def release(self, engine):
45+
engine.mark_idle()
46+
47+
return MockPool(), wrapper, mock_engine_inner
48+
49+
50+
def _make_executor(sync_timeout=5):
51+
pool, wrapper, inner = _make_mock_pool()
52+
tracker = JobTracker()
53+
config = AppConfig()
54+
config.execution = ExecutionConfig(sync_timeout=sync_timeout)
55+
executor = JobExecutor(pool=pool, tracker=tracker, config=config)
56+
return executor, tracker
57+
58+
59+
def _make_security(enabled=True, blocked=None):
60+
if blocked is None:
61+
blocked = ["system", "unix", "dos", "!"]
62+
cfg = SecurityConfig(
63+
blocked_functions_enabled=enabled,
64+
blocked_functions=blocked,
65+
)
66+
return SecurityValidator(cfg)
67+
68+
69+
# ---------------------------------------------------------------------------
70+
# tools/core.py — check_code_impl branches
71+
# ---------------------------------------------------------------------------
72+
73+
74+
class TestCheckCodeImplBranches:
75+
async def test_check_code_valid_json_output(self, tmp_path):
76+
"""When executor returns valid JSON, check_code should parse it."""
77+
from matlab_mcp.tools.core import check_code_impl
78+
79+
class JsonExecutor:
80+
async def execute(self, session_id, code, temp_dir=None):
81+
return {
82+
"status": "completed",
83+
"job_id": "j-1",
84+
"text": json.dumps([{"line": 1, "message": "unused var"}]),
85+
}
86+
87+
result = await check_code_impl(
88+
code="x = 1;",
89+
session_id="s1",
90+
executor=JsonExecutor(),
91+
temp_dir=str(tmp_path),
92+
)
93+
assert result["status"] == "completed"
94+
assert isinstance(result["issues"], list)
95+
assert len(result["issues"]) == 1
96+
97+
async def test_check_code_non_completed_status(self, tmp_path):
98+
"""When executor returns non-completed status, return as-is."""
99+
from matlab_mcp.tools.core import check_code_impl
100+
101+
class FailExecutor:
102+
async def execute(self, session_id, code, temp_dir=None):
103+
return {
104+
"status": "failed",
105+
"job_id": "j-1",
106+
"error": {"type": "MatlabError", "message": "crash"},
107+
}
108+
109+
result = await check_code_impl(
110+
code="x = 1;",
111+
session_id="s1",
112+
executor=FailExecutor(),
113+
temp_dir=str(tmp_path),
114+
)
115+
assert result["status"] == "failed"
116+
117+
async def test_check_code_temp_file_cleanup_exception(self, tmp_path):
118+
"""If temp file cleanup fails, check_code should still return result."""
119+
from matlab_mcp.tools.core import check_code_impl
120+
121+
executor, _ = _make_executor()
122+
123+
# Make a read-only dir so unlink might fail on some systems
124+
# Use patching instead for reliability
125+
with patch("pathlib.Path.unlink", side_effect=PermissionError("read-only")):
126+
result = await check_code_impl(
127+
code="x = 1;",
128+
session_id="s1",
129+
executor=executor,
130+
temp_dir=str(tmp_path),
131+
)
132+
assert isinstance(result, dict)
133+
134+
135+
# ---------------------------------------------------------------------------
136+
# session/manager.py — config=None defaults
137+
# ---------------------------------------------------------------------------
138+
139+
140+
class TestSessionManagerConfigNone:
141+
def test_creates_with_none_config(self):
142+
"""SessionManager(None) should use default values."""
143+
from matlab_mcp.session.manager import SessionManager
144+
145+
manager = SessionManager(None)
146+
assert manager._max_sessions == 50
147+
assert manager._session_timeout == 3600
148+
149+
def test_destroy_session_with_rmtree_failure(self, tmp_path):
150+
"""destroy_session should handle rmtree failure gracefully."""
151+
from matlab_mcp.session.manager import SessionManager
152+
from matlab_mcp.config import load_config
153+
154+
manager = SessionManager(load_config(None))
155+
session = manager.create_session()
156+
sid = session.session_id
157+
158+
with patch("shutil.rmtree", side_effect=OSError("permission denied")):
159+
result = manager.destroy_session(sid)
160+
assert result is True
161+
162+
163+
# ---------------------------------------------------------------------------
164+
# monitoring/health.py — degraded conditions
165+
# ---------------------------------------------------------------------------
166+
167+
168+
class TestHealthDegradedConditions:
169+
def test_health_check_failures_trigger_degraded(self):
170+
"""Health check failures > 0 should trigger degraded status."""
171+
from matlab_mcp.monitoring.health import evaluate_health
172+
173+
collector = MagicMock()
174+
collector.pool = MagicMock()
175+
collector.pool.get_status.return_value = {
176+
"total": 4, "available": 2, "busy": 2, "max": 8,
177+
}
178+
collector.tracker = MagicMock()
179+
collector.tracker.list_jobs.return_value = []
180+
collector.sessions = MagicMock()
181+
collector.sessions.session_count = 1
182+
collector.start_time = time.time() - 3600 # 1 hour uptime
183+
collector.get_counters.return_value = {
184+
"completed_total": 10,
185+
"failed_total": 0,
186+
"cancelled_total": 0,
187+
"total_created_sessions": 5,
188+
"error_total": 0,
189+
"blocked_attempts": 0,
190+
"health_check_failures": 3, # This triggers degraded
191+
}
192+
193+
result = evaluate_health(collector)
194+
assert result["status"] == "degraded"
195+
assert any("health check" in issue.lower() for issue in result["issues"])
196+
197+
198+
# ---------------------------------------------------------------------------
199+
# pool/engine.py — additional coverage
200+
# ---------------------------------------------------------------------------
201+
202+
203+
class TestEngineWrapperAdditional:
204+
def test_is_alive_callable_is_alive(self):
205+
"""is_alive should call engine.is_alive() when it's callable."""
206+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
207+
workspace_cfg = WorkspaceConfig()
208+
wrapper = MatlabEngineWrapper("e-test", pool_cfg, workspace_cfg)
209+
mock_engine = MockMatlabEngine()
210+
wrapper._engine = mock_engine
211+
wrapper._state = EngineState.IDLE
212+
213+
# MockMatlabEngine.is_alive is a property that returns bool
214+
assert wrapper.is_alive is True
215+
216+
def test_is_alive_no_engine(self):
217+
"""is_alive should return False when engine is None."""
218+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
219+
workspace_cfg = WorkspaceConfig()
220+
wrapper = MatlabEngineWrapper("e-test", pool_cfg, workspace_cfg)
221+
assert wrapper.is_alive is False
222+
223+
def test_stop_with_quit_exception(self):
224+
"""stop() should handle quit() exception gracefully."""
225+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
226+
workspace_cfg = WorkspaceConfig()
227+
wrapper = MatlabEngineWrapper("e-test", pool_cfg, workspace_cfg)
228+
mock_engine = MagicMock()
229+
mock_engine.quit.side_effect = RuntimeError("quit failed")
230+
wrapper._engine = mock_engine
231+
wrapper._state = EngineState.IDLE
232+
233+
wrapper.stop()
234+
assert wrapper._state == EngineState.STOPPED
235+
assert wrapper._engine is None
236+
237+
def test_start_addpath_exception(self):
238+
"""start() should handle addpath exception without crashing."""
239+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
240+
workspace_cfg = WorkspaceConfig(default_paths=["/some/path"])
241+
242+
wrapper = MatlabEngineWrapper("e-test", pool_cfg, workspace_cfg)
243+
244+
mock_module = MagicMock()
245+
mock_engine = MockMatlabEngine()
246+
# Make addpath raise for the first path
247+
mock_engine.addpath = MagicMock(side_effect=RuntimeError("path error"))
248+
mock_module.start_matlab.return_value = mock_engine
249+
wrapper._get_matlab_engine_module = lambda: mock_module
250+
251+
wrapper.start()
252+
assert wrapper._state == EngineState.IDLE
253+
254+
def test_start_startup_command_exception(self):
255+
"""start() should handle startup command exception."""
256+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
257+
workspace_cfg = WorkspaceConfig(startup_commands=["cd /nonexistent"])
258+
259+
wrapper = MatlabEngineWrapper("e-test", pool_cfg, workspace_cfg)
260+
261+
mock_module = MagicMock()
262+
mock_engine = MockMatlabEngine()
263+
# eval will ignore unrecognized commands, so mock it to raise
264+
original_eval = mock_engine.eval
265+
def raising_eval(code, **kwargs):
266+
if code == "cd /nonexistent":
267+
raise RuntimeError("command failed")
268+
return original_eval(code, **kwargs)
269+
mock_engine.eval = raising_eval
270+
mock_module.start_matlab.return_value = mock_engine
271+
wrapper._get_matlab_engine_module = lambda: mock_module
272+
273+
wrapper.start()
274+
assert wrapper._state == EngineState.IDLE
275+
276+
def test_reset_workspace_restoredefaultpath_exception(self):
277+
"""reset_workspace should handle restoredefaultpath exception."""
278+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
279+
workspace_cfg = WorkspaceConfig()
280+
wrapper = MatlabEngineWrapper("e-test", pool_cfg, workspace_cfg)
281+
282+
mock_engine = MockMatlabEngine()
283+
mock_engine.restoredefaultpath = MagicMock(side_effect=RuntimeError("restore failed"))
284+
wrapper._engine = mock_engine
285+
wrapper._state = EngineState.IDLE
286+
287+
wrapper.reset_workspace() # should not raise
288+
289+
def test_reset_workspace_readdpath_exception(self):
290+
"""reset_workspace should handle re-addpath exception."""
291+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
292+
workspace_cfg = WorkspaceConfig(default_paths=["/bad/path"])
293+
wrapper = MatlabEngineWrapper("e-test", pool_cfg, workspace_cfg)
294+
295+
mock_engine = MockMatlabEngine()
296+
mock_engine.addpath = MagicMock(side_effect=RuntimeError("addpath failed"))
297+
wrapper._engine = mock_engine
298+
wrapper._state = EngineState.IDLE
299+
300+
wrapper.reset_workspace() # should not raise
301+
302+
def test_reset_workspace_startup_command_exception(self):
303+
"""reset_workspace should handle startup command exception."""
304+
pool_cfg = PoolConfig(min_engines=1, max_engines=2)
305+
workspace_cfg = WorkspaceConfig(startup_commands=["bad_cmd"])
306+
wrapper = MatlabEngineWrapper("e-test", pool_cfg, workspace_cfg)
307+
308+
mock_engine = MockMatlabEngine()
309+
original_eval = mock_engine.eval
310+
def raising_eval(code, **kwargs):
311+
if code == "bad_cmd":
312+
raise RuntimeError("bad command")
313+
return original_eval(code, **kwargs)
314+
mock_engine.eval = raising_eval
315+
wrapper._engine = mock_engine
316+
wrapper._state = EngineState.IDLE
317+
318+
wrapper.reset_workspace() # should not raise

0 commit comments

Comments
 (0)