From c7ee5b783f044d7ff641773aa385840f5ff944cc Mon Sep 17 00:00:00 2001 From: Benjamin Klimczak Date: Thu, 21 Mar 2024 17:33:17 +0000 Subject: refactor: Backend dependencies and more - Add backend dependencies: One backend can now depend on another backend. - Re-factor 'DownloadArtifact': - Rename 'DownloadArtifact' to 'DownloadConfig' - Remove attributes 'name' and 'version' not relevant for downloads - Add helper properties: - 'filename' parses the URL to extract the file name from the end - 'headers' calls the function to generate a HTML header for the download - Add OutputLogger helper class - Re-factor handling of backend configurations in the target profiles. Change-Id: Ifda6cf12c375d0c1747d7e4130a0370d22c3d33a Signed-off-by: Benjamin Klimczak --- tests/test_backend_install.py | 13 ++-- tests/test_backend_manager.py | 149 ++++++++++++++++++++++++++++++++++-------- tests/test_utils_download.py | 35 +++++----- 3 files changed, 149 insertions(+), 48 deletions(-) (limited to 'tests') diff --git a/tests/test_backend_install.py b/tests/test_backend_install.py index 963766e..3636fb4 100644 --- a/tests/test_backend_install.py +++ b/tests/test_backend_install.py @@ -20,6 +20,7 @@ from mlia.backend.install import InstallFromPath from mlia.backend.install import PackagePathChecker from mlia.backend.install import StaticPathChecker from mlia.backend.repo import BackendRepository +from mlia.utils.download import DownloadConfig @pytest.fixture(name="backend_repo") @@ -104,11 +105,9 @@ def test_backend_installation_from_path( def test_backend_installation_download_and_install( - tmp_path: Path, backend_repo: MagicMock + tmp_path: Path, backend_repo: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test methods of backend installation.""" - download_artifact_mock = MagicMock() - tmp_archive = tmp_path.joinpath("sample.tgz") sample_file = tmp_path.joinpath("sample.txt") sample_file.touch() @@ -116,13 +115,17 @@ def test_backend_installation_download_and_install( with tarfile.open(tmp_archive, "w:gz") as archive: archive.add(sample_file) - download_artifact_mock.download_to.return_value = tmp_archive + monkeypatch.setattr("mlia.backend.install.download", MagicMock()) + monkeypatch.setattr( + "mlia.utils.download.DownloadConfig.filename", + tmp_archive, + ) installation = BackendInstallation( "sample_backend", "Sample backend", "sample_backend", - download_artifact_mock, + DownloadConfig(url="NOT_USED", sha256_hash="NOT_USED"), None, lambda path: BackendInfo(path, copy_source=False), lambda eula_agreement, path: path, diff --git a/tests/test_backend_manager.py b/tests/test_backend_manager.py index 879353e..63c11ee 100644 --- a/tests/test_backend_manager.py +++ b/tests/test_backend_manager.py @@ -3,6 +3,7 @@ """Tests for installation manager.""" from __future__ import annotations +from functools import partial from pathlib import Path from typing import Any from unittest.mock import call @@ -23,6 +24,7 @@ from mlia.core.errors import InternalError def get_default_installation_manager_mock( name: str, already_installed: bool = False, + dependencies: list[str] | None = None, ) -> MagicMock: """Get mock instance for DefaultInstallationManager.""" mock = MagicMock(spec=DefaultInstallationManager) @@ -30,6 +32,7 @@ def get_default_installation_manager_mock( props = { "name": name, "already_installed": already_installed, + "dependencies": dependencies if dependencies else [], } for prop, value in props.items(): setattr(type(mock), prop, PropertyMock(return_value=value)) @@ -49,6 +52,7 @@ def get_installation_mock( already_installed: bool = False, could_be_installed: bool = False, supported_install_type: type | tuple | None = None, + dependencies: list[str] | None = None, ) -> MagicMock: """Get mock instance for the installation.""" mock = MagicMock(spec=Installation) @@ -65,6 +69,7 @@ def get_installation_mock( "name": name, "already_installed": already_installed, "could_be_installed": could_be_installed, + "dependencies": dependencies if dependencies else [], } for prop, value in props.items(): setattr(type(mock), prop, PropertyMock(return_value=value)) @@ -72,38 +77,45 @@ def get_installation_mock( return mock -def _already_installed_mock() -> MagicMock: - return get_installation_mock( - name="already_installed", - already_installed=True, - supported_install_type=(DownloadAndInstall, InstallFromPath), - ) +_already_installed_mock = partial( + get_installation_mock, + name="already_installed", + already_installed=True, + supported_install_type=(DownloadAndInstall, InstallFromPath), +) -def _ready_for_installation_mock() -> MagicMock: - return get_installation_mock( - name="ready_for_installation", - already_installed=False, - could_be_installed=True, - ) +_ready_for_installation_mock = partial( + get_installation_mock, + name="ready_for_installation", + already_installed=False, + could_be_installed=True, +) -def _could_be_downloaded_and_installed_mock() -> MagicMock: - return get_installation_mock( - name="could_be_downloaded_and_installed", - already_installed=False, - could_be_installed=True, - supported_install_type=DownloadAndInstall, - ) +_could_be_downloaded_and_installed_mock = partial( + get_installation_mock, + name="could_be_downloaded_and_installed", + already_installed=False, + could_be_installed=True, + supported_install_type=DownloadAndInstall, +) -def _could_be_installed_from_mock() -> MagicMock: - return get_installation_mock( - name="could_be_installed_from", - already_installed=False, - could_be_installed=True, - supported_install_type=InstallFromPath, - ) +_could_be_installed_from_mock = partial( + get_installation_mock, + name="could_be_installed_from", + already_installed=False, + could_be_installed=True, + supported_install_type=InstallFromPath, +) + +_already_installed_dep_mock = partial( + get_installation_mock, + name="already_installed_dep", + already_installed=True, + supported_install_type=(DownloadAndInstall, InstallFromPath), +) def get_installation_manager( @@ -114,13 +126,23 @@ def get_installation_manager( ) -> DefaultInstallationManager: """Get installation manager instance.""" if not noninteractive: - monkeypatch.setattr( - "mlia.backend.manager.yes", MagicMock(return_value=yes_response) + return get_interactive_installation_manager( + installations, monkeypatch, MagicMock(return_value=yes_response) ) return DefaultInstallationManager(installations, noninteractive=noninteractive) +def get_interactive_installation_manager( + installations: list[Any], + monkeypatch: pytest.MonkeyPatch, + mock_interaction: MagicMock, +) -> DefaultInstallationManager: + """Get and interactive installation manager instance using the given mock.""" + monkeypatch.setattr("mlia.backend.manager.yes", mock_interaction) + return DefaultInstallationManager(installations, noninteractive=False) + + def test_installation_manager_filtering() -> None: """Test default installation manager.""" already_installed = _already_installed_mock() @@ -337,3 +359,74 @@ def test_show_env_details(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch, ) manager.show_env_details() + + +@pytest.mark.parametrize( + "dependency", + ( + _ready_for_installation_mock(), + _already_installed_mock(), + ), +) +@pytest.mark.parametrize("yes_response", (True, False)) +def test_could_be_installed_with_dep( + dependency: MagicMock, + yes_response: bool, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test installation with a dependency.""" + install_mock = _could_be_installed_from_mock(dependencies=[dependency.name]) + + yes_mock = MagicMock(return_value=yes_response) + manager = get_interactive_installation_manager( + [install_mock, dependency], monkeypatch, yes_mock + ) + manager.install_from(tmp_path, install_mock.name) + + if yes_response: + install_mock.install.assert_called_once() + else: + install_mock.install.assert_not_called() + install_mock.uninstall.assert_not_called() + + dependency.install.assert_not_called() + dependency.uninstall.assert_not_called() + + +def test_install_with_unknown_dep( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test installation with an unknown dependency.""" + install_mock = _could_be_installed_from_mock(dependencies=["UNKNOWN_BACKEND"]) + + manager = get_installation_manager(False, [install_mock], monkeypatch) + with pytest.raises(ValueError): + manager.install_from(tmp_path, install_mock.name) + + install_mock.install.assert_not_called() + install_mock.uninstall.assert_not_called() + + +@pytest.mark.parametrize("yes_response", (True, False)) +def test_uninstall_with_dep( + yes_response: bool, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test uninstalling a backend with a dependency.""" + dependency = _already_installed_dep_mock() + install_mock = _already_installed_mock(dependencies=[dependency.name]) + yes_mock = MagicMock(return_value=yes_response) + manager = get_interactive_installation_manager( + [install_mock, dependency], monkeypatch, yes_mock + ) + manager.uninstall(install_mock.name) + + install_mock.install.assert_not_called() + if yes_response: + install_mock.uninstall.assert_called_once() + else: + install_mock.uninstall.assert_not_called() + + dependency.install.assert_not_called() + dependency.uninstall.assert_not_called() diff --git a/tests/test_utils_download.py b/tests/test_utils_download.py index 28af74f..7188c62 100644 --- a/tests/test_utils_download.py +++ b/tests/test_utils_download.py @@ -1,8 +1,9 @@ -# SPDX-FileCopyrightText: Copyright 2022, Arm Limited and/or its affiliates. +# SPDX-FileCopyrightText: Copyright 2023, Arm Limited and/or its affiliates. # SPDX-License-Identifier: Apache-2.0 """Tests for download functionality.""" from __future__ import annotations +import hashlib from contextlib import ExitStack as does_not_raise from pathlib import Path from typing import Any @@ -14,7 +15,7 @@ import pytest import requests from mlia.utils.download import download -from mlia.utils.download import DownloadArtifact +from mlia.utils.download import DownloadConfig def response_mock( @@ -69,9 +70,18 @@ def test_download( "mlia.utils.download.requests.get", MagicMock(return_value=response_mock(content_length, content_chunks)), ) + hash_obj = hashlib.sha256() + for chunk in content_chunks: + hash_obj.update(chunk) + sha256_hash = hash_obj.hexdigest() dest = tmp_path / "sample.bin" - download("some_url", dest, show_progress=show_progress, label=label) + download( + dest, + DownloadConfig("some_url", sha256_hash=sha256_hash), + show_progress=show_progress, + label=label, + ) assert dest.is_file() assert dest.read_bytes() == bytes( @@ -92,7 +102,7 @@ def test_download( "10", [bytes(range(10))], "bad_hash", - pytest.raises(ValueError, match="Digests do not match"), + pytest.raises(ValueError, match="Hashes do not match."), ], ], ) @@ -111,15 +121,13 @@ def test_download_artifact_download_to( ) with expected_error: - artifact = DownloadArtifact( - "test_artifact", + cfg = DownloadConfig( "some_url", - "artifact_filename", - "1.0", sha256_hash, ) - dest = artifact.download_to(tmp_path) + dest = tmp_path / "artifact_filename" + download(dest, cfg) assert isinstance(dest, Path) assert dest.name == "artifact_filename" @@ -133,16 +141,13 @@ def test_download_artifact_unable_to_overwrite( MagicMock(return_value=response_mock("10", [bytes(range(10))])), ) - artifact = DownloadArtifact( - "test_artifact", + cfg = DownloadConfig( "some_url", - "artifact_filename", - "1.0", "sha256_hash", ) existing_file = tmp_path / "artifact_filename" existing_file.touch() - with pytest.raises(ValueError, match=f"{existing_file} already exists"): - artifact.download_to(tmp_path) + with pytest.raises(FileExistsError, match=f"{existing_file} already exists."): + download(existing_file, cfg) -- cgit v1.2.1