diff --git a/dem/cli/command/delete_cmd.py b/dem/cli/command/delete_cmd.py index ddef2c5..3c58af2 100644 --- a/dem/cli/command/delete_cmd.py +++ b/dem/cli/command/delete_cmd.py @@ -3,6 +3,7 @@ from dem.core.platform import Platform from dem.core.dev_env import DevEnv +from dem.core.exceptions import PlatformError from dem.cli.console import stderr, stdout import typer @@ -16,7 +17,11 @@ def execute(platform: Platform, dev_env_name: str) -> None: typer.confirm("The Development Environment is installed. Do you want to uninstall it?", abort=True) - platform.uninstall_dev_env(dev_env_to_delete) + try: + platform.uninstall_dev_env(dev_env_to_delete) + except PlatformError as e: + stderr.print(f"[red]Error: The deletion failed, because the Dev Env can't be uninstalled. {str(e)}[/]") + return stdout.print("Deleting the Development Environment...") platform.local_dev_envs.remove(dev_env_to_delete) diff --git a/dem/cli/command/init_cmd.py b/dem/cli/command/init_cmd.py new file mode 100644 index 0000000..4837212 --- /dev/null +++ b/dem/cli/command/init_cmd.py @@ -0,0 +1,48 @@ +"""Implementation of the init command.""" +# dem/cli/command/init_cmd.py + +import os, typer +from dem.core.platform import Platform +from dem.core.dev_env import DevEnv +from dem.core.exceptions import PlatformError +from dem.cli.console import stderr, stdout + +def execute(platform: Platform, project_path: str) -> None: + """ Initialize a project at the given path. + + Args: + platform -- the platform + project_path -- the path to the project to initialize + """ + if not os.path.isdir(project_path): + stderr.print(f"[red]Error: The {project_path} path does not exist.[/]") + return + + try: + dev_env = DevEnv(descriptor_path=f"{project_path}/.axem/dev_env_descriptor.json") + except FileNotFoundError as e: + stderr.print(f"[red]Error: No Dev Env is assigned to this project. You can assign one with `dem assign`.") + return + + for local_dev_env in platform.local_dev_envs: + if local_dev_env.name == dev_env.name: + stdout.print(f"[yellow]Warning: The {dev_env.name} Development Environment is already initialized.[/]") + typer.confirm("Would you like to re-init the Dev Env? All local changes will be lost!", abort=True) + + if local_dev_env.is_installed: + typer.confirm("The Development Environment is installed, so it can't be deleted. Do you want to uninstall it first?", + abort=True) + + try: + platform.uninstall_dev_env(local_dev_env) + except PlatformError as e: + stderr.print(f"[red]Error: The Dev Env can't be uninstalled. {str(e)}") + return + + platform.local_dev_envs.remove(local_dev_env) + break + + platform.local_dev_envs.append(dev_env) + platform.flush_descriptors() + stdout.print(f"[green]Successfully initialized the {dev_env.name} Dev Env for the project at {project_path}![/]") + stdout.print(f"\nNow you can install the Dev Env with the `dem install {dev_env.name}` command.") \ No newline at end of file diff --git a/dem/cli/command/uninstall_cmd.py b/dem/cli/command/uninstall_cmd.py index 9771798..e4199ad 100644 --- a/dem/cli/command/uninstall_cmd.py +++ b/dem/cli/command/uninstall_cmd.py @@ -23,6 +23,6 @@ def execute(platform: Platform, dev_env_name: str) -> None: try: platform.uninstall_dev_env(dev_env_to_uninstall) except PlatformError as e: - stderr.print(f"[red]Error: {e}[/]") + stderr.print(f"[red]Error: {str(e)}[/]") else: stdout.print(f"[green]Successfully deleted the {dev_env_name}![/]") \ No newline at end of file diff --git a/dem/cli/main.py b/dem/cli/main.py index 7678d18..97e342d 100644 --- a/dem/cli/main.py +++ b/dem/cli/main.py @@ -9,7 +9,8 @@ from dem.cli.command import cp_cmd, info_cmd, list_cmd, pull_cmd, create_cmd, modify_cmd, delete_cmd, \ rename_cmd, run_cmd, export_cmd, load_cmd, clone_cmd, add_reg_cmd, \ list_reg_cmd, del_reg_cmd, add_cat_cmd, list_cat_cmd, del_cat_cmd, \ - add_host_cmd, uninstall_cmd, install_cmd, assign_cmd, list_host_cmd, del_host_cmd + add_host_cmd, uninstall_cmd, install_cmd, assign_cmd, init_cmd, \ + list_host_cmd, del_host_cmd from dem.cli.console import stdout from dem.core.platform import Platform from dem.core.exceptions import InternalError @@ -244,6 +245,18 @@ def assign(dev_env_name: Annotated[str, typer.Argument(help="Name of the Dev Env else: raise InternalError("Error: The platform hasn't been initialized properly!") +@typer_cli.command() +def init(project_path: Annotated[str, typer.Argument(help="Path of the project.")] = os.getcwd()) -> None: + """ + Initialize a project to use a Development Environment. + + If the project path is not specified, the current working directory will be used. + """ + if platform: + init_cmd.execute(platform, project_path) + else: + raise InternalError("Error: The platform hasn't been initialized properly!") + @typer_cli.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) def run(dev_env_name: Annotated[str, typer.Argument(help="Run the container in this Development Environment context", autocompletion=autocomplete_dev_env_name)], diff --git a/dem/core/container_engine.py b/dem/core/container_engine.py index e018c4c..f657f2e 100644 --- a/dem/core/container_engine.py +++ b/dem/core/container_engine.py @@ -4,6 +4,7 @@ from dem.core.core import Core from dem.core.exceptions import ContainerEngineError import docker +import docker.errors class ContainerEngine(Core): """ Operations on the Docker Container Engine.""" @@ -109,13 +110,11 @@ def remove(self, image: str) -> None: image -- the tool image to remove """ try: - self._docker_client.images.remove(image) + self._docker_client.images.remove(image) except docker.errors.ImageNotFound: self.user_output.msg(f"[yellow]The {image} doesn't exist. Unable to remove it.[/]\n") - raise ContainerEngineError("") except docker.errors.APIError: - self.user_output.error(f"[red]Error: The {image} is used by a container. Unable to remove it.[/]\n") - raise ContainerEngineError("") + raise ContainerEngineError(f"The {image} is used by a container. Unable to remove it.[/]\n") else: self.user_output.msg(f"[green]Successfully removed the {image}![/]\n") diff --git a/dem/core/dev_env.py b/dem/core/dev_env.py index b254f67..ab757b3 100755 --- a/dem/core/dev_env.py +++ b/dem/core/dev_env.py @@ -21,19 +21,42 @@ class DevEnv(Core): ) def __init__(self, descriptor: dict | None = None, - dev_env_to_copy: "DevEnv | None" = None) -> None: - """ Init the DevEnv class. A new instance can be create from a descriptor or from an already - existing DevEnv instance. + dev_env_to_copy: "DevEnv | None" = None, + descriptor_path: str | None = None) -> None: + """ Init the DevEnv class. + + A new instance can be created: + - from a Dev Env descriptor + - based on another Dev Env + - from a descriptor avaialable at the given path. + + Only one of the arguments can be used at a time. Args: descriptor -- the description of the Development Environment from the dev_env.json file dev_env_to_copy -- the DevEnv instance to copy + descriptor_path -- the path of the descriptor file + + Exceptions: + ValueError -- if more than one of the arguments is not None """ + + # Only one of the arguments can be not None + if sum(arg is not None for arg in [descriptor, dev_env_to_copy, descriptor_path]) > 1: + raise ValueError("Only one of the arguments can be not None.") + + if descriptor_path: + if not os.path.exists(descriptor_path): + raise FileNotFoundError("dev_env_descriptor.json doesn't exist.") + with open(descriptor_path, "r") as file: + descriptor = json.load(file) + if descriptor: self.name: str = descriptor["name"] self.tools: str = descriptor["tools"] - if "True" == descriptor["installed"]: + descriptor_installed = descriptor.get("installed", "False") + if "True" == descriptor_installed: self.is_installed = True else: self.is_installed = False diff --git a/dem/core/platform.py b/dem/core/platform.py index 5f5934f..db22c37 100644 --- a/dem/core/platform.py +++ b/dem/core/platform.py @@ -180,15 +180,17 @@ def uninstall_dev_env(self, dev_env_to_uninstall: DevEnv) -> None: for tool in dev_env.tools: all_required_tool_images.add(tool["image_name"] + ":" + tool["image_version"]) + tool_images_to_remove = set() for tool in dev_env_to_uninstall.tools: tool_image = tool["image_name"] + ":" + tool["image_version"] - if tool_image in all_required_tool_images: - self.user_output.msg(f"\nThe tool image [bold]{tool_image}[/bold] is required by another Development Environment. It won't be deleted.") - else: - try: - self.container_engine.remove(tool_image) - except ContainerEngineError: - raise PlatformError("Dev Env uninstall failed.") + if tool_image not in all_required_tool_images: + tool_images_to_remove.add(tool_image) + + for tool_image in tool_images_to_remove: + try: + self.container_engine.remove(tool_image) + except ContainerEngineError as e: + raise PlatformError(f"Dev Env uninstall failed. {str(e)}") dev_env_to_uninstall.is_installed = False self.flush_descriptors() @@ -218,4 +220,23 @@ def assign_dev_env(self, dev_env_to_assign: DevEnv, project_path: str) -> None: self.user_output.get_confirm("[yellow]A Dev Env is already assigned to the project.[/]", "Overwrite it?") - dev_env_to_assign.export(path) \ No newline at end of file + dev_env_to_assign.export(path) + + def init_project(self, project_path: str) -> None: + """ Init the project by saving the Dev Env's descriptor to the local Dev Env storage. + + Args: + assigned_dev_env -- the Development Environment assigned to the project + """ + descriptor_path = f"{project_path}/.axem/dev_env_descriptor.json" + if not os.path.exists(descriptor_path): + raise FileNotFoundError(f"The {descriptor_path} file does not exist.") + + assigned_dev_env = DevEnv(descriptor_path=descriptor_path) + existing_dev_env = self.get_dev_env_by_name(assigned_dev_env.name) + if existing_dev_env is not None: + self.user_output.get_confirm("[yellow]This project is already initialized.[/]", + "Overwrite it?") + self.local_dev_envs.remove(existing_dev_env) + + self.local_dev_envs.append(assigned_dev_env) \ No newline at end of file diff --git a/dem/core/registry.py b/dem/core/registry.py index fec53af..6bfafa5 100644 --- a/dem/core/registry.py +++ b/dem/core/registry.py @@ -43,7 +43,7 @@ def _list_tags(self, repo: str) -> None: repo -- get the tags of this repository """ try: - response = requests.get(self._get_tag_endpoint_url(repo), timeout=1) + response = requests.get(self._get_tag_endpoint_url(repo), timeout=10) except Exception as e: self.user_output.error(str(e)) else: @@ -135,7 +135,7 @@ def _search(self) -> list[str]: repo_endpoint = self._registry_config["url"] + "/v2/_catalog" try: - response = requests.get(repo_endpoint, timeout=1) + response = requests.get(repo_endpoint, timeout=10) except Exception as e: self.user_output.error(str(e)) else: diff --git a/tests/cli/test_delete_cmd.py b/tests/cli/test_delete_cmd.py index 33b8720..10d6f25 100644 --- a/tests/cli/test_delete_cmd.py +++ b/tests/cli/test_delete_cmd.py @@ -3,13 +3,12 @@ # Unit under test: import dem.cli.main as main +import dem.cli.command.delete_cmd as delete_cmd # Test framework from typer.testing import CliRunner from unittest.mock import patch, MagicMock, call -import docker.errors - ## Global test variables runner = CliRunner() @@ -43,6 +42,32 @@ def test_delete(mock_stdout_print: MagicMock, mock_config: MagicMock) -> None: ]) mock_platform.flush_descriptors.assert_called_once() +@patch("dem.cli.command.delete_cmd.typer.confirm") +@patch("dem.cli.command.delete_cmd.stderr.print") +def test_delete_uninstall_failed(mock_stderr_print: MagicMock, mock_confirm: MagicMock) -> None: + # Test setup + mock_platform = MagicMock() + main.platform = mock_platform + + test_dev_env_name = "test_dev_env_name" + test_dev_env = MagicMock() + test_dev_env.is_installed = True + mock_platform.get_dev_env_by_name.return_value = test_dev_env + test_exception_text = "test_exception_text" + mock_platform.uninstall_dev_env.side_effect = delete_cmd.PlatformError(test_exception_text) + + # Run unit under test + runner_result = runner.invoke(main.typer_cli, ["delete", test_dev_env_name]) + + # Check expectations + assert runner_result.exit_code == 0 + + mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) + mock_confirm.assert_called_once_with("The Development Environment is installed. Do you want to uninstall it?", + abort=True) + mock_platform.uninstall_dev_env.assert_called_once_with(test_dev_env) + mock_stderr_print.assert_called_once_with(f"[red]Error: The deletion failed, because the Dev Env can't be uninstalled. Platform error: {test_exception_text}[/]") + @patch("dem.cli.command.delete_cmd.stderr.print") def test_delete_not_existing(mock_stderr_print: MagicMock) -> None: # Test setup diff --git a/tests/cli/test_init_cmd.py b/tests/cli/test_init_cmd.py new file mode 100644 index 0000000..1af40b6 --- /dev/null +++ b/tests/cli/test_init_cmd.py @@ -0,0 +1,136 @@ +"""Tests for the assign command.""" +# tests/cli/test_assign_cmd.py + +# Unit under test: +import dem.cli.command.init_cmd as init_cmd + +# Test framework +from unittest.mock import MagicMock, patch, call + +@patch("dem.cli.command.init_cmd.os.path.isdir") +@patch("dem.cli.command.init_cmd.stdout.print") +@patch("dem.cli.command.init_cmd.typer.confirm") +@patch("dem.cli.command.init_cmd.DevEnv") +def test_execute(mock_DevEnv, mock_confirm, mock_stdout_print, mock_isdir) -> None: + # Test setup + mock_platform = MagicMock() + mock_project_path = "/path/to/project" + mock_dev_env_name = "test_dev_env" + mock_dev_env = MagicMock() + mock_dev_env.name = mock_dev_env_name + mock_DevEnv.return_value = mock_dev_env + mock_isdir.return_value = True + mock_platform.local_dev_envs = [] + + # Run unit under test + init_cmd.execute(mock_platform, mock_project_path) + + # Check expectations + assert mock_dev_env in mock_platform.local_dev_envs + + mock_isdir.assert_called_once_with(mock_project_path) + mock_DevEnv.assert_called_once_with(descriptor_path=f"{mock_project_path}/.axem/dev_env_descriptor.json") + mock_confirm.assert_not_called() + mock_platform.uninstall_dev_env.assert_not_called() + mock_platform.flush_descriptors.assert_called_once() + mock_stdout_print.assert_has_calls([call(f"[green]Successfully initialized the {mock_dev_env_name} Dev Env for the project at {mock_project_path}![/]"), + call(f"\nNow you can install the Dev Env with the `dem install {mock_dev_env_name}` command.")]) + +@patch("dem.cli.command.init_cmd.os.path.isdir") +@patch("dem.cli.command.init_cmd.stderr.print") +def test_execute_project_path_not_existing(mock_stderr_print, mock_isdir) -> None: + # Test setup + mock_platform = MagicMock() + mock_project_path = "/path/to/project" + mock_isdir.return_value = False + + # Run unit under test + init_cmd.execute(mock_platform, mock_project_path) + + # Check expectations + mock_isdir.assert_called_once_with(mock_project_path) + mock_stderr_print.assert_called_once_with(f"[red]Error: The {mock_project_path} path does not exist.[/]") + +@patch("dem.cli.command.init_cmd.os.path.isdir") +@patch("dem.cli.command.init_cmd.stderr.print") +@patch("dem.cli.command.init_cmd.DevEnv") +def test_execute_missing_descriptor(mock_DevEnv, mock_stderr_print, mock_isdir) -> None: + # Test setup + mock_platform = MagicMock() + mock_project_path = "/path/to/project" + mock_DevEnv.side_effect = FileNotFoundError + mock_isdir.return_value = True + + # Run unit under test + init_cmd.execute(mock_platform, mock_project_path) + + # Check expectations + mock_isdir.assert_called_once_with(mock_project_path) + mock_DevEnv.assert_called_once_with(descriptor_path=f"{mock_project_path}/.axem/dev_env_descriptor.json") + mock_stderr_print.assert_called_once_with("[red]Error: No Dev Env is assigned to this project. You can assign one with `dem assign`.") + +@patch("dem.cli.command.init_cmd.os.path.isdir") +@patch("dem.cli.command.init_cmd.stdout.print") +@patch("dem.cli.command.init_cmd.typer.confirm") +@patch("dem.cli.command.init_cmd.DevEnv") +def test_execute_reinit_installed(mock_DevEnv, mock_confirm, mock_stdout_print, mock_isdir) -> None: + # Test setup + mock_platform = MagicMock() + mock_project_path = "/path/to/project" + mock_dev_env_name = "test_dev_env" + mock_dev_env = MagicMock() + mock_dev_env.name = mock_dev_env_name + mock_DevEnv.return_value = mock_dev_env + mock_isdir.return_value = True + mock_local_dev_env = MagicMock() + mock_local_dev_env.name = mock_dev_env_name + mock_local_dev_env.is_installed = True + mock_platform.local_dev_envs = [mock_local_dev_env] + + # Run unit under test + init_cmd.execute(mock_platform, mock_project_path) + + # Check expectations + assert mock_dev_env in mock_platform.local_dev_envs + assert mock_local_dev_env not in mock_platform.local_dev_envs + + mock_isdir.assert_called_once_with(mock_project_path) + mock_DevEnv.assert_called_once_with(descriptor_path=f"{mock_project_path}/.axem/dev_env_descriptor.json") + mock_confirm.assert_has_calls([call("Would you like to re-init the Dev Env? All local changes will be lost!", abort=True), + call("The Development Environment is installed, so it can't be deleted. Do you want to uninstall it first?", abort=True)]) + mock_platform.uninstall_dev_env.assert_called_once_with(mock_local_dev_env) + mock_platform.flush_descriptors.assert_called_once() + mock_stdout_print.assert_has_calls([call(f"[green]Successfully initialized the {mock_dev_env_name} Dev Env for the project at {mock_project_path}![/]"), + call(f"\nNow you can install the Dev Env with the `dem install {mock_dev_env_name}` command.")]) + +@patch("dem.cli.command.init_cmd.os.path.isdir") +@patch("dem.cli.command.init_cmd.stderr.print") +@patch("dem.cli.command.init_cmd.typer.confirm") +@patch("dem.cli.command.init_cmd.DevEnv") +def test_execute_reinit_installed_uninstall_fails(mock_DevEnv, mock_confirm, mock_stderr_print, + mock_isdir) -> None: + # Test setup + mock_platform = MagicMock() + mock_project_path = "/path/to/project" + mock_dev_env_name = "test_dev_env" + mock_dev_env = MagicMock() + mock_dev_env.name = mock_dev_env_name + mock_DevEnv.return_value = mock_dev_env + mock_isdir.return_value = True + mock_local_dev_env = MagicMock() + mock_local_dev_env.name = mock_dev_env_name + mock_local_dev_env.is_installed = True + mock_platform.local_dev_envs = [mock_local_dev_env] + test_exception_text = "test_exception_text" + mock_platform.uninstall_dev_env.side_effect = init_cmd.PlatformError(test_exception_text) + + # Run unit under test + init_cmd.execute(mock_platform, mock_project_path) + + # Check expectations + mock_isdir.assert_called_once_with(mock_project_path) + mock_DevEnv.assert_called_once_with(descriptor_path=f"{mock_project_path}/.axem/dev_env_descriptor.json") + mock_confirm.assert_has_calls([call("Would you like to re-init the Dev Env? All local changes will be lost!", abort=True), + call("The Development Environment is installed, so it can't be deleted. Do you want to uninstall it first?", abort=True)]) + mock_platform.uninstall_dev_env.assert_called_once_with(mock_local_dev_env) + mock_stderr_print.assert_called_once_with(f"[red]Error: The Dev Env can't be uninstalled. Platform error: {test_exception_text}") \ No newline at end of file diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index caf87b6..71c914c 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -142,6 +142,19 @@ def test_version_exit_raised(mock_importlib_metadata_version): with pytest.raises(typer.Exit): main._version_callback(True) +@patch("dem.cli.main.init_cmd.execute") +def test_init_execute(mock_init_execute: MagicMock) -> None: + # Test setup + test_path = "test_path" + mock_platform = MagicMock() + main.platform = mock_platform + + # Run unit under test + main.init(test_path) + + # Check expectations + mock_init_execute.assert_called_once_with(mock_platform, test_path) + def test_platform_not_initialized() -> None: # Test setup test_dev_env_name = "test_dev_env_name" @@ -163,8 +176,10 @@ def test_platform_not_initialized() -> None: main.rename: [test_dev_env_name, test_dev_env_name], main.modify: [test_dev_env_name], main.delete: [test_dev_env_name], + main.install: [test_dev_env_name], main.uninstall: [test_dev_env_name], main.assign: [test_dev_env_name, test_path], + main.init: [test_path], main.run: [test_dev_env_name, mock_ctx], main.add_reg: [test_name, test_url], main.list_reg: [], diff --git a/tests/core/test_container_engine.py b/tests/core/test_container_engine.py index 9f21e9c..8d1904b 100644 --- a/tests/core/test_container_engine.py +++ b/tests/core/test_container_engine.py @@ -269,16 +269,14 @@ def test_remove_ImageNotFound(mock_from_env: MagicMock, mock_user_output: MagicM test_container_engine = container_engine.ContainerEngine() # Run unit under test - with pytest.raises(container_engine.ContainerEngineError): - test_container_engine.remove(test_image_to_remove) + test_container_engine.remove(test_image_to_remove) - # Check expectations - mock_docker_client.images.remove.assert_called_once_with(test_image_to_remove) - mock_user_output.msg.assert_called_once_with(f"[yellow]The {test_image_to_remove} doesn't exist. Unable to remove it.[/]\n") + # Check expectations + mock_docker_client.images.remove.assert_called_once_with(test_image_to_remove) + mock_user_output.msg.assert_called_once_with(f"[yellow]The {test_image_to_remove} doesn't exist. Unable to remove it.[/]\n") -@patch.object(container_engine.ContainerEngine, "user_output") @patch("docker.from_env") -def test_remove_APIError(mock_from_env: MagicMock, mock_user_output: MagicMock) -> None: +def test_remove_APIError(mock_from_env: MagicMock) -> None: # Test setup mock_docker_client = MagicMock() mock_from_env.return_value = mock_docker_client @@ -289,12 +287,13 @@ def test_remove_APIError(mock_from_env: MagicMock, mock_user_output: MagicMock) test_container_engine = container_engine.ContainerEngine() # Run unit under test - with pytest.raises(container_engine.ContainerEngineError): + with pytest.raises(container_engine.ContainerEngineError) as exported_exception_info: test_container_engine.remove(test_image_to_remove) # Check expectations + assert str(exported_exception_info) == f"The {test_image_to_remove} is used by a container. Unable to remove it.[/]\n" + mock_docker_client.images.remove.assert_called_once_with(test_image_to_remove) - mock_user_output.error.assert_called_once_with(f"[red]Error: The {test_image_to_remove} is used by a container. Unable to remove it.[/]\n") @patch("docker.from_env") def test_search(mock_from_env): diff --git a/tests/core/test_dev_env.py b/tests/core/test_dev_env.py index 32b78c0..1acefdb 100644 --- a/tests/core/test_dev_env.py +++ b/tests/core/test_dev_env.py @@ -6,10 +6,11 @@ # Test framework from unittest.mock import MagicMock, patch +import pytest from typing import Any -def test_DevEnv(): +def test_DevEnv() -> None: # Test setup test_descriptor = { "name": "test_name", @@ -36,6 +37,65 @@ def test_DevEnv(): assert test_dev_env.name is mock_base_dev_env.name assert test_dev_env.tools is mock_base_dev_env.tools +@patch("dem.core.dev_env.json.load") +@patch("dem.core.dev_env.open") +@patch("dem.core.dev_env.os.path.exists") +def test_DevEnv_with_descriptor_path(mock_path_exists: MagicMock, mock_open: MagicMock, + mock_json_load: MagicMock) -> None: + # Test setup + test_descriptor_path = "/path/to/descriptor.json" + test_descriptor = { + "name": "test_name", + "installed": "True", + "tools": [MagicMock()] + } + mock_path_exists.return_value = True + mock_file = MagicMock() + mock_open.return_value.__enter__.return_value = mock_file + mock_json_load.return_value = test_descriptor + + # Run unit under test + test_dev_env = dev_env.DevEnv(descriptor_path=test_descriptor_path) + + # Check expectations + assert test_dev_env.name is test_descriptor["name"] + assert test_dev_env.tools is test_descriptor["tools"] + assert test_dev_env.is_installed is True + + mock_path_exists.assert_called_once_with(test_descriptor_path) + mock_open.assert_called_once_with(test_descriptor_path, "r") + mock_json_load.assert_called_once_with(mock_file) + +def test_DevEnv_with_descriptor_path_not_existing() -> None: + # Test setup + test_descriptor_path = "/path/to/descriptor.json" + mock_path_exists = MagicMock() + mock_path_exists.return_value = False + + with pytest.raises(FileNotFoundError) as exc_info: + # Run unit under test + test_dev_env = dev_env.DevEnv(descriptor_path=test_descriptor_path) + + # Check expectations + assert str(exc_info.value) == f"dev_env_descriptor.json doesn't exist." + + mock_path_exists.assert_called_once_with(test_descriptor_path) + +def test_DevEnv_with_descriptor_and_descriptor_path() -> None: + # Test setup + test_descriptor = { + "name": "test_name", + "installed": "True", + "tools": [MagicMock()] + } + test_descriptor_path = "/path/to/descriptor.json" + + with pytest.raises(ValueError) as exc_info: + # Run unit under test + test_dev_env = dev_env.DevEnv(descriptor=test_descriptor, descriptor_path=test_descriptor_path) + + # Check expectations + assert str(exc_info.value) == "Only one of the arguments can be not None." def test_DevEnv_check_image_availability(): # Test setup diff --git a/tests/core/test_platform.py b/tests/core/test_platform.py index 14ee4a6..971ddcd 100644 --- a/tests/core/test_platform.py +++ b/tests/core/test_platform.py @@ -355,13 +355,11 @@ def test_Platform_install_dev_env_failure(mock___init__: MagicMock, mock_user_ou mock___init__.assert_called_once() assert str(exported_exception_info) == "Platform error: Dev Env install failed." - @patch.object(platform.Platform, "flush_descriptors") @patch.object(platform.Platform, "container_engine") -@patch.object(platform.Platform, "user_output") @patch.object(platform.Platform, "__init__") -def test_Platform_uninstall_dev_env_success(mock___init__: MagicMock, mock_user_output: MagicMock, +def test_Platform_uninstall_dev_env_success(mock___init__: MagicMock, mock_container_engine: MagicMock, mock_flush_descriptors: MagicMock) -> None: # Test setup @@ -418,17 +416,79 @@ def test_Platform_uninstall_dev_env_success(mock___init__: MagicMock, mock_user_ assert mock_dev_env_to_uninstall.is_installed == False - mock_user_output.msg.assert_has_calls([ - call(f"\nThe tool image [bold]test_image_name1:test_image_version1[/bold] is required by another Development Environment. It won't be deleted."), - call(f"\nThe tool image [bold]test_image_name3:test_image_version3[/bold] is required by another Development Environment. It won't be deleted."), - ]) mock_container_engine.remove.asssert_called_once_with("test_image_name4:test_image_version4") mock_flush_descriptors.assert_called_once() +@patch.object(platform.Platform, "flush_descriptors") @patch.object(platform.Platform, "container_engine") -@patch.object(platform.Platform, "user_output") @patch.object(platform.Platform, "__init__") -def test_Platform_uninstall_dev_env_failure(mock___init__: MagicMock, mock_user_output: MagicMock, +def test_Platform_uninstall_dev_env_with_duplicate_images(mock___init__: MagicMock, + mock_container_engine: MagicMock, + mock_flush_descriptors: MagicMock) -> None: + # Test setup + mock___init__.return_value = None + + test_platform = platform.Platform() + mock_dev_env1 = MagicMock() + mock_dev_env2 = MagicMock() + mock_dev_env_to_uninstall = MagicMock() + test_platform.local_dev_envs = [ + mock_dev_env1, mock_dev_env2, mock_dev_env_to_uninstall + ] + mock_dev_env1.tools = [ + { + "image_name": "test_image_name1", + "image_version": "test_image_version1" + }, + { + "image_name": "test_image_name2", + "image_version": "test_image_version2" + } + ] + mock_dev_env1.is_installed = True + mock_dev_env2.tools = [ + { + "image_name": "test_image_name3", + "image_version": "test_image_version3" + } + ] + mock_dev_env2.is_installed = True + mock_dev_env_to_uninstall.tools = [ + { + "image_name": "test_image_name1", + "image_version": "test_image_version1" + }, + { + "image_name": "test_image_name3", + "image_version": "test_image_version3" + }, + { + "image_name": "test_image_name4", + "image_version": "test_image_version4" + }, + { + "image_name": "test_image_name4", + "image_version": "test_image_version4" + } + ] + mock_dev_env_to_uninstall.is_installed = True + + mock_container_engine.remove.return_value = True + + # Run unit under test + test_platform.uninstall_dev_env(mock_dev_env_to_uninstall) + + # Check expectations + mock___init__.assert_called_once() + + assert mock_dev_env_to_uninstall.is_installed == False + + mock_container_engine.remove.asssert_called_once_with("test_image_name4:test_image_version4") + mock_flush_descriptors.assert_called_once() + +@patch.object(platform.Platform, "container_engine") +@patch.object(platform.Platform, "__init__") +def test_Platform_uninstall_dev_env_failure(mock___init__: MagicMock, mock_container_engine: MagicMock) -> None: # Test setup mock___init__.return_value = None @@ -486,10 +546,6 @@ def test_Platform_uninstall_dev_env_failure(mock___init__: MagicMock, mock_user_ assert str(exported_exception_info) == "Platform error: Dev Env uninstall failed." assert mock_dev_env_to_uninstall.is_installed == True - mock_user_output.msg.assert_has_calls([ - call(f"\nThe tool image [bold]test_image_name1:test_image_version1[/bold] is required by another Development Environment. It won't be deleted."), - call(f"\nThe tool image [bold]test_image_name3:test_image_version3[/bold] is required by another Development Environment. It won't be deleted."), - ]) mock_container_engine.remove.asssert_called_once_with("test_image_name4:test_image_version4") @patch.object(platform.Platform, "get_deserialized") @@ -616,4 +672,65 @@ def test_Platform_assign_dev_env_already_assigned(mock___init__: MagicMock, mock_exists.assert_called_once_with(f"{test_project_path}/.axem/dev_env_descriptor.json") mock_user_output.get_confirm.assert_called_once_with("[yellow]A Dev Env is already assigned to the project.[/]", "Overwrite it?") - mock_dev_env.export.assert_called_once_with(f"{test_project_path}/.axem/dev_env_descriptor.json") \ No newline at end of file + mock_dev_env.export.assert_called_once_with(f"{test_project_path}/.axem/dev_env_descriptor.json") + +@patch("dem.core.platform.DevEnv") +@patch("dem.core.platform.os.path.exists") +@patch.object(platform.Core, "user_output") +@patch.object(platform.Platform, "get_dev_env_by_name") +@patch.object(platform.Platform, "__init__") +def test_Platform_init_project(mock___init__: MagicMock, mock_get_dev_env_by_name: MagicMock, + mock_user_output: MagicMock, + mock_path_exists: MagicMock, mock_DevEnv: MagicMock) -> None: + # Test setup + mock___init__.return_value = None + + test_platform = platform.Platform() + + test_project_path = "test_project_path" + mock_assigned_dev_env = MagicMock() + mock_assigned_dev_env.name = "test_assigned_dev_env_name" + mock_existing_dev_env = MagicMock() + + mock_path_exists.return_value = True + mock_DevEnv.return_value = mock_assigned_dev_env + mock_get_dev_env_by_name.return_value = mock_existing_dev_env + + test_platform.local_dev_envs = [mock_existing_dev_env] + + # Run unit under test + test_platform.init_project(test_project_path) + + # Check expectations + assert mock_existing_dev_env not in test_platform.local_dev_envs + assert mock_assigned_dev_env in test_platform.local_dev_envs + + mock_path_exists.assert_called_once_with(f"{test_project_path}/.axem/dev_env_descriptor.json") + mock_DevEnv.assert_called_once_with(descriptor_path=f"{test_project_path}/.axem/dev_env_descriptor.json") + mock_get_dev_env_by_name.assert_called_once_with(mock_assigned_dev_env.name) + mock_user_output.get_confirm.assert_called_once_with("[yellow]This project is already initialized.[/]", + "Overwrite it?") + +@patch("dem.core.platform.DevEnv") +@patch("dem.core.platform.os.path.exists") +@patch.object(platform.Core, "user_output") +@patch.object(platform.Platform, "get_dev_env_by_name") +@patch.object(platform.Platform, "__init__") +def test_Platform_init_project_file_not_exist(mock___init__: MagicMock, mock_get_dev_env_by_name: MagicMock, + mock_user_output: MagicMock, + mock_path_exists: MagicMock, mock_DevEnv: MagicMock) -> None: + # Test setup + mock___init__.return_value = None + + test_platform = platform.Platform() + + test_project_path = "test_project_path" + mock_path_exists.return_value = False + + with pytest.raises(FileNotFoundError) as exported_exception_info: + # Run unit under test + test_platform.init_project(test_project_path) + + # Check expectations + mock_path_exists.assert_called_once_with(f"{test_project_path}/.axem/dev_env_descriptor.json") + assert str(exported_exception_info.value) == f"The {test_project_path}/.axem/dev_env_descriptor.json file does not exist." \ No newline at end of file diff --git a/tests/core/test_registry.py b/tests/core/test_registry.py index 2dda732..61fbb3c 100644 --- a/tests/core/test_registry.py +++ b/tests/core/test_registry.py @@ -49,7 +49,7 @@ def test_Registry__list_tags(mock_requests_get: MagicMock, mock__get_tag_endpoin # Check expectations mock__get_tag_endpoint_url.assert_called_once_with(test_repo) - mock_requests_get.assert_called_once_with(test_tag_endpoint_url, timeout=1) + mock_requests_get.assert_called_once_with(test_tag_endpoint_url, timeout=10) mock_response.json.assert_called_once() mock__append_repo_with_tag.assert_called_once_with(test_endpoint_response, test_repo) @@ -76,7 +76,7 @@ def test_Registry__list_tags_MissingSchema(mock_requests_get: MagicMock, # Check expectations mock__get_tag_endpoint_url.assert_called_once_with(test_repo) - mock_requests_get.assert_called_once_with(test_tag_endpoint_url, timeout=1) + mock_requests_get.assert_called_once_with(test_tag_endpoint_url, timeout=10) mock_user_output.error.assert_called_once_with(test_exception_text) mock_user_output.msg.assert_called_once_with("Skipping repository: " + test_repo) @@ -104,7 +104,7 @@ def test_Registry__list_tags_invalid_status(mock_requests_get: MagicMock, # Check expectations mock__get_tag_endpoint_url.assert_called_once_with(test_repo) - mock_requests_get.assert_called_once_with(test_tag_endpoint_url, timeout=1) + mock_requests_get.assert_called_once_with(test_tag_endpoint_url, timeout=10) mock_user_output.error.assert_called_once_with("Error in communication with the registry. Failed to retrieve tags. Response status code: " + str(mock_response.status_code)) mock_user_output.msg.assert_called_once_with("Skipping repository: " + test_repo) @@ -304,7 +304,7 @@ def test_DockerRegistry__search(mock_requests_get: MagicMock): # Check expectations assert actual_repo_names is test_response["repositories"] - mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=1) + mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=10) mock_response.json.assert_called_once() @patch.object(registry.DockerRegistry, "user_output") @@ -327,7 +327,7 @@ def test_DockerRegistry__search_requests_get_exception(mock_requests_get: MagicM # Check expectations assert actual_repo_names == [] - mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=1) + mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=10) mock_user_output.error(str(mock_requests_get.side_effect)) mock_user_output.msg("Skipping registry: " + test_registry_config["name"]) @@ -353,7 +353,7 @@ def test_DockerRegistry__search_invalid_status_code(mock_requests_get: MagicMock # Check expectations assert actual_repo_names == [] - mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=1) + mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=10) mock_user_output.error("Error in communication with the registry. Failed to retrieve the repositories. Response status code: " + str(mock_response.status_code)) mock_user_output.msg("Skipping registry: " + test_registry_config["name"]) @@ -381,7 +381,7 @@ def test_DockerRegistry__search_json_decode_exception(mock_requests_get: MagicMo # Check expectations assert actual_repo_names == [] - mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=1) + mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=10) mock_response.json.assert_called_once() mock_user_output.error("Invalid JSON format in response. " + str(mock_response.json.side_effect)) mock_user_output.msg("Skipping registry: " + test_registry_config["name"]) @@ -410,7 +410,7 @@ def test_DockerRegistry__search_json_generic_exception(mock_requests_get: MagicM # Check expectations assert actual_repo_names == [] - mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=1) + mock_requests_get.assert_called_once_with(test_registry_config["url"] + "/v2/_catalog", timeout=10) mock_response.json.assert_called_once() mock_user_output.error(str(mock_response.json.side_effect)) mock_user_output.msg("Skipping registry: " + test_registry_config["name"])