matlab-proxy 0.18.1__py3-none-any.whl → 0.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of matlab-proxy might be problematic. Click here for more details.

Files changed (33) hide show
  1. matlab_proxy/app.py +54 -43
  2. matlab_proxy/app_state.py +370 -155
  3. matlab_proxy/constants.py +3 -0
  4. matlab_proxy/gui/asset-manifest.json +6 -6
  5. matlab_proxy/gui/index.html +1 -1
  6. matlab_proxy/gui/static/css/{main.47712126.css → main.da9c4eb8.css} +2 -2
  7. matlab_proxy/gui/static/css/main.da9c4eb8.css.map +1 -0
  8. matlab_proxy/gui/static/js/{main.5b5ca2f2.js → main.e07799e7.js} +3 -3
  9. matlab_proxy/gui/static/js/main.e07799e7.js.map +1 -0
  10. matlab_proxy/matlab/startup.m +0 -20
  11. matlab_proxy/settings.py +28 -3
  12. matlab_proxy/util/__init__.py +101 -1
  13. matlab_proxy/util/event_loop.py +28 -10
  14. matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
  15. matlab_proxy/util/mwi/embedded_connector/helpers.py +9 -0
  16. matlab_proxy/util/mwi/embedded_connector/request.py +51 -21
  17. matlab_proxy/util/mwi/environment_variables.py +6 -1
  18. matlab_proxy/util/mwi/exceptions.py +16 -1
  19. matlab_proxy/util/mwi/validators.py +33 -0
  20. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/METADATA +1 -1
  21. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/RECORD +31 -31
  22. tests/unit/test_app.py +45 -22
  23. tests/unit/test_app_state.py +404 -111
  24. tests/unit/test_constants.py +1 -0
  25. tests/unit/util/mwi/test_validators.py +30 -1
  26. tests/unit/util/test_util.py +83 -0
  27. matlab_proxy/gui/static/css/main.47712126.css.map +0 -1
  28. matlab_proxy/gui/static/js/main.5b5ca2f2.js.map +0 -1
  29. /matlab_proxy/gui/static/js/{main.5b5ca2f2.js.LICENSE.txt → main.e07799e7.js.LICENSE.txt} +0 -0
  30. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/LICENSE.md +0 -0
  31. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/WHEEL +0 -0
  32. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/entry_points.txt +0 -0
  33. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  # Copyright 2023-2024 The MathWorks, Inc.
2
2
 
3
+ import asyncio
3
4
  import json
4
5
  import os
5
6
  from dataclasses import dataclass
@@ -13,13 +14,14 @@ from matlab_proxy import settings
13
14
  from matlab_proxy.app_state import AppState
14
15
  from matlab_proxy.constants import MWI_AUTH_TOKEN_NAME_FOR_HTTP
15
16
  from matlab_proxy.util.mwi.exceptions import LicensingError, MatlabError
16
- from tests.unit.util import MockResponse
17
-
18
17
  from matlab_proxy.constants import (
19
18
  CONNECTOR_SECUREPORT_FILENAME,
20
19
  USER_CODE_OUTPUT_FILE_NAME,
21
20
  )
22
21
 
22
+ from tests.unit.util import MockResponse
23
+ from tests.unit.test_constants import CHECK_MATLAB_STATUS_INTERVAL, FIVE_MAX_TRIES
24
+
23
25
 
24
26
  @pytest.fixture
25
27
  def sample_settings_fixture(tmp_path):
@@ -42,15 +44,19 @@ def sample_settings_fixture(tmp_path):
42
44
  "app_port": 12345,
43
45
  "mwapikey": "asdf",
44
46
  "has_custom_code_to_execute": False,
47
+ "mwi_idle_timeout": 100,
48
+ "mwi_is_token_auth_enabled": False,
49
+ "integration_name": "MATLAB Desktop",
45
50
  }
46
51
 
47
52
 
48
53
  @pytest.fixture
