Skip to content

Commit e80432f

Browse files
krisctlprabhakk-mw
authored andcommitted
Adds tests for MATLABKernelUsingMPM and Updates source code for better test-ability.
1 parent 5974a4c commit e80432f

File tree

7 files changed

+298
-41
lines changed

7 files changed

+298
-41
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ dev = [
6363
"pytest-aiohttp",
6464
"pytest-asyncio",
6565
"pytest-cov",
66-
"pytest-playwright"
66+
"pytest-mock",
67+
"pytest-playwright",
6768
]
6869

6970
[project.entry-points.jupyter_serverproxy_servers]

src/jupyter_matlab_kernel/__main__.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,14 @@
11
# Copyright 2023-2024 The MathWorks, Inc.
22
# Use ipykernel infrastructure to launch the MATLAB Kernel.
3-
import os
4-
5-
6-
def is_fallback_kernel_enabled():
7-
"""
8-
Checks if the fallback kernel is enabled based on an environment variable.
9-
10-
Returns:
11-
bool: True if the fallback kernel is enabled, False otherwise.
12-
"""
13-
14-
# Get the env var toggle
15-
use_fallback_kernel = os.getenv("MWI_USE_FALLBACK_KERNEL", "TRUE")
16-
return use_fallback_kernel.lower().strip() == "true"
17-
183

194
if __name__ == "__main__":
205
from ipykernel.kernelapp import IPKernelApp
6+
217
from jupyter_matlab_kernel import mwi_logger
8+
from jupyter_matlab_kernel.kernel_factory import KernelFactory
229

2310
logger = mwi_logger.get(init=True)
24-
kernel_class = None
25-
26-
if is_fallback_kernel_enabled():
27-
from jupyter_matlab_kernel.jsp_kernel import MATLABKernelUsingJSP
28-
29-
kernel_class = MATLABKernelUsingJSP
30-
else:
31-
from jupyter_matlab_kernel.mpm_kernel import MATLABKernelUsingMPM
32-
33-
kernel_class = MATLABKernelUsingMPM
3411

12+
kernel_class = KernelFactory.get_kernel_class()
13+
logger.debug("Kernel type to be launched: %s", kernel_class)
3514
IPKernelApp.launch_instance(kernel_class=kernel_class, log=logger)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import os
4+
from typing import Union
5+
6+
from jupyter_matlab_kernel.jsp_kernel import MATLABKernelUsingJSP
7+
from jupyter_matlab_kernel.mpm_kernel import MATLABKernelUsingMPM
8+
9+
10+
class KernelFactory:
11+
"""
12+
KernelFactory class for determining and returning the appropriate MATLAB kernel class.
13+
14+
This class provides a static method to decide between different MATLAB kernel
15+
implementations based on configuration settings.
16+
"""
17+
18+
@staticmethod
19+
def _is_fallback_kernel_enabled():
20+
"""
21+
Checks if the fallback kernel is enabled based on an environment variable.
22+
23+
Returns:
24+
bool: True if the fallback kernel is enabled, False otherwise.
25+
"""
26+
27+
# Get the env var toggle
28+
use_fallback_kernel = os.getenv("MWI_USE_FALLBACK_KERNEL", "TRUE")
29+
return use_fallback_kernel.lower().strip() == "true"
30+
31+
@staticmethod
32+
def get_kernel_class() -> Union[MATLABKernelUsingJSP, MATLABKernelUsingMPM]:
33+
"""
34+
Determines and returns the appropriate MATLAB kernel class to use.
35+
36+
Returns:
37+
BaseMATLABKernel: The class of the MATLAB kernel to be used. This will
38+
be either `MATLABKernelUsingJSP` if the fallback kernel is enabled,
39+
or `MATLABKernelUsingMPM` otherwise.
40+
"""
41+
return (
42+
MATLABKernelUsingJSP
43+
if KernelFactory._is_fallback_kernel_enabled()
44+
else MATLABKernelUsingMPM
45+
)

tests/unit/jupyter_matlab_kernel/test_kernel.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
# This file contains tests for jupyter_matlab_kernel.kernel
44
import mocks.mock_jupyter_server as MockJupyterServer
55
import pytest
6-
from jupyter_matlab_kernel.jsp_kernel import start_matlab_proxy
7-
from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError
86
from jupyter_server import serverapp
97
from mocks.mock_jupyter_server import MockJupyterServerFixture
108

9+
from jupyter_matlab_kernel.jsp_kernel import start_matlab_proxy
10+
from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError
11+
1112

