1- # Copyright 2023-2025 The MathWorks, Inc.
1+ # Copyright 2023-2026 The MathWorks, Inc.
22
33import asyncio
44import json
55import os
66from dataclasses import dataclass
77from pathlib import Path
88from typing import Optional
9+ from unittest .mock import AsyncMock , MagicMock
910
1011import pytest
11- from matlab_proxy import settings
1212
1313from matlab_proxy import settings
1414from matlab_proxy .app_state import AppState
1818 MatlabError ,
1919 MatlabInstallError ,
2020)
21- from matlab_proxy .constants import (
22- CONNECTOR_SECUREPORT_FILENAME ,
23- USER_CODE_OUTPUT_FILE_NAME ,
24- )
25-
26- from tests .unit .util import MockResponse
2721from tests .unit .test_constants import CHECK_MATLAB_STATUS_INTERVAL , FIVE_MAX_TRIES
22+ from tests .unit .util import MockResponse
2823
2924
3025@pytest .fixture
@@ -145,20 +140,32 @@ class Mock_xvfb:
145140 returncode : Optional [int ]
146141 pid : Optional [int ]
147142
143+ async def wait (self ) -> int :
144+ return self .returncode
145+
146+ def terminate (self ):
147+ """Mock terminate method"""
148+ pass
149+
148150
149151@dataclass (frozen = True )
150152class Mock_matlab :
151153 """An immutable dataclass representing a mocked MATLAB process"""
152154
153155 returncode : Optional [int ]
154156 pid : Optional [int ]
157+ stderr : MagicMock = MagicMock ()
155158
156159 def is_running (self ) -> bool :
157160 return self .returncode is None
158161
159- def wait (self ) -> int :
162+ async def wait (self ) -> int :
160163 return self .returncode
161164
165+ def terminate (self ):
166+ """Mock terminate method"""
167+ pass
168+
162169
163170@pytest .mark .parametrize (
164171 "licensing, expected" ,
@@ -547,6 +554,14 @@ async def test_requests_sent_by_matlab_proxy_have_headers(
547554 ok = True , payload = {"messages" : {"EvalResponse" : [{"isError" : None }]}}
548555 )
549556 mocked_req = mocker .patch ("aiohttp.ClientSession.request" , return_value = mock_resp )
557+ mock_xvfb = Mock_xvfb (None , 12345 )
558+ mock_matlab = Mock_matlab (None , 12346 )
559+
560+ app_state = app_state_with_token_auth_fixture
561+
562+ app_state .matlab_session_files ["matlab_ready_file" ] = (
563+ app_state .matlab_session_files ["matlab_ready_file" ].touch ()
564+ )
550565
551566 # Patching to make _are_required_processes_ready() to return True
552567 mocker .patch .object (
@@ -560,23 +575,31 @@ async def test_requests_sent_by_matlab_proxy_have_headers(
560575 "get_matlab_state" ,
561576 return_value = "up" ,
562577 )
563- # Wait for _update_matlab_connector_status to run
564- await asyncio .sleep (CHECK_MATLAB_STATUS_INTERVAL )
578+
579+ # Patching xvfb subprocess
580+ mocker .patch .object (
581+ AppState , "_AppState__start_xvfb_process" , return_value = mock_xvfb
582+ )
583+
584+ # Patching xvfb subprocess
585+ mocker .patch .object (
586+ AppState , "_AppState__start_matlab_process" , return_value = mock_matlab
587+ )
588+
589+ # start matlab
590+ await app_state .start_matlab ()
591+
592+ # Wait for fake MATLAB to startup
593+ await asyncio .sleep (CHECK_MATLAB_STATUS_INTERVAL * 5 )
565594
566595 # Act
567- await app_state_with_token_auth_fixture ._AppState__send_stop_request_to_matlab ()
596+ # Send the stop request
597+ await app_state ._AppState__send_stop_request_to_matlab ()
568598
569599 # Assert
570-
571- # 1 request from _update_matlab_connector_status() and another from
572- # /stop_matlab request
573- connector_status_request_headers = list (mocked_req .call_args_list )[0 ].kwargs [
574- "headers"
575- ]
576- send_stop_matlab_request_headers = list (mocked_req .call_args_list )[1 ].kwargs [
600+ send_stop_matlab_request_headers = list (mocked_req .call_args_list )[0 ].kwargs [
577601 "headers"
578602 ]
579- assert sample_token_headers_fixture == connector_status_request_headers
580603 assert sample_token_headers_fixture == send_stop_matlab_request_headers
581604
582605
@@ -711,7 +734,7 @@ async def test_detect_active_client_status_can_reset_active_client(app_state_fix
711734 )
712735 assert (
713736 app_state_fixture .active_client == None
714- ), f "Expected the active_client to be None"
737+ ), "Expected the active_client to be None"
715738
716739
717740@pytest .mark .parametrize (
@@ -815,21 +838,32 @@ async def test_decrement_timer_runs_out(sample_settings_fixture, mocker):
815838 idle_timeout = 1
816839 sample_settings_fixture ["mwi_idle_timeout" ] = idle_timeout
817840 app_state = AppState (settings = sample_settings_fixture )
818- app_state .processes = {"matlab" : None , "xvfb" : None }
841+ mock_xvfb = Mock_xvfb (None , 12345 )
842+ mock_matlab = Mock_matlab (None , 12346 )
843+
819844 app_state .licensing = {"type" : "existing_license" }
820845
821846 # mock util.get_event_loop() to return a new event_loop for the test to assert
822847 mock_loop = asyncio .new_event_loop ()
823848 mocker .patch ("matlab_proxy.app_state.util.get_event_loop" , return_value = mock_loop )
849+ # Patching xvfb subprocess
850+ mocker .patch .object (
851+ AppState , "_AppState__start_xvfb_process" , return_value = mock_xvfb
852+ )
853+
854+ # Patching xvfb subprocess
855+ mocker .patch .object (
856+ AppState , "_AppState__start_matlab_process" , return_value = mock_matlab
857+ )
824858
825859 # Act
826860 # Wait for a little more time than idle_timeout to decrease flakiness of this test on different platforms.
827- # MATLAB state changes from down -> starting -> up -> down (idle timer runs out)
861+ # MATLAB state is set to 'stopping' by stop_matlab() which is called once the decrement timer runs out.
828862 await asyncio .sleep (idle_timeout * FIVE_MAX_TRIES )
829863
830864 # Assert
831865 assert not mock_loop .is_running ()
832- assert app_state .get_matlab_state () == "down "
866+ assert app_state .get_matlab_state () == "stopping "
833867
834868 # Cleanup
835869 mock_loop .stop ()
@@ -1009,12 +1043,12 @@ async def assert_matlab_state(app_state_fixture, expected_matlab_status, count):
10091043@pytest .mark .parametrize (
10101044 "matlab_ready_file, expected_matlab_status" ,
10111045 [
1012- (None , "down " ),
1013- (Path ("dummy" ), "starting " ),
1046+ (None , "starting " ),
1047+ (Path ("dummy" ), "up " ),
10141048 ],
10151049 ids = [
10161050 "no_matlab_ready_file_formed" ,
1017- "no_matlab_ready_file_created " ,
1051+ "matlab_ready_file_created " ,
10181052 ],
10191053)
10201054async def test_check_matlab_connector_status_auto_updates_based_on_matlab_ready_file (
@@ -1034,18 +1068,52 @@ async def test_check_matlab_connector_status_auto_updates_based_on_matlab_ready_
10341068 "_are_required_processes_ready" ,
10351069 return_value = True ,
10361070 )
1071+ if matlab_ready_file :
1072+ matlab_ready_file .touch ()
1073+ print ("File eixts " , matlab_ready_file .exists ())
1074+
10371075 app_state_fixture .matlab_session_files ["matlab_ready_file" ] = matlab_ready_file
10381076
1077+ mock_xvfb = Mock_xvfb (None , 12345 )
1078+ mock_matlab = Mock_matlab (None , 12346 )
1079+
1080+ # Patching xvfb subprocess
1081+ mocker .patch .object (
1082+ AppState , "_AppState__start_xvfb_process" , return_value = mock_xvfb
1083+ )
1084+
1085+ # Patching xvfb subprocess
1086+ mocker .patch .object (
1087+ AppState , "_AppState__start_matlab_process" , return_value = mock_matlab
1088+ )
1089+
1090+ # MATLAB state should be 'down' first
1091+ assert app_state_fixture .get_matlab_state () == "down"
1092+
1093+ mocker .patch .object (
1094+ AppState , "_AppState__matlab_stderr_reader_posix" , new = AsyncMock ()
1095+ )
1096+ mocker .patch .object (
1097+ AppState , "_AppState__track_embedded_connector_state" , new = AsyncMock ()
1098+ )
1099+ mocker .patch .object (AppState , "_AppState__update_matlab_port" , new = AsyncMock ())
1100+ mocker .patch (
1101+ "matlab_proxy.app_state.mwi.embedded_connector.request.get_state" ,
1102+ return_value = "up" ,
1103+ )
1104+
10391105 # Act
1040- # Nothing to act upon as the _update_matlab_state() is started automatically in the constructor.
1106+ await app_state_fixture .start_matlab ()
1107+
1108+ if matlab_ready_file :
1109+ app_state_fixture .matlab_session_files ["matlab_ready_file" ].touch ()
1110+
1111+ # Nothing to act upon as the _update_matlab_state() is started by start_matlab().
10411112 # Have to wait here for the atleast the same interval as the __update_matlab_state_based_on_endpoint_to_use()
1042- # for the MATLAB status to update from 'down'
1113+ # for the MATLAB status to update from 'down' to 'up'
10431114
10441115 # Assert
1045- # MATLAB state should be 'down' first
1046- assert app_state_fixture .get_matlab_state () == "down"
10471116 await asyncio .sleep (CHECK_MATLAB_STATUS_INTERVAL )
1048-
10491117 await assert_matlab_state (app_state_fixture , expected_matlab_status , FIVE_MAX_TRIES )
10501118
10511119
@@ -1062,6 +1130,18 @@ async def test_update_matlab_state_switches_to_busy_endpoint(
10621130 """
10631131 # Arrange
10641132 # Setup mocks for the first ping request to be successful
1133+ mock_xvfb = Mock_xvfb (None , 12345 )
1134+ mock_matlab = Mock_matlab (None , 12346 )
1135+
1136+ # Patching xvfb subprocess
1137+ mocker .patch .object (
1138+ AppState , "_AppState__start_xvfb_process" , return_value = mock_xvfb
1139+ )
1140+
1141+ # Patching xvfb subprocess
1142+ mocker .patch .object (
1143+ AppState , "_AppState__start_matlab_process" , return_value = mock_matlab
1144+ )
10651145 mocker .patch .object (
10661146 AppState ,
10671147 "_are_required_processes_ready" ,
@@ -1071,6 +1151,19 @@ async def test_update_matlab_state_switches_to_busy_endpoint(
10711151 "matlab_proxy.app_state.mwi.embedded_connector.request.get_state" ,
10721152 return_value = "up" ,
10731153 )
1154+ mocker .patch .object (
1155+ AppState , "_AppState__matlab_stderr_reader_posix" , new = AsyncMock ()
1156+ )
1157+ mocker .patch .object (
1158+ AppState , "_AppState__track_embedded_connector_state" , new = AsyncMock ()
1159+ )
1160+ mocker .patch .object (AppState , "_AppState__update_matlab_port" , new = AsyncMock ())
1161+
1162+ # MATLAB state should be 'down' first
1163+ assert app_state_fixture .get_matlab_state () == "down"
1164+
1165+ await app_state_fixture .start_matlab ()
1166+
10741167 tmp_file = tmp_path / Path ("dummy" )
10751168 tmp_file .touch ()
10761169 app_state_fixture .matlab_session_files ["matlab_ready_file" ] = tmp_file
0 commit comments