From 47fc50576e7040680c19e152592b2c5e5cc297f5 Mon Sep 17 00:00:00 2001 From: Ruomei Yan Date: Wed, 2 Nov 2022 16:47:56 +0000 Subject: MLIA-649 Strip mlia backend management into a new command * add entry point for mlia-backend in setup.cfg and main.py * add --force option for install from path: uninstall existing backend in ML Inference Advisor and install from given path * add uninstall and list program parameters: uninstall has backend_name as input arg, install has backend_name as a mandatory argument * add unit tests in test_cli_commands.py, test_cli_main.py, test_tools_metadata_common.py, test_tools_metadata_corstone.py * updated README.md * remove --download option for installing backend * add new lines for the display section when we do mlia-backen list * add case insensitive support for backend names in command line argument Change-Id: Icb89d8957fa6be4b767710e24fa074f26472674b --- README.md | 22 +++++++--- setup.cfg | 1 + src/mlia/cli/commands.py | 41 +++++++++--------- src/mlia/cli/common.py | 3 +- src/mlia/cli/main.py | 81 +++++++++++++++++++++++++++-------- src/mlia/cli/options.py | 37 ++++++++-------- src/mlia/tools/metadata/common.py | 57 ++++++++++++++++++------ src/mlia/tools/metadata/corstone.py | 6 +++ tests/test_cli_commands.py | 77 +++++++++++++++++++-------------- tests/test_cli_main.py | 51 +++++++++++++++++----- tests/test_tools_metadata_common.py | 63 ++++++++++++++++++++++++--- tests/test_tools_metadata_corstone.py | 17 ++++++++ 12 files changed, 326 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 7d3726e..1b9f494 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,22 @@ The ML Inference Advisor is designed to support multiple performance estimators (backends) that could generate performance analysis for individual types of hardware. -The `backend` command is used to manage the installation of new backends. -The `install` sub-command can be used to either - -* install a backend installed locally already (option `--path`) or -* (if available) automatically download the necessary components and - dependencies, install them and configure them properly (option `--download`). +The `mlia-backend` command is used to manage the installation of new backends. + +* The `install` sub-command can be used after `mlia-backend` to: + * install a backend of ML Inference Advisor from a directory + which contains installed backend (option `--path`). The name (mandatory + argument `name`) of the backend can be case insensitive. If backend + is already installed, it is possible to use option `--force` + to force the installation + * (if available) automatically download the necessary components and + dependencies, install them and configure them properly (default behavior) + +* The `uninstall` sub-command can be used with option `backend_name` after + `mlia-backend` to remove the backend installation folder + +* The `list` sub-command can be used after `mlia-backend` to display + the installed and available for installation backends The usage is: diff --git a/setup.cfg b/setup.cfg index f262484..93ffa6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ where = src [options.entry_points] console_scripts = mlia=mlia.cli.main:main + mlia-backend=mlia.cli.main:backend_main [options.extras_require] dev = diff --git a/src/mlia/cli/commands.py b/src/mlia/cli/commands.py index 72ae4bb..4be7f3e 100644 --- a/src/mlia/cli/commands.py +++ b/src/mlia/cli/commands.py @@ -29,7 +29,6 @@ from mlia.api import PathOrFileLike from mlia.cli.config import get_installation_manager from mlia.cli.options import parse_optimization_parameters from mlia.utils.console import create_section_header -from mlia.utils.types import only_one_selected logger = logging.getLogger(__name__) @@ -243,34 +242,36 @@ def optimization( ) -def backend( - backend_action: str, +def backend_install( + name: str, path: Path | None = None, - download: bool = False, - name: str | None = None, i_agree_to_the_contained_eula: bool = False, noninteractive: bool = False, + force: bool = False, ) -> None: - """Backends configuration.""" + """Install configuration.""" logger.info(CONFIG) manager = get_installation_manager(noninteractive) - if backend_action == "status": - manager.show_env_details() + install_from_path = path is not None - if backend_action == "install": - install_from_path = path is not None + if install_from_path: + manager.install_from(cast(Path, path), name, force) + else: + eula_agreement = not i_agree_to_the_contained_eula + manager.download_and_install(name, eula_agreement) - if not only_one_selected(install_from_path, download): - raise Exception( - "Please select only one action: download or " - "provide path to the backend installation" - ) - if install_from_path: - manager.install_from(cast(Path, path), name) +def backend_uninstall( + name: str, +) -> None: + """Uninstall backend(s).""" + manager = get_installation_manager(noninteractive=True) + manager.uninstall(name) + - if download: - eula_agreement = not i_agree_to_the_contained_eula - manager.download_and_install(name, eula_agreement) +def backend_list() -> None: + """List backend status.""" + manager = get_installation_manager(noninteractive=True) + manager.show_env_details() diff --git a/src/mlia/cli/common.py b/src/mlia/cli/common.py index 3f60668..077f456 100644 --- a/src/mlia/cli/common.py +++ b/src/mlia/cli/common.py @@ -16,11 +16,12 @@ class CommandInfo: aliases: list[str] opt_groups: list[Callable[[argparse.ArgumentParser], None]] is_default: bool = False + name: str | None = None @property def command_name(self) -> str: """Return command name.""" - return self.func.__name__ + return self.name or self.func.__name__ @property def command_name_and_aliases(self) -> list[str]: diff --git a/src/mlia/cli/main.py b/src/mlia/cli/main.py index d36d2d9..61b8f05 100644 --- a/src/mlia/cli/main.py +++ b/src/mlia/cli/main.py @@ -12,14 +12,17 @@ from pathlib import Path from mlia import __version__ from mlia.cli.commands import all_tests -from mlia.cli.commands import backend +from mlia.cli.commands import backend_install +from mlia.cli.commands import backend_list +from mlia.cli.commands import backend_uninstall from mlia.cli.commands import operators from mlia.cli.commands import optimization from mlia.cli.commands import performance from mlia.cli.common import CommandInfo from mlia.cli.helpers import CLIActionResolver from mlia.cli.logging import setup_logging -from mlia.cli.options import add_backend_options +from mlia.cli.options import add_backend_install_options +from mlia.cli.options import add_backend_uninstall_options from mlia.cli.options import add_custom_supported_operators_options from mlia.cli.options import add_debug_options from mlia.cli.options import add_evaluation_options @@ -99,42 +102,66 @@ def get_commands() -> list[CommandInfo]: add_evaluation_options, ], ), + ] + + +def backend_commands() -> list[CommandInfo]: + """Return commands configuration.""" + return [ + CommandInfo( + backend_install, + [], + [ + add_backend_install_options, + add_debug_options, + ], + name="install", + ), CommandInfo( - backend, + backend_uninstall, [], [ - add_backend_options, + add_backend_uninstall_options, add_debug_options, ], + name="uninstall", + ), + CommandInfo( + backend_list, + [], + [ + add_debug_options, + ], + name="list", ), ] -def get_default_command() -> str | None: +def get_default_command(commands: list[CommandInfo]) -> str | None: """Get name of the default command.""" - commands = get_commands() - marked_as_default = [cmd.command_name for cmd in commands if cmd.is_default] assert len(marked_as_default) <= 1, "Only one command could be marked as default" return next(iter(marked_as_default), None) -def get_possible_command_names() -> list[str]: +def get_possible_command_names(commands: list[CommandInfo]) -> list[str]: """Get all possible command names including aliases.""" return [ name_or_alias - for cmd in get_commands() + for cmd in commands for name_or_alias in cmd.command_name_and_aliases ] -def init_commands(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: +def init_commands( + parser: argparse.ArgumentParser, commands: list[CommandInfo] +) -> argparse.ArgumentParser: """Init cli subcommands.""" subparsers = parser.add_subparsers(title="Commands", dest="command") subparsers.required = True - for command in get_commands(): + for command in commands: command_parser = subparsers.add_parser( command.command_name, aliases=command.aliases, @@ -188,7 +215,6 @@ def run_command(args: argparse.Namespace) -> int: try: logger.info(INFO_MESSAGE) - args.func(**func_args) return 0 except KeyboardInterrupt: @@ -251,12 +277,14 @@ def init_subcommand_parser(parent: argparse.ArgumentParser) -> argparse.Argument return parser -def add_default_command_if_needed(args: list[str]) -> None: +def add_default_command_if_needed( + args: list[str], input_commands: list[CommandInfo] +) -> None: """Add default command to the list of the arguments if needed.""" - default_command = get_default_command() + default_command = get_default_command(input_commands) if default_command and len(args) > 0: - commands = get_possible_command_names() + commands = get_possible_command_names(input_commands) help_or_version = ["-h", "--help", "-v", "--version"] command_is_missing = args[0] not in [*commands, *help_or_version] @@ -264,16 +292,31 @@ def add_default_command_if_needed(args: list[str]) -> None: args.insert(0, default_command) -def main(argv: list[str] | None = None) -> int: - """Entry point of the application.""" +def generic_main( + commands: list[CommandInfo], argv: list[str] | None = None +) -> argparse.Namespace: + """Enable multiple entry points.""" common_parser = init_common_parser() subcommand_parser = init_subcommand_parser(common_parser) - init_commands(subcommand_parser) + init_commands(subcommand_parser, commands) common_args, subcommand_args = common_parser.parse_known_args(argv) - add_default_command_if_needed(subcommand_args) + + add_default_command_if_needed(subcommand_args, commands) args = subcommand_parser.parse_args(subcommand_args, common_args) + return args + + +def main(argv: list[str] | None = None) -> int: + """Entry point of the main application.""" + args = generic_main(get_commands(), argv) + return run_command(args) + + +def backend_main(argv: list[str] | None = None) -> int: + """Entry point of the backend application.""" + args = generic_main(backend_commands(), argv) return run_command(args) diff --git a/src/mlia/cli/options.py b/src/mlia/cli/options.py index f6dcf75..bf2f09b 100644 --- a/src/mlia/cli/options.py +++ b/src/mlia/cli/options.py @@ -131,7 +131,7 @@ def add_custom_supported_operators_options(parser: argparse.ArgumentParser) -> N ) -def add_backend_options(parser: argparse.ArgumentParser) -> None: +def add_backend_install_options(parser: argparse.ArgumentParser) -> None: """Add options for the backends configuration.""" def valid_directory(param: str) -> Path: @@ -141,42 +141,39 @@ def add_backend_options(parser: argparse.ArgumentParser) -> None: return dir_path - subparsers = parser.add_subparsers(title="Backend actions", dest="backend_action") - subparsers.required = True - - install_subparser = subparsers.add_parser( - "install", help="Install backend", allow_abbrev=False - ) - install_type_group = install_subparser.add_mutually_exclusive_group() - install_type_group.required = True - install_type_group.add_argument( + parser.add_argument( "--path", type=valid_directory, help="Path to the installed backend" ) - install_type_group.add_argument( - "--download", + parser.add_argument( + "--i-agree-to-the-contained-eula", default=False, action="store_true", - help="Download and install backend", + help=argparse.SUPPRESS, ) - install_subparser.add_argument( - "--i-agree-to-the-contained-eula", + parser.add_argument( + "--force", default=False, action="store_true", - help=argparse.SUPPRESS, + help="Force reinstall backend in the specified path", ) - install_subparser.add_argument( + parser.add_argument( "--noninteractive", default=False, action="store_true", help="Non interactive mode with automatic confirmation of every action", ) - install_subparser.add_argument( + parser.add_argument( "name", - nargs="?", help="Name of the backend to install", ) - subparsers.add_parser("status", help="Show backends status") + +def add_backend_uninstall_options(parser: argparse.ArgumentParser) -> None: + """Add options for the backends configuration.""" + parser.add_argument( + "name", + help="Name of the installed backend", + ) def add_evaluation_options(parser: argparse.ArgumentParser) -> None: diff --git a/src/mlia/tools/metadata/common.py b/src/mlia/tools/metadata/common.py index dd4571a..927be74 100644 --- a/src/mlia/tools/metadata/common.py +++ b/src/mlia/tools/metadata/common.py @@ -65,6 +65,10 @@ class Installation(ABC): def install(self, install_type: InstallationType) -> None: """Install the backend.""" + @abstractmethod + def uninstall(self) -> None: + """Uninstall the backend.""" + InstallationFilter = Callable[[Installation], bool] @@ -106,20 +110,21 @@ class SearchByNameFilter: def __call__(self, installation: Installation) -> bool: """Installation filter.""" - return not self.backend_name or installation.name == self.backend_name + return ( + not self.backend_name + or installation.name.casefold() == self.backend_name.casefold() + ) class InstallationManager(ABC): """Helper class for managing installations.""" @abstractmethod - def install_from(self, backend_path: Path, backend_name: str | None) -> None: + def install_from(self, backend_path: Path, backend_name: str, force: bool) -> None: """Install backend from the local directory.""" @abstractmethod - def download_and_install( - self, backend_name: str | None, eula_agreement: bool - ) -> None: + def download_and_install(self, backend_name: str, eula_agreement: bool) -> None: """Download and install backends.""" @abstractmethod @@ -130,6 +135,10 @@ class InstallationManager(ABC): def backend_installed(self, backend_name: str) -> bool: """Return true if requested backend installed.""" + @abstractmethod + def uninstall(self, backend_name: str) -> None: + """Delete the existing installation.""" + class InstallationFiltersMixin: """Mixin for filtering installation based on different conditions.""" @@ -145,7 +154,7 @@ class InstallationFiltersMixin: ] def could_be_installed_from( - self, backend_path: Path, backend_name: str | None + self, backend_path: Path, backend_name: str ) -> list[Installation]: """Return installations that could be installed from provided directory.""" return self.filter_by( @@ -154,7 +163,7 @@ class InstallationFiltersMixin: ) def could_be_downloaded_and_installed( - self, backend_name: str | None = None + self, backend_name: str ) -> list[Installation]: """Return installations that could be downloaded and installed.""" return self.filter_by( @@ -163,7 +172,7 @@ class InstallationFiltersMixin: ReadyForInstallationFilter(), ) - def already_installed(self, backend_name: str | None = None) -> list[Installation]: + def already_installed(self, backend_name: str = None) -> list[Installation]: """Return list of backends that are already installed.""" return self.filter_by( AlreadyInstalledFilter(), SearchByNameFilter(backend_name) @@ -185,7 +194,7 @@ class DefaultInstallationManager(InstallationManager, InstallationFiltersMixin): self.noninteractive = noninteractive def choose_installation_for_path( - self, backend_path: Path, backend_name: str | None + self, backend_path: Path, backend_name: str, force: bool ) -> Installation | None: """Check available installation and select one if possible.""" installs = self.could_be_installed_from(backend_path, backend_name) @@ -210,21 +219,33 @@ class DefaultInstallationManager(InstallationManager, InstallationFiltersMixin): installation = installs[0] if installation.already_installed: logger.info( - "%s was found in %s, but it has been already installed.", + "%s was found in %s, but it has been already installed " + "in the ML Inference Advisor.", installation.name, backend_path, ) - return None + return installation if force else None return installation - def install_from(self, backend_path: Path, backend_name: str | None) -> None: + def install_from( + self, backend_path: Path, backend_name: str, force: bool = False + ) -> None: """Install from the provided directory.""" - installation = self.choose_installation_for_path(backend_path, backend_name) + installation = self.choose_installation_for_path( + backend_path, backend_name, force + ) if not installation: return + if force: + self.uninstall(backend_name) + logger.info( + "Force installing %s, so delete the existing installed backend first.", + installation.name, + ) + prompt = ( f"{installation.name} was found in {backend_path}. " "Would you like to install it?" @@ -232,7 +253,7 @@ class DefaultInstallationManager(InstallationManager, InstallationFiltersMixin): self._install(installation, InstallFromPath(backend_path), prompt) def download_and_install( - self, backend_name: str | None = None, eula_agreement: bool = True + self, backend_name: str, eula_agreement: bool = True ) -> None: """Download and install available backends.""" installations = self.could_be_downloaded_and_installed(backend_name) @@ -275,6 +296,14 @@ class DefaultInstallationManager(InstallationManager, InstallationFiltersMixin): for installation in installations: logger.info(" - %s", installation.name) + def uninstall(self, backend_name: str) -> None: + """Uninstall the backend with name backend_name.""" + installations = self.already_installed(backend_name) + if not installations: + raise Exception("No backend available for uninstall") + for installation in installations: + installation.uninstall() + def _install( self, installation: Installation, diff --git a/src/mlia/tools/metadata/corstone.py b/src/mlia/tools/metadata/corstone.py index cea1ec9..04b13b5 100644 --- a/src/mlia/tools/metadata/corstone.py +++ b/src/mlia/tools/metadata/corstone.py @@ -19,6 +19,7 @@ from typing import Iterable from typing import Optional import mlia.backend.manager as backend_manager +from mlia.backend.system import remove_system from mlia.tools.metadata.common import DownloadAndInstall from mlia.tools.metadata.common import Installation from mlia.tools.metadata.common import InstallationType @@ -205,6 +206,11 @@ class BackendInstallation(Installation): self.install(InstallFromPath(backend_path)) + def uninstall(self) -> None: + """Uninstall the backend.""" + remove_system(self.metadata.fvp_dir_name) + logger.info("%s successfully uninstalled.", self.name) + class PackagePathChecker: """Package path checker.""" diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index fd9e29c..f6e0843 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -10,7 +10,9 @@ from unittest.mock import MagicMock import pytest -from mlia.cli.commands import backend +from mlia.cli.commands import backend_install +from mlia.cli.commands import backend_list +from mlia.cli.commands import backend_uninstall from mlia.cli.commands import operators from mlia.cli.commands import optimization from mlia.cli.commands import performance @@ -19,7 +21,7 @@ from mlia.devices.ethosu.config import EthosUConfiguration from mlia.devices.ethosu.performance import MemoryUsage from mlia.devices.ethosu.performance import NPUCycles from mlia.devices.ethosu.performance import PerformanceMetrics -from mlia.tools.metadata.common import InstallationManager +from mlia.tools.metadata.common import DefaultInstallationManager def test_operators_expected_parameters(sample_context: ExecutionContext) -> None: @@ -139,7 +141,7 @@ def mock_performance_estimation(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.fixture(name="installation_manager_mock") def fixture_mock_installation_manager(monkeypatch: pytest.MonkeyPatch) -> MagicMock: """Mock installation manager.""" - install_manager_mock = MagicMock(spec=InstallationManager) + install_manager_mock = MagicMock(spec=DefaultInstallationManager) monkeypatch.setattr( "mlia.cli.commands.get_installation_manager", MagicMock(return_value=install_manager_mock), @@ -147,32 +149,47 @@ def fixture_mock_installation_manager(monkeypatch: pytest.MonkeyPatch) -> MagicM return install_manager_mock -def test_backend_command_action_status(installation_manager_mock: MagicMock) -> None: - """Test backend command "status".""" - backend(backend_action="status") +def test_backend_command_action_list(installation_manager_mock: MagicMock) -> None: + """Test mlia-backend command list.""" + backend_list() installation_manager_mock.show_env_details.assert_called_once() +@pytest.mark.parametrize( + "backend_name", + [ + "backend_name", + "BACKEND_NAME", + "BaCkend_NAme", + ], +) +def test_backend_command_action_uninstall( + installation_manager_mock: MagicMock, + backend_name: str, +) -> None: + """Test mlia-backend command uninstall.""" + backend_uninstall(backend_name) + + installation_manager_mock.uninstall.assert_called_once() + + @pytest.mark.parametrize( "i_agree_to_the_contained_eula, backend_name, expected_calls", [ - [False, None, [call(None, True)]], - [True, None, [call(None, False)]], [False, "backend_name", [call("backend_name", True)]], [True, "backend_name", [call("backend_name", False)]], + [True, "BACKEND_NAME", [call("BACKEND_NAME", False)]], ], ) -def test_backend_command_action_add_downoad( +def test_backend_command_action_add_download( installation_manager_mock: MagicMock, i_agree_to_the_contained_eula: bool, - backend_name: str | None, + backend_name: str, expected_calls: Any, ) -> None: - """Test backend command "install" with download option.""" - backend( - backend_action="install", - download=True, + """Test mlia-backend command "install" with download option.""" + backend_install( name=backend_name, i_agree_to_the_contained_eula=i_agree_to_the_contained_eula, ) @@ -180,26 +197,20 @@ def test_backend_command_action_add_downoad( assert installation_manager_mock.download_and_install.mock_calls == expected_calls -@pytest.mark.parametrize("backend_name", [None, "backend_name"]) +@pytest.mark.parametrize( + "backend_name, force", + [ + ["backend_name", False], + ["backend_name", True], + ["BACKEND_NAME", True], + ], +) def test_backend_command_action_install_from_path( installation_manager_mock: MagicMock, tmp_path: Path, - backend_name: str | None, -) -> None: - """Test backend command "install" with backend path.""" - backend(backend_action="install", path=tmp_path, name=backend_name) - - installation_manager_mock.install_from(tmp_path, backend_name) - - -def test_backend_command_action_install_only_one_action( - installation_manager_mock: MagicMock, # pylint: disable=unused-argument - tmp_path: Path, + backend_name: str, + force: bool, ) -> None: - """Test that only one of action type allowed.""" - with pytest.raises( - Exception, - match="Please select only one action: download or " - "provide path to the backend installation", - ): - backend(backend_action="install", download=True, path=tmp_path) + """Test mlia-backend command "install" with backend path.""" + backend_install(path=tmp_path, name=backend_name, force=force) + installation_manager_mock.install_from.assert_called_once() diff --git a/tests/test_cli_main.py b/tests/test_cli_main.py index 4b16ac5..d0f7152 100644 --- a/tests/test_cli_main.py +++ b/tests/test_cli_main.py @@ -15,6 +15,7 @@ from unittest.mock import MagicMock import pytest import mlia +from mlia.cli.main import backend_main from mlia.cli.main import CommandInfo from mlia.cli.main import main from mlia.core.context import ExecutionContext @@ -122,6 +123,17 @@ def test_default_command(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Non non_default_command.assert_called_once_with(param="test") +def wrap_mock_command(mock: MagicMock, command: Callable) -> Callable: + """Wrap the command with the mock.""" + + @wraps(command) + def mock_command(*args: Any, **kwargs: Any) -> Any: + """Mock the command.""" + mock(*args, **kwargs) + + return mock_command + + @pytest.mark.parametrize( "params, expected_call", [ @@ -273,16 +285,6 @@ def test_commands_execution( """Test calling commands from the main function.""" mock = MagicMock() - def wrap_mock_command(command: Callable) -> Callable: - """Wrap the command with the mock.""" - - @wraps(command) - def mock_command(*args: Any, **kwargs: Any) -> Any: - """Mock the command.""" - mock(*args, **kwargs) - - return mock_command - monkeypatch.setattr( "mlia.cli.options.get_default_backends", MagicMock(return_value=["Vela"]) ) @@ -295,7 +297,7 @@ def test_commands_execution( for command in ["all_tests", "operators", "performance", "optimization"]: monkeypatch.setattr( f"mlia.cli.main.{command}", - wrap_mock_command(getattr(mlia.cli.main, command)), + wrap_mock_command(mock, getattr(mlia.cli.main, command)), ) main(params) @@ -303,6 +305,33 @@ def test_commands_execution( mock.assert_called_once_with(*expected_call.args, **expected_call.kwargs) +@pytest.mark.parametrize( + "params, expected_call", + [ + [ + ["list"], + call(), + ], + ], +) +def test_commands_execution_backend_main( + monkeypatch: pytest.MonkeyPatch, + params: list[str], + expected_call: Any, +) -> None: + """Test calling commands from the backend_main function.""" + mock = MagicMock() + + monkeypatch.setattr( + "mlia.cli.main.backend_list", + wrap_mock_command(mock, getattr(mlia.cli.main, "backend_list")), + ) + + backend_main(params) + + mock.assert_called_once_with(*expected_call.args, **expected_call.kwargs) + + @pytest.mark.parametrize( "verbose, exc_mock, expected_output", [ diff --git a/tests/test_tools_metadata_common.py b/tests/test_tools_metadata_common.py index 69bc3e5..fefb024 100644 --- a/tests/test_tools_metadata_common.py +++ b/tests/test_tools_metadata_common.py @@ -18,6 +18,30 @@ from mlia.tools.metadata.common import InstallationType from mlia.tools.metadata.common import InstallFromPath +def get_default_installation_manager_mock( + name: str, + already_installed: bool = False, +) -> MagicMock: + """Get mock instance for DefaultInstallationManager.""" + mock = MagicMock(spec=DefaultInstallationManager) + + props = { + "name": name, + "already_installed": already_installed, + } + for prop, value in props.items(): + setattr(type(mock), prop, PropertyMock(return_value=value)) + + return mock + + +def _ready_for_uninstall_mock() -> MagicMock: + return get_default_installation_manager_mock( + name="already_installed", + already_installed=True, + ) + + def get_installation_mock( name: str, already_installed: bool = False, @@ -107,14 +131,14 @@ def test_installation_manager_filtering() -> None: could_be_downloaded_and_installed, ] ) - assert manager.already_installed() == [already_installed] + assert manager.already_installed("already_installed") == [already_installed] assert manager.ready_for_installation() == [ ready_for_installation, could_be_downloaded_and_installed, ] - assert manager.could_be_downloaded_and_installed() == [ - could_be_downloaded_and_installed - ] + assert manager.could_be_downloaded_and_installed( + "could_be_downloaded_and_installed" + ) == [could_be_downloaded_and_installed] assert manager.could_be_downloaded_and_installed("some_installation") == [] @@ -146,7 +170,7 @@ def test_installation_manager_download_and_install( install_mock: MagicMock, noninteractive: bool, eula_agreement: bool, - backend_name: str | None, + backend_name: str, expected_call: Any, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -183,7 +207,7 @@ def test_installation_manager_download_and_install( def test_installation_manager_install_from( install_mock: MagicMock, noninteractive: bool, - backend_name: str | None, + backend_name: str, expected_call: Any, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -194,3 +218,30 @@ def test_installation_manager_install_from( manager.install_from(Path("some_path"), backend_name) assert install_mock.install.mock_calls == expected_call + + +@pytest.mark.parametrize("noninteractive", [True, False]) +@pytest.mark.parametrize( + "install_mock, backend_name, expected_call", + [ + [ + _ready_for_uninstall_mock(), + "already_installed", + [call()], + ], + ], +) +def test_installation_manager_uninstall( + install_mock: MagicMock, + noninteractive: bool, + backend_name: str, + expected_call: Any, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test uninstallation.""" + install_mock.reset_mock() + + manager = get_installation_manager(noninteractive, [install_mock], monkeypatch) + manager.uninstall(backend_name) + + assert install_mock.uninstall.mock_calls == expected_call diff --git a/tests/test_tools_metadata_corstone.py b/tests/test_tools_metadata_corstone.py index 02c04d4..a7d81f2 100644 --- a/tests/test_tools_metadata_corstone.py +++ b/tests/test_tools_metadata_corstone.py @@ -469,3 +469,20 @@ def test_corstone_vht_install( corstone_installation.install(InstallFromPath(Path("/opt/VHT"))) create_destination_and_install_mock.assert_called_once() + + +def test_corstone_uninstall( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test the uninstall function in Corstone.""" + remove_system_mock = MagicMock() + + monkeypatch.setattr( + "mlia.tools.metadata.corstone.remove_system", + remove_system_mock, + ) + + installation = get_corstone_300_installation() + + installation.uninstall() + remove_system_mock.assert_called_once_with("corstone_300") -- cgit v1.2.1