1213
def test_start_matlab_proxy_without_jupyter_server():
1314
"""
@@ -18,7 +19,7 @@ def test_start_matlab_proxy_without_jupyter_server():
1819
start_matlab_proxy()
1920

2021
assert (
21-
"MATLAB Kernel for Jupyter was unable to find the notebook server from which it was spawned!"
22+
"MATLAB Kernel for Jupyter was unable to find the notebook server from which it was spawned"
2223
in str(exceptionInfo.value)
2324
)
2425

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import pytest
4+
5+
from jupyter_matlab_kernel.jsp_kernel import MATLABKernelUsingJSP
6+
from jupyter_matlab_kernel.kernel_factory import KernelFactory
7+
from jupyter_matlab_kernel.mpm_kernel import MATLABKernelUsingMPM
8+
9+
10+
@pytest.mark.parametrize(
11+
"env_value, expected_kernel_class",
12+
[
13+
pytest.param("TRUE", MATLABKernelUsingJSP, id="Default jupyter kernel"),
14+
pytest.param(
15+
"true", MATLABKernelUsingJSP, id="Case-insensitive match - jupyter kernel"
16+
),
17+
pytest.param("FALSE", MATLABKernelUsingMPM, id="Using proxy manager"),
18+
pytest.param(
19+
"false", MATLABKernelUsingMPM, id="Case-insensitive match - proxy manager"
20+
),
21+
pytest.param("TrUe", MATLABKernelUsingJSP, id="Mixed case"),
22+
pytest.param("", MATLABKernelUsingMPM, id="Empty string, use proxy manager"),
23+
],
24+
)
25+
def test_correct_kernel_type_is_returned(monkeypatch, env_value, expected_kernel_class):
26+
"""
27+
Test that the correct kernel type is returned based on the MWI_USE_FALLBACK_KERNEL environment variable.
28+
29+
Args:
30+
monkeypatch: Pytest fixture for modifying the environment.
31+
env_value: The value to set for the MWI_USE_FALLBACK_KERNEL environment variable.
32+
expected_kernel_class: The expected kernel class to be returned.
33+
"""
34+
monkeypatch.setenv("MWI_USE_FALLBACK_KERNEL", env_value)
35+
kernel_class = KernelFactory.get_kernel_class()
36+
assert kernel_class is expected_kernel_class
37+
38+
39+
def test_correct_kernel_type_is_returned_when_env_var_unset(monkeypatch):
40+
"""
41+
Test that the correct kernel type is returned when the MWI_USE_FALLBACK_KERNEL environment variable is unset.
42+
43+
This test ensures that when the environment variable is not set, the default kernel class (MATLABKernelUsingJSP) is returned.
44+
45+
Args:
46+
monkeypatch: Pytest fixture for modifying the environment.
47+
"""
48+
monkeypatch.delenv("MWI_USE_FALLBACK_KERNEL", raising=False)
49+
kernel_class = KernelFactory.get_kernel_class()
50+
assert kernel_class is MATLABKernelUsingJSP
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import uuid
4+
5+
import pytest
6+
7+
from jupyter_matlab_kernel.mpm_kernel import MATLABKernelUsingMPM
8+
from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError
9+
10+
11+
@pytest.fixture
12+
def mpm_kernel_instance(mocker) -> MATLABKernelUsingMPM:
13+
# Mock logger
14+
mock_logger = mocker.Mock()
15+
16+
# Use pytest-mock's mocker fixture to patch the function and log attribute
17+
mocker.patch(
18+
"jupyter_matlab_kernel.base_kernel.BaseMATLABKernel._extract_kernel_id_from_sys_args",
19+
return_value=uuid.uuid4().hex,
20+
)
21+
mocker.patch(
22+
"jupyter_matlab_kernel.base_kernel.BaseMATLABKernel.log", new=mock_logger
23+
)
24+
25+
return MATLABKernelUsingMPM()
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_initialize_matlab_proxy_with_mpm_success(mocker, mpm_kernel_instance):
30+
mpm_lib_start_matlab_proxy_response = {
31+
"absolute_url": "dummyURL",
32+
"mwi_base_url": "/matlab/dummy",
33+
"headers": "dummy_header",
34+
"mpm_auth_token": "dummy_token",
35+
}
36+
37+
# Use pytest-mock's mocker fixture to patch the function
38+
mocker.patch(
39+
"matlab_proxy_manager.lib.api.start_matlab_proxy_for_kernel",
40+
return_value=mpm_lib_start_matlab_proxy_response,
41+
)
42+
43+
# Call the method and assert the result
44+
result = await mpm_kernel_instance._initialize_matlab_proxy_with_mpm(
45+
mpm_kernel_instance.log
46+
)
47+
expected_response = tuple(mpm_lib_start_matlab_proxy_response.values())
48+
assert result == expected_response
49+
50+
51+
async def test_initialize_matlab_proxy_with_mpm_exception(mocker, mpm_kernel_instance):
52+
# Use pytest-mock's mocker fixture to patch the function
53+
mocker.patch(
54+
"matlab_proxy_manager.lib.api.start_matlab_proxy_for_kernel",
55+
side_effect=Exception("Simulated failure"),
56+
)
57+
58+
with pytest.raises(MATLABConnectionError) as exc_info:
59+
await mpm_kernel_instance._initialize_matlab_proxy_with_mpm(
60+
mpm_kernel_instance.log
61+
)
62+
assert (
63+
"Error: MATLAB Kernel could not start the MATLAB proxy process via proxy manager."
64+
in str(exc_info.value)
65+
)
66+
67+
68+
async def test_initialize_mwi_comm_helper(mocker, mpm_kernel_instance):
69+
# Mock the necessary attributes
70+
mpm_kernel_instance.io_loop = mocker.Mock()
71+
mpm_kernel_instance.io_loop.asyncio_loop = mocker.Mock()
72+
73+
mpm_kernel_instance.control_thread = mocker.Mock()
74+
mpm_kernel_instance.control_thread.io_loop = mocker.Mock()
75+
mpm_kernel_instance.control_thread.io_loop.asyncio_loop = mocker.Mock()
76+
77+
# Mock MWICommHelper
78+
mock_mwi_comm_helper = mocker.patch(
79+
"jupyter_matlab_kernel.mpm_kernel.MWICommHelper", autospec=True
80+
)
81+
mock_mwi_comm_helper_instance = mock_mwi_comm_helper.return_value
82+
mock_mwi_comm_helper_instance.connect = mocker.AsyncMock()
83+
84+
# Test parameters
85+
murl = "http://proxy-url.com"
86+
headers = {"MWI_AUTH_TOKEN": "test_token"}
87+
88+
# Run the test method
89+
await mpm_kernel_instance._initialize_mwi_comm_helper(murl, headers)
90+
91+
# Assertions
92+
mock_mwi_comm_helper.assert_called_once_with(
93+
mpm_kernel_instance.kernel_id,
94+
murl,
95+
mpm_kernel_instance.io_loop.asyncio_loop,
96+
mpm_kernel_instance.control_thread.io_loop.asyncio_loop,
97+
headers,
98+
mpm_kernel_instance.log,
99+
)
100+
mock_mwi_comm_helper_instance.connect.assert_awaited_once()
101+
102+
# Verify that the mwi_comm_helper instance variable is set
103+
assert mpm_kernel_instance.mwi_comm_helper == mock_mwi_comm_helper_instance
104+
105+
106+
def test_process_children_return_empty_list(mpm_kernel_instance):
107+
assert mpm_kernel_instance._process_children() == []
108+
109+
110+
async def test_do_shutdown_success(mocker, mpm_kernel_instance):
111+
mpm_kernel_instance.is_matlab_assigned = True
112+
113+
# Mock the mwi_comm_helper and its methods
114+
mpm_kernel_instance.mwi_comm_helper = mocker.Mock()
115+
mpm_kernel_instance.mwi_comm_helper.send_shutdown_request_to_matlab = (
116+
mocker.AsyncMock()
117+
)
118+
mpm_kernel_instance.mwi_comm_helper.disconnect = mocker.AsyncMock()
119+
120+
# Mock the mpm_lib.shutdown function
121+
mock_shutdown = mocker.patch(
122+
"matlab_proxy_manager.lib.api.shutdown", return_value=mocker.AsyncMock()
123+
)
124+
# Call the method
125+
restart = False
126+
await mpm_kernel_instance.do_shutdown(restart)
127+
128+
# Assertions
129+
mpm_kernel_instance.mwi_comm_helper.send_shutdown_request_to_matlab.assert_awaited_once()
130+
mpm_kernel_instance.mwi_comm_helper.disconnect.assert_awaited_once()
131+
mock_shutdown.assert_awaited_once_with(
132+
mpm_kernel_instance.parent_pid,
133+
mpm_kernel_instance.kernel_id,
134+
mpm_kernel_instance.mpm_auth_token,
135+
)
136+
assert not mpm_kernel_instance.is_matlab_assigned
137+
mpm_kernel_instance.log.debug.assert_any_call(
138+
"Received shutdown request from Jupyter"
139+
)
140+
141+
142+
async def test_do_shutdown_exception(mocker, mpm_kernel_instance):
143+
mpm_kernel_instance.is_matlab_assigned = True
144+
145+
# Mock the mwi_comm_helper and its methods
146+
mpm_kernel_instance.mwi_comm_helper = mocker.Mock()
147+
mpm_kernel_instance.mwi_comm_helper.send_shutdown_request_to_matlab = (
148+
mocker.AsyncMock(side_effect=MATLABConnectionError("Test connection error"))
149+
)
150+
mpm_kernel_instance.mwi_comm_helper.disconnect = mocker.AsyncMock()
151+
152+
# Mock the mpm_lib.shutdown function
153+
mock_shutdown = mocker.patch(
154+
"matlab_proxy_manager.lib.api.shutdown", return_value=mocker.AsyncMock()
155+
)
156+
# Call the method
157+
restart = False
158+
await mpm_kernel_instance.do_shutdown(restart)
159+
160+
# Assertions
161+
mpm_kernel_instance.mwi_comm_helper.send_shutdown_request_to_matlab.assert_awaited_once()
162+
163+
# Not awaited since there was an exception right above
164+
mpm_kernel_instance.mwi_comm_helper.disconnect.assert_not_awaited()
165+
mock_shutdown.assert_awaited_once_with(
166+
mpm_kernel_instance.parent_pid,
167+
mpm_kernel_instance.kernel_id,
168+
mpm_kernel_instance.mpm_auth_token,
169+
)
170+
assert not mpm_kernel_instance.is_matlab_assigned

0 commit comments

Comments
 (0)