49
- def app_state_fixture(sample_settings_fixture):
54
+ def app_state_fixture(sample_settings_fixture, loop):
50
55
  """A pytest fixture which returns an instance of AppState class with no errors.
51
56
 
52
57
  Args:
53
58
  sample_settings_fixture (dict): A dictionary of sample settings to be used by
59
+ loop : A pytest builtin fixture
54
60
 
55
61
  Returns:
56
62
  AppState: An object of the AppState class
@@ -58,7 +64,10 @@ def app_state_fixture(sample_settings_fixture):
58
64
  app_state = AppState(settings=sample_settings_fixture)
59
65
  app_state.processes = {"matlab": None, "xvfb": None}
60
66
  app_state.licensing = {"type": "existing_license"}
61
- return app_state
67
+
68
+ yield app_state
69
+
70
+ loop.run_until_complete(app_state.stop_server_tasks())
62
71
 
63
72
 
64
73
  @pytest.fixture
@@ -95,12 +104,13 @@ def app_state_with_token_auth_fixture(
95
104
 
96
105
 
97
106
  @pytest.fixture
98
- def mocker_os_patching_fixture(mocker, platform):
107
+ def mocker_os_patching_fixture(mocker, platform, loop):
99
108
  """A pytest fixture which patches the is_* functions in system.py module
100
109
 
101
110
  Args:
102
111
  mocker : Built in pytest fixture
103
112
  platform (str): A string representing "windows", "linux" or "mac"
113
+ loop : A pytest builtin fixture
104
114
 
105
115
  Returns:
106
116
  mocker: Built in pytest fixture with patched calls to system.py module.
@@ -109,6 +119,7 @@ def mocker_os_patching_fixture(mocker, platform):
109
119
  mocker.patch("matlab_proxy.app_state.system.is_windows", return_value=False)
110
120
  mocker.patch("matlab_proxy.app_state.system.is_mac", return_value=False)
111
121
  mocker.patch("matlab_proxy.app_state.system.is_posix", return_value=False)
122
+ mocker.patch("matlab_proxy.app_state.util.get_event_loop", return_value=loop)
112
123
 
113
124
  if platform == "linux":
114
125
  mocker.patch("matlab_proxy.app_state.system.is_linux", return_value=True)
@@ -140,6 +151,12 @@ class Mock_matlab:
140
151
  returncode: Optional[int]
141
152
  pid: Optional[int]
142
153
 
154
+ def is_running(self) -> bool:
155
+ return self.returncode is None
156
+
157
+ def wait(self) -> int:
158
+ return self.returncode
159
+
143
160
 
144
161
  @pytest.mark.parametrize(
145
162
  "licensing, expected",
@@ -265,6 +282,7 @@ def test_persist_config_data(licensing_data: dict, tmp_path):
265
282
  "error": None,
266
283
  "matlab_version": None,
267
284
  "warnings": [],
285
+ "mwi_idle_timeout": None,
268
286
  }
269
287
  app_state = AppState(settings=settings)
270
288
  app_state.licensing = licensing_data
