Skip to content

Commit

Permalink
Implementation of the init command. Test cases added.
Browse files Browse the repository at this point in the history
  • Loading branch information
janosmurai committed Feb 16, 2024
1 parent b9ae8e3 commit 7a14261
Show file tree
Hide file tree
Showing 15 changed files with 516 additions and 55 deletions.
7 changes: 6 additions & 1 deletion dem/cli/command/delete_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
48 changes: 48 additions & 0 deletions dem/cli/command/init_cmd.py
Original file line number Diff line number Diff line change
@@ -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.")
2 changes: 1 addition & 1 deletion dem/cli/command/uninstall_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}![/]")
15 changes: 14 additions & 1 deletion dem/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)],
Expand Down
7 changes: 3 additions & 4 deletions dem/core/container_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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")

Expand Down
31 changes: 27 additions & 4 deletions dem/core/dev_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 29 additions & 8 deletions dem/core/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
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)
4 changes: 2 additions & 2 deletions dem/core/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
29 changes: 27 additions & 2 deletions tests/cli/test_delete_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 7a14261

Please sign in to comment.