From ad6c95adffe60f854a9a2c7a09c72d0faada9fd4 Mon Sep 17 00:00:00 2001 From: janosmurai Date: Sun, 24 Mar 2024 12:41:07 +0100 Subject: [PATCH 1/2] create and modify command redesigned -> introducing the Dev Env settings window. --- dem/__main__.py | 10 +- dem/cli/command/assign_cmd.py | 5 +- dem/cli/command/clone_cmd.py | 3 + dem/cli/command/cp_cmd.py | 3 + dem/cli/command/create_cmd.py | 149 ++------ dem/cli/command/delete_cmd.py | 3 + dem/cli/command/export_cmd.py | 3 + dem/cli/command/info_cmd.py | 37 +- dem/cli/command/init_cmd.py | 3 + dem/cli/command/install_cmd.py | 4 + dem/cli/command/list_cmd.py | 25 +- dem/cli/command/load_cmd.py | 11 +- dem/cli/command/modify_cmd.py | 301 +++++++--------- dem/cli/command/rename_cmd.py | 3 + dem/cli/command/run_cmd.py | 12 +- dem/cli/command/uninstall_cmd.py | 3 + dem/cli/main.py | 6 +- dem/cli/tui/panel/tool_type_selector.py | 81 ----- dem/cli/tui/printable_tool_image.py | 22 ++ dem/cli/tui/renderable/menu.py | 171 +++------ .../dev_env_settings_window.py} | 34 +- dem/core/dev_env.py | 66 +--- dem/core/exceptions.py | 4 + dem/core/platform.py | 62 ++-- dem/core/registry.py | 1 - dem/core/tool_images.py | 128 ++++--- tests/cli/test_assign_cmd.py | 2 +- tests/cli/test_create_cmd.py | 184 ++++++---- tests/cli/test_info_cmd.py | 217 +++--------- tests/cli/test_list_cmd.py | 75 ++-- tests/cli/test_modify_cmd.py | 255 ++++++-------- tests/cli/test_run_cmd.py | 10 +- tests/cli/tui/renderable/test_menu.py | 332 +++++++++++------- tests/core/test_dev_env.py | 99 ++---- tests/core/test_platform.py | 131 ++++--- tests/core/test_tool_images.py | 127 ++++--- tests/test__main__.py | 8 - 37 files changed, 1145 insertions(+), 1445 deletions(-) delete mode 100644 dem/cli/tui/panel/tool_type_selector.py create mode 100644 dem/cli/tui/printable_tool_image.py rename dem/cli/tui/{panel/tool_image_selector.py => window/dev_env_settings_window.py} (58%) diff --git a/dem/__main__.py b/dem/__main__.py index aa475043..f2dfaeec 100644 --- a/dem/__main__.py +++ b/dem/__main__.py @@ -3,7 +3,8 @@ from dem import __command__ from dem.cli.console import stderr, stdout -from dem.core.exceptions import RegistryError, ContainerEngineError, InternalError, DataStorageError +from dem.core.exceptions import RegistryError, ContainerEngineError, InternalError, DataStorageError, \ + CatalogError, ToolImageError import dem.cli.main from dem.core.core import Core from dem.core.platform import Platform @@ -24,9 +25,6 @@ def main() -> None: # Load the configuration file dem.cli.main.platform.config_file.update() - # Load the Dev Envs - dem.cli.main.platform.load_dev_envs() - # Run the CLI application dem.cli.main.typer_cli(prog_name=__command__) except LookupError as e: @@ -42,7 +40,7 @@ def main() -> None: stdout.print("\nHint: The input repository might not exist in the registry.") elif "400" in str(e): stdout.print("\nHint: The input parameters might not be valid.") - except (ContainerEngineError, InternalError) as e: + except (ContainerEngineError, InternalError, ToolImageError) as e: stderr.print("[red]" + str(e) + "[/]") except DataStorageError as e: stderr.print("[red]" + str(e) + "[/]") @@ -53,6 +51,8 @@ def main() -> None: elif "dev_env.json" in str(e): stdout.print("Restoring the original Dev Env descriptor file...") dem.cli.main.platform.dev_env_json.restore() + except CatalogError as e: + stderr.print(f"[red]{str(e)}[/]") # Call the main() when run as `python -m` if __name__ == "__main__": diff --git a/dem/cli/command/assign_cmd.py b/dem/cli/command/assign_cmd.py index 974893ad..c6c4d409 100644 --- a/dem/cli/command/assign_cmd.py +++ b/dem/cli/command/assign_cmd.py @@ -14,6 +14,9 @@ def execute(platform: Platform, dev_env_name: str, project_path: str) -> None: dev_env_name -- the name of the Development Environment to assign project_path -- the path to the project to assign the Development Environment to """ + # Load the Dev Envs + platform.load_dev_envs() + if not os.path.isdir(project_path): stderr.print(f"[red]Error: The {project_path} path does not exist.[/]") return @@ -24,4 +27,4 @@ def execute(platform: Platform, dev_env_name: str, project_path: str) -> None: stderr.print(f"[red]Error: The {dev_env_name} Development Environment does not exist.[/]") else: platform.assign_dev_env(dev_env_to_assign, project_path) - stdout.print(f"\n[green]Successfully assigned the {dev_env_name} Dev Env to the project at{project_path}![/]") \ No newline at end of file + stdout.print(f"\n[green]Successfully assigned the {dev_env_name} Dev Env to the project at {project_path}![/]") \ No newline at end of file diff --git a/dem/cli/command/clone_cmd.py b/dem/cli/command/clone_cmd.py index 4e6ee7be..1678984a 100644 --- a/dem/cli/command/clone_cmd.py +++ b/dem/cli/command/clone_cmd.py @@ -37,6 +37,9 @@ def execute(platform: Platform, dev_env_name: str) -> None: platform -- the platform dev_env_name -- name of the Dev Env to clone """ + # Load the Dev Envs + platform.load_dev_envs() + catalog_dev_env: DevEnv | None = None if not platform.dev_env_catalogs.catalogs: diff --git a/dem/cli/command/cp_cmd.py b/dem/cli/command/cp_cmd.py index 90bbbbb0..b99655a3 100644 --- a/dem/cli/command/cp_cmd.py +++ b/dem/cli/command/cp_cmd.py @@ -31,6 +31,9 @@ def cp_given_dev_env(platform: Platform, dev_env_to_cp: DevEnv, platform.flush_descriptors() def execute(platform: Platform, dev_env_to_cp_name: str, new_dev_env_name: str) -> None: + # Load the Dev Envs + platform.load_dev_envs() + dev_env_to_cp = get_dev_env_to_cp(platform, dev_env_to_cp_name) if (dev_env_to_cp is not None and diff --git a/dem/cli/command/create_cmd.py b/dem/cli/command/create_cmd.py index f3a4debe..9ca0e8e3 100644 --- a/dem/cli/command/create_cmd.py +++ b/dem/cli/command/create_cmd.py @@ -2,137 +2,55 @@ # dem/cli/command/create_cmd.py import typer -from dem.core.dev_env import DevEnv, DevEnv -from dem.core.tool_images import ToolImages +from dem.core.dev_env import DevEnv +from dem.core.tool_images import ToolImage from dem.core.platform import Platform from dem.core.exceptions import PlatformError from dem.cli.console import stdout, stderr -from dem.cli.tui.panel.tool_type_selector import ToolTypeSelectorPanel -from dem.cli.tui.panel.tool_image_selector import ToolImageSelectorPanel +from dem.cli.tui.window.dev_env_settings_window import DevEnvSettingsWindow +from dem.cli.tui.printable_tool_image import convert_to_printable_tool_images -tool_image_statuses = { - ToolImages.LOCAL_ONLY: "local", - ToolImages.REGISTRY_ONLY: "registry", - ToolImages.LOCAL_AND_REGISTRY: "local and registry" -} +def open_dev_env_settings_panel(all_tool_images: dict[str, ToolImage]) -> list[str]: + """ Open the Development Environment settings panel. + + Args: + all_tool_images -- the Tool Images -def get_tool_image_list(tool_images: ToolImages) -> list[list[str]]: - """ - Combine the registry and local tool images, and assign the availabilities. - - Args: - tool_images -- all the tool images + Returns: + the selected Tool Image names """ - tool_image_list = [] - - for tool_image in tool_images.registry.elements: - if tool_image in tool_images.local.elements: - tool_image_list.append([tool_image, tool_image_statuses[ToolImages.LOCAL_AND_REGISTRY]]) - else: - tool_image_list.append([tool_image, tool_image_statuses[ToolImages.REGISTRY_ONLY]]) - - for tool_image in tool_images.local.elements: - if tool_image not in tool_images.registry.elements: - tool_image_list.append([tool_image, tool_image_statuses[ToolImages.LOCAL_ONLY]]) + dev_env_settings_panel = DevEnvSettingsWindow(convert_to_printable_tool_images(all_tool_images)) + dev_env_settings_panel.wait_for_user() - return tool_image_list - -def handle_tool_type_selector_panel(tool_type_selector_panel: ToolTypeSelectorPanel, - dev_env_name: str) -> list[str]: - tool_type_selector_panel.tool_type_menu.set_title("What kind of tools would you like to include in [cyan]" + dev_env_name + "[/]?") - - tool_type_selector_panel.wait_for_user() - - if "cancel" in tool_type_selector_panel.cancel_next_menu.get_selection(): + if "cancel" in dev_env_settings_panel.cancel_save_menu.get_selection(): raise typer.Abort() - tool_type_selector_panel.cancel_next_menu.is_selected = False - - return tool_type_selector_panel.tool_type_menu.get_selected_tool_types() - -def handle_tool_image_selector_panel(tool_image_selector_panel: ToolImageSelectorPanel, - tool_type:str) -> str | None: - tool_image_selector_panel.tool_image_menu.set_title("Select tool image for: [yellow]" + tool_type + "[/]") - tool_image_selector_panel.wait_for_user() - - if tool_image_selector_panel.back_menu.is_selected is True: - # Reset the back menu selection - tool_image_selector_panel.back_menu.is_selected = False - return None - else: - tool_image_selector_panel.tool_image_menu.is_selected = False - return tool_image_selector_panel.tool_image_menu.get_selected_tool_image() - -def get_dev_env_descriptor_from_user(dev_env_name: str, tool_image_list: list[list[str]]) -> dict: - current_panel = ToolTypeSelectorPanel(list(DevEnv.supported_tool_types)) - panel_list = [current_panel] - - tool_index = 0 - panel_index = 0 - tool_selection = {} - while current_panel is not None: - if isinstance(current_panel, ToolTypeSelectorPanel): - selected_tool_types = handle_tool_type_selector_panel(current_panel, dev_env_name) - - # Remove the not selected tool type from the tool_selection. - for tool_type in list(tool_selection.keys()): - if tool_type not in selected_tool_types: - del tool_selection[tool_type] - - if len(panel_list) > 1: - current_panel = panel_list[1] - current_panel.dev_env_status.reset_table(selected_tool_types) - else: - current_panel = ToolImageSelectorPanel(tool_image_list, selected_tool_types) - panel_list.append(current_panel) - - tool_index = 0 - panel_index = 1 - else: - selected_tool_image = handle_tool_image_selector_panel(current_panel, selected_tool_types[tool_index]) - - if selected_tool_image is None: - tool_selection[selected_tool_types[tool_index]] = "" - - panel_index -= 1 - current_panel = panel_list[panel_index] - - if tool_index != 0: - tool_index -= 1 - else: - tool_selection[selected_tool_types[tool_index]] = selected_tool_image - - tool_index += 1 - - if tool_index == len(selected_tool_types): - break - - panel_index += 1 - if len(panel_list) > panel_index: - current_panel = panel_list[panel_index] - else: - current_panel = ToolImageSelectorPanel(tool_image_list, selected_tool_types) - panel_list.append(current_panel) - - current_panel.dev_env_status.reset_table(selected_tool_types) + return dev_env_settings_panel.selected_tool_images - if isinstance(current_panel, ToolImageSelectorPanel): - current_panel.dev_env_status.set_tool_image(tool_selection) +def create_new_dev_env_descriptor(dev_env_name: str, selected_tool_images: list[str]) -> dict: + """ Create a new Development Environment descriptor. + + Args: + dev_env_name -- the name of the Development Environment + selected_tool_images -- the selected Tool Images + Returns: + the descriptor of the new Development Environment + """ dev_env_descriptor = { "name": dev_env_name, "tools": [] } - for tool_type, tool_image in tool_selection.items(): + for tool_image in selected_tool_images: if "/" in tool_image: registry, image = tool_image.split("/") image_name = registry + '/' + image.split(":")[0] else: image = tool_image image_name = image.split(":")[0] + tool_descriptor = { - "type": tool_type, "image_name": image_name, "image_version": image.split(":")[1] } @@ -147,8 +65,9 @@ def create_new_dev_env(platform: Platform, new_dev_env_descriptor: dict) -> None platform -- the platform new_dev_env_descriptor -- the descriptor of the new Development Environment """ - new_dev_env = DevEnv(new_dev_env_descriptor) - platform.local_dev_envs.append(new_dev_env) + dev_env = DevEnv(descriptor=new_dev_env_descriptor) + dev_env.assign_tool_image_instances(platform.tool_images) + platform.local_dev_envs.append(dev_env) def create_dev_env(platform: Platform, dev_env_name: str) -> None: """ Create a new Development Environment or overwrite an existing one. @@ -178,11 +97,11 @@ def create_dev_env(platform: Platform, dev_env_name: str) -> None: stderr.print(f"[red]{str(e)}[/]") raise typer.Abort() - tool_image_list = get_tool_image_list(platform.tool_images) - new_dev_env_descriptor = get_dev_env_descriptor_from_user(dev_env_name, tool_image_list) + selected_tool_images = open_dev_env_settings_panel(platform.tool_images.all_tool_images) + new_dev_env_descriptor = create_new_dev_env_descriptor(dev_env_name, selected_tool_images) if dev_env_original is not None: - dev_env_original.tools = new_dev_env_descriptor["tools"] + dev_env_original.tool_image_descriptors = new_dev_env_descriptor["tools"] else: create_new_dev_env(platform, new_dev_env_descriptor) @@ -196,6 +115,10 @@ def execute(platform: Platform, dev_env_name: str) -> None: Exceptions: Abort -- if the name of the Development Environment contains whitespace characters """ + # Load the Dev Envs + platform.load_dev_envs() + platform.assign_tool_image_instances_to_all_dev_envs() + create_dev_env(platform, dev_env_name) platform.flush_descriptors() stdout.print(f"The [green]{dev_env_name}[/] Development Environment has been created!") diff --git a/dem/cli/command/delete_cmd.py b/dem/cli/command/delete_cmd.py index 3b468794..40ccf65e 100644 --- a/dem/cli/command/delete_cmd.py +++ b/dem/cli/command/delete_cmd.py @@ -8,6 +8,9 @@ import typer def execute(platform: Platform, dev_env_name: str) -> None: + # Load the Dev Envs + platform.load_dev_envs() + dev_env_to_delete: DevEnv | None = platform.get_dev_env_by_name(dev_env_name) if dev_env_to_delete is None: diff --git a/dem/cli/command/export_cmd.py b/dem/cli/command/export_cmd.py index 4de0f8d7..aa5c968a 100644 --- a/dem/cli/command/export_cmd.py +++ b/dem/cli/command/export_cmd.py @@ -19,6 +19,9 @@ def export(dev_env: DevEnv, export_path: str): dev_env.export(export_path) def execute(platform: Platform, dev_env_name: str, export_path: str) -> None: + # Load the Dev Envs + platform.load_dev_envs() + dev_env = platform.get_dev_env_by_name(dev_env_name) if dev_env: try: diff --git a/dem/cli/command/info_cmd.py b/dem/cli/command/info_cmd.py index d03ac2b0..6cf85220 100644 --- a/dem/cli/command/info_cmd.py +++ b/dem/cli/command/info_cmd.py @@ -1,43 +1,44 @@ """info CLI command implementation.""" # dem/cli/command/info_cmd.py -from dem.core.tool_images import ToolImages +from dem.core.tool_images import ToolImage from dem.core.dev_env import DevEnv, DevEnv from dem.cli.console import stdout, stderr from dem.core.platform import Platform from rich.table import Table image_status_messages = { - ToolImages.NOT_AVAILABLE: "[red]Error: Required image is not available![/]", - ToolImages.LOCAL_ONLY: "Image is available locally.", - ToolImages.REGISTRY_ONLY: "Image is available in the registry.", - ToolImages.LOCAL_AND_REGISTRY: "Image is available locally and in the registry.", + ToolImage.NOT_AVAILABLE: "[red]Error: Required image is not available![/]", + ToolImage.LOCAL_ONLY: "Image is available locally.", + ToolImage.REGISTRY_ONLY: "Image is available in the registry.", + ToolImage.LOCAL_AND_REGISTRY: "Image is available locally and in the registry.", } -def print_info(dev_env: (DevEnv | DevEnv)) -> None: - +def print_info(dev_env: DevEnv) -> None: tool_info_table = Table() - tool_info_table.add_column("Type") tool_info_table.add_column("Image") tool_info_table.add_column("Status") - for tool in dev_env.tools: - tool_info_table.add_row(tool["type"], tool["image_name"] + ':' + tool["image_version"], - image_status_messages[tool["image_status"]]) + + for tool_image in dev_env.tool_images: + tool_info_table.add_row(tool_image.name, + image_status_messages[tool_image.availability]) stdout.print(tool_info_table) def execute(platform: Platform, arg_dev_env_name: str) -> None: + # Load the Dev Envs + platform.load_dev_envs() + platform.assign_tool_image_instances_to_all_dev_envs() + dev_env = platform.get_dev_env_by_name(arg_dev_env_name) if dev_env is None: for catalog in platform.dev_env_catalogs.catalogs: catalog.request_dev_envs() dev_env = catalog.get_dev_env_by_name(arg_dev_env_name) - if dev_env is not None: - dev_env.check_image_availability(platform.tool_images) - print_info(dev_env) - else: - dev_env.check_image_availability(platform.tool_images) - print_info(dev_env) + dev_env.assign_tool_image_instances(platform.tool_images) + break if dev_env is None: - stderr.print("[red]Error: Unknown Development Environment: " + arg_dev_env_name + "[/]") \ No newline at end of file + stderr.print(f"[red]Error: Unknown Development Environment: {arg_dev_env_name}[/]\n") + else: + print_info(dev_env) \ No newline at end of file diff --git a/dem/cli/command/init_cmd.py b/dem/cli/command/init_cmd.py index 2734ff5d..b36482b2 100644 --- a/dem/cli/command/init_cmd.py +++ b/dem/cli/command/init_cmd.py @@ -14,6 +14,9 @@ def execute(platform: Platform, project_path: str) -> None: platform -- the platform project_path -- the path to the project to initialize """ + # Load the Dev Envs + platform.load_dev_envs() + if not os.path.isdir(project_path): stderr.print(f"[red]Error: The {project_path} path does not exist.[/]") return diff --git a/dem/cli/command/install_cmd.py b/dem/cli/command/install_cmd.py index 5f9e0b5d..402d8519 100644 --- a/dem/cli/command/install_cmd.py +++ b/dem/cli/command/install_cmd.py @@ -13,6 +13,10 @@ def execute(platform: Platform, dev_env_name: str) -> None: platform -- the platform dev_env_name -- the name of the Development Environment to install """ + # Load the Dev Envs + platform.load_dev_envs() + platform.assign_tool_image_instances_to_all_dev_envs() + dev_env_to_install: DevEnv | None = platform.get_dev_env_by_name(dev_env_name) if dev_env_to_install is None: diff --git a/dem/cli/command/list_cmd.py b/dem/cli/command/list_cmd.py index 613222fa..a0802419 100644 --- a/dem/cli/command/list_cmd.py +++ b/dem/cli/command/list_cmd.py @@ -3,7 +3,7 @@ from dem.core.platform import Platform from dem.core.dev_env import DevEnv -from dem.core.tool_images import ToolImages +from dem.core.tool_images import ToolImage from dem.cli.console import stdout, stderr from rich.table import Table @@ -34,10 +34,13 @@ } def get_catalog_dev_env_status(platform: Platform, dev_env: DevEnv) -> str: - image_statuses = dev_env.check_image_availability(platform.tool_images) - if (ToolImages.NOT_AVAILABLE in image_statuses) or (ToolImages.LOCAL_ONLY in image_statuses): + image_statuses = [] + for tool_image in dev_env.tool_images: + image_statuses.append(tool_image.availability) + + if (ToolImage.NOT_AVAILABLE in image_statuses) or (ToolImage.LOCAL_ONLY in image_statuses): dev_env_status = dev_env_org_status_messages[DEV_ENV_ORG_NOT_IN_REGISTRY] - elif (image_statuses.count(ToolImages.LOCAL_AND_REGISTRY) == len(image_statuses)) and \ + elif (image_statuses.count(ToolImage.LOCAL_AND_REGISTRY) == len(image_statuses)) and \ platform.get_dev_env_by_name(dev_env.name): dev_env_status = dev_env_org_status_messages[DEV_ENV_ORG_INSTALLED_LOCALLY] else: @@ -47,17 +50,23 @@ def get_catalog_dev_env_status(platform: Platform, dev_env: DevEnv) -> str: dev_env_status = dev_env_org_status_messages[DEV_ENV_ORG_READY] return dev_env_status -def get_local_dev_env_status(dev_env: DevEnv, tool_images: ToolImages) -> str: - image_statuses = dev_env.check_image_availability(tool_images) - if (ToolImages.NOT_AVAILABLE in image_statuses): +def get_local_dev_env_status(dev_env: DevEnv, tool_images: ToolImage) -> str: + image_statuses = [] + for tool_image in dev_env.tool_images: + image_statuses.append(tool_image.availability) + + if (ToolImage.NOT_AVAILABLE in image_statuses): dev_env_status = dev_env_local_status_messages[DEV_ENV_LOCAL_NOT_AVAILABLE] - elif (ToolImages.REGISTRY_ONLY in image_statuses): + elif (ToolImage.REGISTRY_ONLY in image_statuses): dev_env_status = dev_env_local_status_messages[DEV_ENV_LOCAL_REINSTALL] else: dev_env_status = dev_env_local_status_messages[DEV_ENV_LOCAL_INSTALLED] return dev_env_status def list_dev_envs(platform: Platform, local: bool, org: bool)-> None: + # Load the Dev Envs + platform.load_dev_envs() + table = Table() table.add_column("Development Environment") table.add_column("Status") diff --git a/dem/cli/command/load_cmd.py b/dem/cli/command/load_cmd.py index 9f1119e4..305a8a25 100644 --- a/dem/cli/command/load_cmd.py +++ b/dem/cli/command/load_cmd.py @@ -12,18 +12,18 @@ def check_is_file_exist(param: str | None) -> bool: else: return False -def load_dev_env_to_dev_env_json(dev_env_local_setup: Platform,path_to_dev_env: str) -> bool: +def load_dev_env_to_dev_env_json(platform: Platform,path_to_dev_env: str) -> bool: raw_file = open(path_to_dev_env, "r") try: dev_env = json.load(raw_file) - if dev_env_local_setup.get_dev_env_by_name(dev_env["name"]) is not None: + if platform.get_dev_env_by_name(dev_env["name"]) is not None: stderr.print("[red]Error: The Development Environment exist.[/]") return False else: - new_dev_env = DevEnv(dev_env) - dev_env_local_setup.local_dev_envs.append(new_dev_env) + new_dev_env: DevEnv = DevEnv(dev_env) + platform.local_dev_envs.append(new_dev_env) except json.decoder.JSONDecodeError: stderr.print("[red]Error: invalid json format.[/]") return False @@ -32,6 +32,9 @@ def load_dev_env_to_dev_env_json(dev_env_local_setup: Platform,path_to_dev_env: return True def execute(platform: Platform, path_to_dev_env: str) -> None: + # Load the Dev Envs + platform.load_dev_envs() + if check_is_file_exist(path_to_dev_env) is True: retval = load_dev_env_to_dev_env_json(platform,path_to_dev_env) if retval == True: diff --git a/dem/cli/command/modify_cmd.py b/dem/cli/command/modify_cmd.py index 0e56ea08..ac524f42 100644 --- a/dem/cli/command/modify_cmd.py +++ b/dem/cli/command/modify_cmd.py @@ -3,146 +3,20 @@ import copy, typer from dem.core.dev_env import DevEnv, DevEnv -from dem.core.tool_images import ToolImages +from dem.core.tool_images import ToolImage from dem.core.platform import Platform from dem.core.exceptions import PlatformError from dem.cli.console import stderr, stdout from dem.cli.tui.renderable.menu import SelectMenu -from dem.cli.tui.panel.tool_type_selector import ToolTypeSelectorPanel -from dem.cli.tui.panel.tool_image_selector import ToolImageSelectorPanel - -tool_image_statuses = { - ToolImages.LOCAL_ONLY: "local", - ToolImages.REGISTRY_ONLY: "registry", - ToolImages.LOCAL_AND_REGISTRY: "local and registry" -} - -def get_tool_image_list(tool_images: ToolImages) -> list[list[str]]: - """ - Combine the registry and local tool images, and assign the availabilities. - - Args: - tool_images -- all the tool images - """ - tool_image_list = [] - - for tool_image in tool_images.registry.elements: - if tool_image in tool_images.local.elements: - tool_image_list.append([tool_image, tool_image_statuses[ToolImages.LOCAL_AND_REGISTRY]]) - else: - tool_image_list.append([tool_image, tool_image_statuses[ToolImages.REGISTRY_ONLY]]) - - for tool_image in tool_images.local.elements: - if tool_image not in tool_images.registry.elements: - tool_image_list.append([tool_image, tool_image_statuses[ToolImages.LOCAL_ONLY]]) - - return tool_image_list - -def handle_tool_type_selector_panel(tool_type_selector_panel: ToolTypeSelectorPanel, - dev_env_name: str) -> list[str]: - tool_type_selector_panel.tool_type_menu.set_title("What kind of tools would you like to include in [cyan]" + dev_env_name + "[/]?") - - tool_type_selector_panel.wait_for_user() - - if "cancel" in tool_type_selector_panel.cancel_next_menu.get_selection(): - raise typer.Abort() - - tool_type_selector_panel.cancel_next_menu.is_selected = False - - return tool_type_selector_panel.tool_type_menu.get_selected_tool_types() - -def handle_tool_image_selector_panel(tool_image_selector_panel: ToolImageSelectorPanel, - tool_type:str) -> str | None: - tool_image_selector_panel.tool_image_menu.set_title("Select tool image for type " + tool_type) - tool_image_selector_panel.wait_for_user() - - if tool_image_selector_panel.back_menu.is_selected is True: - # Reset the back menu selection - tool_image_selector_panel.back_menu.is_selected = False - return None - else: - tool_image_selector_panel.tool_image_menu.is_selected = False - return tool_image_selector_panel.tool_image_menu.get_selected_tool_image() - -def get_modifications_from_user(dev_env: DevEnv, tool_image_list: list[list[str]]) -> None: - already_selected_tool_types = [] - tool_selection = {} - for tool in dev_env.tools: - already_selected_tool_types.append(tool["type"]) - tool_selection[tool["type"]] = tool["image_name"] + ":" + tool["image_version"] - - current_panel = ToolTypeSelectorPanel(list(DevEnv.supported_tool_types)) - current_panel.tool_type_menu.preset_selection(already_selected_tool_types) - panel_list = [current_panel] - - tool_index = 0 - panel_index = 0 - while current_panel is not None: - if isinstance(current_panel, ToolTypeSelectorPanel): - selected_tool_types = handle_tool_type_selector_panel(current_panel, dev_env.name) - - # Remove the not selected tool type from the tool_selection. - for tool_type in list(tool_selection.keys()): - if tool_type not in selected_tool_types: - del tool_selection[tool_type] - - if len(panel_list) > 1: - current_panel = panel_list[1] - current_panel.dev_env_status.reset_table(selected_tool_types) - else: - current_panel = ToolImageSelectorPanel(tool_image_list, selected_tool_types) - current_panel.dev_env_status.set_tool_image(tool_selection) - panel_list.append(current_panel) - - tool_index = 0 - panel_index = 1 - else: - selected_tool_image = handle_tool_image_selector_panel(current_panel, selected_tool_types[tool_index]) - - if selected_tool_image is None: - tool_selection[selected_tool_types[tool_index]] = "" - - panel_index -= 1 - current_panel = panel_list[panel_index] - - if tool_index != 0: - tool_index -= 1 - else: - tool_selection[selected_tool_types[tool_index]] = selected_tool_image - - tool_index += 1 - - if tool_index == len(selected_tool_types): - break - - panel_index += 1 - if len(panel_list) > panel_index: - current_panel = panel_list[panel_index] - else: - current_panel = ToolImageSelectorPanel(tool_image_list, selected_tool_types) - panel_list.append(current_panel) - - current_panel.dev_env_status.reset_table(selected_tool_types) - - if isinstance(current_panel, ToolImageSelectorPanel): - current_panel.dev_env_status.set_tool_image(tool_selection) - - dev_env.tools = [] - for tool_type, tool_image in tool_selection.items(): - if "/" in tool_image: - registry, image = tool_image.split("/") - image_name = registry + '/' + image.split(":")[0] - else: - image = tool_image - image_name = image.split(":")[0] - tool_descriptor = { - "type": tool_type, - "image_name": image_name, - "image_version": image.split(":")[1] - } - dev_env.tools.append(tool_descriptor) +from dem.cli.tui.window.dev_env_settings_window import DevEnvSettingsWindow +from dem.cli.tui.printable_tool_image import PrintableToolImage, convert_to_printable_tool_images def get_confirm_from_user() -> str: + """ Get the confirmation from the user. + + Returns: + the confirmation option + """ confirm_menu_items = ["confirm", "save as", "cancel"] select_menu = SelectMenu(confirm_menu_items) select_menu.set_title("Are you sure to overwrite the Development Environment?") @@ -150,6 +24,17 @@ def get_confirm_from_user() -> str: return select_menu.get_selected() def handle_user_confirm(confirmation: str, dev_env_local: DevEnv, platform: Platform) -> None: + """ Handle the user confirmation. + + Args: + confirmation -- the confirmation option + dev_env_local -- the local Development Environment + platform -- the platform + + Exceptions: + typer.Abort -- when the user tries to save as the Dev Env with a taken name or cancels + the operation + """ if confirmation == "cancel": raise typer.Abort() @@ -159,7 +44,7 @@ def handle_user_confirm(confirmation: str, dev_env_local: DevEnv, platform: Plat check_for_new_dev_env = platform.get_dev_env_by_name(new_dev_env.name) - if check_for_new_dev_env is None: + if check_for_new_dev_env is None: platform.local_dev_envs.append(new_dev_env) else: stderr.print("[red]The Development Environment already exist.") @@ -167,39 +52,123 @@ def handle_user_confirm(confirmation: str, dev_env_local: DevEnv, platform: Plat platform.flush_descriptors() -def modify_single_tool(platform: Platform, dev_env: DevEnv, tool_type: str, tool_image: str) -> None: - if tool_type and tool_image: - if tool_image not in platform.tool_images.local.elements: - if tool_image in platform.tool_images.registry.elements: - platform.container_engine.pull(tool_image) - else: - stderr.print(f"[red]Error: The {tool_image} is not an available image.[/]") - return - - for tool in dev_env.tools: - if tool["type"] == tool_type: - tool["image_name"] = tool_image.split(":")[0] - tool["image_version"] = tool_image.split(":")[1] - break +def get_already_selected_tool_images(dev_env: DevEnv) -> set[str]: + """ Get the already selected Tool Images. + + Args: + dev_env -- the Development Environment + + Returns: + the already selected Tool Images + """ + already_selected_tool_images = [] + for tool in dev_env.tool_image_descriptors: + already_selected_tool_images.append(tool["image_name"] + ":" + tool["image_version"]) + + return already_selected_tool_images + +def remove_missing_tool_images(all_tool_images: dict[str, ToolImage], + already_selected_tool_images: list[str]) -> None: + """ Remove the missing Tool Images from the Development Environment. + + Args: + all_tool_images -- all available Tool Images + already_selected_tool_images -- the already selected Tool Images + + Exceptions: + typer.Abort -- if the user doesn't want the removal of the missing Tool Images + """ + tool_images_are_missing = False + + for already_selected_tool_image in already_selected_tool_images: + try: + all_tool_images[already_selected_tool_image] + except KeyError: + stderr.print(f"[red]The {already_selected_tool_image} is not available anymore.[/]") + already_selected_tool_images.remove(already_selected_tool_image) + tool_images_are_missing = True + + if tool_images_are_missing: + typer.confirm("By continuing, the missing tool images will be removed from the Development Environment.", + abort=True) + +def open_dev_env_settings_panel(already_selected_tool_images: list[str], + printable_tool_images: list[PrintableToolImage]) -> list[str]: + """ Open the Development Environment settings panel. + + Args: + already_selected_tool_images -- the already selected Tool Images + printable_tool_images -- the printable Tool Images + + Returns: + the selected Tool Images + + Exceptions: + typer.Abort -- if the user cancels the operation + """ + dev_env_settings_panel = DevEnvSettingsWindow(printable_tool_images, already_selected_tool_images) + dev_env_settings_panel.wait_for_user() + + if "cancel" in dev_env_settings_panel.cancel_save_menu.get_selection(): + raise typer.Abort() + + return dev_env_settings_panel.selected_tool_images + +def update_dev_env(dev_env: DevEnv, selected_tool_images: list[str]) -> None: + """ Update the Development Environment. + + Args: + dev_env -- the Development Environment + selected_tool_images -- the selected Tool Images + """ + dev_env.tool_image_descriptors = [] + + for tool_image in selected_tool_images: + if "/" in tool_image: + registry, image = tool_image.split("/") + image_name = registry + '/' + image.split(":")[0] else: - dev_env.tools.append({ - "type": tool_type, - "image_name": tool_image.split(":")[0], - "image_version": tool_image.split(":")[1] - }) - - platform.flush_descriptors() - else: - stderr.print("[red]Error: The tool type and the tool image must be set together.[/]") - return + image = tool_image + image_name = image.split(":")[0] -def open_modify_panel(platform: Platform, dev_env: DevEnv) -> None: - tool_image_list: list[list[str]] = get_tool_image_list(platform.tool_images) - get_modifications_from_user(dev_env, tool_image_list) + dev_env.tool_image_descriptors.append({ + "image_name": image_name, + "image_version": image.split(":")[1] + }) + +def modify_with_tui(platform: Platform, dev_env: DevEnv) -> None: + """ Modify the Dev Env with the TUI. + + Args: + platform -- the platform + dev_env -- the Development Environment + + Exceptions: + typer.Abort -- if the user cancels the operation + """ + already_selected_tool_images = get_already_selected_tool_images(dev_env) + remove_missing_tool_images(platform.tool_images.all_tool_images, already_selected_tool_images) + printable_tool_images = convert_to_printable_tool_images(platform.tool_images.all_tool_images) + selected_tool_images = open_dev_env_settings_panel(already_selected_tool_images, + printable_tool_images) + update_dev_env(dev_env, selected_tool_images) confirmation = get_confirm_from_user() handle_user_confirm(confirmation, dev_env, platform) -def execute(platform: Platform, dev_env_name: str, tool_type: str, tool_image: str) -> None: +def execute(platform: Platform, dev_env_name: str) -> None: + """ Modify the Development Environment. + + Args: + platform -- the platform + dev_env_name -- the name of the Development Environment + + Exceptions: + typer.Abort -- if the user cancels the operation + """ + # Load the Dev Envs + platform.load_dev_envs() + platform.assign_tool_image_instances_to_all_dev_envs() + dev_env = platform.get_dev_env_by_name(dev_env_name) if dev_env is None: @@ -214,7 +183,5 @@ def execute(platform: Platform, dev_env_name: str, tool_type: str, tool_image: s stderr.print(f"[red]{str(e)}[/]") return - if (tool_type or tool_image): - modify_single_tool(platform, dev_env, tool_type, tool_image) - else: - open_modify_panel(platform, dev_env) \ No newline at end of file + modify_with_tui(platform, dev_env) + stdout.print("[green]The Development Environment has been modified successfully![/]") \ No newline at end of file diff --git a/dem/cli/command/rename_cmd.py b/dem/cli/command/rename_cmd.py index 824b5eb2..2005cd73 100644 --- a/dem/cli/command/rename_cmd.py +++ b/dem/cli/command/rename_cmd.py @@ -5,6 +5,9 @@ from dem.cli.console import stderr def execute(platform: Platform, dev_env_name_to_rename: str, new_dev_env_name: str) -> None: + # Load the Dev Envs + platform.load_dev_envs() + dev_env_to_rename = platform.get_dev_env_by_name(dev_env_name_to_rename) if dev_env_to_rename is not None: diff --git a/dem/cli/command/run_cmd.py b/dem/cli/command/run_cmd.py index b8760936..60d3531d 100644 --- a/dem/cli/command/run_cmd.py +++ b/dem/cli/command/run_cmd.py @@ -32,23 +32,23 @@ def execute(platform: Platform, dev_env_name: str, container_arguments: list[str dev_env_name -- name of the Development Environment container_arguments -- arguments passed to the container """ - + # Load the Dev Envs + platform.load_dev_envs() + dev_env_local = platform.get_dev_env_by_name(dev_env_name) if dev_env_local is None: stderr.print("[red]Error: Unknown Development Environment: " + dev_env_name + "[/]") raise typer.Abort() else: - # Update the tool images manually. - Platform.update_tool_images_on_instantiation = False # Only the local images are needed. - platform.tool_images.local.update() + Platform.local_only = True missing_tool_images = set() - for tool in dev_env_local.tools: + for tool in dev_env_local.tool_image_descriptors: tool_image = tool["image_name"] + ":" + tool["image_version"] # Check if the required tool image exists locally. - if tool_image not in platform.tool_images.local.elements: + if tool_image not in platform.tool_images.get_local_ones().keys(): missing_tool_images.add(tool_image) if missing_tool_images: diff --git a/dem/cli/command/uninstall_cmd.py b/dem/cli/command/uninstall_cmd.py index e19451ff..dbde25cf 100644 --- a/dem/cli/command/uninstall_cmd.py +++ b/dem/cli/command/uninstall_cmd.py @@ -13,6 +13,9 @@ def execute(platform: Platform, dev_env_name: str) -> None: platform -- the platform dev_env_name -- the name of the Development Environment to uninstall """ + # Load the Dev Envs + platform.load_dev_envs() + dev_env_to_uninstall: DevEnv | None = platform.get_dev_env_by_name(dev_env_name) if dev_env_to_uninstall is None: diff --git a/dem/cli/main.py b/dem/cli/main.py index 583c64d7..a0939805 100644 --- a/dem/cli/main.py +++ b/dem/cli/main.py @@ -180,16 +180,14 @@ def rename(dev_env_name: Annotated[str, typer.Argument(help="Name of the Develop @typer_cli.command() def modify(dev_env_name: Annotated[str, typer.Argument(help="Name of the Development Environment to modify.", - autocompletion=autocomplete_dev_env_name)], - tool_type: Annotated[str, typer.Argument(help="The tool type to change.")] = "", - tool_image: Annotated[str, typer.Argument(help="The tool image to set for the tool type.")] = "") -> None: + autocompletion=autocomplete_dev_env_name)]) -> None: """ Change a tool in a Development Environment. If the tool type is not specified, the Dev Env settings panel will be opened. """ if platform: - modify_cmd.execute(platform, dev_env_name, tool_type, tool_image) + modify_cmd.execute(platform, dev_env_name) else: raise InternalError("Error: The platform hasn't been initialized properly!") diff --git a/dem/cli/tui/panel/tool_type_selector.py b/dem/cli/tui/panel/tool_type_selector.py deleted file mode 100644 index 4b4da8f3..00000000 --- a/dem/cli/tui/panel/tool_type_selector.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Tool selector panel.""" -# dem/cli/tui/panel/tool_selector.py - -from dem.cli.tui.renderable.menu import ToolTypeMenu, CancelNextMenu -from rich.panel import Panel -from rich.layout import Layout -from rich.console import RenderableType, Group -from rich.live import Live -from rich.align import Align -from readchar import readkey, key - -class NavigationHint(): - hint_test = """ -- [bold]move cursor[/]: arrows or vi mode -- [bold]select[/]: space or enter -- [bold]jump to next/cancel[/]: tab -- [bold]finish selection[/]: press enter when [italic]next[/] is selected -""" - - def __init__(self) -> None: - self.panel = Panel(self.hint_test, title="Navigation", expand=False) - self.aligned_panel = Align(self.panel, align="center") - - def get_renderable(self) -> RenderableType: - return self.aligned_panel - -class ToolTypeSelectorPanel(): - def __init__(self, elements: list[str]) -> None: - # Panel content - self.tool_type_menu = ToolTypeMenu(elements) - self.cancel_next_menu = CancelNextMenu() - self.cancel_next_menu.remove_cursor() - - self.menus = Align(Group( - Align(self.tool_type_menu, align="center", vertical="middle"), - Align(self.cancel_next_menu, align="center", vertical="middle"), - ), align="center", vertical="middle") - - self.navigation_hint = NavigationHint() - - self.layout = Layout(name="root") - self.layout.split( - Layout(name="menus"), - Layout(name="info", size=8), - ) - self.layout["menus"].update(self.menus) - self.layout["info"].update(self.navigation_hint.get_renderable()) - - self.no_tool_type_selected_error = Align(Panel("[yellow]You need to select at least one tool type![/]"), - align="center", vertical="middle") - - self.active_menu = self.tool_type_menu - - def wait_for_user(self) -> None: - with Live(self.layout, refresh_per_second=8, screen=True): - selection = "" - is_error_presented = False - while selection == "": - input = readkey() - if input is key.TAB: - if is_error_presented is True: - self.layout["info"].update(self.navigation_hint.get_renderable()) - self.active_menu.remove_cursor() - - if self.active_menu is self.tool_type_menu: - self.active_menu = self.cancel_next_menu - else: - self.active_menu = self.tool_type_menu - - self.active_menu.add_cursor() - else: - self.active_menu.handle_user_input(input) - - if self.cancel_next_menu.is_selected is True: - selection = self.cancel_next_menu.get_selection() - - if ("next" in selection) and (len(self.tool_type_menu.get_selected_tool_types()) == 0): - self.cancel_next_menu.is_selected = False - self.layout["info"].update(self.no_tool_type_selected_error) - is_error_presented = True - selection = "" \ No newline at end of file diff --git a/dem/cli/tui/printable_tool_image.py b/dem/cli/tui/printable_tool_image.py new file mode 100644 index 00000000..ac5a363a --- /dev/null +++ b/dem/cli/tui/printable_tool_image.py @@ -0,0 +1,22 @@ +"""A class representing a tool image with its printable availability.""" +# dem/cli/tui/printable_tool_image.py + +from dem.core.tool_images import ToolImage + +tool_image_statuses = { + ToolImage.LOCAL_ONLY: "local", + ToolImage.REGISTRY_ONLY: "registry", + ToolImage.LOCAL_AND_REGISTRY: "local and registry" +} + +class PrintableToolImage(): + def __init__ (self, tool_image: ToolImage): + self.name = tool_image.name + self.status = tool_image_statuses[tool_image.availability] + +def convert_to_printable_tool_images(all_tool_images: dict[str, ToolImage]) -> list[PrintableToolImage]: + printable_tool_images = [] + for tool_image in all_tool_images.values(): + printable_tool_images.append(PrintableToolImage(tool_image)) + + return printable_tool_images \ No newline at end of file diff --git a/dem/cli/tui/renderable/menu.py b/dem/cli/tui/renderable/menu.py index dfdd824e..eba3b83e 100644 --- a/dem/cli/tui/renderable/menu.py +++ b/dem/cli/tui/renderable/menu.py @@ -1,14 +1,13 @@ """Menu TUI renderable.""" # dem/cli/tui/renderable/menu.py +from dem.cli.tui.printable_tool_image import PrintableToolImage from rich import live, table, align, panel, box from readchar import readkey, key class BaseMenu(table.Table): """ Base class for the menus. - Shouldn't be instantiated directly. - Class attributes: cursor_on -- text that represents the cursor cursor_off -- the cursor is not in this cell @@ -171,13 +170,13 @@ def handle_user_input(self, input:str) -> None: case key.RIGHT | 'l': self.move_cursor(self.CURSOR_RIGHT) -class CancelNextMenu(HorizontalMenu): - """ Horizontal menu with two items: cancel and next. +class CancelSaveMenu(HorizontalMenu): + """ Horizontal menu with two items: cancel and save. Class attributes: menu_items -- the menu items with rich text formatting """ - menu_items = ("[underline]cancel[/]", "[underline]next[/]") + menu_items = ("[underline]cancel[/]", "[underline]save[/]") def __init__(self) -> None: """ Construct a HorizontalMenu with 2 columns and 1 row.""" @@ -204,123 +203,51 @@ def get_selection(self) -> str: """ return self.columns[self.cursor_pos]._cells[0] -class BackMenu(HorizontalMenu): - """ Horizontal menu with a single item: back. - - Class attributes: - menu_item -- the menu item with rich text formatting - """ - menu_item = "[underline]back[/]" - - def __init__(self) -> None: - """ Construct a HorizontalMenu with 1 column and 1 row.""" - super().__init__() - self.add_row(self.cursor_off + self.menu_item) - self.is_selected = False - - def handle_user_input(self, input: str) -> None: - """ Handle user input. - - Args: - input -- the user input (handle enter) - """ - if input is key.ENTER: - self.is_selected = True - -class ToolTypeMenu(VerticalMenu): - """ Vertical menu for the tool type selection. - - Class attributes: - selected -- selected text - not_selected -- not selected text - """ - selected = "[green]yes[/]" - not_selected = "no" - - def __init__(self, supported_tool_types: list[str]) -> None: - """ Construct a VerticalMenu with 2 columns and rows that contain the supported tool types. - - Args: - supported_tool_types -- the supported tool types - """ - super().__init__() - self.add_column("Tool types") - self.add_column("Selected") - - for index, tool_type in enumerate(supported_tool_types): - if (index == 0): - # Set the cursor indicator for the first element. - self.add_row(self.cursor_on + tool_type, self.not_selected) - else: - self.add_row(self.cursor_off + tool_type, self.not_selected) - - def preset_selection(self, already_selected: list[str]) -> None: - """ Preset the given tool types. - - Args: - already_selected -- list of tool types to set as selected - """ - for row_idx, cell in enumerate(self.columns[0]._cells): - if cell[2:] in already_selected: - self.columns[1]._cells[row_idx] = self.selected - - def toggle_select(self) -> None: - """ Toggle selection at the cursor's position. """ - if (self.columns[1]._cells[self.cursor_pos] is self.selected): - self.columns[1]._cells[self.cursor_pos] = self.not_selected - else: - self.columns[1]._cells[self.cursor_pos] = self.selected - - def handle_user_input(self, input: str) -> None: - """ Handle user input or pass to parent. - - Args: - input -- the user input (handle enter or space) - """ - if (input == key.ENTER) or (input == ' '): - self.toggle_select() - else: - super().handle_user_input(input) - - def get_selected_tool_types(self) -> list[str]: - """ Get selected tool types. - - Returns with a list of tool types that are in selected state. - """ - selected_tool_types = [] - for row_index, cell in enumerate(self.columns[1]._cells): - if cell == self.selected: - selected_tool_types.append(self.columns[0]._cells[row_index][2:]) - return selected_tool_types - class ToolImageMenu(VerticalMenu): - def __init__(self, tool_images: list[list[str]]) -> None: + def __init__(self, printable_tool_images: list[PrintableToolImage], + already_selected_tool_images: list[str]) -> None: super().__init__() self.add_column("Tool images", no_wrap=True) self.add_column("Availability", no_wrap=True) - for index, tool_image in enumerate(tool_images): + for index, tool_image in enumerate(printable_tool_images): + row_content = [] if (index == 0): # Set the cursor indicator for the first element. - self.add_row("* " + tool_image[0], tool_image[1]) + row_content = "* " + tool_image.name, tool_image.status else: - self.add_row(" " + tool_image[0], tool_image[1]) + row_content = " " + tool_image.name, tool_image.status - self.is_selected = False + if tool_image.name in already_selected_tool_images: + row_content = f"{row_content[0][:2]}[green]{row_content[0][2:]}[/]", str(row_content[1]) + + self.add_row(*row_content) def handle_user_input(self, input: str) -> None: - if input == key.ENTER: - self.is_selected = True + if input == key.ENTER or input == key.SPACE: + selected_cell = self.columns[0]._cells[self.cursor_pos] + + if "[green]" in selected_cell: + selected_cell = selected_cell.replace("[green]", "") + selected_cell = selected_cell.replace("[/]", "") + else: + selected_cell = f"{selected_cell[:2]}[green]{selected_cell[2:]}[/]" + + self.columns[0]._cells[self.cursor_pos] = selected_cell else: super().handle_user_input(input) - def get_selected_tool_image(self) -> str: - return self.columns[0]._cells[self.cursor_pos][2:] + def get_selected_tool_images(self) -> list[str]: + selected_tool_images = [] + for cell in self.columns[0]._cells: + if "[green]" in cell: + selected_tool_images.append(cell[2:].replace("[/]", "").replace("[green]", "")) + + return selected_tool_images class SelectMenu(VerticalMenu): def __init__(self, selection: list[str]) -> None: super().__init__() - self.alignment = align.Align(self, align="center", vertical="middle") self.show_edge = False self.show_lines = False @@ -351,33 +278,19 @@ def set_title(self, title: str) -> None: self.width = len(title) super().set_title(title) -class DevEnvStatus(panel.Panel): - def __init__(self, tool_types: list[str]) -> None: +class DevEnvStatusPanel(panel.Panel): + def __init__(self, already_selected_tool_images: list[str]) -> None: self.outer_table = table.Table(box=None) - super().__init__(self.outer_table, title="Development Environment", expand=False) + super().__init__(self.outer_table, title="Development Environment", expand=True) self.aligned_renderable = align.Align(self, vertical="middle") - - self._fill_table(tool_types) - - def _fill_table(self, tool_types: list[str]) -> None: - panel_height = 3 + len(tool_types) * 4 - self.height = panel_height - for tool_type in tool_types: - inner_table = table.Table(box=None) - inner_table.add_row("[bold]" + tool_type + ":[/]") - inner_table.add_row("") - inner_table.add_row("") + for tool_image in already_selected_tool_images: + self.outer_table.add_row(tool_image) - self.outer_table.add_row(inner_table) - - def reset_table(self, tool_types: list[str]) -> None: + def update_table(self, selected_tool_images: list[str]) -> None: self.outer_table = table.Table(box=None) - self._fill_table(tool_types) - self.renderable = self.outer_table - - def set_tool_image(self, tool_selection: dict) -> None: - for tool_type, tool_image in tool_selection.items(): - for row in self.outer_table.columns[0].cells: - if tool_type in row.columns[0]._cells[0]: - row.columns[0]._cells[1] = tool_image \ No newline at end of file + + for tool_image in selected_tool_images: + self.outer_table.add_row(tool_image) + + self.renderable = self.outer_table \ No newline at end of file diff --git a/dem/cli/tui/panel/tool_image_selector.py b/dem/cli/tui/window/dev_env_settings_window.py similarity index 58% rename from dem/cli/tui/panel/tool_image_selector.py rename to dem/cli/tui/window/dev_env_settings_window.py index 67a40ae2..9d067e31 100644 --- a/dem/cli/tui/panel/tool_image_selector.py +++ b/dem/cli/tui/window/dev_env_settings_window.py @@ -1,10 +1,11 @@ """Image selector panel.""" # dem/cli/tui/panel/image_selector.py -from dem.cli.tui.renderable.menu import ToolImageMenu, BackMenu, DevEnvStatus +from dem.cli.tui.renderable.menu import ToolImageMenu, DevEnvStatusPanel, CancelSaveMenu +from dem.cli.tui.printable_tool_image import PrintableToolImage from rich.panel import Panel from rich.layout import Layout -from rich.console import RenderableType, Group +from rich.console import Group from rich.align import Align from rich.live import Live from readchar import readkey, key @@ -13,24 +14,24 @@ class NavigationHint(Panel): hint_text = """ - [bold]move cursor[/]: arrows or vi mode - [bold]select[/]: space or enter -- [bold]jump to back[/]: tab +- [bold]jump to save/cancel[/]: tab """ - def __init__(self) -> None: super().__init__(self.hint_text, title="Navigation", expand=False) self.aligned_renderable = Align(self, align="center") -class ToolImageSelectorPanel(): - def __init__(self, elements: list[list[str]], tool_types: list[str]) -> None: +class DevEnvSettingsWindow(): + def __init__(self, printable_tool_images: list[PrintableToolImage], + already_selected_tool_images: list[str] = []) -> None: # Panel content - self.tool_image_menu = ToolImageMenu(elements) - self.dev_env_status = DevEnvStatus(tool_types) - self.back_menu = BackMenu() + self.tool_image_menu = ToolImageMenu(printable_tool_images, already_selected_tool_images) + self.dev_env_status = DevEnvStatusPanel(already_selected_tool_images) + self.cancel_save_menu = CancelSaveMenu() self.navigation_hint = NavigationHint() self.menus = Align(Group( Align(self.tool_image_menu, align="center", vertical="middle"), - Align(self.back_menu, align="center", vertical="middle"), + Align(self.cancel_save_menu, align="center", vertical="middle"), ), align="center", vertical="middle") self.layout = Layout(name="root") @@ -48,21 +49,26 @@ def __init__(self, elements: list[list[str]], tool_types: list[str]) -> None: self.layout["dev_env_status"].update(self.dev_env_status.aligned_renderable) self.layout["navigation_hint"].update(self.navigation_hint.aligned_renderable) - self.back_menu.remove_cursor() + self.cancel_save_menu.remove_cursor() self.active_menu = self.tool_image_menu def wait_for_user(self) -> None: + self.selected_tool_images = [] with Live(self.layout, refresh_per_second=8, screen=True): - while self.active_menu.is_selected is False: + while self.cancel_save_menu.is_selected is False: input = readkey() if input is key.TAB: self.active_menu.remove_cursor() if self.active_menu is self.tool_image_menu: - self.active_menu = self.back_menu + self.active_menu = self.cancel_save_menu else: self.active_menu = self.tool_image_menu self.active_menu.add_cursor() else: - self.active_menu.handle_user_input(input) \ No newline at end of file + self.active_menu.handle_user_input(input) + + if (self.active_menu is self.tool_image_menu) and (input in [key.ENTER, key.SPACE]): + self.selected_tool_images = self.tool_image_menu.get_selected_tool_images() + self.dev_env_status.update_table(self.selected_tool_images) \ No newline at end of file diff --git a/dem/core/dev_env.py b/dem/core/dev_env.py index fa16f1b4..dbae1682 100755 --- a/dem/core/dev_env.py +++ b/dem/core/dev_env.py @@ -1,23 +1,11 @@ """This module represents a Development Environment.""" # dem/core/dev_env.py -from dem.core.tool_images import ToolImages +from dem.core.tool_images import ToolImage, ToolImages import json, os class DevEnv(): - """ A Development Environment. - - Class variables: - supported_tool_types -- supported tool types - """ - supported_tool_types = ( - "build system", - "toolchain", - "debugger", - "deployer", - "test framework", - "CI/CD server", - ) + """ A Development Environment. """ def __init__(self, descriptor: dict | None = None, descriptor_path: str | None = None) -> None: """ Init the DevEnv class. @@ -48,53 +36,19 @@ def __init__(self, descriptor: dict | None = None, descriptor_path: str | None = descriptor = json.load(file) self.name: str = descriptor["name"] - self.tools: list[dict[str, str]] = descriptor["tools"] + self.tool_image_descriptors: list[dict[str, str]] = descriptor["tools"] + self.tool_images: list[ToolImage] = [] descriptor_installed = descriptor.get("installed", "False") if "True" == descriptor_installed: self.is_installed = True else: self.is_installed = False - def check_image_availability(self, all_tool_images: ToolImages, - update_tool_image_store: bool = False, - local_only: bool = False) -> list: - """ Checks the tool image's availability. - - Updates the "image_status" key for the tool dictionary. - Returns with the statuses of the Dev Env tool images. - - Args: - all_tool_images -- the images the Dev Envs can access - update_tool_images -- update the list of available tool images - local_only -- only local images are used - """ - if update_tool_image_store == True: - all_tool_images.local.update() - if local_only is False: - all_tool_images.registry.update() - - local_tool_images = all_tool_images.local.elements - - if local_only is True: - registry_tool_images = [] - else: - registry_tool_images = all_tool_images.registry.elements - - image_statuses = [] - for tool in self.tools: - tool_image_name = tool["image_name"] + ':' + tool["image_version"] - if (tool_image_name in local_tool_images) and (tool_image_name in registry_tool_images): - image_status = ToolImages.LOCAL_AND_REGISTRY - elif (tool_image_name in local_tool_images): - image_status = ToolImages.LOCAL_ONLY - elif (tool_image_name in registry_tool_images): - image_status = ToolImages.REGISTRY_ONLY - else: - image_status = ToolImages.NOT_AVAILABLE - image_statuses.append(image_status) - tool["image_status"] = image_status - - return image_statuses + def assign_tool_image_instances(self, tool_images: ToolImages) -> None: + for tool_descriptor in self.tool_image_descriptors: + tool_image_name = tool_descriptor["image_name"] + ':' + tool_descriptor["image_version"] + tool_image = tool_images.all_tool_images.get(tool_image_name, ToolImage(tool_image_name)) + self.tool_images.append(tool_image) def get_deserialized(self, omit_is_installed: bool = False) -> dict[str, str]: """ Create the deserialized json. @@ -103,7 +57,7 @@ def get_deserialized(self, omit_is_installed: bool = False) -> dict[str, str]: """ dev_env_json_deserialized: dict = { "name": self.name, - "tools": self.tools + "tools": self.tool_image_descriptors } if omit_is_installed is False: diff --git a/dem/core/exceptions.py b/dem/core/exceptions.py index 31401fea..2873156e 100644 --- a/dem/core/exceptions.py +++ b/dem/core/exceptions.py @@ -13,6 +13,10 @@ class RegistryError(Exception): """Raised when the communication with registry fails.""" pass +class ToolImageError(Exception): + """Raised when there is a problem with the tool image.""" + pass + class ContainerEngineError(Exception): """Raised when there is a problem with the container engine.""" diff --git a/dem/core/platform.py b/dem/core/platform.py index f7216c87..e590f2d3 100644 --- a/dem/core/platform.py +++ b/dem/core/platform.py @@ -10,7 +10,7 @@ from dem.core.data_management import LocalDevEnvJSON from dem.core.container_engine import ContainerEngine from dem.core.registry import Registries -from dem.core.tool_images import ToolImages +from dem.core.tool_images import ToolImages, ToolImage from dem.core.dev_env import DevEnv from dem.core.hosts import Hosts @@ -21,12 +21,9 @@ class Platform(Core): - External resources. Class variables: - _tool_images -- the available tool images - _container_engine -- the container engine - _regisitries -- managing the registries - update_tool_images_on_instantiation -- can be used to disable tool update if not needed + local_only -- work with the local tool images only """ - update_tool_images_on_instantiation = True + local_only = False def _dev_env_json_version_check(self, dev_env_json_major_version: int) -> None: """ Check that the json file is supported. @@ -47,7 +44,10 @@ def __init__(self) -> None: self._hosts = None def load_dev_envs(self) -> None: - """ Load the Development Environments from the dev_env.json file.""" + """ Load the Development Environments from the dev_env.json file. + + The Dev Envs will only contain the descriptors and not the ToolImage instances. + """ self.dev_env_json = LocalDevEnvJSON() self.dev_env_json.update() self.version = self.dev_env_json.deserialized["version"] @@ -56,6 +56,11 @@ def load_dev_envs(self) -> None: for dev_env_descriptor in self.dev_env_json.deserialized["development_environments"]: self.local_dev_envs.append(DevEnv(descriptor=dev_env_descriptor)) + def assign_tool_image_instances_to_all_dev_envs(self) -> None: + """ Assign the ToolImage instances to all Development Environments.""" + for dev_env in self.local_dev_envs: + dev_env.assign_tool_image_instances(self.tool_images) + @property def tool_images(self) -> ToolImages: """ The tool images. @@ -63,8 +68,8 @@ def tool_images(self) -> ToolImages: The ToolImages() gets instantiated only at the first access. """ if self._tool_images is None: - self._tool_images = ToolImages(self.container_engine, self.registries, - self.update_tool_images_on_instantiation) + self._tool_images = ToolImages(self.container_engine, self.registries) + self._tool_images.update(local_only=self.local_only) return self._tool_images @property @@ -144,19 +149,17 @@ def install_dev_env(self, dev_env_to_install: DevEnv) -> None: Args: dev_env_to_install -- the Development Environment to install """ - dev_env_to_install.check_image_availability(self.tool_images, False) - - # First check if the missing images are available in the registries. - for tool in dev_env_to_install.tools: - if tool["image_status"] == ToolImages.NOT_AVAILABLE: - raise PlatformError(f"The {tool['image_name']}:{tool['image_version']} image is not available.") - - for tool in dev_env_to_install.tools: - if tool["image_status"] == ToolImages.REGISTRY_ONLY: - self.user_output.msg(f"\nPulling image {tool['image_name']}:{tool['image_version']}", - is_title=True) + # First check if the missing images are available in the registries, so DEM won't start to + # pull the images and then fail. + for tool_image in dev_env_to_install.tool_images: + if tool_image.availability == ToolImage.NOT_AVAILABLE: + raise PlatformError(f"The {tool_image.name} image is not available.") + + for tool_image in dev_env_to_install.tool_images: + if tool_image.availability == ToolImage.REGISTRY_ONLY: + self.user_output.msg(f"\nPulling image {tool_image.name}", is_title=True) try: - self.container_engine.pull(f"{tool['image_name']}:{tool['image_version']}") + self.container_engine.pull(tool_image.name) except ContainerEngineError as e: raise PlatformError(f"Dev Env install failed. --> {str(e)}") @@ -175,18 +178,18 @@ def uninstall_dev_env(self, dev_env_to_uninstall: DevEnv) -> None: all_required_tool_images = set() for dev_env in self.local_dev_envs: if (dev_env is not dev_env_to_uninstall) and dev_env.is_installed: - for tool in dev_env.tools: - all_required_tool_images.add(tool["image_name"] + ":" + tool["image_version"]) + for tool_image_descriptor in dev_env.tool_image_descriptors: + all_required_tool_images.add(tool_image_descriptor["image_name"] + ":" + tool_image_descriptor["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 not in all_required_tool_images: - tool_images_to_remove.add(tool_image) + for tool_image_descriptor in dev_env_to_uninstall.tool_image_descriptors: + tool_image_name = tool_image_descriptor["image_name"] + ":" + tool_image_descriptor["image_version"] + if tool_image_name not in all_required_tool_images: + tool_images_to_remove.add(tool_image_name) - for tool_image in tool_images_to_remove: + for tool_image_name in tool_images_to_remove: try: - self.container_engine.remove(tool_image) + self.container_engine.remove(tool_image_name) except ContainerEngineError as e: raise PlatformError(f"Dev Env uninstall failed. --> {str(e)}") @@ -231,6 +234,7 @@ def init_project(self, project_path: str) -> None: raise FileNotFoundError(f"The {descriptor_path} file does not exist.") assigned_dev_env = DevEnv(descriptor_path=descriptor_path) + assigned_dev_env.assign_tool_image_instances(self.tool_images) 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.[/]", diff --git a/dem/core/registry.py b/dem/core/registry.py index f9c92236..7dbf9a45 100644 --- a/dem/core/registry.py +++ b/dem/core/registry.py @@ -3,7 +3,6 @@ from dem.core.core import Core from dem.core.container_engine import ContainerEngine -from dem.core.data_management import ConfigFile import requests from typing import Generator from abc import ABC, abstractmethod diff --git a/dem/core/tool_images.py b/dem/core/tool_images.py index bc573785..565358d7 100644 --- a/dem/core/tool_images.py +++ b/dem/core/tool_images.py @@ -1,56 +1,11 @@ """Local and registry tool images.""" # dem/core/tool_images.py -from dem.core.exceptions import RegistryError from dem.core.container_engine import ContainerEngine from dem.core.registry import Registries +from dem.core.exceptions import ToolImageError -class BaseToolImages(): - """ Base class for the tool images. - - Do not instantiate it directly! - """ - def __init__(self) -> None: - """ Init the class. """ - self.elements = [] - -class LocalToolImages(BaseToolImages): - """ Local tool images.""" - def __init__(self, container_engine: ContainerEngine) -> None: - """ Init the class. - - Args: - container_engine -- container engine - """ - super().__init__() - self.container_egine = container_engine - - def update(self) -> None: - """ Update the local tool image list using the container engine.""" - self.elements = self.container_egine.get_local_tool_images() - -class RegistryToolImages(BaseToolImages): - """ Registry tool images. """ - - def __init__(self, registries: Registries) -> None: - """ Init the class. - - Args: - registries -- registries - """ - super().__init__() - self._registries = registries - - def update(self) -> None: - """ Update the list of available tools in the registry using the registry interface.""" - try: - self.elements = self._registries.list_repos() - except RegistryError as e: - self.elements = [] - raise - -class ToolImages(): - """ Available tool images.""" +class ToolImage(): ( LOCAL_ONLY, REGISTRY_ONLY, @@ -58,18 +13,83 @@ class ToolImages(): NOT_AVAILABLE, ) = range(4) - def __init__(self, container_engine: ContainerEngine, registries: Registries, - update_on_instantiation: bool = True) -> None: + def __init__(self, name: str) -> None: + self.name = name + try: + self.repository = self.name.split(":")[0] + self.tag = self.name.split(":")[1] + except IndexError: + raise ToolImageError(f"Invalid tool image name: {name}") + self.availability = self.NOT_AVAILABLE + +class ToolImages(): + """ Available tool images.""" + def __init__(self, container_engine: ContainerEngine, registries: Registries) -> None: """ Init the class. + + The tool images will be obtained by running the update method. Args: container_engine -- container engine registries -- registries update_on_instantiation -- set to false for manual update """ - self.local = LocalToolImages(container_engine) - self.registry = RegistryToolImages(registries) + self.container_engine = container_engine + self.registries = registries + self.all_tool_images = {} + + def update(self, local_only = False, registry_only = False) -> None: + """ Update the list of available tools. + + Args: + local_only -- update the local tools only + registry_only -- update the registry tools only + """ + registry_tool_image_names = [] + local_tool_image_names = [] + + if not registry_only: + local_tool_image_names = self.container_engine.get_local_tool_images() + + if not local_only: + registry_tool_image_names = self.registries.list_repos() + + for tool_image_name in local_tool_image_names: + tool_image = ToolImage(tool_image_name) + if tool_image_name in registry_tool_image_names: + tool_image.availability = ToolImage.LOCAL_AND_REGISTRY + else: + tool_image.availability = ToolImage.LOCAL_ONLY + self.all_tool_images[tool_image_name] = tool_image + + for tool_image_name in registry_tool_image_names: + if tool_image_name not in local_tool_image_names: + tool_image = ToolImage(tool_image_name) + tool_image.availability = ToolImage.REGISTRY_ONLY + self.all_tool_images[tool_image_name] = tool_image + + def get_local_ones(self) -> dict[str, ToolImage]: + """ Get the local tool images. + + Return with the local tool images. + """ + local_tool_images = {} + for key, tool_image in self.all_tool_images.items(): + if ((tool_image.availability == ToolImage.LOCAL_ONLY) or + (tool_image.availability == ToolImage.LOCAL_AND_REGISTRY)): + local_tool_images[key] = tool_image + + return local_tool_images + + def get_registry_ones(self) -> dict[str, ToolImage]: + """ Get the registry tool images. + + Return with the registry tool images. + """ + registry_tool_images = {} + for key, tool_image in self.all_tool_images.items(): + if ((tool_image.availability == ToolImage.REGISTRY_ONLY) or + (tool_image.availability == ToolImage.LOCAL_AND_REGISTRY)): + registry_tool_images[key] = tool_image - if update_on_instantiation is True: - self.local.update() - self.registry.update() \ No newline at end of file + return registry_tool_images \ No newline at end of file diff --git a/tests/cli/test_assign_cmd.py b/tests/cli/test_assign_cmd.py index e173b2d8..01239dee 100644 --- a/tests/cli/test_assign_cmd.py +++ b/tests/cli/test_assign_cmd.py @@ -38,7 +38,7 @@ def test_assign(mock_stdout_print: MagicMock, mock_isdir: MagicMock) -> None: mock_isdir.assert_called_once_with(test_project_path) mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) mock_platform.assign_dev_env.assert_called_once_with(mock_dev_env, test_project_path) - mock_stdout_print.assert_called_once_with(f"\n[green]Successfully assigned the {test_dev_env_name} Dev Env to the project at{test_project_path}![/]") + mock_stdout_print.assert_called_once_with(f"\n[green]Successfully assigned the {test_dev_env_name} Dev Env to the project at {test_project_path}![/]") @patch("dem.cli.command.assign_cmd.stderr.print") @patch("dem.cli.command.assign_cmd.os.path.isdir") diff --git a/tests/cli/test_create_cmd.py b/tests/cli/test_create_cmd.py index f8820007..715400c4 100644 --- a/tests/cli/test_create_cmd.py +++ b/tests/cli/test_create_cmd.py @@ -9,110 +9,168 @@ import pytest from typer.testing import CliRunner from unittest.mock import patch, MagicMock, call +from pytest import raises ## Global test variables # In order to test stdout and stderr separately, the stderr can't be mixed into the stdout. runner = CliRunner(mix_stderr=False) -def test_get_tool_image_list(): +@patch("dem.cli.command.create_cmd.DevEnvSettingsWindow") +@patch("dem.cli.command.create_cmd.convert_to_printable_tool_images") +def test_open_dev_env_settings_panel(mock_convert_to_printable_tool_images : MagicMock, + mock_DevEnvSettingsWindow: MagicMock) -> None: # Test setup - mock_tool_images = MagicMock() - mock_tool_images.registry.elements = [ - "local_and_registry_image", - "registry_image", - ] - mock_tool_images.local.elements = [ - "local_image", - "local_and_registry_image", - ] + mock_all_tool_images = MagicMock() + mock_printable_tool_images = MagicMock() + mock_convert_to_printable_tool_images.return_value = mock_printable_tool_images + + mock_dev_env_settings_panel = MagicMock() + mock_selected_tool_images = MagicMock() + mock_dev_env_settings_panel.selected_tool_images = mock_selected_tool_images + mock_DevEnvSettingsWindow.return_value = mock_dev_env_settings_panel + + mock_dev_env_settings_panel.cancel_save_menu.get_selection.return_value = "save" # Run unit under test - actual_tool_images = create_cmd.get_tool_image_list(mock_tool_images) + actual_selected_tool_image_name = create_cmd.open_dev_env_settings_panel(mock_all_tool_images) # Check expectations - expected_tool_iamges = [ - ["local_and_registry_image", "local and registry"], - ["registry_image", "registry"], - ["local_image", "local"], - ] - assert actual_tool_images == expected_tool_iamges + assert actual_selected_tool_image_name is mock_selected_tool_images + + mock_convert_to_printable_tool_images.assert_called_once_with(mock_all_tool_images) + mock_DevEnvSettingsWindow.assert_called_once_with(mock_printable_tool_images) + mock_dev_env_settings_panel.wait_for_user.assert_called_once() + mock_dev_env_settings_panel.cancel_save_menu.get_selection.assert_called_once() + +@patch("dem.cli.command.create_cmd.DevEnvSettingsWindow") +@patch("dem.cli.command.create_cmd.convert_to_printable_tool_images") +def test_open_dev_env_settings_panel_cancel(mock_convert_to_printable_tool_images: MagicMock, + mock_DevEnvSettingsWindow: MagicMock) -> None: + # Test setup + mock_all_tool_images = MagicMock() + mock_printable_tool_images = MagicMock() + mock_convert_to_printable_tool_images.return_value = mock_printable_tool_images + + mock_dev_env_settings_panel = MagicMock() + mock_selected_tool_images = MagicMock() + mock_dev_env_settings_panel.selected_tool_images = mock_selected_tool_images + mock_DevEnvSettingsWindow.return_value = mock_dev_env_settings_panel + + mock_dev_env_settings_panel.cancel_save_menu.get_selection.return_value = "cancel" + + # Run unit under test + with raises(create_cmd.typer.Abort): + create_cmd.open_dev_env_settings_panel(mock_all_tool_images) + + # Check expectations + mock_convert_to_printable_tool_images.assert_called_once_with(mock_all_tool_images) + mock_DevEnvSettingsWindow.assert_called_once_with(mock_printable_tool_images) + mock_dev_env_settings_panel.wait_for_user.assert_called_once() + mock_dev_env_settings_panel.cancel_save_menu.get_selection.assert_called_once() + +def test_create_new_dev_env_descriptor() -> None: + # Test setup + test_dev_env_name = "test_dev_env" + test_selected_tool_images = ["axemsolutions/make_gnu_arm:latest", "stlink_org:latest"] + + # Run unit under test + actual_dev_env_descriptor = create_cmd.create_new_dev_env_descriptor(test_dev_env_name, + test_selected_tool_images) + + # Check expectations + expected_dev_env_descriptor = { + "name": test_dev_env_name, + "tools": [ + { + "image_name": "axemsolutions/make_gnu_arm", + "image_version": "latest" + }, + { + "image_name": "stlink_org", + "image_version": "latest" + } + ] + } + + assert expected_dev_env_descriptor == actual_dev_env_descriptor @patch("dem.cli.command.create_cmd.DevEnv") -def test_create_new_dev_env(mock_DevEnvLocal): +def test_create_new_dev_env(mock_DevEnv: MagicMock) -> None: # Test setup mock_new_dev_env = MagicMock() - mock_DevEnvLocal.return_value = mock_new_dev_env + mock_DevEnv.return_value = mock_new_dev_env mock_platform = MagicMock() + mock_platform.local_dev_envs = [] mock_new_dev_env_descriptor = MagicMock() # Run unit under test create_cmd.create_new_dev_env(mock_platform, mock_new_dev_env_descriptor) # Check expectations - mock_DevEnvLocal.assert_called_once_with(mock_new_dev_env_descriptor) - mock_platform.local_dev_envs.append.assert_called_once_with(mock_new_dev_env) + assert mock_new_dev_env in mock_platform.local_dev_envs + + mock_DevEnv.assert_called_once_with(descriptor=mock_new_dev_env_descriptor) + mock_new_dev_env.assign_tool_image_instances.assert_called_once_with(mock_platform.tool_images) @patch("dem.cli.command.create_cmd.create_new_dev_env") -@patch("dem.cli.command.create_cmd.get_dev_env_descriptor_from_user") -@patch("dem.cli.command.create_cmd.get_tool_image_list") -def test_create_dev_env_new(mock_get_tool_image_list, mock_get_dev_env_descriptor_from_user, - mock_create_new_dev_env): +@patch("dem.cli.command.create_cmd.create_new_dev_env_descriptor") +@patch("dem.cli.command.create_cmd.open_dev_env_settings_panel") +def test_create_dev_env_new(mock_open_dev_env_settings_panel: MagicMock, + mock_create_new_dev_env_descriptor: MagicMock, + mock_create_new_dev_env: MagicMock) -> None: # Test setup - mock_dev_env_local_setup = MagicMock() - mock_dev_env_local_setup.get_dev_env_by_name.return_value = None - - mock_tool_images = MagicMock() - mock_get_tool_image_list.return_value = mock_tool_images + mock_platform = MagicMock() + mock_platform.get_dev_env_by_name.return_value = None mock_dev_env_descriptor = MagicMock() - mock_get_dev_env_descriptor_from_user.return_value = mock_dev_env_descriptor + mock_create_new_dev_env_descriptor.return_value = mock_dev_env_descriptor - mock_new_dev_env = MagicMock() - mock_create_new_dev_env.return_value = mock_new_dev_env + mock_selected_tool_images = MagicMock() + mock_open_dev_env_settings_panel.return_value = mock_selected_tool_images - expected_dev_env_name = "test_dev_env" + test_dev_env_name = "test_dev_env" # Run unit under test - create_cmd.create_dev_env(mock_dev_env_local_setup, expected_dev_env_name) + create_cmd.create_dev_env(mock_platform, test_dev_env_name) # Check expectations - mock_dev_env_local_setup.get_dev_env_by_name.assert_called_once_with(expected_dev_env_name) - mock_get_tool_image_list.assert_called_once_with(mock_dev_env_local_setup.tool_images) - mock_get_dev_env_descriptor_from_user.assert_called_once_with(expected_dev_env_name, mock_tool_images) - mock_create_new_dev_env.assert_called_once_with(mock_dev_env_local_setup, mock_dev_env_descriptor) - mock_new_dev_env.check_image_availability.return_value = mock_dev_env_local_setup.tool_images + mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) + mock_open_dev_env_settings_panel.assert_called_once_with(mock_platform.tool_images.all_tool_images) + mock_create_new_dev_env_descriptor.assert_called_once_with(test_dev_env_name, + mock_selected_tool_images) + mock_create_new_dev_env.assert_called_once_with(mock_platform, mock_dev_env_descriptor) -@patch("dem.cli.command.create_cmd.get_dev_env_descriptor_from_user") -@patch("dem.cli.command.create_cmd.get_tool_image_list") +@patch("dem.cli.command.create_cmd.create_new_dev_env_descriptor") +@patch("dem.cli.command.create_cmd.open_dev_env_settings_panel") @patch("dem.cli.command.create_cmd.typer.confirm") -def test_create_dev_env_overwrite(mock_confirm, mock_get_tool_image_list, - mock_get_dev_env_descriptor_from_user) -> None: +def test_create_dev_env_overwrite_installed(mock_confirm: MagicMock, + mock_open_dev_env_settings_panel: MagicMock, + mock_create_new_dev_env_descriptor: MagicMock) -> None: # Test setup mock_platform = MagicMock() - mock_dev_env_original = MagicMock() - mock_dev_env_original.is_installed = True - mock_platform.get_dev_env_by_name.return_value = mock_dev_env_original + + fake_dev_env_descriptor = {} mock_tools = MagicMock() - mock_dev_env_descriptor = { - "tools": mock_tools - } + fake_dev_env_descriptor["tools"] = mock_tools + mock_create_new_dev_env_descriptor.return_value = fake_dev_env_descriptor - mock_tool_images = MagicMock() - mock_get_tool_image_list.return_value = mock_tool_images + mock_dev_env_original = MagicMock() + mock_platform.get_dev_env_by_name.return_value = mock_dev_env_original - mock_get_dev_env_descriptor_from_user.return_value = mock_dev_env_descriptor + mock_selected_tool_images = MagicMock() + mock_open_dev_env_settings_panel.return_value = mock_selected_tool_images - expected_dev_env_name = "test_dev_env" + test_dev_env_name = "test_dev_env" # Run unit under test - create_cmd.create_dev_env(mock_platform, expected_dev_env_name) + create_cmd.create_dev_env(mock_platform, test_dev_env_name) # Check expectations - assert mock_dev_env_original.tools is mock_tools + assert mock_dev_env_original.tool_image_descriptors is mock_tools - mock_platform.get_dev_env_by_name.assert_called_once_with(expected_dev_env_name) + mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) mock_confirm.assert_has_calls([ call("The input name is already used by a Development Environment. Overwrite it?", abort=True), @@ -120,9 +178,9 @@ def test_create_dev_env_overwrite(mock_confirm, mock_get_tool_image_list, "Uninstall it first?", abort=True) ]) mock_platform.uninstall_dev_env.assert_called_once_with(mock_dev_env_original) - mock_get_tool_image_list(mock_platform.tool_images) - mock_get_dev_env_descriptor_from_user.assert_called_once_with(expected_dev_env_name, - mock_tool_images) + mock_open_dev_env_settings_panel.assert_called_once_with(mock_platform.tool_images.all_tool_images) + mock_create_new_dev_env_descriptor.assert_called_once_with(test_dev_env_name, + mock_selected_tool_images) @patch("dem.cli.command.create_cmd.stderr.print") @patch("dem.cli.command.create_cmd.typer.confirm") @@ -153,9 +211,8 @@ def test_create_dev_env_overwrite_PlatformError(mock_confirm: MagicMock, mock_platform.uninstall_dev_env.assert_called_once_with(mock_dev_env_original) mock_stderr_print.assert_called_once_with(f"[red]Platform error: {test_exception_text}[/]") -@patch("dem.cli.command.create_cmd.get_dev_env_descriptor_from_user") @patch("dem.cli.command.create_cmd.typer.confirm") -def test_create_dev_env_abort(mock_confirm, mock_get_dev_env_descriptor_from_user): +def test_create_dev_env_abort(mock_confirm: MagicMock) -> None: # Test setup mock_dev_env_local_setup = MagicMock() mock_dev_env_original = MagicMock() @@ -172,11 +229,10 @@ def test_create_dev_env_abort(mock_confirm, mock_get_dev_env_descriptor_from_use mock_dev_env_local_setup.get_dev_env_by_name.assert_called_once_with(expected_dev_env_name) mock_confirm.assert_called_once_with("The input name is already used by a Development Environment. Overwrite it?", abort=True) - mock_get_dev_env_descriptor_from_user.assert_not_called() @patch("dem.cli.command.create_cmd.stdout.print") @patch("dem.cli.command.create_cmd.create_dev_env") -def test_execute(mock_create_dev_env, mock_stdout_print): +def test_execute(mock_create_dev_env: MagicMock, mock_stdout_print: MagicMock) -> None: # Test setup mock_platform = MagicMock() main.platform = mock_platform diff --git a/tests/cli/test_info_cmd.py b/tests/cli/test_info_cmd.py index 975cab19..63b25465 100644 --- a/tests/cli/test_info_cmd.py +++ b/tests/cli/test_info_cmd.py @@ -3,10 +3,11 @@ # Unit under test: import dem.cli.main as main +import dem.cli.command.info_cmd as info_cmd # Test framework from typer.testing import CliRunner -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, call, patch import io from rich.console import Console @@ -33,207 +34,89 @@ def get_expected_table(expected_tools: list[list[str]]) ->str: ## Test cases -def test_info_local_dev_env_demo(): +@patch("dem.cli.command.info_cmd.stdout.print") +@patch("dem.cli.command.info_cmd.Table") +def test_print_info(mock_Table: MagicMock, mock_stdout_print: MagicMock) -> None: # Test setup - mock_platform = MagicMock() - main.platform = mock_platform + mock_table = MagicMock() + mock_Table.return_value = mock_table - mock_dev_env = MagicMock() - mock_dev_env.tools = [ - { - "type": "build system", - "image_name": "axemsolutions/make_gnu_arm", - "image_version": "latest", - }, - { - "type": "toolchain", - "image_name": "axemsolutions/make_gnu_arm", - "image_version": "latest", - }, - { - "type": "debugger", - "image_name": "axemsolutions/stlink_org", - "image_version": "latest", - }, - { - "type": "deployer", - "image_name": "axemsolutions/stlink_org", - "image_version": "latest", - }, - { - "type": "test framework", - "image_name": "axemsolutions/cpputest", - "image_version": "latest" - }, - ] - mock_platform.get_dev_env_by_name.return_value = mock_dev_env - def stub_check_image_availability(*args, **kwargs): - for tool in mock_dev_env.tools: - tool["image_status"] = ToolImages.LOCAL_AND_REGISTRY - mock_dev_env.check_image_availability.side_effect = stub_check_image_availability + mock_tool_image1 = MagicMock() + mock_tool_image1.name = "axemsolutions/make_gnu_arm:latest" + mock_tool_image1.availability = info_cmd.ToolImage.LOCAL_AND_REGISTRY - # Run unit under test - test_dev_env_name = "demo" - runner_result = runner.invoke(main.typer_cli, ["info", test_dev_env_name], color=True) - - # Check expectations - assert runner_result.exit_code == 0 - - mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) - mock_dev_env.check_image_availability.assert_called_once_with(mock_platform.tool_images) - - expected_tools = [ - ["build system", "axemsolutions/make_gnu_arm:latest", "Image is available locally and in the registry."], - ["toolchain", "axemsolutions/make_gnu_arm:latest", "Image is available locally and in the registry."], - ["debugger", "axemsolutions/stlink_org:latest", "Image is available locally and in the registry."], - ["deployer", "axemsolutions/stlink_org:latest", "Image is available locally and in the registry."], - ["test framework", "axemsolutions/cpputest:latest", "Image is available locally and in the registry."], - ] - assert get_expected_table(expected_tools) == runner_result.stdout - -def test_info_local_dev_env_nagy_cica_project(): - # Test setup - mock_platform = MagicMock() - main.platform = mock_platform + mock_tool_image2 = MagicMock() + mock_tool_image2.name = "axemsolutions/stlink_org:latest" + mock_tool_image2.availability = info_cmd.ToolImage.LOCAL_ONLY - fake_dev_env = MagicMock() - fake_dev_env.tools = [ - { - "type": "build system", - "image_name": "axemsolutions/bazel", - "image_version": "latest", - }, - { - "type": "toolchain", - "image_name": "axemsolutions/gnu_arm", - "image_version": "latest", - }, - { - "type": "debugger", - "image_name": "axemsolutions/jlink", - "image_version": "latest", - }, - { - "type": "deployer", - "image_name": "axemsolutions/jlink", - "image_version": "latest", - }, - { - "type": "test framework", - "image_name": "axemsolutions/cpputest", - "image_version": "latest" - }, - ] - mock_platform.get_dev_env_by_name.return_value = fake_dev_env - def stub_check_image_availability(*args, **kwargs): - fake_dev_env.tools[0]["image_status"] = ToolImages.NOT_AVAILABLE - fake_dev_env.tools[1]["image_status"] = ToolImages.NOT_AVAILABLE - fake_dev_env.tools[2]["image_status"] = ToolImages.LOCAL_ONLY - fake_dev_env.tools[3]["image_status"] = ToolImages.LOCAL_ONLY - fake_dev_env.tools[4]["image_status"] = ToolImages.LOCAL_AND_REGISTRY - fake_dev_env.check_image_availability.side_effect = stub_check_image_availability + mock_tool_image3 = MagicMock() + mock_tool_image3.name = "axemsolutions/cpputest:latest" + mock_tool_image3.availability = info_cmd.ToolImage.REGISTRY_ONLY + mock_dev_env = MagicMock() + mock_dev_env.tool_images = [ mock_tool_image1, mock_tool_image2, mock_tool_image3] + # Run unit under test - test_dev_env_name = "nagy_cica_project" - runner_result = runner.invoke(main.typer_cli, ["info", test_dev_env_name], color=True) + info_cmd.print_info(mock_dev_env) # Check expectations - assert runner_result.exit_code == 0 - - mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) - fake_dev_env.check_image_availability.assert_called_once_with(mock_platform.tool_images) - - expected_tools = [ - ["build system", "axemsolutions/bazel:latest", "[red]Error: Required image is not available![/]"], - ["toolchain", "axemsolutions/gnu_arm:latest", "[red]Error: Required image is not available![/]"], - ["debugger", "axemsolutions/jlink:latest", "Image is available locally."], - ["deployer", "axemsolutions/jlink:latest", "Image is available locally."], - ["test framework", "axemsolutions/cpputest:latest", "Image is available locally and in the registry."], - ] - assert get_expected_table(expected_tools) == runner_result.stdout - -def test_info_dev_env_invalid(): + mock_table.add_column.assert_has_calls([ + call("Image"), + call("Status") + ]) + mock_table.add_row.assert_has_calls([ + call("axemsolutions/make_gnu_arm:latest", "Image is available locally and in the registry."), + call("axemsolutions/stlink_org:latest", "Image is available locally."), + call("axemsolutions/cpputest:latest", "Image is available in the registry."), + ]) + mock_stdout_print.assert_called_once_with(mock_table) + +@patch("dem.cli.command.info_cmd.stderr.print") +def test_execute_dev_env_not_found(mock_stderr_print: MagicMock) -> None: # Test setup mock_platform = MagicMock() main.platform = mock_platform + mock_platform.dev_env_catalogs.catalogs = [] + test_dev_env_name = "test_dev_env_name" mock_platform.get_dev_env_by_name.return_value = None - mock_platform.dev_env_catalogs.catalogs = [] # Run unit under test - test_dev_env_name = "not_existing_environment" runner_result = runner.invoke(main.typer_cli, ["info", test_dev_env_name], color=True) # Check expectations assert runner_result.exit_code == 0 - + + mock_platform.load_dev_envs.assert_called_once() + mock_platform.assign_tool_image_instances_to_all_dev_envs.assert_called_once() mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) + mock_stderr_print.assert_called_once_with(f"[red]Error: Unknown Development Environment: {test_dev_env_name}[/]\n") - console = Console(file=io.StringIO()) - console.print("[red]Error: Unknown Development Environment: not_existing_environment[/]") - expected_output = console.file.getvalue() - assert expected_output == runner_result.stderr - -def test_info_org_dev_env(): +@patch("dem.cli.command.info_cmd.print_info") +def test_execute(mock_print_info: MagicMock) -> None: # Test setup mock_platform = MagicMock() main.platform = mock_platform + test_dev_env_name = "test_dev_env_name" mock_platform.get_dev_env_by_name.return_value = None + mock_dev_env = MagicMock() + mock_catalog = MagicMock() + mock_catalog.get_dev_env_by_name.return_value = mock_dev_env mock_platform.dev_env_catalogs.catalogs = [mock_catalog] - fake_dev_env = MagicMock() - fake_dev_env.tools = [ - { - "type": "build system", - "image_name": "axemsolutions/cmake", - "image_version": "latest", - }, - { - "type": "toolchain", - "image_name": "axemsolutions/llvm", - "image_version": "latest", - }, - { - "type": "debugger", - "image_name": "axemsolutions/pemicro", - "image_version": "latest", - }, - { - "type": "deployer", - "image_name": "axemsolutions/pemicro", - "image_version": "latest", - }, - { - "type": "test framework", - "image_name": "axemsolutions/unity", - "image_version": "latest" - }, - ] - mock_catalog.get_dev_env_by_name.return_value = fake_dev_env - def stub_check_image_availability(*args, **kwargs): - for tool in fake_dev_env.tools: - tool["image_status"] = ToolImages.REGISTRY_ONLY - fake_dev_env.check_image_availability.side_effect = stub_check_image_availability # Run unit under test - test_dev_env_name = "org_only_env" runner_result = runner.invoke(main.typer_cli, ["info", test_dev_env_name], color=True) # Check expectations assert runner_result.exit_code == 0 + mock_platform.load_dev_envs.assert_called_once() + mock_platform.assign_tool_image_instances_to_all_dev_envs.assert_called_once() mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) - + mock_catalog.request_dev_envs.assert_called_once() mock_catalog.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) - fake_dev_env.check_image_availability.assert_called_once_with(mock_platform.tool_images) - - expected_tools = [ - ["build system", "axemsolutions/cmake:latest", "Image is available in the registry."], - ["toolchain", "axemsolutions/llvm:latest", "Image is available in the registry."], - ["debugger", "axemsolutions/pemicro:latest", "Image is available in the registry."], - ["deployer", "axemsolutions/pemicro:latest", "Image is available in the registry."], - ["test framework", "axemsolutions/unity:latest", "Image is available in the registry."], - ] - assert get_expected_table(expected_tools) == runner_result.stdout \ No newline at end of file + mock_dev_env.assign_tool_image_instances.assert_called_once_with(mock_platform.tool_images) + mock_print_info.assert_called_once_with(mock_dev_env) \ No newline at end of file diff --git a/tests/cli/test_list_cmd.py b/tests/cli/test_list_cmd.py index 7ea1e26f..9796cb1e 100644 --- a/tests/cli/test_list_cmd.py +++ b/tests/cli/test_list_cmd.py @@ -12,8 +12,6 @@ import io from rich.console import Console from rich.table import Table -from dem.core.dev_env import DevEnv, DevEnv -from dem.core.tool_images import ToolImages ## Global test variables @@ -25,41 +23,44 @@ ## Test listing the local dev envs. -def test_with_valid_dev_env_json(): +@patch("dem.cli.command.list_cmd.stdout.print") +@patch("dem.cli.command.list_cmd.get_local_dev_env_status") +@patch("dem.cli.command.list_cmd.Table") +def test_with_valid_dev_env_json(mock_Table: MagicMock, mock_get_local_dev_env_status: MagicMock, + mock_stdout_print: MagicMock) -> None: # Test setup mock_platform = MagicMock() expected_dev_env_list = [ ["demo", "Installed."], ["nagy_cica_project", "[red]Error: Required image is not available![/]"] ] - fake_image_statuses = [ - [ToolImages.LOCAL_AND_REGISTRY] * 5, - [ToolImages.NOT_AVAILABLE] * 5 - ] fake_dev_envs = [] - for idx, expected_dev_env in enumerate(expected_dev_env_list): - fake_dev_env = MagicMock(spec=DevEnv) + for expected_dev_env in expected_dev_env_list: + fake_dev_env = MagicMock() fake_dev_env.name = expected_dev_env[0] - fake_dev_env.check_image_availability.return_value = fake_image_statuses[idx] fake_dev_envs.append(fake_dev_env) mock_platform.local_dev_envs = fake_dev_envs main.platform = mock_platform + mock_table = MagicMock() + mock_Table.return_value = mock_table + + mock_get_local_dev_env_status.side_effect = [ + expected_dev_env_list[0][1], + expected_dev_env_list[1][1] + ] + # Run unit under test runner_result = runner.invoke(main.typer_cli, ["list", "--local", "--env"]) # Check expectations assert 0 == runner_result.exit_code - expected_table = Table() - expected_table.add_column("Development Environment") - expected_table.add_column("Status") - expected_table.add_row(*expected_dev_env_list[0]) - expected_table.add_row(*expected_dev_env_list[1]) - console = Console(file=io.StringIO()) - console.print(expected_table) - expected_output = console.file.getvalue() - assert expected_output == runner_result.stdout + mock_Table.assert_called_once() + mock_table.add_column.assert_has_calls([call("Development Environment"), call("Status")]) + mock_table.add_row.assert_has_calls([call(*(expected_dev_env_list[0])), + call(*(expected_dev_env_list[1]))]) + mock_stdout_print.assert_called_once_with(mock_table) def test_with_empty_dev_env_json(): # Test setup @@ -113,7 +114,8 @@ def test_with_empty_catalog(): console.print("[yellow]No Development Environments are available in the catalogs.[/]") assert console.file.getvalue() == runner_result.stdout -def test_with_valid_dev_env_org_json(): +@patch("dem.cli.command.list_cmd.get_catalog_dev_env_status") +def test_with_valid_dev_env_org_json(mock_get_catalog_dev_env_status: MagicMock) -> None: # Test setup mock_platform = MagicMock() main.platform = mock_platform @@ -124,23 +126,23 @@ def test_with_valid_dev_env_org_json(): ["nagy_cica_project", "Incomplete local install. The missing images are available in the registry. Use `dem pull` to reinstall."], ["unavailable_image_env", "[red]Error: Required image is not available in the registry![/]"] ] - fake_image_statuses = [ - [ToolImages.REGISTRY_ONLY] * 5, - [ToolImages.LOCAL_AND_REGISTRY] * 6, - [ToolImages.LOCAL_AND_REGISTRY, ToolImages.REGISTRY_ONLY], - [ToolImages.NOT_AVAILABLE] * 4 - ] fake_catalog_dev_envs = [] - for idx, expected_dev_env in enumerate(expected_dev_env_list): - fake_dev_env = MagicMock(spec=DevEnv) + for expected_dev_env in expected_dev_env_list: + fake_dev_env = MagicMock() fake_dev_env.name = expected_dev_env[0] - fake_dev_env.check_image_availability.return_value = fake_image_statuses[idx] fake_catalog_dev_envs.append(fake_dev_env) mock_catalog = MagicMock() mock_platform.dev_env_catalogs.catalogs = [mock_catalog] mock_catalog.dev_envs = fake_catalog_dev_envs mock_platform.get_dev_env_by_name.side_effect = [None, MagicMock(), MagicMock()] + mock_get_catalog_dev_env_status.side_effect = [ + expected_dev_env_list[0][1], + expected_dev_env_list[1][1], + expected_dev_env_list[2][1], + expected_dev_env_list[3][1] + ] + # Run unit under test runner_result = runner.invoke(main.typer_cli, ["list", "--all", "--env"]) @@ -297,17 +299,4 @@ def test_no_registries_available(mock_stdout_print: MagicMock): # Check expectations assert 0 == runner_result.exit_code - mock_stdout_print.assert_called_once_with("[yellow]No registries are available!") - -def test_get_local_dev_env_status_reinstall(): - # Test setup - mock_dev_env = MagicMock() - mock_tool_images = MagicMock() - - mock_dev_env.check_image_availability.return_value = [list_cmd.ToolImages.REGISTRY_ONLY] - - # Run unit under test - actual_dev_env_status = list_cmd.get_local_dev_env_status(mock_dev_env, mock_tool_images) - - # Check expectations - assert actual_dev_env_status is list_cmd.dev_env_local_status_messages[list_cmd.DEV_ENV_LOCAL_REINSTALL] \ No newline at end of file + mock_stdout_print.assert_called_once_with("[yellow]No registries are available!") \ No newline at end of file diff --git a/tests/cli/test_modify_cmd.py b/tests/cli/test_modify_cmd.py index 9e3821e1..1803a244 100644 --- a/tests/cli/test_modify_cmd.py +++ b/tests/cli/test_modify_cmd.py @@ -18,29 +18,6 @@ # In order to test stdout and stderr separately, the stderr can't be mixed into the stdout. runner = CliRunner(mix_stderr=False) -def test_get_tool_image_list(): - # Test setup - mock_tool_images = MagicMock() - mock_tool_images.registry.elements = [ - "local_and_registry_image", - "registry_image", - ] - mock_tool_images.local.elements = [ - "local_image", - "local_and_registry_image", - ] - - # Run unit under test - actual_tool_images = modify_cmd.get_tool_image_list(mock_tool_images) - - # Check expectations - expected_tool_iamges = [ - ["local_and_registry_image", "local and registry"], - ["registry_image", "registry"], - ["local_image", "local"], - ] - assert actual_tool_images == expected_tool_iamges - @patch("dem.cli.command.modify_cmd.SelectMenu") def test_get_confirm_from_user(mock_SelectMenu): # Test setup @@ -110,152 +87,146 @@ def test_handle_user_confirm_save_as_already_exist(mock_prompt): with pytest.raises(typer.Abort): modify_cmd.handle_user_confirm("save as", mock_dev_env_local, mock_platform) - def test_handle_user_confirm_cancel(): # Test setup # Run unit under test with pytest.raises(typer.Abort): modify_cmd.handle_user_confirm("cancel", MagicMock(), MagicMock()) -@patch("dem.cli.command.modify_cmd.handle_user_confirm") -@patch("dem.cli.command.modify_cmd.get_confirm_from_user") -@patch("dem.cli.command.modify_cmd.get_modifications_from_user") -@patch("dem.cli.command.modify_cmd.get_tool_image_list") -def test_open_modify_panel(mock_get_tool_image_list, mock_get_modifications_from_user, - mock_get_confirm_from_user, mock_handle_user_confirm): +def test_get_already_selected_tool_images() -> None: # Test setup - mock_platform = MagicMock() mock_dev_env = MagicMock() - - mock_tool_images = MagicMock() - mock_platform.tool_images = mock_tool_images - - mock_tool_image_list = MagicMock() - mock_get_tool_image_list.return_value = mock_tool_image_list - - mock_confirmation = MagicMock() - mock_get_confirm_from_user.return_value = mock_confirmation + mock_dev_env.tool_image_descriptors = [ + {"image_name": "test1", "image_version": "1.0"}, + {"image_name": "test2", "image_version": "2.0"} + ] # Run unit under test - modify_cmd.open_modify_panel(mock_platform, mock_dev_env) + actual_selected_tool_images = modify_cmd.get_already_selected_tool_images(mock_dev_env) # Check expectations - mock_get_tool_image_list.assert_called_once_with(mock_tool_images) - mock_get_modifications_from_user.assert_called_once_with(mock_dev_env, - mock_tool_image_list) - mock_get_confirm_from_user.assert_called_once() - mock_handle_user_confirm.assert_called_once_with(mock_confirmation, mock_dev_env, mock_platform) + expected_selected_tool_images = ["test1:1.0", "test2:2.0"] + assert actual_selected_tool_images == expected_selected_tool_images -def test_modify_single_tool_new_item() -> None: +@patch("dem.cli.command.modify_cmd.stderr.print") +@patch("dem.cli.command.modify_cmd.typer.confirm") +def test_remove_missing_tool_images(mock_confirm: MagicMock, mock_stderr_print: MagicMock) -> None: # Test setup - mock_dev_env = MagicMock() - mock_dev_env.tools = [] - mock_platform = MagicMock() - test_tool_type = "test_tool_type" - test_tool_image_name = "test_tool_image_name" - test_tool_image_version = "test_tool_image_version" - test_tool_image = test_tool_image_name + ":" + test_tool_image_version - - mock_platform.tool_images.registry.elements = [test_tool_image] - + test_all_tool_images = { + "test1:1.0": MagicMock(), + "test2:2.0": MagicMock() + } + test_already_selected_tool_images = ["test1:1.0", "test2:2.0", "test3:3.0"] + mock_confirm.side_effect = Exception("abort") + # Run unit under test - modify_cmd.modify_single_tool(mock_platform, mock_dev_env, test_tool_type, - test_tool_image) + with pytest.raises(Exception): + modify_cmd.remove_missing_tool_images(test_all_tool_images, test_already_selected_tool_images) # Check expectations - assert mock_dev_env.tools[0]["type"] == test_tool_type - assert mock_dev_env.tools[0]["image_name"] == test_tool_image_name - assert mock_dev_env.tools[0]["image_version"] == test_tool_image_version + assert "test3:3.0" not in test_already_selected_tool_images - mock_platform.container_engine.pull.assert_called_once_with(test_tool_image) - mock_platform.flush_descriptors.assert_called_once() + mock_stderr_print.assert_called_once_with("[red]The test3:3.0 is not available anymore.[/]") + mock_confirm.assert_called_once_with("By continuing, the missing tool images will be removed from the Development Environment.", + abort=True) -def test_modify_single_tool_overwrite_item() -> None: +@patch("dem.cli.command.modify_cmd.DevEnvSettingsWindow") +def test_open_dev_env_settings_panel(mock_DevEnvSettingsWindow: MagicMock) -> None: # Test setup - mock_dev_env = MagicMock() - mock_dev_env.tools = [ - { - "type": "test_tool_type", - "image_name": "old_test_tool_image_name", - "image_version": "old_test_tool_image_version" - } - ] - mock_platform = MagicMock() - test_tool_type = "test_tool_type" - test_tool_image_name = "test_tool_image_name" - test_tool_image_version = "test_tool_image_version" - test_tool_image = test_tool_image_name + ":" + test_tool_image_version + mock_dev_env_settings_panel = MagicMock() + mock_DevEnvSettingsWindow.return_value = mock_dev_env_settings_panel + expected_selected_tool_images = ["test1:1.0", "test2:2.0"] + mock_dev_env_settings_panel.selected_tool_images = expected_selected_tool_images + mock_dev_env_settings_panel.cancel_save_menu.get_selection.return_value = "save" - mock_platform.tool_images.registry.elements = [test_tool_image] + mock_printable_tool_images = MagicMock() # Run unit under test - modify_cmd.modify_single_tool(mock_platform, mock_dev_env, test_tool_type, - test_tool_image) + actual_selected_tool_images = modify_cmd.open_dev_env_settings_panel(["test1:1.0", "test2:2.0"], + mock_printable_tool_images) # Check expectations - assert mock_dev_env.tools[0]["type"] == test_tool_type - assert mock_dev_env.tools[0]["image_name"] == test_tool_image_name - assert mock_dev_env.tools[0]["image_version"] == test_tool_image_version + mock_DevEnvSettingsWindow.assert_called_once_with(mock_printable_tool_images, + ["test1:1.0", "test2:2.0"]) + mock_dev_env_settings_panel.wait_for_user.assert_called_once() - mock_platform.container_engine.pull.assert_called_once_with(test_tool_image) - mock_platform.flush_descriptors.assert_called_once() + assert actual_selected_tool_images == expected_selected_tool_images -@patch("dem.cli.command.modify_cmd.stderr.print") -def test_modify_single_tool_invalid_image(mock_stderr_print: MagicMock) -> None: +@patch("dem.cli.command.modify_cmd.DevEnvSettingsWindow") +def test_open_dev_env_settings_panel_cancel(mock_DevEnvSettingsWindow: MagicMock) -> None: # Test setup - mock_dev_env = MagicMock() - mock_platform = MagicMock() - test_tool_type = "test_tool_type" - test_tool_image = "test_tool_image" + mock_dev_env_settings_panel = MagicMock() + mock_DevEnvSettingsWindow.return_value = mock_dev_env_settings_panel + expected_selected_tool_images = ["test1:1.0", "test2:2.0"] + mock_dev_env_settings_panel.selected_tool_images = expected_selected_tool_images + mock_dev_env_settings_panel.cancel_save_menu.get_selection.return_value = "cancel" - mock_platform.tool_images.local.elements = [] - mock_platform.tool_images.registry.elements = [] + mock_printable_tool_images = MagicMock() # Run unit under test - modify_cmd.modify_single_tool(mock_platform, mock_dev_env, test_tool_type, - test_tool_image) + with pytest.raises(typer.Abort): + modify_cmd.open_dev_env_settings_panel(["test1:1.0", "test2:2.0"], mock_printable_tool_images) # Check expectations - mock_platform.container_engine.pull.assert_not_called() - mock_platform.flush_descriptors.assert_not_called() - - mock_stderr_print.assert_called_once_with(f"[red]Error: The {test_tool_image} is not an available image.[/]") + mock_DevEnvSettingsWindow.assert_called_once_with(mock_printable_tool_images, + ["test1:1.0", "test2:2.0"]) + mock_dev_env_settings_panel.wait_for_user.assert_called_once() -@patch("dem.cli.command.modify_cmd.stderr.print") -def test_modify_single_tool_no_image(mock_stderr_print: MagicMock) -> None: +def test_update_dev_env() -> None: # Test setup mock_dev_env = MagicMock() - mock_platform = MagicMock() - test_tool_type = "test_tool_type" - test_tool_image = "" + mock_selected_tool_images = ["axem/test1:1.0", "test2:2.0"] # Run unit under test - modify_cmd.modify_single_tool(mock_platform, mock_dev_env, test_tool_type, - test_tool_image) + modify_cmd.update_dev_env(mock_dev_env, mock_selected_tool_images) # Check expectations - mock_platform.container_engine.pull.assert_not_called() - mock_platform.flush_descriptors.assert_not_called() - - mock_stderr_print.assert_called_once_with("[red]Error: The tool type and the tool image must be set together.[/]") + expected_tool_image_descriptors = [ + {"image_name": "axem/test1", "image_version": "1.0"}, + {"image_name": "test2", "image_version": "2.0"} + ] + assert mock_dev_env.tool_image_descriptors == expected_tool_image_descriptors -@patch("dem.cli.command.modify_cmd.stderr.print") -def test_modify_single_tool_no_type(mock_stderr_print: MagicMock) -> None: +@patch("dem.cli.command.modify_cmd.handle_user_confirm") +@patch("dem.cli.command.modify_cmd.get_confirm_from_user") +@patch("dem.cli.command.modify_cmd.update_dev_env") +@patch("dem.cli.command.modify_cmd.open_dev_env_settings_panel") +@patch("dem.cli.command.modify_cmd.convert_to_printable_tool_images") +@patch("dem.cli.command.modify_cmd.remove_missing_tool_images") +@patch("dem.cli.command.modify_cmd.get_already_selected_tool_images") +def test_modify_with_tui(mock_get_already_selected_tool_images: MagicMock, + mock_remove_missing_tool_images: MagicMock, + mock_convert_to_printable_tool_images: MagicMock, + mock_open_dev_env_settings_panel: MagicMock, + mock_update_dev_env: MagicMock, + mock_get_confirm_from_user: MagicMock, + mock_handle_user_confirm: MagicMock) -> None: # Test setup - mock_dev_env = MagicMock() mock_platform = MagicMock() - test_tool_type = "" - test_tool_image = "test_tool_image" + mock_dev_env = MagicMock() + + mock_already_selected_tool_images = MagicMock() + mock_get_already_selected_tool_images.return_value = mock_already_selected_tool_images + mock_printable_tool_images = MagicMock() + mock_convert_to_printable_tool_images.return_value = mock_printable_tool_images + mock_selected_tool_images = MagicMock() + mock_open_dev_env_settings_panel.return_value = mock_selected_tool_images + mock_confirmation = MagicMock() + mock_get_confirm_from_user.return_value = mock_confirmation # Run unit under test - modify_cmd.modify_single_tool(mock_platform, mock_dev_env, test_tool_type, - test_tool_image) + modify_cmd.modify_with_tui(mock_platform, mock_dev_env) # Check expectations - mock_platform.container_engine.pull.assert_not_called() - mock_platform.flush_descriptors.assert_not_called() - - mock_stderr_print.assert_called_once_with("[red]Error: The tool type and the tool image must be set together.[/]") + mock_get_already_selected_tool_images.assert_called_once_with(mock_dev_env) + mock_remove_missing_tool_images.assert_called_once_with(mock_platform.tool_images.all_tool_images, + mock_already_selected_tool_images) + mock_convert_to_printable_tool_images.assert_called_once_with(mock_platform.tool_images.all_tool_images) + mock_open_dev_env_settings_panel.assert_called_once_with(mock_already_selected_tool_images, + mock_printable_tool_images) + mock_update_dev_env.assert_called_once_with(mock_dev_env, mock_selected_tool_images) + mock_get_confirm_from_user.assert_called_once() + mock_handle_user_confirm.assert_called_once_with(mock_confirmation, mock_dev_env, mock_platform) def test_execute_invalid_name(): # Test setup @@ -276,50 +247,24 @@ def test_execute_invalid_name(): console.print("[red]The Development Environment doesn't exist.") assert console.file.getvalue() == runner_result.stderr -@patch("dem.cli.command.modify_cmd.modify_single_tool") -@patch("dem.cli.command.modify_cmd.typer.confirm") @patch("dem.cli.command.modify_cmd.stdout.print") -def test_execute_single_tool_dev_env_installed(mock_stdout_print: MagicMock, mock_confirm: MagicMock, - mock_modify_single_tool: MagicMock) -> None: - # Test setup - mock_platform = MagicMock() - test_dev_env_name = "test_dev_env_name" - test_tool_type = "test_tool_type" - test_tool_image = "test_tool_image" - mock_dev_env = MagicMock() - mock_dev_env.is_installed = True - - mock_platform.get_dev_env_by_name.return_value = mock_dev_env - - # Run unit under test - modify_cmd.execute(mock_platform, test_dev_env_name, test_tool_type, test_tool_image) - - # Check expectations - mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) - mock_stdout_print.assert_called_once_with("[yellow]The Development Environment is installed, so it can't be modified.[/]") - mock_confirm.assert_called_once_with("Do you want to uninstall it first?", abort=True) - mock_platform.uninstall_dev_env.assert_called_once_with(mock_dev_env) - mock_modify_single_tool.assert_called_once_with(mock_platform, mock_dev_env, test_tool_type, - test_tool_image) - -@patch("dem.cli.command.modify_cmd.open_modify_panel") -def test_execute_open_modify_panel(mock_open_modify_panel: MagicMock) -> None: +@patch("dem.cli.command.modify_cmd.modify_with_tui") +def test_execute(mock_modify_with_tui: MagicMock, mock_stdout_print: MagicMock) -> None: # Test setup mock_platform = MagicMock() test_dev_env_name = "test_dev_env_name" - test_tool_type = "" - test_tool_image = "" mock_dev_env = MagicMock() mock_dev_env.is_installed = False mock_platform.get_dev_env_by_name.return_value = mock_dev_env # Run unit under test - modify_cmd.execute(mock_platform, test_dev_env_name, test_tool_type, test_tool_image) + modify_cmd.execute(mock_platform, test_dev_env_name) # Check expectations mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) - mock_open_modify_panel.assert_called_once_with(mock_platform, mock_dev_env) + mock_modify_with_tui.assert_called_once_with(mock_platform, mock_dev_env) + mock_stdout_print.assert_called_once_with("[green]The Development Environment has been modified successfully![/]") @patch("dem.cli.command.modify_cmd.stderr.print") @patch("dem.cli.command.modify_cmd.typer.confirm") @@ -329,8 +274,6 @@ def test_execute_PlatformError(mock_stdout_print: MagicMock, mock_confirm: Magic # Test setup mock_platform = MagicMock() test_dev_env_name = "test_dev_env_name" - test_tool_type = "test_tool_type" - test_tool_image = "test_tool_image" mock_dev_env = MagicMock() mock_dev_env.is_installed = True @@ -339,7 +282,7 @@ def test_execute_PlatformError(mock_stdout_print: MagicMock, mock_confirm: Magic mock_platform.uninstall_dev_env.side_effect = modify_cmd.PlatformError(test_exception_text) # Run unit under test - modify_cmd.execute(mock_platform, test_dev_env_name, test_tool_type, test_tool_image) + modify_cmd.execute(mock_platform, test_dev_env_name) # Check expectations mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) diff --git a/tests/cli/test_run_cmd.py b/tests/cli/test_run_cmd.py index f5a91790..9af2d7f6 100644 --- a/tests/cli/test_run_cmd.py +++ b/tests/cli/test_run_cmd.py @@ -102,14 +102,14 @@ def test_execute(mock_handle_missing_tool_images): test_args = ["run", test_dev_env_name, test_tool_type, test_workspace_path, test_command] mock_platform = MagicMock() - mock_platform.tool_images.local.elements = ["test_image_name:test_image_version"] + mock_platform.tool_images.get_local_ones.return_value = { + "test_image_name:test_image_version": MagicMock() + } main.platform = mock_platform mock_dev_env_local = MagicMock() mock_platform.get_dev_env_by_name.return_value = mock_dev_env_local - main.Platform.update_tool_images_on_instantiation = True - - mock_dev_env_local.tools = [ + mock_dev_env_local.tool_image_descriptors = [ { "image_name": "test_image_name", "image_version": "test_image_version", @@ -127,10 +127,8 @@ def test_execute(mock_handle_missing_tool_images): # Check expectations assert 0 == runner_result.exit_code - assert main.Platform.update_tool_images_on_instantiation is False mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name) - mock_platform.tool_images.local.update.assert_called_once() expected_missing_tool_image = {"missing_image_name:missing_image_version"} mock_handle_missing_tool_images.assert_called_once_with(expected_missing_tool_image, diff --git a/tests/cli/tui/renderable/test_menu.py b/tests/cli/tui/renderable/test_menu.py index 8e8538c4..c15150b5 100644 --- a/tests/cli/tui/renderable/test_menu.py +++ b/tests/cli/tui/renderable/test_menu.py @@ -207,11 +207,11 @@ def test_HorizontalMenu_handle_user_input(mock_move_cursor, mock_key): ] mock_move_cursor.assert_has_calls(calls) -@patch.object(menu.CancelNextMenu, "add_row") +@patch.object(menu.CancelSaveMenu, "add_row") @patch.object(menu.HorizontalMenu, "__init__") -def test_CancelNextMenu(mock_super___init__, mock_add_row): +def test_CancelSaveMenu(mock_super___init__, mock_add_row): # Run unit under test - test_cancel_next_menu = menu.CancelNextMenu() + test_cancel_next_menu = menu.CancelSaveMenu() # Check expectations mock_super___init__.assert_called_once() @@ -219,11 +219,11 @@ def test_CancelNextMenu(mock_super___init__, mock_add_row): test_cancel_next_menu.cursor_off + test_cancel_next_menu.menu_items[1]) @patch.object(menu.HorizontalMenu, "__init__", MagicMock()) -@patch.object(menu.CancelNextMenu, "add_row", MagicMock()) +@patch.object(menu.CancelSaveMenu, "add_row", MagicMock()) @patch("dem.cli.tui.renderable.menu.key") -def test_CancelNextMenu_handle_user_input_enter(mock_key): +def test_CancelSaveMenu_handle_user_input_enter(mock_key): # Test setup - test_cancel_next_menu = menu.CancelNextMenu() + test_cancel_next_menu = menu.CancelSaveMenu() # Run unit under test test_cancel_next_menu.handle_user_input(mock_key.ENTER) @@ -232,12 +232,12 @@ def test_CancelNextMenu_handle_user_input_enter(mock_key): assert test_cancel_next_menu.is_selected is True @patch.object(menu.HorizontalMenu, "__init__", MagicMock()) -@patch.object(menu.CancelNextMenu, "add_row", MagicMock()) +@patch.object(menu.CancelSaveMenu, "add_row", MagicMock()) @patch("dem.cli.tui.renderable.menu.key", MagicMock()) @patch.object(menu.HorizontalMenu, "handle_user_input") -def test_CancelNextMenu_handle_user_input_else(mock_super_handle_user_input): +def test_CancelSaveMenu_handle_user_input_else(mock_super_handle_user_input): # Test setup - test_cancel_next_menu = menu.CancelNextMenu() + test_cancel_next_menu = menu.CancelSaveMenu() fake_input = MagicMock() # Run unit under test @@ -246,166 +246,252 @@ def test_CancelNextMenu_handle_user_input_else(mock_super_handle_user_input): # Check expectations mock_super_handle_user_input.assert_called_once_with(fake_input) -@patch.object(menu.BackMenu, "add_row") -@patch.object(menu.HorizontalMenu, "__init__") -def test_BackMenu(mock_super___init__, mock_add_row): +@patch.object(menu.table.Table, "add_column") +@patch.object(menu.table.Table, "add_row") +@patch.object(menu.VerticalMenu, "__init__") +def test_ToolImageMenu(mock___init__: MagicMock, mock_add_row: MagicMock, + mock_add_column: MagicMock) -> None: + # Test setup + mock___init__.return_value = None + + mock_printable_tool_image1 = MagicMock() + mock_printable_tool_image1.name = "test_image1" + mock_printable_tool_image1.status = "test_available1" + mock_printable_tool_image2 = MagicMock() + mock_printable_tool_image2.name = "test_image2" + mock_printable_tool_image2.status = "test_available2" + test_printable_tool_images = [mock_printable_tool_image1, mock_printable_tool_image2] + + test_already_selected_tool_images = [ + mock_printable_tool_image1.name, + mock_printable_tool_image2.name + ] + # Run unit under test - test_back_next_menu = menu.BackMenu() + menu.ToolImageMenu(test_printable_tool_images, test_already_selected_tool_images) # Check expectations - mock_super___init__.assert_called_once() - mock_add_row.assert_called_once_with(test_back_next_menu.cursor_off + test_back_next_menu.menu_item) + mock___init__.assert_called_once() + mock_add_column.has_calls([ + call("Tool Image", no_wrap=True), + call("Available", no_wrap=True) + ]) + mock_add_row.has_calls([ + call(f"* [green]{mock_printable_tool_image1.name}[/]", mock_printable_tool_image1.status), + call(f" [green]{mock_printable_tool_image2.name}[/]", mock_printable_tool_image2.status) + ]) + +@patch.object(menu.ToolImageMenu, "__init__") +def test_handle_user_input_when_select(mock___init__: MagicMock) -> None: + # Test setup + mock___init__.return_value = None -@patch.object(menu.HorizontalMenu, "__init__", MagicMock()) -@patch.object(menu.BackMenu, "add_row", MagicMock()) -@patch("dem.cli.tui.renderable.menu.key") -def test_BackMenu_handle_user_input_enter(mock_key): + test_input = menu.key.ENTER + test_tool_image_menu = menu.ToolImageMenu([], []) + test_tool_image_menu.cursor_pos = 0 + mock_columns = [MagicMock()] + test_tool_image_menu.columns = mock_columns + test_tool_image_menu.columns[0]._cells = [] + test_tool_image_menu.columns[0]._cells.append("* test_image") + + # Run unit under test + test_tool_image_menu.handle_user_input(test_input) + + # Check expectations + assert "* [green]test_image[/]" == test_tool_image_menu.columns[0]._cells[test_tool_image_menu.cursor_pos] + + mock___init__.assert_called_once() + +@patch.object(menu.ToolImageMenu, "__init__") +def test_handle_user_input_when_deselect(mock___init__: MagicMock) -> None: # Test setup - test_back_next_menu = menu.BackMenu() + mock___init__.return_value = None + + test_input = menu.key.ENTER + test_tool_image_menu = menu.ToolImageMenu([], []) + test_tool_image_menu.cursor_pos = 0 + mock_columns = [MagicMock()] + test_tool_image_menu.columns = mock_columns + test_tool_image_menu.columns[0]._cells = [] + test_tool_image_menu.columns[0]._cells.append("* [green]test_image[/]") # Run unit under test - test_back_next_menu.handle_user_input(mock_key.ENTER) + test_tool_image_menu.handle_user_input(test_input) # Check expectations - assert test_back_next_menu.is_selected is True + assert "* test_image" in test_tool_image_menu.columns[0]._cells[test_tool_image_menu.cursor_pos] -@patch.object(menu.VerticalMenu, "add_row") -@patch.object(menu.ToolTypeMenu, "add_column") -@patch.object(menu.VerticalMenu, "__init__") -def test_ToolTypeMenu(mock_super___init__, mock_add_column, mock_add_row): + mock___init__.assert_called_once() + +@patch.object(menu.VerticalMenu, "handle_user_input") +@patch.object(menu.ToolImageMenu, "__init__") +def test_handle_user_input_when_other(mock___init__: MagicMock, mock_handle_user_input: MagicMock) -> None: # Test setup - test_supported_tool_types = [ - "test1", - "test2", - "test3", - "test4", - "test5", - ] + mock___init__.return_value = None + + test_input = "test_input" + test_tool_image_menu = menu.ToolImageMenu([], []) # Run unit under test - test_tool_type_menu = menu.ToolTypeMenu(test_supported_tool_types) + test_tool_image_menu.handle_user_input(test_input) # Check expectations - mock_super___init__.assert_called_once() - - calls = [ - call("Tool types"), - call("Selected"), - ] - mock_add_column.assert_has_calls(calls) - - calls = [] - for index, test_tool_type in enumerate(test_supported_tool_types): - if (index == 0): - calls.append(call(test_tool_type_menu.cursor_on + test_tool_type, - test_tool_type_menu.not_selected)) - else: - calls.append(call(test_tool_type_menu.cursor_off + test_tool_type, - test_tool_type_menu.not_selected)) - mock_add_row.assert_has_calls(calls) - -def test_ToolTypeMenu_preset_selection(): + mock___init__.assert_called_once() + mock_handle_user_input.assert_called_once_with(test_input) + +@patch.object(menu.ToolImageMenu, "__init__") +def test_get_selected_tool_images(mock___init__: MagicMock) -> None: # Test setup - test_supported_tool_types = [ - "test1", - "test2", - "test3", - "test4", - "test5", - ] - test_already_selected_tool_types = [ - "test1", - "test3", - "test5", - ] + mock___init__.return_value = None - test_tool_type_menu = menu.ToolTypeMenu(test_supported_tool_types) + test_tool_image_menu = menu.ToolImageMenu([], []) + mock_columns = [MagicMock()] + test_tool_image_menu.columns = mock_columns + test_tool_image_menu.columns[0]._cells = [] + test_tool_image_menu.columns[0]._cells.append("* [green]test_image1[/]") + test_tool_image_menu.columns[0]._cells.append(" test_image2") + test_tool_image_menu.columns[0]._cells.append(" [green]test_image3[/]") # Run unit under test - test_tool_type_menu.preset_selection(test_already_selected_tool_types) + actual_selected_tool_images = test_tool_image_menu.get_selected_tool_images() # Check expectations - expected_selection = [ - test_tool_type_menu.selected, - test_tool_type_menu.not_selected, - test_tool_type_menu.selected, - test_tool_type_menu.not_selected, - test_tool_type_menu.selected, - ] - for row_idx, cell in enumerate(test_tool_type_menu.columns[1]._cells): - assert cell == expected_selection[row_idx] + mock___init__.assert_called_once() -def test_ToolTypeMenu_toggle_select(): + assert ["test_image1", "test_image3"] == actual_selected_tool_images + +@patch("dem.cli.tui.renderable.menu.align.Align") +@patch.object(menu.VerticalMenu, "add_row") +@patch.object(menu.VerticalMenu, "__init__") +def test_SelectMenu(mock___init__: MagicMock, mock_add_row: MagicMock, mock_Align: MagicMock) -> None: # Test setup - test_supported_tool_types = [ - "test1", - ] + mock___init__.return_value = None - test_tool_type_menu = menu.ToolTypeMenu(test_supported_tool_types) + mock_alignment = MagicMock() + mock_Align.return_value = mock_alignment - # Run unit under test - not selected -> selected - test_tool_type_menu.toggle_select() + test_selection = ["test1", "test2"] + + # Run unit under test + actual_select_menu = menu.SelectMenu(test_selection) # Check expectations - assert test_tool_type_menu.columns[1]._cells[test_tool_type_menu.cursor_pos] is test_tool_type_menu.selected + assert actual_select_menu.show_edge is False + assert actual_select_menu.show_lines is False + + mock___init__.assert_called_once() + mock_add_row.assert_has_calls([ + call("* test1"), + call(" test2") + + ]) + mock_Align.assert_called_once_with(actual_select_menu, align="center", vertical="middle") + +@patch.object(menu.SelectMenu, "move_cursor") +@patch("dem.cli.tui.renderable.menu.readkey") +@patch("dem.cli.tui.renderable.menu.live.Live") +@patch.object(menu.SelectMenu, "__init__") +def test_SelectMenu_wait_for_user(mock___init__: MagicMock, mock_Live: MagicMock, + mock_readkey: MagicMock, mock_move_cursor: MagicMock) -> None: + # Test setup + mock___init__.return_value = None + + mock_readkey.side_effect = [menu.key.UP, menu.key.DOWN, menu.key.ENTER] + mock_alignment = MagicMock() - # Run unit under test - selected -> not selected - test_tool_type_menu.toggle_select() + test_select_menu = menu.SelectMenu([]) + test_select_menu.alignment = mock_alignment + + # Run unit under test + test_select_menu.wait_for_user() # Check expectations - assert test_tool_type_menu.columns[1]._cells[test_tool_type_menu.cursor_pos] is test_tool_type_menu.not_selected + mock_Live.assert_called_once_with(mock_alignment, refresh_per_second=8, screen=True) + mock_move_cursor.assert_has_calls([ + call(test_select_menu.CURSOR_UP), + call(test_select_menu.CURSOR_DOWN) + ]) -@patch.object(menu.VerticalMenu, "__init__", MagicMock()) -@patch.object(menu.ToolTypeMenu, "add_column", MagicMock()) -@patch.object(menu.ToolTypeMenu, "toggle_select") -@patch("dem.cli.tui.renderable.menu.key") -def test_ToolTypeMenu_handle_user_input_enter(mock_key, mock_toggle_select): +def test_get_selected() -> None: # Test setup - test_tool_type_menu = menu.ToolTypeMenu([]) + test_select_menu = menu.SelectMenu([]) + test_select_menu.cursor_pos = 1 + test_select_menu.columns = [MagicMock()] + test_select_menu.columns[0]._cells = ["* test1", " test2"] # Run unit under test - test_tool_type_menu.handle_user_input(mock_key.ENTER) + actual_selected = test_select_menu.get_selected() # Check expectations - mock_toggle_select.assert_called_once() + assert "test2" == actual_selected -@patch.object(menu.VerticalMenu, "__init__", MagicMock()) -@patch.object(menu.ToolTypeMenu, "add_column", MagicMock()) -@patch("dem.cli.tui.renderable.menu.key", MagicMock()) -@patch.object(menu.VerticalMenu, "handle_user_input") -def test_ToolTypeMenu_handle_user_input_else(mock_super_handle_user_input): +@patch.object(menu.VerticalMenu, "set_title") +def test_set_title(mock_set_title: MagicMock) -> None: # Test setup - test_tool_type_menu = menu.ToolTypeMenu([]) - fake_input = MagicMock() + test_select_menu = menu.SelectMenu([]) + test_select_menu.width = 0 + test_title = "test_title" # Run unit under test - test_tool_type_menu.handle_user_input(fake_input) + test_select_menu.set_title(test_title) - # # Check expectations - mock_super_handle_user_input.assert_called_once_with(fake_input) + # Check expectations + assert test_select_menu.width == len(test_title) -def test_ToolTypeMenu_get_selected_tool_types(): + mock_set_title.assert_called_once_with(test_title) + +@patch("dem.cli.tui.renderable.menu.align.Align") +@patch("dem.cli.tui.renderable.menu.table.Table") +@patch.object(menu.panel.Panel, "__init__") +def test_DevEnvStatusPanel(mock___init__: MagicMock, mock_Table: MagicMock, mock_Align: MagicMock) -> None: # Test setup - test_supported_tool_types = [ - "test1", - "test2", - "test3", - "test4", - "test5", - ] + mock___init__.return_value = None - test_tool_type_menu = menu.ToolTypeMenu(test_supported_tool_types) - test_tool_type_menu.columns[1]._cells[0] = test_tool_type_menu.selected - test_tool_type_menu.columns[1]._cells[2] = test_tool_type_menu.selected - test_tool_type_menu.columns[1]._cells[4] = test_tool_type_menu.selected + mock_outer_table = MagicMock() + mock_Table.return_value = mock_outer_table + + mock_aligned_renderable = MagicMock() + mock_Align.return_value = mock_aligned_renderable + + test_already_selected_tool_images = ["test1", "test2"] # Run unit under test - actual_selected_tool_types = test_tool_type_menu.get_selected_tool_types() + test_dev_env_status_panel = menu.DevEnvStatusPanel(test_already_selected_tool_images) # Check expectations - expected_selected_tool_types = [ - "test1", - "test3", - "test5", + mock___init__.assert_called_once() + mock_Table.assert_called_once_with(box=None) + mock___init__.assert_called_once_with(mock_outer_table, title="Development Environment", + expand=True) + mock_Align.assert_called_once_with(test_dev_env_status_panel, vertical="middle") + mock_outer_table.add_row.assert_has_calls = [ + call(test_already_selected_tool_images[0]), + call(test_already_selected_tool_images[1]) ] - assert actual_selected_tool_types == expected_selected_tool_types + +@patch("dem.cli.tui.renderable.menu.table.Table") +@patch.object(menu.DevEnvStatusPanel, "__init__") +def test_DevEnvStatusPanel_update_table(mock___init__: MagicMock, mock_Table: MagicMock) -> None: + # Test setup + mock___init__.return_value = None + + test_dev_env_status_panel = menu.DevEnvStatusPanel([]) + mock_outer_table = MagicMock() + mock_Table.return_value = mock_outer_table + + test_dev_env_status_panel.renderable = None + + test_selected_tool_images = ["test1", "test2"] + + # Run unit under test + test_dev_env_status_panel.update_table(test_selected_tool_images) + + # Check expectations + assert test_dev_env_status_panel.renderable is mock_outer_table + + mock___init__.assert_called_once() + mock_Table.assert_called_once_with(box=None) + mock_outer_table.add_row.assert_has_calls = [ + call(test_selected_tool_images[0]), + call(test_selected_tool_images[1]) + ] \ No newline at end of file diff --git a/tests/core/test_dev_env.py b/tests/core/test_dev_env.py index 3c6f6ad8..61efb088 100644 --- a/tests/core/test_dev_env.py +++ b/tests/core/test_dev_env.py @@ -23,7 +23,7 @@ def test_DevEnv() -> None: # Check expectations assert test_dev_env.name is test_descriptor["name"] - assert test_dev_env.tools is test_descriptor["tools"] + assert test_dev_env.tool_image_descriptors is test_descriptor["tools"] @patch("dem.core.dev_env.json.load") @patch("dem.core.dev_env.open") @@ -47,7 +47,7 @@ def test_DevEnv_with_descriptor_path(mock_path_exists: MagicMock, mock_open: Mag # Check expectations assert test_dev_env.name is test_descriptor["name"] - assert test_dev_env.tools is test_descriptor["tools"] + assert test_dev_env.tool_image_descriptors is test_descriptor["tools"] assert test_dev_env.is_installed is True mock_path_exists.assert_called_once_with(test_descriptor_path) @@ -85,7 +85,7 @@ def test_DevEnv_with_descriptor_and_descriptor_path() -> None: # Check expectations assert str(exc_info.value) == "Only one of the arguments can be not None." -def test_DevEnv_check_image_availability(): +def test_DevEnv_assign_tool_image_instances() -> None: # Test setup test_descriptor = { "name": "test_name", @@ -102,89 +102,34 @@ def test_DevEnv_check_image_availability(): { "image_name": "test_image_name3", "image_version": "test_image_tag3" - }, - { - "image_name": "test_image_name4", - "image_version": "test_image_tag4" - }, + } ] } - mock_tool_images = MagicMock() - mock_tool_images.local.elements = [ - "test_image_name1:test_image_tag1", - "test_image_name2:test_image_tag2" - ] - mock_tool_images.registry.elements = [ - "test_image_name1:test_image_tag1", - "test_image_name3:test_image_tag3" - ] test_dev_env = dev_env.DevEnv(test_descriptor) - # Run unit under test - actual_image_statuses = test_dev_env.check_image_availability(mock_tool_images, True) - - # Check expectations - expected_statuses = [ - dev_env.ToolImages.LOCAL_AND_REGISTRY, - dev_env.ToolImages.LOCAL_ONLY, - dev_env.ToolImages.REGISTRY_ONLY, - dev_env.ToolImages.NOT_AVAILABLE - ] - assert expected_statuses == actual_image_statuses - for idx, tool in enumerate(test_dev_env.tools): - assert expected_statuses[idx] == tool["image_status"] - - - mock_tool_images.local.update.assert_called_once() - mock_tool_images.registry.update.assert_called_once() - - -def test_DevEnv_check_image_availability_local_only(): - # Test setup - test_descriptor = { - "name": "test_name", - "installed": "True", - "tools": [ - { - "image_name": "test_image_name1", - "image_version": "test_image_tag1" - }, - { - "image_name": "test_image_name2", - "image_version": "test_image_tag2" - }, - { - "image_name": "test_image_name3", - "image_version": "test_image_tag3" - }, - { - "image_name": "test_image_name4", - "image_version": "test_image_tag4" - }, - ] - } mock_tool_images = MagicMock() - mock_tool_images.local.elements = [ - "test_image_name1:test_image_tag1", - "test_image_name2:test_image_tag2" - ] - test_dev_env = dev_env.DevEnv(test_descriptor) + mock_tool_image1 = MagicMock() + mock_tool_image1.name = "test_image_name1:test_image_tag1" + mock_tool_image2 = MagicMock() + mock_tool_image2.name = "test_image_name2:test_image_tag2" + mock_tool_image3 = MagicMock() + mock_tool_image3.name = "test_image_name3:test_image_tag3" + mock_tool_image4 = MagicMock() + mock_tool_image4.name = "test_image_name4:test_image_tag4" + mock_tool_images.all_tool_images = { + "test_image_name1:test_image_tag1": mock_tool_image1, + "test_image_name2:test_image_tag2": mock_tool_image2, + "test_image_name3:test_image_tag3": mock_tool_image3, + "test_image_name4:test_image_tag4": mock_tool_image4 + } # Run unit under test - actual_image_statuses = test_dev_env.check_image_availability(mock_tool_images, True, True) + test_dev_env.assign_tool_image_instances(mock_tool_images) # Check expectations - expected_statuses = [ - dev_env.ToolImages.LOCAL_ONLY, - dev_env.ToolImages.LOCAL_ONLY, - dev_env.ToolImages.NOT_AVAILABLE, - dev_env.ToolImages.NOT_AVAILABLE - ] - assert expected_statuses == actual_image_statuses - for idx, tool in enumerate(test_dev_env.tools): - assert expected_statuses[idx] == tool["image_status"] - - mock_tool_images.local.update.assert_called_once() + assert len(test_dev_env.tool_images) == 3 + for tool_image in test_dev_env.tool_images: + assert tool_image is mock_tool_images.all_tool_images[tool_image.name] def test_DevEnv_get_deserialized_is_installed_true() -> None: # Test setup diff --git a/tests/core/test_platform.py b/tests/core/test_platform.py index e6005923..ae451d4e 100644 --- a/tests/core/test_platform.py +++ b/tests/core/test_platform.py @@ -64,6 +64,20 @@ def test_Platfrom_load_dev_envs_invalid_version_expect_error(mock_LocalDevEnvJSO excepted_error_message = "Invalid file: The dev_env.json version v1.0 is not supported." assert str(exported_exception_info.value) == excepted_error_message +def test_assign_tool_image_instances_to_all_dev_envs() -> None: + # Test setup + mock_tool_images = MagicMock() + test_platform = platform.Platform() + test_platform._tool_images = mock_tool_images + mock_dev_env = MagicMock() + test_platform.local_dev_envs = [mock_dev_env] + + # Run unit under test + test_platform.assign_tool_image_instances_to_all_dev_envs() + + # Check expectations + mock_dev_env.assign_tool_image_instances.assert_called_once_with(mock_tool_images) + @patch("dem.core.platform.ToolImages") @patch.object(platform.Platform, "__init__") def test_Platform_tool_images(mock___init__: MagicMock, mock_ToolImages: MagicMock) -> None: @@ -72,12 +86,10 @@ def test_Platform_tool_images(mock___init__: MagicMock, mock_ToolImages: MagicMo mock_container_engine = MagicMock() mock_registries = MagicMock() - test_update_tool_images_on_instantiation = True test_platform = platform.Platform() test_platform._container_engine = mock_container_engine test_platform._registries = mock_registries - test_platform.update_tool_images_on_instantiation = test_update_tool_images_on_instantiation test_platform._tool_images = None mock_tool_images = MagicMock() @@ -91,8 +103,8 @@ def test_Platform_tool_images(mock___init__: MagicMock, mock_ToolImages: MagicMo assert test_platform._tool_images is mock_tool_images mock___init__.assert_called_once() - mock_ToolImages.assert_called_once_with(mock_container_engine, mock_registries, - test_update_tool_images_on_instantiation) + mock_ToolImages.assert_called_once_with(mock_container_engine, mock_registries) + mock_tool_images.update.assert_called_once() @patch("dem.core.platform.ContainerEngine") @patch.object(platform.Platform, "__init__") @@ -267,40 +279,30 @@ def test_Platform_get_dev_env_by_name_no_match(mock___init__: MagicMock) -> None mock___init__.assert_called_once() @patch.object(platform.Platform, "flush_descriptors") -@patch.object(platform.Platform, "tool_images") @patch.object(platform.Platform, "container_engine") @patch.object(platform.Platform, "user_output") @patch.object(platform.Platform, "__init__") def test_Platform_install_dev_env_succes(mock___init__: MagicMock, mock_user_input: MagicMock, - mock_container_engine: MagicMock, mock_tool_images, + mock_container_engine: MagicMock, mock_flush_descriptors: MagicMock) -> None: # Test setup mock___init__.return_value = None - mock_dev_env = MagicMock() - mock_dev_env.tools = [ - { - "image_name": "test_image_name0", - "image_version": "test_image_version0", - "image_status": platform.ToolImages.LOCAL_AND_REGISTRY - }, - { - "image_name": "test_image_name1", - "image_version": "test_image_version1", - "image_status": platform.ToolImages.REGISTRY_ONLY - }, - { - "image_name": "test_image_name2", - "image_version": "test_image_version2", - "image_status": platform.ToolImages.REGISTRY_ONLY - }, - { - "image_name": "test_image_name3", - "image_version": "test_image_version3", - "image_status": platform.ToolImages.LOCAL_ONLY - } - ] + mock_tool_image0 = MagicMock() + mock_tool_image0.name = "test_image_name0:test_image_version0" + mock_tool_image0.availability = platform.ToolImage.LOCAL_AND_REGISTRY + mock_tool_image1 = MagicMock() + mock_tool_image1.name = "test_image_name1:test_image_version1" + mock_tool_image1.availability = platform.ToolImage.REGISTRY_ONLY + mock_tool_image2 = MagicMock() + mock_tool_image2.name = "test_image_name2:test_image_version2" + mock_tool_image2.availability = platform.ToolImage.REGISTRY_ONLY + mock_tool_image3 = MagicMock() + mock_tool_image3.name = "test_image_name3:test_image_version3" + mock_tool_image3.availability = platform.ToolImage.LOCAL_ONLY + mock_dev_env.tool_images = [mock_tool_image0, mock_tool_image1, + mock_tool_image2, mock_tool_image3] test_platform = platform.Platform() @@ -310,9 +312,8 @@ def test_Platform_install_dev_env_succes(mock___init__: MagicMock, mock_user_inp # Check expectations mock___init__.assert_called_once() - mock_dev_env.check_image_availability.assert_called_once_with(mock_tool_images, False) expected_registry_only_tool_images: list[str] = ["test_image_name1:test_image_version1", - "test_image_name2:test_image_version2"] + "test_image_name2:test_image_version2"] mock_user_input.msg.assert_has_calls([ call(f"\nPulling image {expected_registry_only_tool_images[0]}", is_title=True), call(f"\nPulling image {expected_registry_only_tool_images[1]}", is_title=True) @@ -323,23 +324,19 @@ def test_Platform_install_dev_env_succes(mock___init__: MagicMock, mock_user_inp ]) mock_flush_descriptors.assert_called_once() -@patch.object(platform.Platform, "tool_images") @patch.object(platform.Platform, "container_engine") @patch.object(platform.Platform, "user_output") @patch.object(platform.Platform, "__init__") def test_Platform_install_dev_env_pull_failure(mock___init__: MagicMock, mock_user_output: MagicMock, - mock_container_engine: MagicMock,mock_tool_images) -> None: + mock_container_engine: MagicMock) -> None: # Test setup mock___init__.return_value = None - mock_dev_env = MagicMock() - mock_dev_env.tools = [ - { - "image_name": "test_image_name1", - "image_version": "test_image_version1", - "image_status": platform.ToolImages.REGISTRY_ONLY - } - ] + mock_dev_env = MagicMock() + mock_tool_image = MagicMock() + mock_tool_image.name = "test_image_name1:test_image_version1" + mock_tool_image.availability = platform.ToolImage.REGISTRY_ONLY + mock_dev_env.tool_images = [mock_tool_image] test_exception_text = "test_exception_text" mock_container_engine.pull.side_effect = platform.ContainerEngineError(test_exception_text) @@ -356,7 +353,6 @@ def test_Platform_install_dev_env_pull_failure(mock___init__: MagicMock, mock_us mock___init__.assert_called_once() - mock_dev_env.check_image_availability.assert_called_once_with(mock_tool_images, False) expected_registry_only_tool_image = "test_image_name1:test_image_version1" mock_user_output.msg.assert_called_once_with(f"\nPulling image {expected_registry_only_tool_image}", is_title=True) @@ -366,19 +362,18 @@ def test_Platform_install_dev_env_pull_failure(mock___init__: MagicMock, mock_us @patch.object(platform.Platform, "container_engine") @patch.object(platform.Platform, "user_output") @patch.object(platform.Platform, "__init__") -def test_Platform_install_dev_env_not_avilable(mock___init__: MagicMock, mock_user_output: MagicMock, - mock_container_engine: MagicMock,mock_tool_images) -> None: +def test_Platform_install_dev_env_with_not_avilable_tool_image(mock___init__: MagicMock, + mock_user_output: MagicMock, + mock_container_engine: MagicMock, + mock_tool_images) -> None: # Test setup mock___init__.return_value = None mock_dev_env = MagicMock() - mock_dev_env.tools = [ - { - "image_name": "test_image_name1", - "image_version": "test_image_version1", - "image_status": platform.ToolImages.NOT_AVAILABLE - } - ] + mock_tool_image = MagicMock() + mock_tool_image.name = "test_image_name1:test_image_version1" + mock_tool_image.availability = platform.ToolImage.NOT_AVAILABLE + mock_dev_env.tool_images = [mock_tool_image] test_platform = platform.Platform() @@ -391,8 +386,6 @@ def test_Platform_install_dev_env_not_avilable(mock___init__: MagicMock, mock_us mock___init__.assert_called_once() - mock_dev_env.check_image_availability.assert_called_once_with(mock_tool_images, False) - @patch.object(platform.Platform, "flush_descriptors") @patch.object(platform.Platform, "container_engine") @patch.object(platform.Platform, "__init__") @@ -409,7 +402,7 @@ def test_Platform_uninstall_dev_env_success(mock___init__: MagicMock, test_platform.local_dev_envs = [ mock_dev_env1, mock_dev_env2, mock_dev_env_to_uninstall ] - mock_dev_env1.tools = [ + mock_dev_env1.tool_image_descriptors = [ { "image_name": "test_image_name1", "image_version": "test_image_version1" @@ -420,14 +413,14 @@ def test_Platform_uninstall_dev_env_success(mock___init__: MagicMock, } ] mock_dev_env1.is_installed = True - mock_dev_env2.tools = [ + mock_dev_env2.tool_image_descriptors = [ { "image_name": "test_image_name3", "image_version": "test_image_version3" } ] mock_dev_env2.is_installed = True - mock_dev_env_to_uninstall.tools = [ + mock_dev_env_to_uninstall.tool_image_descriptors = [ { "image_name": "test_image_name1", "image_version": "test_image_version1" @@ -472,7 +465,7 @@ def test_Platform_uninstall_dev_env_with_duplicate_images(mock___init__: MagicMo test_platform.local_dev_envs = [ mock_dev_env1, mock_dev_env2, mock_dev_env_to_uninstall ] - mock_dev_env1.tools = [ + mock_dev_env1.tool_image_descriptors = [ { "image_name": "test_image_name1", "image_version": "test_image_version1" @@ -483,14 +476,14 @@ def test_Platform_uninstall_dev_env_with_duplicate_images(mock___init__: MagicMo } ] mock_dev_env1.is_installed = True - mock_dev_env2.tools = [ + mock_dev_env2.tool_image_descriptors = [ { "image_name": "test_image_name3", "image_version": "test_image_version3" } ] mock_dev_env2.is_installed = True - mock_dev_env_to_uninstall.tools = [ + mock_dev_env_to_uninstall.tool_image_descriptors = [ { "image_name": "test_image_name1", "image_version": "test_image_version1" @@ -537,7 +530,7 @@ def test_Platform_uninstall_dev_env_failure(mock___init__: MagicMock, test_platform.local_dev_envs = [ mock_dev_env1, mock_dev_env2, mock_dev_env_to_uninstall ] - mock_dev_env1.tools = [ + mock_dev_env1.tool_image_descriptors = [ { "image_name": "test_image_name1", "image_version": "test_image_version1" @@ -548,14 +541,14 @@ def test_Platform_uninstall_dev_env_failure(mock___init__: MagicMock, } ] mock_dev_env1.is_installed = True - mock_dev_env2.tools = [ + mock_dev_env2.tool_image_descriptors = [ { "image_name": "test_image_name3", "image_version": "test_image_version3" } ] mock_dev_env2.is_installed = True - mock_dev_env_to_uninstall.tools = [ + mock_dev_env_to_uninstall.tool_image_descriptors = [ { "image_name": "test_image_name1", "image_version": "test_image_version1" @@ -577,13 +570,13 @@ def test_Platform_uninstall_dev_env_failure(mock___init__: MagicMock, with pytest.raises(platform.PlatformError) as exported_exception_info: test_platform.uninstall_dev_env(mock_dev_env_to_uninstall) - # Check expectations - mock___init__.assert_called_once() + # Check expectations + mock___init__.assert_called_once() - assert str(exported_exception_info) == "Platform error: Dev Env uninstall failed. --> " - assert mock_dev_env_to_uninstall.is_installed == True + assert str(exported_exception_info.value) == "Platform error: Dev Env uninstall failed. --> Container engine error: " + assert mock_dev_env_to_uninstall.is_installed == True - mock_container_engine.remove.asssert_called_once_with("test_image_name4:test_image_version4") + mock_container_engine.remove.asssert_called_once_with("test_image_name4:test_image_version4") @patch.object(platform.Platform, "get_deserialized") @patch.object(platform.Platform, "__init__") @@ -724,6 +717,9 @@ def test_Platform_init_project(mock___init__: MagicMock, mock_get_dev_env_by_nam test_platform = platform.Platform() + mock_tool_images = MagicMock() + test_platform._tool_images = mock_tool_images + test_project_path = "test_project_path" mock_assigned_dev_env = MagicMock() mock_assigned_dev_env.name = "test_assigned_dev_env_name" @@ -744,6 +740,7 @@ def test_Platform_init_project(mock___init__: MagicMock, mock_get_dev_env_by_nam 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_assigned_dev_env.assign_tool_image_instances.assert_called_once_with(mock_tool_images) 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?") diff --git a/tests/core/test_tool_images.py b/tests/core/test_tool_images.py index 7514e532..d6b98b9e 100644 --- a/tests/core/test_tool_images.py +++ b/tests/core/test_tool_images.py @@ -5,73 +5,114 @@ import dem.core.tool_images as tool_images # Test framework -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock import pytest -from dem.core.exceptions import RegistryError +def test_ToolImage() -> None: + # Run unit under test + tool_image = tool_images.ToolImage("test_repo:test_tag") -def test_LocalToolImages(): - # Test setup - test_container_engine = MagicMock() - mock_elements = MagicMock() - test_container_engine.get_local_tool_images.return_value = mock_elements + # Check expectations + assert tool_image.name == "test_repo:test_tag" + assert tool_image.repository == "test_repo" + assert tool_image.tag == "test_tag" + assert tool_image.availability == tool_images.ToolImage.NOT_AVAILABLE + +def test_ToolImage_InvalidName() -> None: + # Test setup + test_name = "test_repo" # Run unit under test - local_tool_images = tool_images.LocalToolImages(test_container_engine) - local_tool_images.update() + with pytest.raises(tool_images.ToolImageError) as e: + tool_images.ToolImage(test_name) # Check expectations - test_container_engine.get_local_tool_images.assert_called_once() - assert local_tool_images.elements is mock_elements + assert str(e.value) == f"Invalid tool image name: {test_name}" -def test_RegistryToolImages(): +def test_ToolImages_update() -> None: # Test setup - test_registries = MagicMock() - mock_elements = MagicMock() - test_registries.list_repos.return_value = mock_elements + mock_container_engine = MagicMock() + mock_registries = MagicMock() + test_local_tool_images = ["local_tool_image_1:tag", + "local_tool_image_2:tag", + "local_and_registry_tool_image:tag"] + test_registry_tool_images = ["registry_tool_image_1:tag", + "registry_tool_image_2:tag", + "local_and_registry_tool_image:tag"] + + mock_container_engine.get_local_tool_images.return_value = test_local_tool_images + mock_registries.list_repos.return_value = test_registry_tool_images + + tool_images_instance = tool_images.ToolImages(mock_container_engine, mock_registries) # Run unit under test - registry_tool_images = tool_images.RegistryToolImages(test_registries) - registry_tool_images.update() + tool_images_instance.update() # Check expectations - test_registries.list_repos.assert_called_once() - assert registry_tool_images.elements is mock_elements + assert len(tool_images_instance.all_tool_images) == 5 + assert tool_images_instance.all_tool_images["local_tool_image_1:tag"].availability == tool_images.ToolImage.LOCAL_ONLY + assert tool_images_instance.all_tool_images["local_tool_image_2:tag"].availability == tool_images.ToolImage.LOCAL_ONLY + assert tool_images_instance.all_tool_images["registry_tool_image_1:tag"].availability == tool_images.ToolImage.REGISTRY_ONLY + assert tool_images_instance.all_tool_images["registry_tool_image_2:tag"].availability == tool_images.ToolImage.REGISTRY_ONLY + assert tool_images_instance.all_tool_images["local_and_registry_tool_image:tag"].availability == tool_images.ToolImage.LOCAL_AND_REGISTRY + + mock_container_engine.get_local_tool_images.assert_called_once() + mock_registries.list_repos.assert_called_once() -def test_RegistryToolImages_RegistryError(): +def test_ToolImages_get_local_ones() -> None: # Test setup - test_registries = MagicMock() - test_registries.list_repos.side_effect = RegistryError() + mock_container_engine = MagicMock() + mock_registries = MagicMock() + test_local_tool_images = ["local_tool_image_1:tag", + "local_tool_image_2:tag", + "local_and_registry_tool_image:tag"] + test_registry_tool_images = ["registry_tool_image_1:tag", + "registry_tool_image_2:tag", + "local_and_registry_tool_image:tag"] + + mock_container_engine.get_local_tool_images.return_value = test_local_tool_images + mock_registries.list_repos.return_value = test_registry_tool_images + + tool_images_instance = tool_images.ToolImages(mock_container_engine, mock_registries) + tool_images_instance.update() # Run unit under test - with pytest.raises(Exception): - registry_tool_images = tool_images.RegistryToolImages(test_registries) - registry_tool_images.update() + local_tool_images = tool_images_instance.get_local_ones() - # Check expectations - test_registries.list_repos.assert_called_once() - assert not registry_tool_images.elements + # Check expectations + assert len(local_tool_images) == 3 + assert "local_tool_image_1:tag" in local_tool_images + assert "local_tool_image_2:tag" in local_tool_images + assert "local_and_registry_tool_image:tag" in local_tool_images -@patch("dem.core.tool_images.RegistryToolImages") -@patch("dem.core.tool_images.LocalToolImages") -def test_ToolImages(mock_LocalToolImages: MagicMock, mock_RegistryToolImages: MagicMock): + mock_container_engine.get_local_tool_images.assert_called_once() + mock_registries.list_repos.assert_called_once() + +def test_ToolImages_get_registry_ones() -> None: # Test setup mock_container_engine = MagicMock() mock_registries = MagicMock() - mock_local_tool_images = MagicMock() - mock_LocalToolImages.return_value = mock_local_tool_images - mock_registry_tool_images = MagicMock() - mock_RegistryToolImages.return_value = mock_registry_tool_images + test_local_tool_images = ["local_tool_image_1:tag", + "local_tool_image_2:tag", + "local_and_registry_tool_image:tag"] + test_registry_tool_images = ["registry_tool_image_1:tag", + "registry_tool_image_2:tag", + "local_and_registry_tool_image:tag"] + + mock_container_engine.get_local_tool_images.return_value = test_local_tool_images + mock_registries.list_repos.return_value = test_registry_tool_images + + tool_images_instance = tool_images.ToolImages(mock_container_engine, mock_registries) + tool_images_instance.update() # Run unit under test - tool_images_obj = tool_images.ToolImages(mock_container_engine, mock_registries) + registry_tool_images = tool_images_instance.get_registry_ones() # Check expectations - mock_LocalToolImages.assert_called_once_with(mock_container_engine) - mock_RegistryToolImages.assert_called_once_with(mock_registries) - - mock_local_tool_images.update.assert_called_once() - mock_registry_tool_images.update.assert_called_once() + assert len(registry_tool_images) == 3 + assert "registry_tool_image_1:tag" in registry_tool_images + assert "registry_tool_image_2:tag" in registry_tool_images + assert "local_and_registry_tool_image:tag" in registry_tool_images - assert tool_images_obj.local is mock_local_tool_images - assert tool_images_obj.registry is mock_registry_tool_images \ No newline at end of file + mock_container_engine.get_local_tool_images.assert_called_once() + mock_registries.list_repos.assert_called_once() \ No newline at end of file diff --git a/tests/test__main__.py b/tests/test__main__.py index 435abf71..555431cc 100644 --- a/tests/test__main__.py +++ b/tests/test__main__.py @@ -34,7 +34,6 @@ def test_cli_success(mock_Platform: MagicMock, mock_cli_main: MagicMock, mock_Co mock_TUIUserOutput.assert_called_once() mock_Core.set_user_output.assert_called_once_with(mock_tui_user_output) mock_platform.config_file.update.assert_called_once() - mock_platform.load_dev_envs.assert_called_once() mock_cli_main.typer_cli.assert_called_once_with(prog_name=__command__) @patch("dem.__main__.stderr.print") @@ -63,7 +62,6 @@ def test_cli_LookupError(mock_Platform: MagicMock, mock_cli_main: MagicMock, moc mock_TUIUserOutput.assert_called_once() mock_Core.set_user_output.assert_called_once_with(mock_tui_user_output) mock_platform.config_file.update.assert_called_once() - mock_platform.load_dev_envs.assert_called_once() mock_cli_main.typer_cli.assert_called_once_with(prog_name=__command__) mock_stderr_print.assert_called_once_with("[red]" + test_exception_text + "[/]") @@ -93,7 +91,6 @@ def test_cli_RegistryError(mock_Platform: MagicMock, mock_cli_main: MagicMock, m mock_TUIUserOutput.assert_called_once() mock_Core.set_user_output.assert_called_once_with(mock_tui_user_output) mock_platform.config_file.update.assert_called_once() - mock_platform.load_dev_envs.assert_called_once() mock_cli_main.typer_cli.assert_called_once_with(prog_name=__command__) mock_stderr_print.assert_called_once_with("[red]" + test_exception_text + "\nUsing local tool images only![/]") @@ -126,7 +123,6 @@ def test_cli_DockerException_permission_denied(mock_Platform: MagicMock, mock_cl mock_TUIUserOutput.assert_called_once() mock_Core.set_user_output.assert_called_once_with(mock_tui_user_output) mock_platform.config_file.update.assert_called_once() - mock_platform.load_dev_envs.assert_called_once() mock_cli_main.typer_cli.assert_called_once_with(prog_name=__command__) mock_stderr_print.assert_called_once_with("[red]" + test_exception_text + "[/]") mock_stdout_print.assert_called_once_with("\nHint: Is your user part of the docker group?") @@ -161,7 +157,6 @@ def test_cli_DockerException_invalid_reference_format(mock_Platform: MagicMock, mock_TUIUserOutput.assert_called_once() mock_Core.set_user_output.assert_called_once_with(mock_tui_user_output) mock_platform.config_file.update.assert_called_once() - mock_platform.load_dev_envs.assert_called_once() mock_cli_main.typer_cli.assert_called_once_with(prog_name=__command__) mock_stderr_print.assert_called_once_with("[red]" + test_exception_text + "[/]") mock_stdout_print.assert_called_once_with("\nHint: The input repository might not exist in the registry.") @@ -194,7 +189,6 @@ def test_cli_DockerException_400(mock_Platform: MagicMock, mock_cli_main: MagicM mock_TUIUserOutput.assert_called_once() mock_Core.set_user_output.assert_called_once_with(mock_tui_user_output) mock_platform.config_file.update.assert_called_once() - mock_platform.load_dev_envs.assert_called_once() mock_cli_main.typer_cli.assert_called_once_with(prog_name=__command__) mock_stderr_print.assert_called_once_with("[red]" + test_exception_text + "[/]") mock_stdout_print.assert_called_once_with("\nHint: The input parameters might not be valid.") @@ -226,7 +220,6 @@ def test_cli_DockerException_unknown(mock_Platform: MagicMock, mock_cli_main: Ma mock_TUIUserOutput.assert_called_once() mock_Core.set_user_output.assert_called_once_with(mock_tui_user_output) mock_platform.config_file.update.assert_called_once() - mock_platform.load_dev_envs.assert_called_once() mock_cli_main.typer_cli.assert_called_once_with(prog_name=__command__) mock_stderr_print.assert_called_once_with("[red]" + test_exception_text + "[/]") @@ -257,7 +250,6 @@ def test_cli_ContainerEngineError(mock_Platform: MagicMock, mock_cli_main: Magic mock_TUIUserOutput.assert_called_once() mock_Core.set_user_output.assert_called_once_with(mock_tui_user_output) mock_platform.config_file.update.assert_called_once() - mock_platform.load_dev_envs.assert_called_once() mock_cli_main.typer_cli.assert_called_once_with(prog_name=__command__) mock_stderr_print.assert_called_once_with("[red]Container engine error: " + test_exception_text + "[/]") From c8fe0a64a66ab72ba93bd7a8a44d59dbba70b272 Mon Sep 17 00:00:00 2001 From: janosmurai Date: Thu, 28 Mar 2024 12:45:07 +0100 Subject: [PATCH 2/2] Docs updated according to the new Dev Env settings window (but images are not yet included). --- docs/commands.md | 25 +++---------------------- docs/quickstart.md | 2 +- mkdocs.yml | 7 ++++--- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index b72dd57e..8b56410b 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -36,12 +36,7 @@ Arguments: Create a new Development Environment descriptor and save it to the local descriptor storage (catalog). -Running this command will open up an interactive UI on the command line. Follow the steps below to -configure the new Environment. - -1. First you need to select the tool types. You can navigate with the :material-arrow-up: and -:material-arrow-down: or :material-alpha-k: and :material-alpha-j: keys. Select the required -tool types with :material-keyboard-space:. Select next if you finished the selection. +Running this command will open up the Dev Env settings window: ![tool select](wp-content/tool_select.png) @@ -49,8 +44,6 @@ tool types with :material-keyboard-space:. Select next if you finished the selec :material-arrow-up: and :material-arrow-down: or :material-alpha-k: and :material-alpha-j: keys. Select the required tool image and press :material-keyboard-return:. - ![image select](wp-content/image_select.png) - :info: After creation, the Development Environment can be installed with the `install` command. Arguments: @@ -71,7 +64,7 @@ will be asked if they want to overwrite it or not. Arguments: -`DEV_ENV_NAME` Name of the Development Environment, whose descriptor to clone. [required] +`DEV_ENV_NAME` Clone the descriptor of the Dev Env. [required] --- @@ -88,28 +81,16 @@ Arguments: ## **`dem modify DEV_ENV_NAME`** -Change a tool in a Development Environment. - -If the tool type is not specified, the Dev Env settings panel will be opened: - -1. The dem shows a list of the already selected tools. You can modify the selection. You can -navigate with the :material-arrow-up: and :material-arrow-down: or :material-alpha-k: and -:material-alpha-j: keys. Modify the required tool types with :material-keyboard-space:. Select next -when you're done with the selection. - - ![tool select](wp-content/tool_select.png) +Open the Development Environment settings window to modify the Development Environment descriptor. 2. Assign the required tool images for the selected types. You can navigate with the :material-arrow-up: and :material-arrow-down: or :material-alpha-k: and :material-alpha-j: keys. Select the required tool image and press :material-keyboard-return:. - ![image select](wp-content/image_select.png) Arguments: `DEV_ENV_NAME` Name of the Development Environment to modify. [required] -`[TOOL_TYPE]` The tool type to change. [optional] -`[TOOL_IMAGE]` The tool image to set for the tool type. [optional] --- diff --git a/docs/quickstart.md b/docs/quickstart.md index ea778dda..be984931 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -46,7 +46,7 @@ catalog, which you can modify to your needs. You might want to: - Add/remove tools. - Change the tool image for a given tool. -You can edit it with the Development Environment settings view: +You can edit it with the Development Environment settings window: dem modify DEV_ENV_NAME diff --git a/mkdocs.yml b/mkdocs.yml index ac2ada90..f4ee6301 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,7 +59,7 @@ extra: provider: mike copyright: > - Copyright 2023 - axem solutions - All rights reserved | + Copyright 2024 - axem solutions - All rights reserved | Change cookie settings | Made with Material for MkDocs @@ -75,8 +75,9 @@ markdown_extensions: - pymdownx.details - pymdownx.superfences - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + nav: - 'index.md'