matlab-proxy 0.25.1__py3-none-any.whl → 0.27.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.
- matlab_proxy/app.py +68 -16
- matlab_proxy/app_state.py +8 -2
- matlab_proxy/constants.py +1 -0
- matlab_proxy/default_configuration.py +2 -2
- matlab_proxy/gui/index.html +1 -1
- matlab_proxy/gui/static/js/{index.CZgGkMCD.js → index.BcDShXfH.js} +16 -16
- matlab_proxy/settings.py +24 -2
- matlab_proxy/util/cookie_jar.py +72 -0
- matlab_proxy/util/list_servers.py +2 -2
- matlab_proxy/util/mwi/environment_variables.py +15 -0
- matlab_proxy/util/mwi/session_name.py +28 -0
- matlab_proxy/util/mwi/validators.py +2 -4
- {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info}/METADATA +37 -23
- {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info}/RECORD +29 -56
- {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info}/WHEEL +1 -2
- matlab_proxy_manager/README.md +85 -0
- matlab_proxy_manager/lib/README.md +53 -0
- matlab_proxy_manager/lib/api.py +156 -114
- matlab_proxy_manager/storage/README.md +54 -0
- matlab_proxy_manager/storage/server.py +5 -2
- matlab_proxy_manager/utils/constants.py +2 -1
- matlab_proxy_manager/utils/environment_variables.py +6 -1
- matlab_proxy_manager/utils/exceptions.py +45 -0
- matlab_proxy_manager/utils/helpers.py +2 -2
- matlab_proxy_manager/utils/logger.py +4 -1
- matlab_proxy_manager/web/README.md +37 -0
- matlab_proxy_manager/web/app.py +71 -19
- matlab_proxy-0.25.1.dist-info/top_level.txt +0 -3
- tests/integration/__init__.py +0 -1
- tests/integration/integration_tests_with_license/__init__.py +0 -1
- tests/integration/integration_tests_with_license/conftest.py +0 -47
- tests/integration/integration_tests_with_license/test_http_end_points.py +0 -397
- tests/integration/integration_tests_without_license/__init__.py +0 -1
- tests/integration/integration_tests_without_license/conftest.py +0 -116
- tests/integration/integration_tests_without_license/test_matlab_is_down_if_unlicensed.py +0 -49
- tests/integration/utils/__init__.py +0 -1
- tests/integration/utils/integration_tests_utils.py +0 -352
- tests/integration/utils/licensing.py +0 -152
- tests/unit/__init__.py +0 -1
- tests/unit/conftest.py +0 -66
- tests/unit/test_app.py +0 -1200
- tests/unit/test_app_state.py +0 -1094
- tests/unit/test_constants.py +0 -7
- tests/unit/test_ddux.py +0 -22
- tests/unit/test_devel.py +0 -246
- tests/unit/test_non_dev_mode.py +0 -169
- tests/unit/test_settings.py +0 -659
- tests/unit/util/__init__.py +0 -3
- tests/unit/util/mwi/__init__.py +0 -1
- tests/unit/util/mwi/embedded_connector/__init__.py +0 -1
- tests/unit/util/mwi/embedded_connector/test_helpers.py +0 -29
- tests/unit/util/mwi/embedded_connector/test_request.py +0 -64
- tests/unit/util/mwi/test_custom_http_headers.py +0 -281
- tests/unit/util/mwi/test_download.py +0 -152
- tests/unit/util/mwi/test_logger.py +0 -82
- tests/unit/util/mwi/test_token_auth.py +0 -303
- tests/unit/util/mwi/test_validators.py +0 -364
- tests/unit/util/test_mw.py +0 -550
- tests/unit/util/test_util.py +0 -221
- tests/utils/__init__.py +0 -1
- tests/utils/logging_util.py +0 -81
- {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info}/entry_points.txt +0 -0
- {matlab_proxy-0.25.1.dist-info → matlab_proxy-0.27.0.dist-info/licenses}/LICENSE.md +0 -0
matlab_proxy_manager/web/app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 The MathWorks, Inc.
|
|
1
|
+
# Copyright 2024-2025 The MathWorks, Inc.
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
@@ -14,6 +14,7 @@ from aiohttp import ClientSession, client_exceptions, web
|
|
|
14
14
|
import matlab_proxy.util.mwi.environment_variables as mwi_env
|
|
15
15
|
import matlab_proxy.util.system as mwi_sys
|
|
16
16
|
import matlab_proxy_manager.lib.api as mpm_lib
|
|
17
|
+
import matlab_proxy.constants as mp_constants
|
|
17
18
|
from matlab_proxy_manager.utils import constants, helpers, logger
|
|
18
19
|
from matlab_proxy_manager.utils import environment_variables as mpm_env
|
|
19
20
|
from matlab_proxy_manager.utils.auth import authenticate_access_decorator
|
|
@@ -44,6 +45,9 @@ def init_app() -> web.Application:
|
|
|
44
45
|
# Async event is utilized to signal app termination from this and other modules
|
|
45
46
|
app["shutdown_event"] = asyncio.Event()
|
|
46
47
|
|
|
48
|
+
# Tracks whether default matlab proxy is started or not
|
|
49
|
+
app["has_default_matlab_proxy_started"] = False
|
|
50
|
+
|
|
47
51
|
# Create and get the proxy manager data directory
|
|
48
52
|
try:
|
|
49
53
|
data_dir = helpers.create_and_get_proxy_manager_data_dir()
|
|
@@ -54,6 +58,10 @@ def init_app() -> web.Application:
|
|
|
54
58
|
# Setup idle timeout monitor for the app
|
|
55
59
|
monitor = OrphanedProcessMonitor(app)
|
|
56
60
|
|
|
61
|
+
# Load existing matlab proxy servers into app state for consistency
|
|
62
|
+
app["servers"] = helpers.pre_load_from_state_file(app.get("data_dir"))
|
|
63
|
+
log.debug("Loaded existing matlab proxy servers into app state: %s", app["servers"])
|
|
64
|
+
|
|
57
65
|
async def start_idle_monitor(app):
|
|
58
66
|
"""Start the idle timeout monitor."""
|
|
59
67
|
app["monitor_task"] = asyncio.create_task(monitor.start())
|
|
@@ -102,7 +110,7 @@ def init_app() -> web.Application:
|
|
|
102
110
|
return app
|
|
103
111
|
|
|
104
112
|
|
|
105
|
-
async def start_app(env_vars
|
|
113
|
+
async def start_app(env_vars):
|
|
106
114
|
"""
|
|
107
115
|
Initialize and start the web application.
|
|
108
116
|
|
|
@@ -118,9 +126,7 @@ async def start_app(env_vars: namedtuple):
|
|
|
118
126
|
app["port"] = env_vars.mpm_port
|
|
119
127
|
app["auth_token"] = env_vars.mpm_auth_token
|
|
120
128
|
app["parent_pid"] = env_vars.mpm_parent_pid
|
|
121
|
-
|
|
122
|
-
# Start default matlab proxy
|
|
123
|
-
await _start_default_proxy(app)
|
|
129
|
+
app["base_url_prefix"] = env_vars.base_url_prefix
|
|
124
130
|
|
|
125
131
|
web_logger = None if not mwi_env.is_web_logging_enabled() else log
|
|
126
132
|
|
|
@@ -175,6 +181,8 @@ def _register_signal_handler(loop, app):
|
|
|
175
181
|
def _catch_signals(app):
|
|
176
182
|
"""Handle termination signals for graceful shutdown."""
|
|
177
183
|
# Poll for parent process to clean up to avoid race conditions in cleanup of matlab proxies
|
|
184
|
+
# Ideally, we should minimize the work done when we catch exit signals, which would mean the
|
|
185
|
+
# polling for parent process should happen elsewhere
|
|
178
186
|
helpers.poll_for_server_deletion()
|
|
179
187
|
app.get("shutdown_event").set()
|
|
180
188
|
|
|
@@ -190,13 +198,13 @@ async def _start_default_proxy(app):
|
|
|
190
198
|
parent_id=app.get("parent_pid"),
|
|
191
199
|
is_shared_matlab=True,
|
|
192
200
|
mpm_auth_token=app.get("auth_token"),
|
|
201
|
+
base_url_prefix=app.get("base_url_prefix"),
|
|
193
202
|
)
|
|
194
|
-
|
|
195
|
-
log.error("Failed to start default matlab proxy using Jupyter")
|
|
196
|
-
return
|
|
203
|
+
errors = server_process.get("errors")
|
|
197
204
|
|
|
198
|
-
#
|
|
199
|
-
|
|
205
|
+
# Raising an exception if there was an error starting the default MATLAB proxy
|
|
206
|
+
if errors:
|
|
207
|
+
raise Exception(":".join(errors))
|
|
200
208
|
|
|
201
209
|
# Add the new/existing server to the app state
|
|
202
210
|
app["servers"][server_process.get("id")] = server_process
|
|
@@ -259,12 +267,21 @@ async def proxy(req):
|
|
|
259
267
|
f"Required header: ${constants.HEADER_MWI_MPM_CONTEXT} not found in the request"
|
|
260
268
|
)
|
|
261
269
|
|
|
270
|
+
# Raising exception from here is not cleanly handled by Jupyter server proxy.
|
|
271
|
+
# It only shows 599 with a generic stream closed error message.
|
|
272
|
+
# Hence returning 503 with custom error message as response.
|
|
273
|
+
try:
|
|
274
|
+
await _start_default_proxy_if_not_already_started(req)
|
|
275
|
+
except Exception as e:
|
|
276
|
+
log.error("Error starting default proxy: %s", e)
|
|
277
|
+
return _render_error_page(f"Error during startup: {e}")
|
|
278
|
+
|
|
262
279
|
client_key = f"{ctx}_{ident}"
|
|
263
280
|
default_key = f"{ctx}_default"
|
|
264
281
|
group_two_rel_url = match.group(2)
|
|
265
282
|
|
|
266
283
|
backend_server = _get_backend_server(req, client_key, default_key)
|
|
267
|
-
proxy_url = f
|
|
284
|
+
proxy_url = f"{backend_server.get('absolute_url')}/{group_two_rel_url}"
|
|
268
285
|
log.debug("Proxy URL: %s", proxy_url)
|
|
269
286
|
|
|
270
287
|
if (
|
|
@@ -326,7 +343,9 @@ async def _forward_websocket_request(
|
|
|
326
343
|
Returns:
|
|
327
344
|
web.WebSocketResponse: The response from the backend server
|
|
328
345
|
"""
|
|
329
|
-
ws_server = web.WebSocketResponse(
|
|
346
|
+
ws_server = web.WebSocketResponse(
|
|
347
|
+
max_msg_size=mp_constants.MAX_WEBSOCKET_MESSAGE_SIZE_IN_MB, compress=True
|
|
348
|
+
)
|
|
330
349
|
await ws_server.prepare(req)
|
|
331
350
|
|
|
332
351
|
async with aiohttp.ClientSession(
|
|
@@ -335,7 +354,11 @@ async def _forward_websocket_request(
|
|
|
335
354
|
connector=aiohttp.TCPConnector(ssl=False),
|
|
336
355
|
) as client_session:
|
|
337
356
|
try:
|
|
338
|
-
async with client_session.ws_connect(
|
|
357
|
+
async with client_session.ws_connect(
|
|
358
|
+
proxy_url,
|
|
359
|
+
max_msg_size=mp_constants.MAX_WEBSOCKET_MESSAGE_SIZE_IN_MB, # max websocket message size from MATLAB to browser
|
|
360
|
+
compress=12, # enable websocket messages compression
|
|
361
|
+
) as ws_client:
|
|
339
362
|
|
|
340
363
|
async def ws_forward(ws_src, ws_dest):
|
|
341
364
|
async for msg in ws_src:
|
|
@@ -373,6 +396,13 @@ async def _forward_websocket_request(
|
|
|
373
396
|
await ws_dest.close(
|
|
374
397
|
code=ws_dest.close_code, message=msg.extra
|
|
375
398
|
)
|
|
399
|
+
elif msg_type == aiohttp.WSMsgType.ERROR:
|
|
400
|
+
log.error(f"WebSocket error received: {msg}")
|
|
401
|
+
if "exceeds limit" in str(msg.data):
|
|
402
|
+
log.error(
|
|
403
|
+
f"Message too large: {msg.data}. Please refresh browser tab to reconnect."
|
|
404
|
+
)
|
|
405
|
+
break
|
|
376
406
|
else:
|
|
377
407
|
raise ValueError(f"Unexpected message type: {msg}")
|
|
378
408
|
|
|
@@ -421,6 +451,20 @@ async def _forward_http_request(
|
|
|
421
451
|
return web.Response(headers=headers, status=res.status, body=body)
|
|
422
452
|
|
|
423
453
|
|
|
454
|
+
async def _start_default_proxy_if_not_already_started(req):
|
|
455
|
+
app = req.app
|
|
456
|
+
req_path = req.rel_url
|
|
457
|
+
|
|
458
|
+
# Start default matlab-proxy only when it is not already started and
|
|
459
|
+
# if the request path contains the default MATLAB path (/matlab/default)
|
|
460
|
+
if not app.get(
|
|
461
|
+
"has_default_matlab_proxy_started", False
|
|
462
|
+
) and constants.MWI_DEFAULT_MATLAB_PATH in str(req_path):
|
|
463
|
+
log.debug("Starting default matlab-proxy for request path: %s", str(req_path))
|
|
464
|
+
await _start_default_proxy(app)
|
|
465
|
+
app["has_default_matlab_proxy_started"] = True
|
|
466
|
+
|
|
467
|
+
|
|
424
468
|
def _get_backend_server(req: web.Request, client_key: str, default_key: str) -> dict:
|
|
425
469
|
"""
|
|
426
470
|
Retrieves the backend server configuration for a given client key.
|
|
@@ -444,18 +488,22 @@ def _redirect_to_default(req_path) -> None:
|
|
|
444
488
|
Raises:
|
|
445
489
|
web.HTTPFound: Redirects the client to the new URL.
|
|
446
490
|
"""
|
|
447
|
-
new_redirect_url = f
|
|
491
|
+
new_redirect_url = f"{str(req_path).rstrip('/')}/default/"
|
|
448
492
|
log.info("Redirecting to %s", new_redirect_url)
|
|
449
493
|
raise web.HTTPFound(new_redirect_url)
|
|
450
494
|
|
|
451
495
|
|
|
452
496
|
def _render_error_page(error_msg: str) -> web.Response:
|
|
453
497
|
"""Returns 503 with error text"""
|
|
454
|
-
return web.HTTPServiceUnavailable(
|
|
498
|
+
return web.HTTPServiceUnavailable(
|
|
499
|
+
text=f'<p style="color: red;">{error_msg}</p>', content_type="text/html"
|
|
500
|
+
)
|
|
455
501
|
|
|
456
502
|
|
|
457
|
-
def _fetch_and_validate_required_env_vars()
|
|
458
|
-
EnvVars = namedtuple(
|
|
503
|
+
def _fetch_and_validate_required_env_vars():
|
|
504
|
+
EnvVars = namedtuple(
|
|
505
|
+
"EnvVars", ["mpm_port", "mpm_auth_token", "mpm_parent_pid", "base_url_prefix"]
|
|
506
|
+
)
|
|
459
507
|
|
|
460
508
|
port = os.getenv(mpm_env.get_env_name_mwi_mpm_port())
|
|
461
509
|
mpm_auth_token = os.getenv(mpm_env.get_env_name_mwi_mpm_auth_token())
|
|
@@ -466,9 +514,13 @@ def _fetch_and_validate_required_env_vars() -> namedtuple:
|
|
|
466
514
|
sys.exit(1)
|
|
467
515
|
|
|
468
516
|
try:
|
|
517
|
+
base_url_prefix = os.getenv(mpm_env.get_env_name_base_url_prefix(), "")
|
|
469
518
|
mwi_mpm_port: int = int(port)
|
|
470
519
|
return EnvVars(
|
|
471
|
-
mpm_port=mwi_mpm_port,
|
|
520
|
+
mpm_port=mwi_mpm_port,
|
|
521
|
+
mpm_auth_token=mpm_auth_token,
|
|
522
|
+
mpm_parent_pid=ctx,
|
|
523
|
+
base_url_prefix=base_url_prefix,
|
|
472
524
|
)
|
|
473
525
|
except ValueError as ve:
|
|
474
526
|
log.error("Error: Invalid type for port: %s", ve)
|
|
@@ -480,7 +532,7 @@ def main() -> None:
|
|
|
480
532
|
The main entry point of the application. Starts the app and run until the shutdown
|
|
481
533
|
signal to terminate the app is received.
|
|
482
534
|
"""
|
|
483
|
-
env_vars
|
|
535
|
+
env_vars = _fetch_and_validate_required_env_vars()
|
|
484
536
|
asyncio.run(start_app(env_vars))
|
|
485
537
|
|
|
486
538
|
|
tests/integration/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Copyright 2023-2024 The MathWorks, Inc.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Copyright 2023 The MathWorks, Inc.
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# Copyright 2023-2024 The MathWorks, Inc.
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from tests.integration.utils import integration_tests_utils as utils
|
|
5
|
-
import pytest
|
|
6
|
-
from matlab_proxy.util.mwi import environment_variables as mwi_env
|
|
7
|
-
from tests.utils.logging_util import create_integ_test_logger
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
_logger = create_integ_test_logger(
|
|
11
|
-
__name__, log_file_path=os.getenv("MWI_INTEG_TESTS_LOG_FILE_PATH")
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@pytest.fixture(scope="module", name="module_monkeypatch")
|
|
16
|
-
def monkeypatch_module_scope_fixture():
|
|
17
|
-
"""
|
|
18
|
-
To ensure that modifications made with the monkeypatch fixture
|
|
19
|
-
persist across all tests in the module, this fixture
|
|
20
|
-
has been created in 'module' scope. This is done because a 'module'
|
|
21
|
-
scope object is needed with matlab-proxy 'module' scope fixture.
|
|
22
|
-
This allows us to patch certain aspects, like environment variables,
|
|
23
|
-
for all tests within the module.
|
|
24
|
-
|
|
25
|
-
Yields:
|
|
26
|
-
class object: Object of class MonkeyPatch
|
|
27
|
-
"""
|
|
28
|
-
with pytest.MonkeyPatch.context() as mp:
|
|
29
|
-
yield mp
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.fixture(autouse=True, scope="module")
|
|
33
|
-
def matlab_proxy_fixture(module_monkeypatch, request):
|
|
34
|
-
"""
|
|
35
|
-
Pytest fixture for managing a standalone matlab-proxy process
|
|
36
|
-
for testing purposes. This fixture sets up a matlab-proxy process in
|
|
37
|
-
the module scope, and tears it down after all the tests are executed.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
utils.perform_basic_checks()
|
|
41
|
-
|
|
42
|
-
module_monkeypatch.setenv(mwi_env.get_env_name_testing(), "false")
|
|
43
|
-
module_monkeypatch.setenv(mwi_env.get_env_name_development(), "false")
|
|
44
|
-
_logger.info("Started MATLAB Proxy process")
|
|
45
|
-
|
|
46
|
-
# Run the matlab proxy tests
|
|
47
|
-
yield
|
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
# Copyright 2023-2025 The MathWorks, Inc.
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
Contains integration tests which exercise HTTP endpoints of interest exposed by matlab-proxy-app
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
# Imports
|
|
8
|
-
from enum import Enum
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
import pytest
|
|
12
|
-
import re
|
|
13
|
-
import requests
|
|
14
|
-
import time
|
|
15
|
-
from requests.adapters import HTTPAdapter, Retry
|
|
16
|
-
from urllib.parse import urlparse, parse_qs
|
|
17
|
-
|
|
18
|
-
# Local module imports
|
|
19
|
-
import matlab_proxy.settings as settings
|
|
20
|
-
from matlab_proxy.constants import MWI_AUTH_TOKEN_NAME_FOR_HTTP
|
|
21
|
-
from matlab_proxy.util import system
|
|
22
|
-
from tests.integration.utils import integration_tests_utils as utils
|
|
23
|
-
from tests.utils.logging_util import create_integ_test_logger
|
|
24
|
-
|
|
25
|
-
# Logger Setup
|
|
26
|
-
_logger = create_integ_test_logger(__name__)
|
|
27
|
-
|
|
28
|
-
# Constants
|
|
29
|
-
|
|
30
|
-
# Timeout for polling the matlab-proxy http endpoints
|
|
31
|
-
# matlab proxy in Mac machines takes more time to be 'up'
|
|
32
|
-
MAX_TIMEOUT = settings.get_process_startup_timeout()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class Format(Enum):
|
|
36
|
-
"""
|
|
37
|
-
An enumeration to specify different format types.
|
|
38
|
-
|
|
39
|
-
Attributes:
|
|
40
|
-
JSON (int): Represents the JSON format type.
|
|
41
|
-
TEXT (int): Represents the plain text format type.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
JSON = 1
|
|
45
|
-
TEXT = 2
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Utility Functions
|
|
49
|
-
def _http_get_request(
|
|
50
|
-
uri, connection_scheme, headers, http_endpoint="", outputFormat=Format.TEXT
|
|
51
|
-
):
|
|
52
|
-
"""
|
|
53
|
-
Sends an HTTP GET request to a specified URI, optionally appending an endpoint to the URI.
|
|
54
|
-
|
|
55
|
-
This function uses a session with retries configured for transient network errors. It can return
|
|
56
|
-
the response in either text or JSON format, based on the outputFormat parameter.
|
|
57
|
-
|
|
58
|
-
Parameters:
|
|
59
|
-
- uri (str): The base URI for the HTTP request.
|
|
60
|
-
- connection_scheme (str): The scheme to use for the connection (e.g., 'http' or 'https').
|
|
61
|
-
- headers (dict): A dictionary of HTTP headers to include in the request.
|
|
62
|
-
- http_endpoint (str, optional): An additional endpoint to append to the base URI. Defaults to an empty string.
|
|
63
|
-
- outputFormat (format, optional): The desired format for the response content. This should be an attribute
|
|
64
|
-
of a format enumeration, supporting at least 'TEXT' and 'JSON' options. Defaults to Format.TEXT.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
- str or dict: The response content as a string if outputFormat is Format.TEXT, or as a dictionary
|
|
68
|
-
if outputFormat is Format.JSON.
|
|
69
|
-
|
|
70
|
-
Raises:
|
|
71
|
-
- Exception: If an invalid output format is specified.
|
|
72
|
-
|
|
73
|
-
Note:
|
|
74
|
-
- The function disables SSL certificate verification (`verify=False`). This may introduce security risks,
|
|
75
|
-
such as vulnerability to man-in-the-middle attacks. Use with caution in a production environment.
|
|
76
|
-
"""
|
|
77
|
-
request_uri = uri + http_endpoint
|
|
78
|
-
with requests.Session() as s:
|
|
79
|
-
retries = Retry(total=10, backoff_factor=0.1)
|
|
80
|
-
s.mount(f"{connection_scheme}://", HTTPAdapter(max_retries=retries))
|
|
81
|
-
response = s.get(request_uri, headers=headers, verify=False)
|
|
82
|
-
|
|
83
|
-
if outputFormat == Format.TEXT:
|
|
84
|
-
return response.text
|
|
85
|
-
elif outputFormat == Format.JSON:
|
|
86
|
-
return json.loads(response.text)
|
|
87
|
-
|
|
88
|
-
raise Exception("Invalid output format specified.")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _check_matlab_status(matlab_proxy_app_fixture, status):
|
|
92
|
-
"""
|
|
93
|
-
Check the status of a MATLAB session until a specified status is reached or a timeout occurs.
|
|
94
|
-
|
|
95
|
-
This function repeatedly sends HTTP GET requests to a MATLAB proxy application to check the current
|
|
96
|
-
status of MATLAB. It continues checking until MATLAB's status matches the specified target status or
|
|
97
|
-
until a maximum timeout is reached.
|
|
98
|
-
|
|
99
|
-
Parameters:
|
|
100
|
-
- matlab_proxy_app_fixture: An object containing configuration for connecting to the MATLAB proxy application.
|
|
101
|
-
This object must have the following attributes:
|
|
102
|
-
- url (str): The base URL of the MATLAB proxy application.
|
|
103
|
-
- connection_scheme (str): The scheme used for the connection (e.g., 'http' or 'https').
|
|
104
|
-
- headers (dict): A dictionary of HTTP headers to be sent with each request.
|
|
105
|
-
- status (str): The target status to wait for MATLAB to reach.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
- str: The status of MATLAB at the end of the function execution. This could be the target status if
|
|
109
|
-
it was reached within the timeout period, or the last known status of MATLAB if the timeout was reached
|
|
110
|
-
first.
|
|
111
|
-
|
|
112
|
-
Notes:
|
|
113
|
-
- The function waits for a maximum of MAX_TIMEOUT seconds, defined elsewhere, before exiting.
|
|
114
|
-
- It checks the MATLAB status every 1 second.
|
|
115
|
-
- The MATLAB status is obtained by sending a GET request to the '/get_status' endpoint of the proxy application.
|
|
116
|
-
- The response from the proxy application is expected to be in JSON format, with MATLAB's status accessible
|
|
117
|
-
via `res["matlab"]["status"]`.
|
|
118
|
-
|
|
119
|
-
Exceptions:
|
|
120
|
-
- This function may raise exceptions related to network issues or JSON parsing errors, which are not
|
|
121
|
-
explicitly handled within the function.
|
|
122
|
-
"""
|
|
123
|
-
uri = matlab_proxy_app_fixture.url
|
|
124
|
-
connection_scheme = matlab_proxy_app_fixture.connection_scheme
|
|
125
|
-
headers = matlab_proxy_app_fixture.headers
|
|
126
|
-
|
|
127
|
-
matlab_status = None
|
|
128
|
-
|
|
129
|
-
start_time = time.time()
|
|
130
|
-
while matlab_status != status and (time.time() - start_time < MAX_TIMEOUT):
|
|
131
|
-
time.sleep(1)
|
|
132
|
-
res = _http_get_request(
|
|
133
|
-
uri,
|
|
134
|
-
connection_scheme,
|
|
135
|
-
headers,
|
|
136
|
-
http_endpoint="/get_status",
|
|
137
|
-
outputFormat=Format.JSON,
|
|
138
|
-
)
|
|
139
|
-
matlab_status = res["matlab"]["status"]
|
|
140
|
-
|
|
141
|
-
return matlab_status
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _download_test_file(matlab_proxy_app_fixture, test_file):
|
|
145
|
-
"""Returns result of hitting the /download endpoint for test_file.
|
|
146
|
-
|
|
147
|
-
Returns:
|
|
148
|
-
str: The contents of the test_file being downloaded through matlab-proxy.
|
|
149
|
-
"""
|
|
150
|
-
uri = matlab_proxy_app_fixture.url
|
|
151
|
-
connection_scheme = matlab_proxy_app_fixture.connection_scheme
|
|
152
|
-
headers = matlab_proxy_app_fixture.headers
|
|
153
|
-
|
|
154
|
-
res = _http_get_request(
|
|
155
|
-
uri,
|
|
156
|
-
connection_scheme,
|
|
157
|
-
headers,
|
|
158
|
-
http_endpoint="/download/" + test_file,
|
|
159
|
-
outputFormat=Format.TEXT,
|
|
160
|
-
)
|
|
161
|
-
return res
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# Main Classes
|
|
165
|
-
class RealMATLABServer:
|
|
166
|
-
"""
|
|
167
|
-
Context Manager class which returns matlab proxy web server serving real MATLAB
|
|
168
|
-
for testing.
|
|
169
|
-
|
|
170
|
-
Setting up the server in the context of Pytest.
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
def __init__(self, event_loop):
|
|
174
|
-
self.event_loop = event_loop
|
|
175
|
-
|
|
176
|
-
def __enter__(self):
|
|
177
|
-
# Store the matlab proxy logs in os.pipe for testing
|
|
178
|
-
# os.pipe2 is only supported in Linux systems
|
|
179
|
-
_logger.info("Setting up MATLAB Server for integration test")
|
|
180
|
-
|
|
181
|
-
_logger.debug("Entering RealMATLABServer enter section.")
|
|
182
|
-
self.dpipe = os.pipe2(os.O_NONBLOCK) if system.is_linux() else os.pipe()
|
|
183
|
-
self.mwi_app_port = utils.get_random_free_port()
|
|
184
|
-
self.matlab_config_file_path = str(utils.get_matlab_config_file())
|
|
185
|
-
|
|
186
|
-
self.temp_dir_path = os.path.dirname(
|
|
187
|
-
os.path.dirname(self.matlab_config_file_path)
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
self.temp_dir_name = "temp_dir"
|
|
191
|
-
self.mwi_base_url = "/matlab-test"
|
|
192
|
-
|
|
193
|
-
# Environment variables to launch matlab proxy
|
|
194
|
-
input_env = {
|
|
195
|
-
"MWI_APP_PORT": self.mwi_app_port,
|
|
196
|
-
"MWI_BASE_URL": self.mwi_base_url,
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
self.proc = self.event_loop.run_until_complete(
|
|
200
|
-
utils.start_matlab_proxy_app(out=self.dpipe[1], input_env=input_env)
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
utils.wait_server_info_ready(self.mwi_app_port)
|
|
204
|
-
parsed_url = urlparse(utils.get_connection_string(self.mwi_app_port))
|
|
205
|
-
|
|
206
|
-
self.headers = {
|
|
207
|
-
MWI_AUTH_TOKEN_NAME_FOR_HTTP: (
|
|
208
|
-
parse_qs(parsed_url.query)[MWI_AUTH_TOKEN_NAME_FOR_HTTP][0]
|
|
209
|
-
if MWI_AUTH_TOKEN_NAME_FOR_HTTP in parse_qs(parsed_url.query)
|
|
210
|
-
else ""
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
self.connection_scheme = parsed_url.scheme
|
|
214
|
-
self.url = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path
|
|
215
|
-
return self
|
|
216
|
-
|
|
217
|
-
async def _terminate_process(self, timeout=0):
|
|
218
|
-
"""
|
|
219
|
-
Asynchronous helper method to terminate the process with a timeout.
|
|
220
|
-
|
|
221
|
-
Args:
|
|
222
|
-
timeout: Maximum number of seconds to wait
|
|
223
|
-
for the process to terminate
|
|
224
|
-
|
|
225
|
-
"""
|
|
226
|
-
import asyncio
|
|
227
|
-
|
|
228
|
-
process = self.proc
|
|
229
|
-
try:
|
|
230
|
-
process.terminate()
|
|
231
|
-
await asyncio.wait_for(process.wait(), timeout=timeout)
|
|
232
|
-
except asyncio.TimeoutError:
|
|
233
|
-
_logger.warning(
|
|
234
|
-
"Termination of the MATLAB Server process timed out. Attempting to kill."
|
|
235
|
-
)
|
|
236
|
-
process.kill()
|
|
237
|
-
await process.wait()
|
|
238
|
-
_logger.debug("Killed the MATLAB process after timeout.")
|
|
239
|
-
|
|
240
|
-
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
241
|
-
_logger.info("Tearing down the MATLAB Server.")
|
|
242
|
-
self.event_loop.run_until_complete(self._terminate_process(timeout=10))
|
|
243
|
-
_logger.debug("Terminated the MATLAB process.")
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
# Fixtures
|
|
247
|
-
@pytest.fixture
|
|
248
|
-
def matlab_proxy_app_fixture(
|
|
249
|
-
event_loop,
|
|
250
|
-
):
|
|
251
|
-
"""A pytest fixture which yields a real matlab server to be used by tests.
|
|
252
|
-
|
|
253
|
-
Args:
|
|
254
|
-
event_loop (Event loop): The built-in event loop provided by pytest.
|
|
255
|
-
|
|
256
|
-
Yields:
|
|
257
|
-
real_matlab_server : A real matlab web server used by tests.
|
|
258
|
-
"""
|
|
259
|
-
|
|
260
|
-
try:
|
|
261
|
-
with RealMATLABServer(event_loop) as matlab_proxy_app:
|
|
262
|
-
yield matlab_proxy_app
|
|
263
|
-
except ProcessLookupError as e:
|
|
264
|
-
_logger.debug("ProcessLookupError found in matlab proxy app fixture")
|
|
265
|
-
_logger.debug(e)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
@pytest.fixture
|
|
269
|
-
def test_file_contents():
|
|
270
|
-
"""
|
|
271
|
-
A pytest fixture that provides a string for testing purposes.
|
|
272
|
-
|
|
273
|
-
This fixture returns a predefined string that can be used in tests to simulate
|
|
274
|
-
the contents of a file or any scenario where a constant string value is needed.
|
|
275
|
-
|
|
276
|
-
Returns:
|
|
277
|
-
str: A string containing the text "I LOVE MATLAB."
|
|
278
|
-
"""
|
|
279
|
-
return "I LOVE MATLAB."
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
@pytest.fixture
|
|
283
|
-
def test_file(tmp_path, test_file_contents):
|
|
284
|
-
"""
|
|
285
|
-
A pytest fixture that creates a temporary test file with given contents.
|
|
286
|
-
|
|
287
|
-
This fixture utilizes pytest's `tmp_path` fixture to generate a temporary directory,
|
|
288
|
-
then creates a file named "temporary_test_file.txt" within this directory,
|
|
289
|
-
and writes the provided contents to this file. It is useful for tests that require
|
|
290
|
-
reading from or writing to files without affecting the actual file system.
|
|
291
|
-
|
|
292
|
-
Parameters:
|
|
293
|
-
- tmp_path (Path): A pytest fixture that provides a temporary directory unique to the test function.
|
|
294
|
-
- test_file_contents (str): The content to be written into the temporary test file.
|
|
295
|
-
|
|
296
|
-
Returns:
|
|
297
|
-
- str: The path to the created temporary test file as a string.
|
|
298
|
-
"""
|
|
299
|
-
test_file = os.path.join(tmp_path, "temporary_test_file.txt")
|
|
300
|
-
with open(test_file, "w+") as f:
|
|
301
|
-
f.write(test_file_contents)
|
|
302
|
-
return test_file
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
# Test Functions
|
|
306
|
-
def test_matlab_is_up(matlab_proxy_app_fixture):
|
|
307
|
-
"""Test that the status switches from 'starting' to 'up' within a timeout.
|
|
308
|
-
|
|
309
|
-
Args:
|
|
310
|
-
matlab_proxy_app_fixture: A pytest fixture which yields a real matlab server to be used by tests.
|
|
311
|
-
"""
|
|
312
|
-
|
|
313
|
-
status = _check_matlab_status(matlab_proxy_app_fixture, "up")
|
|
314
|
-
assert status == "up"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def test_stop_matlab(matlab_proxy_app_fixture):
|
|
318
|
-
"""Test to check that matlab is in 'down' state when
|
|
319
|
-
we send the delete request to 'stop_matlab' endpoint
|
|
320
|
-
|
|
321
|
-
Args:
|
|
322
|
-
matlab_proxy_app_fixture: A pytest fixture which yields a real matlab server to be used by tests.
|
|
323
|
-
"""
|
|
324
|
-
status = _check_matlab_status(matlab_proxy_app_fixture, "up")
|
|
325
|
-
assert status == "up"
|
|
326
|
-
|
|
327
|
-
http_endpoint_to_test = "/stop_matlab"
|
|
328
|
-
stop_url = matlab_proxy_app_fixture.url + http_endpoint_to_test
|
|
329
|
-
|
|
330
|
-
with requests.Session() as s:
|
|
331
|
-
retries = Retry(total=10, backoff_factor=0.1)
|
|
332
|
-
s.mount(
|
|
333
|
-
f"{matlab_proxy_app_fixture.connection_scheme}://",
|
|
334
|
-
HTTPAdapter(max_retries=retries),
|
|
335
|
-
)
|
|
336
|
-
s.delete(stop_url, headers=matlab_proxy_app_fixture.headers, verify=False)
|
|
337
|
-
|
|
338
|
-
status = _check_matlab_status(matlab_proxy_app_fixture, "down")
|
|
339
|
-
assert status == "down"
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
async def test_print_message(matlab_proxy_app_fixture):
|
|
343
|
-
"""Test if the right logs are printed
|
|
344
|
-
|
|
345
|
-
Args:
|
|
346
|
-
matlab_proxy_app_fixture: A pytest fixture which yields a real matlab server to be used by tests.
|
|
347
|
-
|
|
348
|
-
FIXME: If output has logging or extra debug info, 600 bytes might not be enough.
|
|
349
|
-
"""
|
|
350
|
-
# Checks if matlab proxy is in "up" state or not
|
|
351
|
-
status = _check_matlab_status(matlab_proxy_app_fixture, "up")
|
|
352
|
-
assert status == "up"
|
|
353
|
-
|
|
354
|
-
uri_regex = f"{matlab_proxy_app_fixture.connection_scheme}://[a-zA-Z0-9\-_.]+:{matlab_proxy_app_fixture.mwi_app_port}{matlab_proxy_app_fixture.mwi_base_url}"
|
|
355
|
-
|
|
356
|
-
read_descriptor, write_descriptor = matlab_proxy_app_fixture.dpipe
|
|
357
|
-
number_of_bytes = 600
|
|
358
|
-
|
|
359
|
-
if read_descriptor:
|
|
360
|
-
line = os.read(read_descriptor, number_of_bytes).decode("utf-8")
|
|
361
|
-
process_logs = line.strip()
|
|
362
|
-
|
|
363
|
-
assert bool(re.search(uri_regex, process_logs)) == True
|
|
364
|
-
|
|
365
|
-
# Close the read and write descriptors.
|
|
366
|
-
os.close(read_descriptor)
|
|
367
|
-
os.close(write_descriptor)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def test_download_file_from_matlab(
|
|
371
|
-
matlab_proxy_app_fixture, test_file, test_file_contents
|
|
372
|
-
):
|
|
373
|
-
"""
|
|
374
|
-
Test the downloading of a file from a MATLAB proxy application.
|
|
375
|
-
|
|
376
|
-
This test function checks if the MATLAB proxy application is up and running, and then attempts to download
|
|
377
|
-
a specific test file from it. It validates both the status of the MATLAB proxy and the contents of the downloaded file.
|
|
378
|
-
|
|
379
|
-
Parameters:
|
|
380
|
-
- matlab_proxy_app_fixture (fixture): A test fixture representing the MATLAB proxy application environment.
|
|
381
|
-
- test_file (str): The name or path of the test file to be downloaded from the MATLAB proxy application.
|
|
382
|
-
- test_file_contents (str): The expected contents of the test file to validate the download operation.
|
|
383
|
-
|
|
384
|
-
Assertions:
|
|
385
|
-
- Asserts that the MATLAB proxy application is "up".
|
|
386
|
-
- Asserts that the content of the downloaded file matches the expected `test_file_contents`.
|
|
387
|
-
|
|
388
|
-
Raises:
|
|
389
|
-
- AssertionError: If any of the assertions fail, indicating either the MATLAB proxy application is not running
|
|
390
|
-
as expected or there is a mismatch in the file content.
|
|
391
|
-
"""
|
|
392
|
-
status = _check_matlab_status(matlab_proxy_app_fixture, "up")
|
|
393
|
-
assert status == "up"
|
|
394
|
-
|
|
395
|
-
# Once MATLAB is up, we can then attempt to download
|
|
396
|
-
result = _download_test_file(matlab_proxy_app_fixture, test_file)
|
|
397
|
-
assert result == test_file_contents
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Copyright 2023 The MathWorks, Inc.
|