@@ -346,89 +364,6 @@ def test_are_required_processes_ready(
346
364
  assert actual == expected
347
365
 
348
366
 
349
- get_matlab_status_based_on_connector_status_test_data = [
350
- ("up", True, "up"),
351
- ("down", True, "starting"),
352
- ("up", False, "starting"),
353
- ]
354
-
355
-
356
- @pytest.mark.parametrize(
357
- "connector_status, ready_file_present, matlab_status",
358
- get_matlab_status_based_on_connector_status_test_data,
359
- ids=["connector_up", "connector_down", "connector_up_ready_file_not_present"],
360
- )
361
- async def test_get_matlab_status_based_on_connector_status(
362
- mocker, app_state_fixture, connector_status, ready_file_present, matlab_status
363
- ):
364
- """Test to check matlab status based on connector status
365
-
366
- Args:
367
- mocker : Built in pytest fixture.
368
- connector_status (str): Status of Embedded Connector.
369
- ready_file_present (bool): Represents if the ready file has been created or not.
370
- matlab_status (str): Represents the status of MATLAB process.
371
- """
372
- # Arrange
373
- mocker.patch(
374
- "matlab_proxy.app_state.mwi.embedded_connector.request.get_state",
375
- return_value=connector_status,
376
- )
377
- mocker.patch.object(Path, "exists", return_value=ready_file_present)
378
- app_state_fixture.settings["mwi_is_token_auth_enabled"] = False
379
- app_state_fixture.matlab_session_files["matlab_ready_file"] = Path("dummy")
380
-
381
- # Act
382
- actual_matlab_status = await app_state_fixture._get_matlab_connector_status()
383
-
384
- # Assert
385
- assert actual_matlab_status == matlab_status
386
-
387
-
388
- @pytest.mark.parametrize(
389
- "valid_processes, connector_status, expected",
390
- [
391
- (True, "up", "up"),
392
- (False, "up", "down"),
393
- (True, "down", "down"),
394
- ],
395
- ids=[
396
- "valid_processes_connector_up",
397
- "invalid_processes_connector_up",
398
- "valid_processes_connector_down",
399
- ],
400
- )
401
- async def test_get_matlab_state(
402
- app_state_fixture, mocker, valid_processes, connector_status, expected
403
- ):
404
- """Test to check get_matlab_state returns the correct MATLAB state based on the connector status
405
-
406
- Args:
407
- app_state_fixture (AppState): Object of AppState class with defaults set
408
- mocker : Built in pytest fixture
409
- valid_processes (bool): Represents if the processes are valid or not
410
- connector_status (str): Status of Embedded Connector.
411
- expected (str): Expected status of MATLAB process.
412
- """
413
- # Arrange
414
- mocker.patch.object(
415
- AppState,
416
- "_are_required_processes_ready",
417
- return_value=valid_processes,
418
- )
419
- mocker.patch.object(
420
- AppState,
421
- "_get_matlab_connector_status",
422
- return_value=connector_status,
423
- )
424
-
425
- # Act
426
- actual_state = await app_state_fixture.get_matlab_state()
427
-
428
- # Assert
429
- assert actual_state == expected
430
-
431
-
432
367
  @pytest.mark.parametrize("platform", [("linux"), ("windows"), ("mac")])
433
368
  async def test_track_embedded_connector(mocker_os_patching_fixture, app_state_fixture):
434
369
  """Test to check track_embedded_connector task
@@ -517,47 +452,55 @@ async def test_setup_env_for_matlab(
517
452
  assert expected_output in matlab_env["MW_DIAGNOSTIC_DEST"]
518
453
 
519
454
 
520
- @pytest.mark.parametrize(
521
- "function_to_call ,mock_response",
522
- [
523
- ("_get_matlab_connector_status", MockResponse(ok=True)),
524
- (
525
- "_AppState__send_stop_request_to_matlab",
526
- MockResponse(
527
- ok=True, payload={"messages": {"EvalResponse": [{"isError": None}]}}
528
- ),
529
- ),
530
- ],
531
- ids=["request matlab connector status", "send request to stop matlab"],
532
- )
533
455
  async def test_requests_sent_by_matlab_proxy_have_headers(
534
456
  app_state_with_token_auth_fixture,
535
- function_to_call,
536
- mock_response,
537
- mocker,
538
457
  sample_token_headers_fixture,
458
+ mocker,
539
459
  ):
540
- """Test to check if token headers are included in requests sent by matlab-proxy when authentication is enabled
460
+ """Test to check if token headers are included in requests sent by matlab-proxy when authentication is enabled.
461
+ Test checks if the headers are included in the request to stop matlab and get connector status.
541
462
 
542
463
  Args:
543
464
  app_state_fixture_with_token_auth (AppState): Instance of AppState class with token authentication enabled
465
+ sample_token_headers_fixture (dict): Dict which represents the token headers
544
466
  mocker : Built-in pytest fixture
545
467
  """
546
468
  # Arrange
547
- mocked_request = mocker.patch(
548
- "aiohttp.ClientSession.request", return_value=mock_response
469
+ mock_resp = MockResponse(
470
+ ok=True, payload={"messages": {"EvalResponse": [{"isError": None}]}}
471
+ )
472
+ mocked_req = mocker.patch("aiohttp.ClientSession.request", return_value=mock_resp)
473
+
474
+ # Patching to make _are_required_processes_ready() to return True
475
+ mocker.patch.object(
476
+ AppState,
477
+ "_are_required_processes_ready",
478
+ return_value=True,
479
+ )
480
+ # Patching to make get_matlab_state() to return up
481
+ mocker.patch.object(
482
+ AppState,
483
+ "get_matlab_state",
484
+ return_value="up",
549
485
  )
486
+ # Wait for _update_matlab_connector_status to run
487
+ await asyncio.sleep(CHECK_MATLAB_STATUS_INTERVAL)
550
488
 
551
489
  # Act
552
- # Call the function passed as a string
553
- method = getattr(app_state_with_token_auth_fixture, function_to_call)
554
- _ = await method()
490
+ await app_state_with_token_auth_fixture._AppState__send_stop_request_to_matlab()
555
491
 
556
492
  # Assert
557
- connector_status_request_headers = list(mocked_request.call_args_list)[0].kwargs[
493
+
494
+ # 1 request from _update_matlab_connector_status() and another from
495
+ # /stop_matlab request
496
+ connector_status_request_headers = list(mocked_req.call_args_list)[0].kwargs[
497
+ "headers"
498
+ ]
499
+ send_stop_matlab_request_headers = list(mocked_req.call_args_list)[1].kwargs[
558
500
  "headers"
559
501
  ]
560
502
  assert sample_token_headers_fixture == connector_status_request_headers
503
+ assert sample_token_headers_fixture == send_stop_matlab_request_headers
561
504
 
562
505
 
563
506
  async def test_start_matlab_without_xvfb(app_state_fixture, mocker):
@@ -696,3 +639,353 @@ def test_create_logs_dir_for_MATLAB(
696
639
  assert app_state_fixture.mwi_logs_dir == Path(session_file_path).parent
697
640
 
698
641
  assert len(app_state_fixture.matlab_session_files) == session_file_count
642
+
643
+
644
+ async def test_check_idle_timer_started(app_state_fixture):
645
+ """Test to check if the IDLE timer starts automatically
646
+
647
+ Args:
648
+ app_state_fixture (AppState): Object of AppState class with defaults set
649
+ """
650
+ # Arrange
651
+ # Nothing to arrange
652
+
653
+ # Act
654
+ # constructor is called automatically
655
+
656
+ # Assert
657
+ assert app_state_fixture.is_idle_timeout_enabled is True
658
+ assert "decrement_idle_timer" in app_state_fixture.server_tasks
659
+ assert app_state_fixture.idle_timeout_lock is not None
660
+
661
+
662
+ async def test_reset_timer(app_state_fixture):
663
+ """Test to check if the IDLE timer is reset to its initial value
664
+
665
+ Args:
666
+ app_state_fixture (AppState): Object of AppState class with defaults set
667
+ """
668
+ # Arrange
669
+ # Sleep for 1 second for the decrement_timer task to decrease IDLE timer by
670
+ # more than 1 second. This is for decreasing flakiness of this test on different platforms.
671
+ await asyncio.sleep(CHECK_MATLAB_STATUS_INTERVAL + CHECK_MATLAB_STATUS_INTERVAL)
672
+
673
+ # Act
674
+ await app_state_fixture.reset_timer()
675
+
676
+ # Assert
677
+ assert (
678
+ app_state_fixture.get_remaining_idle_timeout()
679
+ == app_state_fixture.settings["mwi_idle_timeout"]
680
+ )
681
+
682
+
683
+ async def test_decrement_timer(app_state_fixture):
684
+ """Test to check if the IDLE timer value decrements automatically
685
+
686
+ Args:
687
+ app_state_fixture (AppState): Object of AppState class with defaults set
688
+ """
689
+ # Arrange
690
+ # Nothing to arrange
691
+ # decrement_timer task is started automatically by the constructor
692
+
693
+ # Sleep for 1 second for the decrement_timer task to decrease IDLE timer by
694
+ # more than 1 second. This is for decreasing flakiness of this test on different platforms.
695
+ await asyncio.sleep(CHECK_MATLAB_STATUS_INTERVAL + CHECK_MATLAB_STATUS_INTERVAL)
696
+
697
+ # Act
698
+ # Nothing to act
699
+
700
+ # Assert
701
+ assert (
702
+ app_state_fixture.get_remaining_idle_timeout()
703
+ < app_state_fixture.settings["mwi_idle_timeout"]
704
+ )
705
+
706
+
707
+ async def test_decrement_timer_runs_out(sample_settings_fixture, mocker):
708
+ """Test to check if the IDLE timer eventually runs out.
709
+
710
+ Args:
711
+ app_state_fixture (AppState): Object of AppState class with defaults set
712
+ """
713
+ # Arrange
714
+ # Set the IDLE timeout to a low value
715
+ idle_timeout = 1
716
+ sample_settings_fixture["mwi_idle_timeout"] = idle_timeout
717
+ app_state = AppState(settings=sample_settings_fixture)
718
+ app_state.processes = {"matlab": None, "xvfb": None}
719
+ app_state.licensing = {"type": "existing_license"}
720
+
721
+ # mock util.get_event_loop() to return a new loop for the test to assert
722
+ mock_loop = asyncio.new_event_loop()
723
+ mocker.patch("matlab_proxy.app_state.util.get_event_loop", return_value=mock_loop)
724
+
725
+ # Act
726
+ # Wait for a little more time than idle_timeout to decrease flakiness of this test on different platforms.
727
+ # MATLAB state changes from down -> starting -> up -> down (idle timer runs out)
728
+ await asyncio.sleep(idle_timeout * FIVE_MAX_TRIES)
729
+
730
+ # Assert
731
+ assert not mock_loop.is_running()
732
+ assert app_state.get_matlab_state() == "down"
733
+
734
+ # Cleanup
735
+ mock_loop.stop()
736
+ await app_state.stop_server_tasks()
737
+
738
+
739
+ @pytest.mark.parametrize(
740
+ "connector_status, matlab_status",
741
+ [("down", "starting"), ("up", "up")],
742
+ ids=["connector_down", "connector_up"],
743
+ )
744
+ async def test_update_matlab_state_based_on_connector_state(
745
+ app_state_fixture, connector_status, matlab_status
746
+ ):
747
+ """Test to check if MATLAB state is updated correctly based on connector state
748
+
749
+ Args:
750
+ app_state_fixture (AppState): Object of AppState class with defaults set
751
+ connector_status (str): Represents connector status
752
+ matlab_status (str): Represents expected MATLAB status
753
+ """
754
+ # Arrange
755
+ app_state_fixture.embedded_connector_state = connector_status
756
+
757
+ # Act
758
+ await app_state_fixture._AppState__update_matlab_state_based_on_connector_state()
759
+
760
+ # Assert
761
+ assert app_state_fixture.get_matlab_state() == matlab_status
762
+
763
+
764
+ @pytest.mark.parametrize(
765
+ "matlab_status, matlab_busy_status",
766
+ [("starting", None), ("up", "busy"), ("up", "idle")],
767
+ ids=["No response from busy status endpoint", "MATLAB is busy", "MATLAB is idle"],
768
+ )
769
+ async def test_update_matlab_state_using_busy_endpoint(
770
+ mocker, app_state_fixture, matlab_status, matlab_busy_status
771
+ ):
772
+ """Test to check if MATLAB and its busy status updates correctly when the
773
+ busy status endpoint is used.
774
+
775
+ Args:
776
+ mocker (mocker): Built-in pytest fixture
777
+ app_state_fixture (AppState): Object of AppState class with defaults set
778
+ matlab_status (str): Represents MATLAB status
779
+ matlab_busy_status (str): Represents MATLAB busy status
780
+ """
781
+ # Arrange
782
+ mocker.patch(
783
+ "matlab_proxy.app_state.mwi.embedded_connector.request.get_busy_state",
784
+ return_value=matlab_busy_status,
785
+ )
786
+
787
+ # Act
788
+ await app_state_fixture._AppState__update_matlab_state_using_busy_status_endpoint()
789
+
790
+ # Assert
791
+ assert app_state_fixture.get_matlab_state() == matlab_status
792
+ assert app_state_fixture.matlab_busy_state == matlab_busy_status
793
+
794
+
795
+ @pytest.mark.parametrize(
796
+ "connector_status, matlab_status, matlab_busy_status",
797
+ [("down", "starting", None), ("up", "up", "busy")],
798
+ ids=["connector_down", "connector_up"],
799
+ )
800
+ async def test_update_matlab_state_using_ping_endpoint(
801
+ mocker, app_state_fixture, connector_status, matlab_status, matlab_busy_status
802
+ ):
803
+ """Test to check if MATLAB and its busy status updates correctly when the
804
+ ping endpoint is used.
805
+
806
+ Args:
807
+ mocker (mocker): Built-in pytest fixture
808
+ app_state_fixture (AppState): Object of AppState class with defaults set
809
+ connector_status (str): Represents Connector status
810
+ matlab_status (str): Represents MATLAB status
811
+ matlab_busy_status (str): Represents MATLAB busy status
812
+ """
813
+ # Arrange
814
+ mocker.patch(
815
+ "matlab_proxy.app_state.mwi.embedded_connector.request.get_state",
816
+ return_value=connector_status,
817
+ )
818
+
819
+ # Act
820
+ await app_state_fixture._AppState__update_matlab_state_using_ping_endpoint()
821
+
822
+ # Assert
823
+ assert app_state_fixture.get_matlab_state() == matlab_status
824
+ assert app_state_fixture.matlab_busy_state == matlab_busy_status
825
+
826
+
827
+ async def test_update_matlab_state_based_on_endpoint_to_use_required_processes_not_ready(
828
+ mocker, app_state_fixture
829
+ ):
830
+ """Test to check if MATLAB state is 'down' when the required processes are not ready
831
+
832
+ Args:
833
+ mocker (mocker): Built-in pytest fixture
834
+ app_state_fixture (AppState): Object of AppState class with defaults set
835
+ """
836
+ # Arrange
837
+ mocker.patch(
838
+ "matlab_proxy.app_state.mwi.embedded_connector.request.get_state",
839
+ return_value="up",
840
+ )
841
+
842
+ await app_state_fixture._AppState__update_matlab_state_based_on_endpoint_to_use(
843
+ app_state_fixture._AppState__update_matlab_state_using_ping_endpoint
844
+ )
845
+
846
+ assert app_state_fixture.get_matlab_state() == "down"
847
+
848
+
849
+ async def test_update_matlab_state_based_on_endpoint_to_use_happy_path(
850
+ mocker, tmp_path, app_state_fixture
851
+ ):
852
+ """Test to check if MATLAB state is 'starting' when the required processes
853
+ are up but the ready file is not created yet.
854
+
855
+ Args:
856
+ mocker (mocker): Built-in pytest fixture
857
+ tmp_path (Path): Built-in pytest fixture
858
+ app_state_fixture (AppState): Object of AppState class with defaults set
859
+ """
860
+ # Arrange
861
+ mocker.patch.object(
862
+ AppState,
863
+ "_are_required_processes_ready",
864
+ return_value=True,
865
+ )
866
+ tmp_file = tmp_path / Path("dummy")
867
+ tmp_file.touch()
868
+ app_state_fixture.matlab_session_files["matlab_ready_file"] = tmp_file
869
+
870
+ # Act
871
+ await app_state_fixture._AppState__update_matlab_state_based_on_endpoint_to_use(
872
+ app_state_fixture._AppState__update_matlab_state_using_ping_endpoint
873
+ )
874
+
875
+ # Assert
876
+ await assert_matlab_state(app_state_fixture, "starting", FIVE_MAX_TRIES)
877
+
878
+
879
+ async def assert_matlab_state(app_state_fixture, expected_matlab_status, count):
880
+ """Tries to assert the MATLAB state to expected_matlab_status for count times.
881
+ Will raise Assertion error after.
882
+
883
+ The count is needed to decrease flakiness of this tests when run on different platforms.
884
+
885
+ Args:
886
+ app_state_fixture (AppState): Instance of AppState class.
887
+ expected_matlab_status (str): Expected MATLAB status
888
+ count (int): Max tries for assertion before AssertionError is raised.
889
+
890
+ Raises:
891
+ AssertionError: Raised when assertion fails after 'count' tries
892
+ """
893
+ i = 0
894
+ while i < count:
895
+ try:
896
+ assert app_state_fixture.get_matlab_state() == expected_matlab_status
897
+ return
898
+
899
+ except:
900
+ await asyncio.sleep(CHECK_MATLAB_STATUS_INTERVAL)
901
+
902
+ i += 1
903
+
904
+ raise AssertionError(
905
+ f"MATLAB status failed to change to '{expected_matlab_status}'"
906
+ )
907
+
908
+
909
+ @pytest.mark.parametrize(
910
+ "matlab_ready_file, expected_matlab_status",
911
+ [
912
+ (None, "down"),
913
+ (Path("dummy"), "starting"),
914
+ ],
915
+ ids=[
916
+ "no_matlab_ready_file_formed",
917
+ "no_matlab_ready_file_created",
918
+ ],
919
+ )
920
+ async def test_check_matlab_connector_status_auto_updates_based_on_matlab_ready_file(
921
+ mocker, app_state_fixture, matlab_ready_file, expected_matlab_status
922
+ ):
923
+ """Test to check if the status of MATLAB is updated automatically
924
+
925
+ Args:
926
+ mocker (mocker): Built-in pytest fixture
927
+ app_state_fixture (AppState): Object of AppState class with defaults set
928
+ matlab_ready_file (Path): Path to the ready file
929
+ expected_matlab_status (str): Expected MATLAB status
930
+ """
931
+ # Arrange
932
+ mocker.patch.object(
933
+ AppState,
934
+ "_are_required_processes_ready",
935
+ return_value=True,
936
+ )
937
+ app_state_fixture.matlab_session_files["matlab_ready_file"] = matlab_ready_file
938
+
939
+ # Act
940
+ # Nothing to act upon as the _update_matlab_state() is started automatically in the constructor.
941
+ # Have to wait here for the atleast the same interval as the __update_matlab_state_based_on_endpoint_to_use()
942
+ # for the MATLAB status to update from 'down'
943
+
944
+ # Assert
945
+ # MATLAB state should be 'down' first
946
+ assert app_state_fixture.get_matlab_state() == "down"
947
+ await asyncio.sleep(CHECK_MATLAB_STATUS_INTERVAL)
948
+
949
+ await assert_matlab_state(app_state_fixture, expected_matlab_status, FIVE_MAX_TRIES)
950
+
951
+
952
+ async def test_update_matlab_state_switches_to_busy_endpoint(
953
+ mocker, tmp_path, app_state_fixture
954
+ ):
955
+ """Test to check if the endpoint to determine MATLAB state changes from the ping
956
+ endpoint to busy status endpoint after the first successful ping request.
957
+
958
+ Args:
959
+ mocker (mocker): Built-in pytest fixture
960
+ tmp_path (Path): Built-in pytest fixture
961
+ app_state_fixture (AppState): Object of AppState class with defaults set
962
+ """
963
+ # Arrange
964
+ # Setup mocks for the first ping request to be successful
965
+ mocker.patch.object(
966
+ AppState,
967
+ "_are_required_processes_ready",
968
+ return_value=True,
969
+ )
970
+ mocker.patch(
971
+ "matlab_proxy.app_state.mwi.embedded_connector.request.get_state",
972
+ return_value="up",
973
+ )
974
+ tmp_file = tmp_path / Path("dummy")
975
+ tmp_file.touch()
976
+ app_state_fixture.matlab_session_files["matlab_ready_file"] = tmp_file
977
+ mocked_busy_status_endpoint_function = mocker.patch.object(
978
+ app_state_fixture, "_AppState__update_matlab_state_using_busy_status_endpoint"
979
+ )
980
+
981
+ # Act
982
+ # Nothing to act upon as the _update_matlab_state() is started automatically in the constructor.
983
+ # Have to wait here for the atleast the same interval as the __update_matlab_state_based_on_endpoint_to_use()
984
+ # for the MATLAB status to update from 'down'
985
+
986
+ # Wait for the ping endpoint request. Waiting for more time than what
987
+ # is needed to decrease flakiness of this test on different platforms.
988
+ await asyncio.sleep(1 * FIVE_MAX_TRIES)
989
+
990
+ # Assert
991
+ assert mocked_busy_status_endpoint_function.call_count > 1
@@ -4,3 +4,4 @@ TWO_MAX_TRIES = 2
4
4
  FIVE_MAX_TRIES = 5
5
5
  HALF_SECOND_DELAY = 0.5
6
6
  ONE_SECOND_DELAY = 1
7
+ CHECK_MATLAB_STATUS_INTERVAL = 1
@@ -7,7 +7,10 @@ import os
7
7
  import random
8
8
  import socket
9
9
  import tempfile
10
- from matlab_proxy.util.mwi.validators import validate_matlab_root_path
10
+ from matlab_proxy.util.mwi.validators import (
11
+ validate_idle_timeout,
12
+ validate_matlab_root_path,
13
+ )
11
14
  from matlab_proxy import constants
12
15
  from pathlib import Path
13
16
 
@@ -329,3 +332,29 @@ def test_validate_matlab_root_path_non_existent_versioninfo_file(tmp_path):
329
332
  # Assert
330
333
  assert actual_matlab_root is None
331
334
  assert actual_matlab_root_custom is None
335
+
336
+
337
+ @pytest.mark.parametrize(
338
+ "timeout, validated_timeout",
339
+ [
340
+ (None, None),
341
+ ("abc", None),
342
+ (-10, None),
343
+ (123, 60 * 123),
344
+ ],
345
+ ids=[
346
+ "No IDLE timeout specified",
347
+ "Invalid IDLE timeout specified",
348
+ "Negative number supplied as IDLE timeout",
349
+ "Valid IDLE timeout specified",
350
+ ],
351
+ )
352
+ def test_validate_idle_timeout(timeout, validated_timeout):
353
+ # Arrange
354
+ # Nothing to arrange
355
+
356
+ # Act
357
+ actual_timeout = validate_idle_timeout(timeout)
358
+
359
+ # Assert
360
+ assert actual_timeout == validated_timeout