From eb4bea9ad0ef1c542ef882ed2b8b84b1f1f8de08 Mon Sep 17 00:00:00 2001 From: Jericho Tolentino <68654047+jericht@users.noreply.github.com> Date: Thu, 2 May 2024 03:11:36 +0000 Subject: [PATCH] add tests Signed-off-by: Jericho Tolentino <68654047+jericht@users.noreply.github.com> --- .../integ/background/test_background_mode.py | 55 +++++++++ .../unit/background/test_backend_runner.py | 53 +++++++++ .../unit/background/test_frontend_runner.py | 112 ++++++++++++++++++ .../adaptor_runtime/unit/http/test_sockets.py | 21 +++- .../adaptor_runtime/unit/test_entrypoint.py | 47 ++++++++ 5 files changed, 284 insertions(+), 4 deletions(-) diff --git a/test/openjd/adaptor_runtime/integ/background/test_background_mode.py b/test/openjd/adaptor_runtime/integ/background/test_background_mode.py index 21fce78..a65c011 100644 --- a/test/openjd/adaptor_runtime/integ/background/test_background_mode.py +++ b/test/openjd/adaptor_runtime/integ/background/test_background_mode.py @@ -262,6 +262,61 @@ def test_heartbeat_acks( # THEN assert f"Received ACK for chunk: {response.output.id}" in new_response.output.output + @pytest.mark.skipif(not OSName.is_posix(), reason="Posix-specific test") + def test_init_uses_working_dir( + self, + tmp_path: pathlib.Path, + caplog: pytest.LogCaptureFixture, + ) -> None: + backend_proc = None + conn_settings = None + try: + # GIVEN + caplog.set_level(0) + working_dir = tmp_path / "working_dir" + working_dir.mkdir() + frontend = FrontendRunner(working_dir=str(working_dir), timeout_s=5.0) + + # WHEN + frontend.init(sys.modules[AdaptorExample.__module__]) + + # THEN + + # Connection file should be in the working dir + connection_file = working_dir / "connection.json" + conn_settings = _load_connection_settings(str(connection_file)) + + # Backend process started successfully + match = re.search("Started backend process. PID: ([0-9]+)", caplog.text) + assert match is not None + pid = int(match.group(1)) + backend_proc = psutil.Process(pid) + + # Unix socket matches connection file and is also in the working dir + assert any( + [ + conn.laddr == conn_settings.socket + for conn in backend_proc.connections(kind="unix") + ] + ) + assert conn_settings.socket.startswith(str(working_dir)) + + finally: + if backend_proc: + try: + backend_proc.kill() + except psutil.NoSuchProcess: + pass # Already stopped + + # We don't need to call the `remove` for the NamedPipe server. + # NamedPipe servers are managed by Named Pipe File System it is not a regular file. + # Once all handles are closed, the system automatically cleans up the named pipe. + if OSName.is_posix() and conn_settings: + try: + os.remove(conn_settings.socket) + except FileNotFoundError: + pass # Already deleted + class TestAuthentication: """ Tests for background mode authentication. diff --git a/test/openjd/adaptor_runtime/unit/background/test_backend_runner.py b/test/openjd/adaptor_runtime/unit/background/test_backend_runner.py index a39f10c..1627694 100644 --- a/test/openjd/adaptor_runtime/unit/background/test_backend_runner.py +++ b/test/openjd/adaptor_runtime/unit/background/test_backend_runner.py @@ -1,4 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +from __future__ import annotations import json import os @@ -201,3 +202,55 @@ def test_signal_hook(self, mock_submit, signal_mock: MagicMock) -> None: else: signal_mock.assert_any_call(signal.SIGBREAK, runner._sigint_handler) # type: ignore[attr-defined] mock_submit.assert_called_with(server_mock, adaptor_runner._cancel, force_immediate=True) + + class ConnectionFileCompat: + @pytest.mark.parametrize( + argnames=["connection_file", "working_dir"], + argvalues=[ + ["path", "dir"], + [None, None], + ], + ids=["both provided", "neither provided"], + ) + def test_rejects_not_exactly_one_of_connection_file_and_working_dir( + self, + connection_file: str | None, + working_dir: str | None, + ) -> None: + # GIVEN + with pytest.raises(RuntimeError) as raised_err: + # WHEN + BackendRunner( + Mock(), + connection_file_path=connection_file, + working_dir=working_dir, + ) + + # THEN + assert ( + f"Exactly one of 'connection_file_path' or 'working_dir' must be provided, but got: connection_file_path={connection_file} working_dir={working_dir}" + == str(raised_err.value) + ) + + @pytest.mark.skipif(not OSName.is_posix(), reason="Posix-specific test") + @pytest.mark.parametrize( + argnames=["working_dir"], argvalues=[["dir"], [None]], ids=["provided", "not provided"] + ) + def test_run_uses_working_dir_for_socket_path(self, working_dir: str | None) -> None: + # GIVEN + runner = BackendRunner(Mock(), working_dir=working_dir) + + with patch.object( + backend_runner.SocketPaths, + "get_process_socket_path", + wraps=backend_runner.SocketPaths.get_process_socket_path, + ) as mock_get_process_socket_path: + # WHEN + runner.run() + + # THEN + mock_get_process_socket_path.assert_called_once_with( + "runtime", + base_dir=working_dir, + create_dir=True, + ) diff --git a/test/openjd/adaptor_runtime/unit/background/test_frontend_runner.py b/test/openjd/adaptor_runtime/unit/background/test_frontend_runner.py index a9a93c6..b16807c 100644 --- a/test/openjd/adaptor_runtime/unit/background/test_frontend_runner.py +++ b/test/openjd/adaptor_runtime/unit/background/test_frontend_runner.py @@ -821,6 +821,118 @@ def test_hook(self, signal_mock: MagicMock, cancel_mock: MagicMock) -> None: signal_mock.assert_any_call(signal.SIGBREAK, runner._sigint_handler) # type: ignore[attr-defined] cancel_mock.assert_called_once() + class TestConnectionFileCompat: + @pytest.mark.parametrize( + argnames=["connection_file", "working_dir"], + argvalues=[ + ["path", "dir"], + [None, None], + ], + ids=["both provided", "neither provided"], + ) + def test_rejects_not_exactly_one_of_connection_file_and_working_dir( + self, + connection_file: str | None, + working_dir: str | None, + ) -> None: + # GIVEN + with pytest.raises(RuntimeError) as raised_err: + # WHEN + FrontendRunner( + connection_file_path=connection_file, + working_dir=working_dir, + ) + + # THEN + assert ( + f"Expected exactly one of 'connection_file_path' or 'working_dir', but got: connection_file_path={connection_file} working_dir={working_dir}" + == str(raised_err.value) + ) + + @patch.object(frontend_runner, "_wait_for_file") + @patch.object(frontend_runner.os.path, "exists", return_value=False) + @patch.object(frontend_runner.subprocess, "Popen") + def test_init_provides_connection_file_arg( + self, + mock_popen: MagicMock, + mock_exists: MagicMock, + mock_wait_for_file: MagicMock, + ) -> None: + # GIVEN + connection_file = os.path.join(os.sep, "path", "to", "connection.json") + runner = FrontendRunner(connection_file_path=connection_file) + adaptor_module = ModuleType("") + adaptor_module.__package__ = "package" + + with patch.object(runner, "_heartbeat"): + # WHEN + runner.init(adaptor_module) + + # THEN + mock_popen.assert_called_once_with( + [ + sys.executable, + "-m", + adaptor_module.__package__, + "daemon", + "_serve", + "--init-data", + json.dumps({}), + "--path-mapping-rules", + json.dumps({}), + "--connection-file", + connection_file, + ], + shell=False, + close_fds=True, + start_new_session=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + @patch.object(frontend_runner, "_wait_for_file") + @patch.object(frontend_runner.os.path, "exists", return_value=False) + @patch.object(frontend_runner.subprocess, "Popen") + def test_init_provides_working_dir_arg( + self, + mock_popen: MagicMock, + mock_exists: MagicMock, + mock_wait_for_file: MagicMock, + ) -> None: + # GIVEN + working_dir = os.path.join(os.sep, "path", "to", "working") + runner = FrontendRunner(working_dir=working_dir) + adaptor_module = ModuleType("") + adaptor_module.__package__ = "package" + + with patch.object(runner, "_heartbeat"): + # WHEN + runner.init(adaptor_module) + + # THEN + mock_popen.assert_called_once_with( + [ + sys.executable, + "-m", + adaptor_module.__package__, + "daemon", + "_serve", + "--init-data", + json.dumps({}), + "--path-mapping-rules", + json.dumps({}), + "--working-dir", + working_dir, + ], + shell=False, + close_fds=True, + start_new_session=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + class TestLoadConnectionSettings: """ diff --git a/test/openjd/adaptor_runtime/unit/http/test_sockets.py b/test/openjd/adaptor_runtime/unit/http/test_sockets.py index 9967fbe..453b1c5 100644 --- a/test/openjd/adaptor_runtime/unit/http/test_sockets.py +++ b/test/openjd/adaptor_runtime/unit/http/test_sockets.py @@ -157,6 +157,17 @@ def test_create_dir(self, mock_makedirs: MagicMock, create: bool) -> None: else: mock_makedirs.assert_not_called() + def test_uses_base_dir(self) -> None: + # GIVEN + subject = SocketPathsStub() + base_dir = os.path.join(os.sep, "base", "dir") + + # WHEN + result = subject.get_socket_path("sock", base_dir=base_dir) + + # THEN + assert result.startswith(base_dir) + def test_uses_namespace(self) -> None: # GIVEN namespace = "my-namespace" @@ -240,10 +251,11 @@ class TestLinuxSocketPaths: argvalues=[ ["a"], ["a" * 107], + ["/this/part/should/not/matter/" + "a" * 107], ], - ids=["one byte", "107 bytes"], + ids=["one byte", "107 bytes", "path with name of 107 bytes"], ) - def test_accepts_paths_within_107_bytes(self, path: str): + def test_accepts_names_within_107_bytes(self, path: str): """ Verifies the function accepts paths up to 100 bytes (108 byte max - 1 byte null terminator) """ @@ -259,7 +271,7 @@ def test_accepts_paths_within_107_bytes(self, path: str): # THEN pass # success - def test_rejects_paths_over_107_bytes(self): + def test_rejects_names_over_107_bytes(self): # GIVEN length = 108 path = "a" * length @@ -283,8 +295,9 @@ class TestMacOSSocketPaths: argvalues=[ ["a"], ["a" * 103], + ["/this/part/should/not/matter/" + "a" * 103], ], - ids=["one byte", "103 bytes"], + ids=["one byte", "103 bytes", "path with name of 103 bytes"], ) def test_accepts_paths_within_103_bytes(self, path: str): """ diff --git a/test/openjd/adaptor_runtime/unit/test_entrypoint.py b/test/openjd/adaptor_runtime/unit/test_entrypoint.py index 66ba2b8..928ff8b 100644 --- a/test/openjd/adaptor_runtime/unit/test_entrypoint.py +++ b/test/openjd/adaptor_runtime/unit/test_entrypoint.py @@ -742,6 +742,53 @@ def test_makes_connection_file_path_absolute( connection_file_path=mock_abspath.return_value, working_dir=None ) + class TestConnectionFileCompat: + @pytest.mark.parametrize( + argnames=["connection_file", "working_dir"], + argvalues=[ + ["path", "dir"], + [None, None], + ], + ids=["both provided", "neither provided"], + ) + def test_rejects_not_exactly_one_of_connection_file_and_working_dir( + self, + connection_file: str | None, + working_dir: str | None, + ) -> None: + # GIVEN + entrypoint = EntryPoint(FakeAdaptor) + args = [ + "Adaptor", + "daemon", + "run", + ] + if connection_file: + args.extend( + [ + "--connection-file", + connection_file, + ] + ) + if working_dir: + args.extend( + [ + "--working-dir", + working_dir, + ] + ) + + with patch.object(runtime_entrypoint.sys, "argv", args): + with pytest.raises(RuntimeError) as raised_err: + # WHEN + entrypoint.start() + + # THEN + assert ( + "Expected exactly one of 'connection_file' or 'working_dir' to be provided, but got args: " + in str(raised_err.value) + ) + class TestLoadData: """