diff --git a/charmcraft/commands/store.py b/charmcraft/commands/store.py index f8805aae9..ecf7c4c87 100644 --- a/charmcraft/commands/store.py +++ b/charmcraft/commands/store.py @@ -16,7 +16,6 @@ """Commands related to Charmhub.""" import collections -import dataclasses import os import pathlib import shutil @@ -1479,154 +1478,6 @@ def run(self, parsed_args): emit.message(self.format_content(parsed_args.format, output_data)) -class FetchLibCommand(BaseCommand): - """Fetch one or more charm libraries.""" - - name = "fetch-lib" - help_msg = "Fetch one or more charm libraries" - overview = textwrap.dedent( - """ - Fetch charm libraries. - - The first time a library is downloaded the command will create the needed - directories to place it, subsequent fetches will just update the local copy. - - You can specify the library to update or download by building its fully - qualified name with the charm and library names, and the desired API - version. For example, to fetch the API version 3 of library 'somelib' - from charm `specialcharm`, do: - - $ charmcraft fetch-lib charms.specialcharm.v3.somelib - Library charms.specialcharm.v3.somelib version 3.7 downloaded. - - If the command is executed without parameters, it will update all the currently - downloaded libraries. - """ - ) - - def fill_parser(self, parser): - """Add own parameters to the general parser.""" - self.include_format_option(parser) - parser.add_argument( - "library", - nargs="?", - help="Library to fetch (e.g. charms.mycharm.v2.foo.); optional, default to all", - ) - - def run(self, parsed_args): - """Run the command.""" - if parsed_args.library: - local_libs_data = [utils.get_lib_info(full_name=parsed_args.library)] - else: - local_libs_data = utils.get_libs_from_tree() - found_libs = [lib_data.full_name for lib_data in local_libs_data] - emit.debug(f"Libraries found under 'lib/charms': {found_libs}") - - # get tips from the Store - store = Store(self.config.charmhub, needs_auth=False) - to_query = [] - for lib in local_libs_data: - if lib.lib_id is None: - item = {"charm_name": lib.charm_name, "lib_name": lib.lib_name} - else: - item = {"lib_id": lib.lib_id} - item["api"] = lib.api - to_query.append(item) - libs_tips = store.get_libraries_tips(to_query) - - # check if something needs to be done - analysis = [] - for lib_data in local_libs_data: - emit.debug(f"Verifying local lib {lib_data}") - # fix any missing lib id using the Store info - if lib_data.lib_id is None: - for tip in libs_tips.values(): - if lib_data.charm_name == tip.charm_name and lib_data.lib_name == tip.lib_name: - lib_data = dataclasses.replace(lib_data, lib_id=tip.lib_id) - break - - tip = libs_tips.get((lib_data.lib_id, lib_data.api)) - emit.debug(f"Store tip: {tip}") - error_message = None - if tip is None: - error_message = f"Library {lib_data.full_name} not found in Charmhub." - elif tip.patch > lib_data.patch: - # the store has a higher version than local - pass - elif tip.patch < lib_data.patch: - # the store has a lower version numbers than local - error_message = ( - f"Library {lib_data.full_name} has local changes, cannot be updated." - ) - else: - # same versions locally and in the store - if tip.content_hash == lib_data.content_hash: - error_message = ( - f"Library {lib_data.full_name} was already up to date in " - f"version {tip.api:d}.{tip.patch:d}." - ) - else: - error_message = ( - f"Library {lib_data.full_name} has local changes, cannot be updated." - ) - analysis.append((lib_data, error_message)) - - full_lib_data = [] - for lib_data, error_message in analysis: - if error_message is None: - downloaded = store.get_library(lib_data.charm_name, lib_data.lib_id, lib_data.api) - if lib_data.content is None: - # locally new - lib_data.path.parent.mkdir(parents=True, exist_ok=True) - lib_data.path.write_text(downloaded.content) - message = ( - f"Library {lib_data.full_name} version " - f"{downloaded.api:d}.{downloaded.patch:d} downloaded." - ) - else: - # XXX Facundo 2020-12-17: manage the case where the library was renamed - # (related GH issue: #214) - lib_data.path.write_text(downloaded.content) - message = ( - f"Library {lib_data.full_name} updated to version " - f"{downloaded.api:d}.{downloaded.patch:d}." - ) - - # fix lib_data with new info so it's later available - # for the case of programmatic output - lib_data = dataclasses.replace( - lib_data, - patch=downloaded.patch, - content=downloaded.content, - content_hash=downloaded.content_hash, - ) - else: - message = error_message - full_lib_data.append((lib_data, error_message)) - - if not parsed_args.format: - emit.message(message) - - if parsed_args.format: - output_data = [] - for lib_data, error_message in full_lib_data: - datum = { - "charm_name": lib_data.charm_name, - "library_name": lib_data.lib_name, - "library_id": lib_data.lib_id, - "api": lib_data.api, - } - if error_message is None: - datum["fetched"] = { - "patch": lib_data.patch, - "content_hash": lib_data.content_hash, - } - else: - datum["error_message"] = error_message - output_data.append(datum) - emit.message(self.format_content(parsed_args.format, output_data)) - - class ListLibCommand(BaseCommand): """List all libraries belonging to a charm.""" diff --git a/charmcraft/main.py b/charmcraft/main.py index 972671277..d6871a7df 100644 --- a/charmcraft/main.py +++ b/charmcraft/main.py @@ -82,7 +82,6 @@ store.CreateLibCommand, store.PublishLibCommand, store.ListLibCommand, - store.FetchLibCommand, # resources support store.ListResourcesCommand, store.UploadResourceCommand, diff --git a/tests/commands/test_store_commands.py b/tests/commands/test_store_commands.py index d77ada65f..365d98b09 100644 --- a/tests/commands/test_store_commands.py +++ b/tests/commands/test_store_commands.py @@ -36,7 +36,6 @@ CloseCommand, CreateLibCommand, EntityType, - FetchLibCommand, ListLibCommand, ListNamesCommand, ListResourcesCommand, @@ -3383,452 +3382,6 @@ def test_publishlib_store_has_same_revision_other_hash( emitter.assert_messages([error_message]) -# -- tests for fetch libraries command - - -@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) -def test_fetchlib_simple_downloaded(emitter, store_mock, tmp_path, monkeypatch, config, formatted): - """Happy path fetching the lib for the first time (downloading it).""" - monkeypatch.chdir(tmp_path) - - lib_id = "test-example-lib-id" - lib_content = "some test content with uñicode ;)" - store_mock.get_libraries_tips.return_value = { - (lib_id, 0): Library( - lib_id=lib_id, - content=None, - content_hash="abc", - api=0, - patch=7, - lib_name="testlib", - charm_name="testcharm", - ), - } - store_mock.get_library.return_value = Library( - lib_id=lib_id, - content=lib_content, - content_hash="abc", - api=0, - patch=7, - lib_name="testlib", - charm_name="testcharm", - ) - - args = Namespace(library="charms.testcharm.v0.testlib", format=formatted) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips([{"charm_name": "testcharm", "lib_name": "testlib", "api": 0}]), - call.get_library("testcharm", lib_id, 0), - ] - if formatted: - expected = [ - { - "charm_name": "testcharm", - "library_name": "testlib", - "library_id": lib_id, - "api": 0, - "fetched": { - "patch": 7, - "content_hash": "abc", - }, - }, - ] - emitter.assert_json_output(expected) - else: - expected = "Library charms.testcharm.v0.testlib version 0.7 downloaded." - emitter.assert_message(expected) - saved_file = tmp_path / "lib" / "charms" / "testcharm" / "v0" / "testlib.py" - assert saved_file.read_text() == lib_content - - -def test_fetchlib_simple_dash_in_name(emitter, store_mock, tmp_path, monkeypatch, config): - """Happy path fetching the lib for the first time (downloading it).""" - monkeypatch.chdir(tmp_path) - - lib_id = "test-example-lib-id" - lib_content = "some test content with uñicode ;)" - store_mock.get_libraries_tips.return_value = { - (lib_id, 0): Library( - lib_id=lib_id, - content=None, - content_hash="abc", - api=0, - patch=7, - lib_name="testlib", - charm_name="test-charm", - ), - } - store_mock.get_library.return_value = Library( - lib_id=lib_id, - content=lib_content, - content_hash="abc", - api=0, - patch=7, - lib_name="testlib", - charm_name="test-charm", - ) - - args = Namespace(library="charms.test_charm.v0.testlib", format=None) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips([{"charm_name": "test-charm", "lib_name": "testlib", "api": 0}]), - call.get_library("test-charm", lib_id, 0), - ] - expected = "Library charms.test_charm.v0.testlib version 0.7 downloaded." - emitter.assert_message(expected) - saved_file = tmp_path / "lib" / "charms" / "test_charm" / "v0" / "testlib.py" - assert saved_file.read_text() == lib_content - - -def test_fetchlib_simple_dash_in_name_on_disk(emitter, store_mock, tmp_path, monkeypatch, config): - """Happy path fetching the lib for the first time (downloading it).""" - monkeypatch.chdir(tmp_path) - - lib_id = "test-example-lib-id" - lib_content = "test-content" - store_mock.get_libraries_tips.return_value = { - (lib_id, 0): Library( - lib_id=lib_id, - content=None, - content_hash="abc", - api=0, - patch=7, - lib_name="testlib", - charm_name="test-charm", - ), - } - store_mock.get_library.return_value = Library( - lib_id=lib_id, - content=lib_content, - content_hash="abc", - api=0, - patch=7, - lib_name="testlib", - charm_name="test-charm", - ) - factory.create_lib_filepath("test-charm", "testlib", api=0, patch=1, lib_id=lib_id) - - args = Namespace(library=None, format=None) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips([{"lib_id": "test-example-lib-id", "api": 0}]), - call.get_library("test-charm", lib_id, 0), - ] - expected = "Library charms.test_charm.v0.testlib updated to version 0.7." - emitter.assert_message(expected) - - -def test_fetchlib_simple_updated(emitter, store_mock, tmp_path, monkeypatch, config): - """Happy path fetching the lib for Nth time (updating it).""" - monkeypatch.chdir(tmp_path) - - lib_id = "test-example-lib-id" - content, content_hash = factory.create_lib_filepath( - "testcharm", "testlib", api=0, patch=1, lib_id=lib_id - ) - - new_lib_content = "some test content with uñicode ;)" - store_mock.get_libraries_tips.return_value = { - (lib_id, 0): Library( - lib_id=lib_id, - content=None, - content_hash="abc", - api=0, - patch=2, - lib_name="testlib", - charm_name="testcharm", - ), - } - store_mock.get_library.return_value = Library( - lib_id=lib_id, - content=new_lib_content, - content_hash="abc", - api=0, - patch=2, - lib_name="testlib", - charm_name="testcharm", - ) - - args = Namespace(library="charms.testcharm.v0.testlib", format=None) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips([{"lib_id": lib_id, "api": 0}]), - call.get_library("testcharm", lib_id, 0), - ] - expected = "Library charms.testcharm.v0.testlib updated to version 0.2." - emitter.assert_message(expected) - saved_file = tmp_path / "lib" / "charms" / "testcharm" / "v0" / "testlib.py" - assert saved_file.read_text() == new_lib_content - - -@pytest.mark.skipif(sys.platform == "win32", reason="Windows not [yet] supported") -@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) -def test_fetchlib_all(emitter, store_mock, tmp_path, monkeypatch, config, formatted): - """Update all the libraries found in disk.""" - monkeypatch.chdir(tmp_path) - - c1, h1 = factory.create_lib_filepath( - "testcharm1", "testlib1", api=0, patch=1, lib_id="lib_id_1" - ) - c2, h2 = factory.create_lib_filepath( - "testcharm2", "testlib2", api=3, patch=5, lib_id="lib_id_2" - ) - - store_mock.get_libraries_tips.return_value = { - ("lib_id_1", 0): Library( - lib_id="lib_id_1", - content=None, - content_hash="abc", - api=0, - patch=2, - lib_name="testlib1", - charm_name="testcharm1", - ), - ("lib_id_2", 3): Library( - lib_id="lib_id_2", - content=None, - content_hash="def", - api=3, - patch=14, - lib_name="testlib2", - charm_name="testcharm2", - ), - } - _store_libs_info = [ - Library( - lib_id="lib_id_1", - content="new lib content 1", - content_hash="xxx", - api=0, - patch=2, - lib_name="testlib1", - charm_name="testcharm1", - ), - Library( - lib_id="lib_id_2", - content="new lib content 2", - content_hash="yyy", - api=3, - patch=14, - lib_name="testlib2", - charm_name="testcharm2", - ), - ] - store_mock.get_library.side_effect = lambda *a: _store_libs_info.pop(0) - - args = Namespace(library=None, format=formatted) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips( - [ - {"lib_id": "lib_id_1", "api": 0}, - {"lib_id": "lib_id_2", "api": 3}, - ] - ), - call.get_library("testcharm1", "lib_id_1", 0), - call.get_library("testcharm2", "lib_id_2", 3), - ] - names = [ - "charms.testcharm1.v0.testlib1", - "charms.testcharm2.v3.testlib2", - ] - emitter.assert_debug("Libraries found under 'lib/charms': " + str(names)) - if formatted: - expected = [ - { - "charm_name": "testcharm1", - "library_name": "testlib1", - "library_id": "lib_id_1", - "api": 0, - "fetched": { - "patch": 2, - "content_hash": "xxx", - }, - }, - { - "charm_name": "testcharm2", - "library_name": "testlib2", - "library_id": "lib_id_2", - "api": 3, - "fetched": { - "patch": 14, - "content_hash": "yyy", - }, - }, - ] - emitter.assert_json_output(expected) - else: - emitter.assert_messages( - [ - "Library charms.testcharm1.v0.testlib1 updated to version 0.2.", - "Library charms.testcharm2.v3.testlib2 updated to version 3.14.", - ] - ) - - saved_file = tmp_path / "lib" / "charms" / "testcharm1" / "v0" / "testlib1.py" - assert saved_file.read_text() == "new lib content 1" - saved_file = tmp_path / "lib" / "charms" / "testcharm2" / "v3" / "testlib2.py" - assert saved_file.read_text() == "new lib content 2" - - -@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) -def test_fetchlib_store_not_found(emitter, store_mock, config, formatted): - """The indicated library is not found in the store.""" - store_mock.get_libraries_tips.return_value = {} - args = Namespace(library="charms.testcharm.v0.testlib", format=formatted) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips([{"charm_name": "testcharm", "lib_name": "testlib", "api": 0}]), - ] - error_message = "Library charms.testcharm.v0.testlib not found in Charmhub." - if formatted: - expected = [ - { - "charm_name": "testcharm", - "library_name": "testlib", - "library_id": None, - "api": 0, - "error_message": error_message, - }, - ] - emitter.assert_json_output(expected) - else: - emitter.assert_message(error_message) - - -@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) -def test_fetchlib_store_is_old(emitter, store_mock, tmp_path, monkeypatch, config, formatted): - """The store has an older version that what is found locally.""" - monkeypatch.chdir(tmp_path) - - lib_id = "test-example-lib-id" - factory.create_lib_filepath("testcharm", "testlib", api=0, patch=7, lib_id=lib_id) - - store_mock.get_libraries_tips.return_value = { - (lib_id, 0): Library( - lib_id=lib_id, - content=None, - content_hash="abc", - api=0, - patch=6, - lib_name="testlib", - charm_name="testcharm", - ), - } - args = Namespace(library="charms.testcharm.v0.testlib", format=formatted) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips([{"lib_id": lib_id, "api": 0}]), - ] - error_message = "Library charms.testcharm.v0.testlib has local changes, cannot be updated." - if formatted: - expected = [ - { - "charm_name": "testcharm", - "library_name": "testlib", - "library_id": lib_id, - "api": 0, - "error_message": error_message, - }, - ] - emitter.assert_json_output(expected) - else: - emitter.assert_message(error_message) - - -@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) -def test_fetchlib_store_same_versions_same_hash( - emitter, store_mock, tmp_path, monkeypatch, config, formatted -): - """The store situation is the same than locally.""" - monkeypatch.chdir(tmp_path) - - lib_id = "test-example-lib-id" - _, c_hash = factory.create_lib_filepath("testcharm", "testlib", api=0, patch=7, lib_id=lib_id) - - store_mock.get_libraries_tips.return_value = { - (lib_id, 0): Library( - lib_id=lib_id, - content=None, - content_hash=c_hash, - api=0, - patch=7, - lib_name="testlib", - charm_name="testcharm", - ), - } - args = Namespace(library="charms.testcharm.v0.testlib", format=formatted) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips([{"lib_id": lib_id, "api": 0}]), - ] - error_message = "Library charms.testcharm.v0.testlib was already up to date in version 0.7." - if formatted: - expected = [ - { - "charm_name": "testcharm", - "library_name": "testlib", - "library_id": lib_id, - "api": 0, - "error_message": error_message, - }, - ] - emitter.assert_json_output(expected) - else: - emitter.assert_message(error_message) - - -@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) -def test_fetchlib_store_same_versions_different_hash( - emitter, store_mock, tmp_path, monkeypatch, config, formatted -): - """The store has the lib in the same version, but with different content.""" - monkeypatch.chdir(tmp_path) - - lib_id = "test-example-lib-id" - factory.create_lib_filepath("testcharm", "testlib", api=0, patch=7, lib_id=lib_id) - - store_mock.get_libraries_tips.return_value = { - (lib_id, 0): Library( - lib_id=lib_id, - content=None, - content_hash="abc", - api=0, - patch=7, - lib_name="testlib", - charm_name="testcharm", - ), - } - args = Namespace(library="charms.testcharm.v0.testlib", format=formatted) - FetchLibCommand(config).run(args) - - assert store_mock.mock_calls == [ - call.get_libraries_tips([{"lib_id": lib_id, "api": 0}]), - ] - error_message = "Library charms.testcharm.v0.testlib has local changes, cannot be updated." - if formatted: - expected = [ - { - "charm_name": "testcharm", - "library_name": "testlib", - "library_id": lib_id, - "api": 0, - "error_message": error_message, - }, - ] - emitter.assert_json_output(expected) - else: - emitter.assert_message(error_message) - - # -- tests for list libraries command diff --git a/tests/integration/commands/test_store_commands.py b/tests/integration/commands/test_store_commands.py new file mode 100644 index 000000000..01742b61c --- /dev/null +++ b/tests/integration/commands/test_store_commands.py @@ -0,0 +1,490 @@ +# Copyright 2024 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# For further info, check https://github.com/canonical/charmcraft +"""Integration tests for store commands.""" +import argparse +import sys +from unittest import mock + +import pytest + +from charmcraft import env +from charmcraft.application.commands import FetchLibCommand +from charmcraft.cmdbase import JSON_FORMAT +from charmcraft.store.models import Library +from tests import factory + + +@pytest.fixture() +def store_mock(): + """The fixture to fake the store layer in all the tests.""" + store_mock = mock.MagicMock() + + def validate_params(config, ephemeral=False, needs_auth=True): + """Check that the store received the Charmhub configuration and ephemeral flag.""" + assert config == env.CharmhubConfig() + assert isinstance(ephemeral, bool) + assert isinstance(needs_auth, bool) + return store_mock + + with mock.patch("charmcraft.application.commands.store.Store", validate_params): + yield store_mock + + +# region fetch-lib tests +@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) +def test_fetchlib_simple_downloaded(emitter, store_mock, tmp_path, monkeypatch, config, formatted): + """Happy path fetching the lib for the first time (downloading it).""" + monkeypatch.chdir(tmp_path) + + lib_id = "test-example-lib-id" + lib_content = "some test content with uñicode ;)" + store_mock.get_libraries_tips.return_value = { + (lib_id, 0): Library( + lib_id=lib_id, + content=None, + content_hash="abc", + api=0, + patch=7, + lib_name="testlib", + charm_name="testcharm", + ), + } + store_mock.get_library.return_value = Library( + lib_id=lib_id, + content=lib_content, + content_hash="abc", + api=0, + patch=7, + lib_name="testlib", + charm_name="testcharm", + ) + + args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=formatted) + FetchLibCommand(config).run(args) + + assert store_mock.mock_calls == [ + mock.call.get_libraries_tips( + [{"charm_name": "testcharm", "lib_name": "testlib", "api": 0}] + ), + mock.call.get_library("testcharm", lib_id, 0), + ] + if formatted: + expected = [ + { + "charm_name": "testcharm", + "library_name": "testlib", + "library_id": lib_id, + "api": 0, + "fetched": { + "patch": 7, + "content_hash": "abc", + }, + }, + ] + emitter.assert_json_output(expected) + else: + expected = "Library charms.testcharm.v0.testlib version 0.7 downloaded." + emitter.assert_message(expected) + saved_file = tmp_path / "lib" / "charms" / "testcharm" / "v0" / "testlib.py" + assert saved_file.read_text() == lib_content + + +def test_fetchlib_simple_dash_in_name(emitter, store_mock, tmp_path, monkeypatch, config): + """Happy path fetching the lib for the first time (downloading it).""" + monkeypatch.chdir(tmp_path) + + lib_id = "test-example-lib-id" + lib_content = "some test content with uñicode ;)" + store_mock.get_libraries_tips.return_value = { + (lib_id, 0): Library( + lib_id=lib_id, + content=None, + content_hash="abc", + api=0, + patch=7, + lib_name="testlib", + charm_name="test-charm", + ), + } + store_mock.get_library.return_value = Library( + lib_id=lib_id, + content=lib_content, + content_hash="abc", + api=0, + patch=7, + lib_name="testlib", + charm_name="test-charm", + ) + + args = argparse.Namespace(library="charms.test_charm.v0.testlib", format=None) + FetchLibCommand(config).run(args) + + assert store_mock.mock_calls == [ + mock.call.get_libraries_tips( + [{"charm_name": "test-charm", "lib_name": "testlib", "api": 0}] + ), + mock.call.get_library("test-charm", lib_id, 0), + ] + expected = "Library charms.test_charm.v0.testlib version 0.7 downloaded." + emitter.assert_message(expected) + saved_file = tmp_path / "lib" / "charms" / "test_charm" / "v0" / "testlib.py" + assert saved_file.read_text() == lib_content + + +def test_fetchlib_simple_dash_in_name_on_disk(emitter, store_mock, tmp_path, monkeypatch, config): + """Happy path fetching the lib for the first time (downloading it).""" + monkeypatch.chdir(tmp_path) + + lib_id = "test-example-lib-id" + lib_content = "test-content" + store_mock.get_libraries_tips.return_value = { + (lib_id, 0): Library( + lib_id=lib_id, + content=None, + content_hash="abc", + api=0, + patch=7, + lib_name="testlib", + charm_name="test-charm", + ), + } + store_mock.get_library.return_value = Library( + lib_id=lib_id, + content=lib_content, + content_hash="abc", + api=0, + patch=7, + lib_name="testlib", + charm_name="test-charm", + ) + factory.create_lib_filepath("test-charm", "testlib", api=0, patch=1, lib_id=lib_id) + + args = argparse.Namespace(library=None, format=None) + FetchLibCommand(config).run(args) + + assert store_mock.mock_calls == [ + mock.call.get_libraries_tips([{"lib_id": "test-example-lib-id", "api": 0}]), + mock.call.get_library("test-charm", lib_id, 0), + ] + expected = "Library charms.test_charm.v0.testlib updated to version 0.7." + emitter.assert_message(expected) + + +def test_fetchlib_simple_updated(emitter, store_mock, tmp_path, monkeypatch, config): + """Happy path fetching the lib for Nth time (updating it).""" + monkeypatch.chdir(tmp_path) + + lib_id = "test-example-lib-id" + content, content_hash = factory.create_lib_filepath( + "testcharm", "testlib", api=0, patch=1, lib_id=lib_id + ) + + new_lib_content = "some test content with uñicode ;)" + store_mock.get_libraries_tips.return_value = { + (lib_id, 0): Library( + lib_id=lib_id, + content=None, + content_hash="abc", + api=0, + patch=2, + lib_name="testlib", + charm_name="testcharm", + ), + } + store_mock.get_library.return_value = Library( + lib_id=lib_id, + content=new_lib_content, + content_hash="abc", + api=0, + patch=2, + lib_name="testlib", + charm_name="testcharm", + ) + + args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=None) + FetchLibCommand(config).run(args) + + assert store_mock.mock_calls == [ + mock.call.get_libraries_tips([{"lib_id": lib_id, "api": 0}]), + mock.call.get_library("testcharm", lib_id, 0), + ] + expected = "Library charms.testcharm.v0.testlib updated to version 0.2." + emitter.assert_message(expected) + saved_file = tmp_path / "lib" / "charms" / "testcharm" / "v0" / "testlib.py" + assert saved_file.read_text() == new_lib_content + + +@pytest.mark.skipif(sys.platform == "win32", reason="Windows not [yet] supported") +@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) +def test_fetchlib_all(emitter, store_mock, tmp_path, monkeypatch, config, formatted): + """Update all the libraries found in disk.""" + monkeypatch.chdir(tmp_path) + + c1, h1 = factory.create_lib_filepath( + "testcharm1", "testlib1", api=0, patch=1, lib_id="lib_id_1" + ) + c2, h2 = factory.create_lib_filepath( + "testcharm2", "testlib2", api=3, patch=5, lib_id="lib_id_2" + ) + + store_mock.get_libraries_tips.return_value = { + ("lib_id_1", 0): Library( + lib_id="lib_id_1", + content=None, + content_hash="abc", + api=0, + patch=2, + lib_name="testlib1", + charm_name="testcharm1", + ), + ("lib_id_2", 3): Library( + lib_id="lib_id_2", + content=None, + content_hash="def", + api=3, + patch=14, + lib_name="testlib2", + charm_name="testcharm2", + ), + } + _store_libs_info = [ + Library( + lib_id="lib_id_1", + content="new lib content 1", + content_hash="xxx", + api=0, + patch=2, + lib_name="testlib1", + charm_name="testcharm1", + ), + Library( + lib_id="lib_id_2", + content="new lib content 2", + content_hash="yyy", + api=3, + patch=14, + lib_name="testlib2", + charm_name="testcharm2", + ), + ] + store_mock.get_library.side_effect = lambda *a: _store_libs_info.pop(0) + + args = argparse.Namespace(library=None, format=formatted) + FetchLibCommand(config).run(args) + + assert store_mock.mock_calls == [ + mock.call.get_libraries_tips( + [ + {"lib_id": "lib_id_1", "api": 0}, + {"lib_id": "lib_id_2", "api": 3}, + ] + ), + mock.call.get_library("testcharm1", "lib_id_1", 0), + mock.call.get_library("testcharm2", "lib_id_2", 3), + ] + names = [ + "charms.testcharm1.v0.testlib1", + "charms.testcharm2.v3.testlib2", + ] + emitter.assert_debug("Libraries found under 'lib/charms': " + str(names)) + if formatted: + expected = [ + { + "charm_name": "testcharm1", + "library_name": "testlib1", + "library_id": "lib_id_1", + "api": 0, + "fetched": { + "patch": 2, + "content_hash": "xxx", + }, + }, + { + "charm_name": "testcharm2", + "library_name": "testlib2", + "library_id": "lib_id_2", + "api": 3, + "fetched": { + "patch": 14, + "content_hash": "yyy", + }, + }, + ] + emitter.assert_json_output(expected) + else: + emitter.assert_messages( + [ + "Library charms.testcharm1.v0.testlib1 updated to version 0.2.", + "Library charms.testcharm2.v3.testlib2 updated to version 3.14.", + ] + ) + + saved_file = tmp_path / "lib" / "charms" / "testcharm1" / "v0" / "testlib1.py" + assert saved_file.read_text() == "new lib content 1" + saved_file = tmp_path / "lib" / "charms" / "testcharm2" / "v3" / "testlib2.py" + assert saved_file.read_text() == "new lib content 2" + + +@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) +def test_fetchlib_store_not_found(emitter, store_mock, config, formatted): + """The indicated library is not found in the store.""" + store_mock.get_libraries_tips.return_value = {} + args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=formatted) + FetchLibCommand(config).run(args) + + store_mock.get_libraries_tips.assert_called_once_with( + [{"charm_name": "testcharm", "lib_name": "testlib", "api": 0}] + ), + error_message = "Library charms.testcharm.v0.testlib not found in Charmhub." + if formatted: + expected = [ + { + "charm_name": "testcharm", + "library_name": "testlib", + "library_id": None, + "api": 0, + "error_message": error_message, + }, + ] + emitter.assert_json_output(expected) + else: + emitter.assert_message(error_message) + + +@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) +def test_fetchlib_store_is_old(emitter, store_mock, tmp_path, monkeypatch, config, formatted): + """The store has an older version that what is found locally.""" + monkeypatch.chdir(tmp_path) + + lib_id = "test-example-lib-id" + factory.create_lib_filepath("testcharm", "testlib", api=0, patch=7, lib_id=lib_id) + + store_mock.get_libraries_tips.return_value = { + (lib_id, 0): Library( + lib_id=lib_id, + content=None, + content_hash="abc", + api=0, + patch=6, + lib_name="testlib", + charm_name="testcharm", + ), + } + args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=formatted) + FetchLibCommand(config).run(args) + + store_mock.get_libraries_tips.assert_called_once_with([{"lib_id": lib_id, "api": 0}]) + error_message = "Library charms.testcharm.v0.testlib has local changes, cannot be updated." + if formatted: + expected = [ + { + "charm_name": "testcharm", + "library_name": "testlib", + "library_id": lib_id, + "api": 0, + "error_message": error_message, + }, + ] + emitter.assert_json_output(expected) + else: + emitter.assert_message(error_message) + + +@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) +def test_fetchlib_store_same_versions_same_hash( + emitter, store_mock, tmp_path, monkeypatch, config, formatted +): + """The store situation is the same than locally.""" + monkeypatch.chdir(tmp_path) + + lib_id = "test-example-lib-id" + _, c_hash = factory.create_lib_filepath("testcharm", "testlib", api=0, patch=7, lib_id=lib_id) + + store_mock.get_libraries_tips.return_value = { + (lib_id, 0): Library( + lib_id=lib_id, + content=None, + content_hash=c_hash, + api=0, + patch=7, + lib_name="testlib", + charm_name="testcharm", + ), + } + args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=formatted) + FetchLibCommand(config).run(args) + + store_mock.get_libraries_tips.assert_called_once_with([{"lib_id": lib_id, "api": 0}]) + error_message = "Library charms.testcharm.v0.testlib was already up to date in version 0.7." + if formatted: + expected = [ + { + "charm_name": "testcharm", + "library_name": "testlib", + "library_id": lib_id, + "api": 0, + "error_message": error_message, + }, + ] + emitter.assert_json_output(expected) + else: + emitter.assert_message(error_message) + + +@pytest.mark.parametrize("formatted", [None, JSON_FORMAT]) +def test_fetchlib_store_same_versions_different_hash( + emitter, store_mock, tmp_path, monkeypatch, config, formatted +): + """The store has the lib in the same version, but with different content.""" + monkeypatch.chdir(tmp_path) + + lib_id = "test-example-lib-id" + factory.create_lib_filepath("testcharm", "testlib", api=0, patch=7, lib_id=lib_id) + + store_mock.get_libraries_tips.return_value = { + (lib_id, 0): Library( + lib_id=lib_id, + content=None, + content_hash="abc", + api=0, + patch=7, + lib_name="testlib", + charm_name="testcharm", + ), + } + args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=formatted) + FetchLibCommand(config).run(args) + + assert store_mock.mock_calls == [ + mock.call.get_libraries_tips([{"lib_id": lib_id, "api": 0}]), + ] + error_message = "Library charms.testcharm.v0.testlib has local changes, cannot be updated." + if formatted: + expected = [ + { + "charm_name": "testcharm", + "library_name": "testlib", + "library_id": lib_id, + "api": 0, + "error_message": error_message, + }, + ] + emitter.assert_json_output(expected) + else: + emitter.assert_message(error_message) + + +# endregion