diff options
Diffstat (limited to 'test/units/galaxy/test_collection_install.py')
-rw-r--r-- | test/units/galaxy/test_collection_install.py | 558 |
1 files changed, 331 insertions, 227 deletions
diff --git a/test/units/galaxy/test_collection_install.py b/test/units/galaxy/test_collection_install.py index 9acf55fd52..a4f5c35a3b 100644 --- a/test/units/galaxy/test_collection_install.py +++ b/test/units/galaxy/test_collection_install.py @@ -24,12 +24,24 @@ import ansible.module_utils.six.moves.urllib.error as urllib_error from ansible import context from ansible.cli.galaxy import GalaxyCLI from ansible.errors import AnsibleError -from ansible.galaxy import collection, api +from ansible.galaxy import collection, api, dependency_resolution +from ansible.galaxy.dependency_resolution.dataclasses import Candidate, Requirement from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.utils import context_objects as co from ansible.utils.display import Display +class RequirementCandidates(): + def __init__(self): + self.candidates = [] + + def func_wrapper(self, func): + def run(*args, **kwargs): + self.candidates = func(*args, **kwargs) + return self.candidates + return run + + def call_galaxy_cli(args): orig = co.GlobalCLIArgs._Singleton__instance co.GlobalCLIArgs._Singleton__instance = None @@ -160,16 +172,14 @@ def galaxy_server(): def test_build_requirement_from_path(collection_artifact): - actual = collection.CollectionRequirement.from_path(collection_artifact[0], True) + tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp') + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False) + actual = Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm) assert actual.namespace == u'ansible_namespace' assert actual.name == u'collection' - assert actual.b_path == collection_artifact[0] - assert actual.api is None - assert actual.skip is True - assert actual.versions == set([u'*']) - assert actual.latest_version == u'*' - assert actual.dependencies == {} + assert actual.src == collection_artifact[0] + assert actual.ver == u'0.1.0' @pytest.mark.parametrize('version', ['1.1.1', '1.1.0', '1.0.0']) @@ -188,17 +198,15 @@ def test_build_requirement_from_path_with_manifest(version, collection_artifact) with open(manifest_path, 'wb') as manifest_obj: manifest_obj.write(to_bytes(manifest_value)) - actual = collection.CollectionRequirement.from_path(collection_artifact[0], True) + tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp') + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False) + actual = Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm) # While the folder name suggests a different collection, we treat MANIFEST.json as the source of truth. assert actual.namespace == u'namespace' assert actual.name == u'name' - assert actual.b_path == collection_artifact[0] - assert actual.api is None - assert actual.skip is True - assert actual.versions == set([to_text(version)]) - assert actual.latest_version == to_text(version) - assert actual.dependencies == {'ansible_namespace.collection': '*'} + assert actual.src == collection_artifact[0] + assert actual.ver == to_text(version) def test_build_requirement_from_path_invalid_manifest(collection_artifact): @@ -206,12 +214,19 @@ def test_build_requirement_from_path_invalid_manifest(collection_artifact): with open(manifest_path, 'wb') as manifest_obj: manifest_obj.write(b"not json") - expected = "Collection file at '%s' does not contain a valid json string." % to_native(manifest_path) + tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp') + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False) + + expected = "Collection tar file member MANIFEST.json does not contain a valid json string." with pytest.raises(AnsibleError, match=expected): - collection.CollectionRequirement.from_path(collection_artifact[0], True) + Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm) -def test_build_requirement_from_path_no_version(collection_artifact, monkeypatch): +def test_build_artifact_from_path_no_version(collection_artifact, monkeypatch): + mock_display = MagicMock() + monkeypatch.setattr(Display, 'display', mock_display) + + # a collection artifact should always contain a valid version manifest_path = os.path.join(collection_artifact[0], b'MANIFEST.json') manifest_value = json.dumps({ 'collection_info': { @@ -224,40 +239,56 @@ def test_build_requirement_from_path_no_version(collection_artifact, monkeypatch with open(manifest_path, 'wb') as manifest_obj: manifest_obj.write(to_bytes(manifest_value)) + tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp') + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False) + + expected = ( + '^Collection metadata file at `.*` is expected to have a valid SemVer ' + 'version value but got {empty_unicode_string!r}$'. + format(empty_unicode_string=u'') + ) + with pytest.raises(AnsibleError, match=expected): + Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm) + + +def test_build_requirement_from_path_no_version(collection_artifact, monkeypatch): mock_display = MagicMock() monkeypatch.setattr(Display, 'display', mock_display) - actual = collection.CollectionRequirement.from_path(collection_artifact[0], True) + # version may be falsey/arbitrary strings for collections in development + manifest_path = os.path.join(collection_artifact[0], b'galaxy.yml') + metadata = { + 'authors': ['Ansible'], + 'readme': 'README.md', + 'namespace': 'namespace', + 'name': 'name', + 'version': '', + 'dependencies': {}, + } + with open(manifest_path, 'wb') as manifest_obj: + manifest_obj.write(to_bytes(yaml.safe_dump(metadata))) + + tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp') + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False) + actual = Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm) # While the folder name suggests a different collection, we treat MANIFEST.json as the source of truth. assert actual.namespace == u'namespace' assert actual.name == u'name' - assert actual.b_path == collection_artifact[0] - assert actual.api is None - assert actual.skip is True - assert actual.versions == set(['*']) - assert actual.latest_version == u'*' - assert actual.dependencies == {} - - assert mock_display.call_count == 1 - - actual_warn = ' '.join(mock_display.mock_calls[0][1][0].split('\n')) - expected_warn = "Collection at '%s' does not have a valid version set, falling back to '*'. Found version: ''" \ - % to_text(collection_artifact[0]) - assert expected_warn in actual_warn + assert actual.src == collection_artifact[0] + assert actual.ver == u'*' def test_build_requirement_from_tar(collection_artifact): - actual = collection.CollectionRequirement.from_tar(collection_artifact[1], True, True) + tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp') + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False) + + actual = Requirement.from_requirement_dict({'name': to_text(collection_artifact[1])}, concrete_artifact_cm) assert actual.namespace == u'ansible_namespace' assert actual.name == u'collection' - assert actual.b_path == collection_artifact[1] - assert actual.api is None - assert actual.skip is False - assert actual.versions == set([u'0.1.0']) - assert actual.latest_version == u'0.1.0' - assert actual.dependencies == {} + assert actual.src == to_text(collection_artifact[1]) + assert actual.ver == u'0.1.0' def test_build_requirement_from_tar_fail_not_tar(tmp_path_factory): @@ -266,9 +297,11 @@ def test_build_requirement_from_tar_fail_not_tar(tmp_path_factory): with open(test_file, 'wb') as test_obj: test_obj.write(b"\x00\x01\x02\x03") + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + expected = "Collection artifact at '%s' is not a valid tar file." % to_native(test_file) with pytest.raises(AnsibleError, match=expected): - collection.CollectionRequirement.from_tar(test_file, True, True) + Requirement.from_requirement_dict({'name': to_text(test_file)}, concrete_artifact_cm) def test_build_requirement_from_tar_no_manifest(tmp_path_factory): @@ -289,9 +322,11 @@ def test_build_requirement_from_tar_no_manifest(tmp_path_factory): tar_info.mode = 0o0644 tfile.addfile(tarinfo=tar_info, fileobj=b_io) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + expected = "Collection at '%s' does not contain the required file MANIFEST.json." % to_native(tar_path) with pytest.raises(AnsibleError, match=expected): - collection.CollectionRequirement.from_tar(tar_path, True, True) + Requirement.from_requirement_dict({'name': to_text(tar_path)}, concrete_artifact_cm) def test_build_requirement_from_tar_no_files(tmp_path_factory): @@ -311,9 +346,9 @@ def test_build_requirement_from_tar_no_files(tmp_path_factory): tar_info.mode = 0o0644 tfile.addfile(tarinfo=tar_info, fileobj=b_io) - expected = "Collection at '%s' does not contain the required file FILES.json." % to_native(tar_path) - with pytest.raises(AnsibleError, match=expected): - collection.CollectionRequirement.from_tar(tar_path, True, True) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + with pytest.raises(KeyError, match='namespace'): + Requirement.from_requirement_dict({'name': to_text(tar_path)}, concrete_artifact_cm) def test_build_requirement_from_tar_invalid_manifest(tmp_path_factory): @@ -329,95 +364,128 @@ def test_build_requirement_from_tar_invalid_manifest(tmp_path_factory): tar_info.mode = 0o0644 tfile.addfile(tarinfo=tar_info, fileobj=b_io) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + expected = "Collection tar file member MANIFEST.json does not contain a valid json string." with pytest.raises(AnsibleError, match=expected): - collection.CollectionRequirement.from_tar(tar_path, True, True) + Requirement.from_requirement_dict({'name': to_text(tar_path)}, concrete_artifact_cm) -def test_build_requirement_from_name(galaxy_server, monkeypatch): +def test_build_requirement_from_name(galaxy_server, monkeypatch, tmp_path_factory): mock_get_versions = MagicMock() mock_get_versions.return_value = ['2.1.9', '2.1.10'] monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) - actual = collection.CollectionRequirement.from_name('namespace.collection', [galaxy_server], '*', True, True) + mock_version_metadata = MagicMock( + namespace='namespace', name='collection', + version='2.1.10', artifact_sha256='', dependencies={} + ) + monkeypatch.setattr(api.GalaxyAPI, 'get_collection_version_metadata', mock_version_metadata) + + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + + collections = ['namespace.collection'] + requirements_file = None + + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', collections[0]]) + requirements = cli._require_one_of_collections_requirements( + collections, requirements_file, artifacts_manager=concrete_artifact_cm + )['collections'] + actual = collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, True, False)['namespace.collection'] assert actual.namespace == u'namespace' assert actual.name == u'collection' - assert actual.b_path is None - assert actual.api == galaxy_server - assert actual.skip is False - assert actual.versions == set([u'2.1.9', u'2.1.10']) - assert actual.latest_version == u'2.1.10' - assert actual.dependencies == {} + assert actual.ver == u'2.1.10' + assert actual.src == galaxy_server assert mock_get_versions.call_count == 1 assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection') -def test_build_requirement_from_name_with_prerelease(galaxy_server, monkeypatch): +def test_build_requirement_from_name_with_prerelease(galaxy_server, monkeypatch, tmp_path_factory): mock_get_versions = MagicMock() mock_get_versions.return_value = ['1.0.1', '2.0.1-beta.1', '2.0.1'] monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) - actual = collection.CollectionRequirement.from_name('namespace.collection', [galaxy_server], '*', True, True) + mock_get_info = MagicMock() + mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.1', None, None, {}) + monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) + + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection'], None, artifacts_manager=concrete_artifact_cm + )['collections'] + actual = collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, True, False)['namespace.collection'] assert actual.namespace == u'namespace' assert actual.name == u'collection' - assert actual.b_path is None - assert actual.api == galaxy_server - assert actual.skip is False - assert actual.versions == set([u'1.0.1', u'2.0.1']) - assert actual.latest_version == u'2.0.1' - assert actual.dependencies == {} + assert actual.src == galaxy_server + assert actual.ver == u'2.0.1' assert mock_get_versions.call_count == 1 assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection') -def test_build_requirment_from_name_with_prerelease_explicit(galaxy_server, monkeypatch): +def test_build_requirment_from_name_with_prerelease_explicit(galaxy_server, monkeypatch, tmp_path_factory): + mock_get_versions = MagicMock() + mock_get_versions.return_value = ['1.0.1', '2.0.1-beta.1', '2.0.1'] + monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) + mock_get_info = MagicMock() mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.1-beta.1', None, None, {}) monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) - actual = collection.CollectionRequirement.from_name('namespace.collection', [galaxy_server], '2.0.1-beta.1', True, - True) + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:2.0.1-beta.1']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection:2.0.1-beta.1'], None, artifacts_manager=concrete_artifact_cm + )['collections'] + actual = collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, True, False)['namespace.collection'] assert actual.namespace == u'namespace' assert actual.name == u'collection' - assert actual.b_path is None - assert actual.api == galaxy_server - assert actual.skip is False - assert actual.versions == set([u'2.0.1-beta.1']) - assert actual.latest_version == u'2.0.1-beta.1' - assert actual.dependencies == {} + assert actual.src == galaxy_server + assert actual.ver == u'2.0.1-beta.1' assert mock_get_info.call_count == 1 assert mock_get_info.mock_calls[0][1] == ('namespace', 'collection', '2.0.1-beta.1') -def test_build_requirement_from_name_second_server(galaxy_server, monkeypatch): +def test_build_requirement_from_name_second_server(galaxy_server, monkeypatch, tmp_path_factory): mock_get_versions = MagicMock() mock_get_versions.return_value = ['1.0.1', '1.0.2', '1.0.3'] monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) + mock_get_info = MagicMock() + mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '1.0.3', None, None, {}) + monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) + broken_server = copy.copy(galaxy_server) broken_server.api_server = 'https://broken.com/' mock_version_list = MagicMock() mock_version_list.return_value = [] monkeypatch.setattr(broken_server, 'get_collection_versions', mock_version_list) - actual = collection.CollectionRequirement.from_name('namespace.collection', [broken_server, galaxy_server], - '>1.0.1', False, True) + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:>1.0.1']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection:>1.0.1'], None, artifacts_manager=concrete_artifact_cm + )['collections'] + actual = collection._resolve_depenency_map(requirements, [broken_server, galaxy_server], concrete_artifact_cm, None, True, False)['namespace.collection'] assert actual.namespace == u'namespace' assert actual.name == u'collection' - assert actual.b_path is None - # assert actual.api == galaxy_server - assert actual.skip is False - assert actual.versions == set([u'1.0.2', u'1.0.3']) - assert actual.latest_version == u'1.0.3' - assert actual.dependencies == {} + assert actual.src == galaxy_server + assert actual.ver == u'1.0.3' assert mock_version_list.call_count == 1 assert mock_version_list.mock_calls[0][1] == ('namespace', 'collection') @@ -426,53 +494,91 @@ def test_build_requirement_from_name_second_server(galaxy_server, monkeypatch): assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection') -def test_build_requirement_from_name_missing(galaxy_server, monkeypatch): +def test_build_requirement_from_name_missing(galaxy_server, monkeypatch, tmp_path_factory): mock_open = MagicMock() mock_open.return_value = [] monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_open) - expected = "Failed to find collection namespace.collection:*" - with pytest.raises(AnsibleError, match=expected): - collection.CollectionRequirement.from_name('namespace.collection', [galaxy_server, galaxy_server], '*', False, - True) + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:>1.0.1']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection'], None, artifacts_manager=concrete_artifact_cm + )['collections'] -def test_build_requirement_from_name_401_unauthorized(galaxy_server, monkeypatch): + expected = "Failed to resolve the requested dependencies map. Could not satisfy the following requirements:\n* namespace.collection:* (direct request)" + with pytest.raises(AnsibleError, match=re.escape(expected)): + collection._resolve_depenency_map(requirements, [galaxy_server, galaxy_server], concrete_artifact_cm, None, False, True) + + +def test_build_requirement_from_name_401_unauthorized(galaxy_server, monkeypatch, tmp_path_factory): mock_open = MagicMock() mock_open.side_effect = api.GalaxyError(urllib_error.HTTPError('https://galaxy.server.com', 401, 'msg', {}, StringIO()), "error") monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_open) + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:>1.0.1']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection'], None, artifacts_manager=concrete_artifact_cm + )['collections'] + expected = "error (HTTP Code: 401, Message: msg)" with pytest.raises(api.GalaxyError, match=re.escape(expected)): - collection.CollectionRequirement.from_name('namespace.collection', [galaxy_server, galaxy_server], '*', False) + collection._resolve_depenency_map(requirements, [galaxy_server, galaxy_server], concrete_artifact_cm, None, False, False) -def test_build_requirement_from_name_single_version(galaxy_server, monkeypatch): +def test_build_requirement_from_name_single_version(galaxy_server, monkeypatch, tmp_path_factory): + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + multi_api_proxy = collection.galaxy_api_proxy.MultiGalaxyAPIProxy([galaxy_server], concrete_artifact_cm) + dep_provider = dependency_resolution.providers.CollectionDependencyProvider(apis=multi_api_proxy, concrete_artifacts_manager=concrete_artifact_cm) + + matches = RequirementCandidates() + mock_find_matches = MagicMock(side_effect=matches.func_wrapper(dep_provider.find_matches), autospec=True) + monkeypatch.setattr(dependency_resolution.providers.CollectionDependencyProvider, 'find_matches', mock_find_matches) + + mock_get_versions = MagicMock() + mock_get_versions.return_value = ['2.0.0'] + monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) + mock_get_info = MagicMock() mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.0', None, None, {}) monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) - actual = collection.CollectionRequirement.from_name('namespace.collection', [galaxy_server], '2.0.0', True, - True) + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:==2.0.0']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection:==2.0.0'], None, artifacts_manager=concrete_artifact_cm + )['collections'] + + actual = collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, False, True)['namespace.collection'] assert actual.namespace == u'namespace' assert actual.name == u'collection' - assert actual.b_path is None - assert actual.api == galaxy_server - assert actual.skip is False - assert actual.versions == set([u'2.0.0']) - assert actual.latest_version == u'2.0.0' - assert actual.dependencies == {} + assert actual.src == galaxy_server + assert actual.ver == u'2.0.0' + assert [c.ver for c in matches.candidates] == [u'2.0.0'] assert mock_get_info.call_count == 1 assert mock_get_info.mock_calls[0][1] == ('namespace', 'collection', '2.0.0') -def test_build_requirement_from_name_multiple_versions_one_match(galaxy_server, monkeypatch): +def test_build_requirement_from_name_multiple_versions_one_match(galaxy_server, monkeypatch, tmp_path_factory): + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + multi_api_proxy = collection.galaxy_api_proxy.MultiGalaxyAPIProxy([galaxy_server], concrete_artifact_cm) + dep_provider = dependency_resolution.providers.CollectionDependencyProvider(apis=multi_api_proxy, concrete_artifacts_manager=concrete_artifact_cm) + + matches = RequirementCandidates() + mock_find_matches = MagicMock(side_effect=matches.func_wrapper(dep_provider.find_matches), autospec=True) + monkeypatch.setattr(dependency_resolution.providers.CollectionDependencyProvider, 'find_matches', mock_find_matches) + mock_get_versions = MagicMock() mock_get_versions.return_value = ['2.0.0', '2.0.1', '2.0.2'] monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) @@ -482,17 +588,18 @@ def test_build_requirement_from_name_multiple_versions_one_match(galaxy_server, {}) monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) - actual = collection.CollectionRequirement.from_name('namespace.collection', [galaxy_server], '>=2.0.1,<2.0.2', - True, True) + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:>=2.0.1,<2.0.2']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection:>=2.0.1,<2.0.2'], None, artifacts_manager=concrete_artifact_cm + )['collections'] + + actual = collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, False, True)['namespace.collection'] assert actual.namespace == u'namespace' assert actual.name == u'collection' - assert actual.b_path is None - assert actual.api == galaxy_server - assert actual.skip is False - assert actual.versions == set([u'2.0.1']) - assert actual.latest_version == u'2.0.1' - assert actual.dependencies == {} + assert actual.src == galaxy_server + assert actual.ver == u'2.0.1' + assert [c.ver for c in matches.candidates] == [u'2.0.1'] assert mock_get_versions.call_count == 1 assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection') @@ -501,122 +608,118 @@ def test_build_requirement_from_name_multiple_versions_one_match(galaxy_server, assert mock_get_info.mock_calls[0][1] == ('namespace', 'collection', '2.0.1') -def test_build_requirement_from_name_multiple_version_results(galaxy_server, monkeypatch): +def test_build_requirement_from_name_multiple_version_results(galaxy_server, monkeypatch, tmp_path_factory): + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + multi_api_proxy = collection.galaxy_api_proxy.MultiGalaxyAPIProxy([galaxy_server], concrete_artifact_cm) + dep_provider = dependency_resolution.providers.CollectionDependencyProvider(apis=multi_api_proxy, concrete_artifacts_manager=concrete_artifact_cm) + + matches = RequirementCandidates() + mock_find_matches = MagicMock(side_effect=matches.func_wrapper(dep_provider.find_matches), autospec=True) + monkeypatch.setattr(dependency_resolution.providers.CollectionDependencyProvider, 'find_matches', mock_find_matches) + + mock_get_info = MagicMock() + mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.5', None, None, {}) + monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) + mock_get_versions = MagicMock() + mock_get_versions.return_value = ['1.0.1', '1.0.2', '1.0.3'] + monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) + mock_get_versions.return_value = ['2.0.0', '2.0.1', '2.0.2', '2.0.3', '2.0.4', '2.0.5'] monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) - actual = collection.CollectionRequirement.from_name('namespace.collection', [galaxy_server], '!=2.0.2', - True, True) + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:!=2.0.2']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection:!=2.0.2'], None, artifacts_manager=concrete_artifact_cm + )['collections'] + + actual = collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, False, True)['namespace.collection'] assert actual.namespace == u'namespace' assert actual.name == u'collection' - assert actual.b_path is None - assert actual.api == galaxy_server - assert actual.skip is False - assert actual.versions == set([u'2.0.0', u'2.0.1', u'2.0.3', u'2.0.4', u'2.0.5']) - assert actual.latest_version == u'2.0.5' - assert actual.dependencies == {} + assert actual.src == galaxy_server + assert actual.ver == u'2.0.5' + # should be ordered latest to earliest + assert [c.ver for c in matches.candidates] == [u'2.0.5', u'2.0.4', u'2.0.3', u'2.0.1', u'2.0.0'] assert mock_get_versions.call_count == 1 assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection') -@pytest.mark.parametrize('versions, requirement, expected_filter, expected_latest', [ - [['1.0.0', '1.0.1'], '*', ['1.0.0', '1.0.1'], '1.0.1'], - [['1.0.0', '1.0.5', '1.1.0'], '>1.0.0,<1.1.0', ['1.0.5'], '1.0.5'], - [['1.0.0', '1.0.5', '1.1.0'], '>1.0.0,<=1.0.5', ['1.0.5'], '1.0.5'], - [['1.0.0', '1.0.5', '1.1.0'], '>=1.1.0', ['1.1.0'], '1.1.0'], - [['1.0.0', '1.0.5', '1.1.0'], '!=1.1.0', ['1.0.0', '1.0.5'], '1.0.5'], - [['1.0.0', '1.0.5', '1.1.0'], '==1.0.5', ['1.0.5'], '1.0.5'], - [['1.0.0', '1.0.5', '1.1.0'], '1.0.5', ['1.0.5'], '1.0.5'], - [['1.0.0', '2.0.0', '3.0.0'], '>=2', ['2.0.0', '3.0.0'], '3.0.0'], -]) -def test_add_collection_requirements(versions, requirement, expected_filter, expected_latest): - req = collection.CollectionRequirement('namespace', 'name', None, 'https://galaxy.com', versions, requirement, - False) - assert req.versions == set(expected_filter) - assert req.latest_version == expected_latest - - -def test_add_collection_requirement_to_unknown_installed_version(monkeypatch): - mock_display = MagicMock() - monkeypatch.setattr(Display, 'display', mock_display) - - req = collection.CollectionRequirement('namespace', 'name', None, 'https://galaxy.com', ['*'], '*', False, - skip=True) +def test_candidate_with_conflict(monkeypatch, tmp_path_factory, galaxy_server): - req.add_requirement('parent.collection', '1.0.0') - assert req.latest_version == '*' + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) - assert mock_display.call_count == 1 + mock_get_info = MagicMock() + mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.5', None, None, {}) + monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) - actual_warn = ' '.join(mock_display.mock_calls[0][1][0].split('\n')) - assert "Failed to validate the collection requirement 'namespace.name:1.0.0' for parent.collection" in actual_warn + mock_get_versions = MagicMock() + mock_get_versions.return_value = ['2.0.5'] + monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:!=2.0.5']) + requirements = cli._require_one_of_collections_requirements( + ['namespace.collection:!=2.0.5'], None, artifacts_manager=concrete_artifact_cm + )['collections'] -def test_add_collection_wildcard_requirement_to_unknown_installed_version(): - req = collection.CollectionRequirement('namespace', 'name', None, 'https://galaxy.com', ['*'], '*', False, - skip=True) - req.add_requirement(str(req), '*') + expected = "Failed to resolve the requested dependencies map. Could not satisfy the following requirements:\n" + expected += "* namespace.collection:!=2.0.5 (direct request)" + with pytest.raises(AnsibleError, match=re.escape(expected)): + collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, False, True) - assert req.versions == set('*') - assert req.latest_version == '*' +def test_dep_candidate_with_conflict(monkeypatch, tmp_path_factory, galaxy_server): + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) -def test_add_collection_requirement_with_conflict(galaxy_server): - expected = "Cannot meet requirement ==1.0.2 for dependency namespace.name from source '%s'. Available versions " \ - "before last requirement added: 1.0.0, 1.0.1\n" \ - "Requirements from:\n" \ - "\tbase - 'namespace.name:==1.0.2'" % galaxy_server.api_server - with pytest.raises(AnsibleError, match=expected): - collection.CollectionRequirement('namespace', 'name', None, galaxy_server, ['1.0.0', '1.0.1'], '==1.0.2', - False) + mock_get_info_return = [ + api.CollectionVersionMetadata('parent', 'collection', '2.0.5', None, None, {'namespace.collection': '!=1.0.0'}), + api.CollectionVersionMetadata('namespace', 'collection', '1.0.0', None, None, {}), + ] + mock_get_info = MagicMock(side_effect=mock_get_info_return) + monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) + mock_get_versions = MagicMock(side_effect=[['2.0.5'], ['1.0.0']]) + monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) -def test_add_requirement_to_existing_collection_with_conflict(galaxy_server): - req = collection.CollectionRequirement('namespace', 'name', None, galaxy_server, ['1.0.0', '1.0.1'], '*', False) + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'parent.collection:2.0.5']) + requirements = cli._require_one_of_collections_requirements( + ['parent.collection:2.0.5'], None, artifacts_manager=concrete_artifact_cm + )['collections'] - expected = "Cannot meet dependency requirement 'namespace.name:1.0.2' for collection namespace.collection2 from " \ - "source '%s'. Available versions before last requirement added: 1.0.0, 1.0.1\n" \ - "Requirements from:\n" \ - "\tbase - 'namespace.name:*'\n" \ - "\tnamespace.collection2 - 'namespace.name:1.0.2'" % galaxy_server.api_server + expected = "Failed to resolve the requested dependencies map. Could not satisfy the following requirements:\n" + expected += "* namespace.collection:!=1.0.0 (dependency of parent.collection:2.0.5)" with pytest.raises(AnsibleError, match=re.escape(expected)): - req.add_requirement('namespace.collection2', '1.0.2') + collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, False, True) -def test_add_requirement_to_installed_collection_with_conflict(): - source = 'https://galaxy.ansible.com' - req = collection.CollectionRequirement('namespace', 'name', None, source, ['1.0.0', '1.0.1'], '*', False, - skip=True) +def test_install_installed_collection(monkeypatch, tmp_path_factory, galaxy_server): - expected = "Cannot meet requirement namespace.name:1.0.2 as it is already installed at version '1.0.1'. " \ - "Use --force to overwrite" - with pytest.raises(AnsibleError, match=re.escape(expected)): - req.add_requirement(None, '1.0.2') + mock_installed_collections = MagicMock(return_value=[Candidate('namespace.collection', '1.2.3', None, 'dir')]) + monkeypatch.setattr(collection, 'find_existing_collections', mock_installed_collections) -def test_add_requirement_to_installed_collection_with_conflict_as_dep(): - source = 'https://galaxy.ansible.com' - req = collection.CollectionRequirement('namespace', 'name', None, source, ['1.0.0', '1.0.1'], '*', False, - skip=True) + test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) - expected = "Cannot meet requirement namespace.name:1.0.2 as it is already installed at version '1.0.1'. " \ - "Use --force-with-deps to overwrite" - with pytest.raises(AnsibleError, match=re.escape(expected)): - req.add_requirement('namespace.collection2', '1.0.2') - - -def test_install_skipped_collection(monkeypatch): mock_display = MagicMock() monkeypatch.setattr(Display, 'display', mock_display) - req = collection.CollectionRequirement('namespace', 'name', None, 'source', ['1.0.0'], '*', False, skip=True) - req.install(None, None) + mock_get_info = MagicMock() + mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '1.2.3', None, None, {}) + monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info) + + mock_get_versions = MagicMock(return_value=['1.2.3', '1.3.0']) + monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions) + + cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection']) + cli.run() - assert mock_display.call_count == 1 - assert mock_display.mock_calls[0][1][0] == "Skipping 'namespace.name' as it is already installed" + expected = "Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`." + assert mock_display.mock_calls[1][1][0] == expected def test_install_collection(collection_artifact, monkeypatch): @@ -624,15 +727,17 @@ def test_install_collection(collection_artifact, monkeypatch): monkeypatch.setattr(Display, 'display', mock_display) collection_tar = collection_artifact[1] - output_path = os.path.join(os.path.split(collection_tar)[0], b'output') - collection_path = os.path.join(output_path, b'ansible_namespace', b'collection') - os.makedirs(os.path.join(collection_path, b'delete_me')) # Create a folder to verify the install cleans out the dir temp_path = os.path.join(os.path.split(collection_tar)[0], b'temp') os.makedirs(temp_path) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) - req = collection.CollectionRequirement.from_tar(collection_tar, True, True) - req.install(to_text(output_path), temp_path) + output_path = os.path.join(os.path.split(collection_tar)[0]) + collection_path = os.path.join(output_path, b'ansible_namespace', b'collection') + os.makedirs(os.path.join(collection_path, b'delete_me')) # Create a folder to verify the install cleans out the dir + + candidate = Candidate('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file') + collection.install(candidate, to_text(output_path), concrete_artifact_cm) # Ensure the temp directory is empty, nothing is left behind assert os.listdir(temp_path) == [] @@ -649,33 +754,29 @@ def test_install_collection(collection_artifact, monkeypatch): assert mock_display.call_count == 2 assert mock_display.mock_calls[0][1][0] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" \ % to_text(collection_path) - assert mock_display.mock_calls[1][1][0] == "ansible_namespace.collection (0.1.0) was installed successfully" + assert mock_display.mock_calls[1][1][0] == "ansible_namespace.collection:0.1.0 was installed successfully" def test_install_collection_with_download(galaxy_server, collection_artifact, monkeypatch): - collection_tar = collection_artifact[1] - output_path = os.path.join(os.path.split(collection_tar)[0], b'output') - collection_path = os.path.join(output_path, b'ansible_namespace', b'collection') + collection_path, collection_tar = collection_artifact + shutil.rmtree(collection_path) + + collections_dir = ('%s' % os.path.sep).join(to_text(collection_path).split('%s' % os.path.sep)[:-2]) + + temp_path = os.path.join(os.path.split(collection_tar)[0], b'temp') + os.makedirs(temp_path) mock_display = MagicMock() monkeypatch.setattr(Display, 'display', mock_display) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) + mock_download = MagicMock() mock_download.return_value = collection_tar - monkeypatch.setattr(collection, '_download_file', mock_download) - - monkeypatch.setattr(galaxy_server, '_available_api_versions', {'v2': 'v2/'}) - temp_path = os.path.join(os.path.split(collection_tar)[0], b'temp') - os.makedirs(temp_path) + monkeypatch.setattr(concrete_artifact_cm, 'get_galaxy_artifact_path', mock_download) - meta = api.CollectionVersionMetadata('ansible_namespace', 'collection', '0.1.0', 'https://downloadme.com', - 'myhash', {}) - req = collection.CollectionRequirement('ansible_namespace', 'collection', None, galaxy_server, - ['0.1.0'], '*', False, metadata=meta) - req.install(to_text(output_path), temp_path) - - # Ensure the temp directory is empty, nothing is left behind - assert os.listdir(temp_path) == [] + req = Requirement('ansible_namespace.collection', '0.1.0', 'https://downloadme.com', 'galaxy') + collection.install(req, to_text(collections_dir), concrete_artifact_cm) actual_files = os.listdir(collection_path) actual_files.sort() @@ -685,13 +786,11 @@ def test_install_collection_with_download(galaxy_server, collection_artifact, mo assert mock_display.call_count == 2 assert mock_display.mock_calls[0][1][0] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" \ % to_text(collection_path) - assert mock_display.mock_calls[1][1][0] == "ansible_namespace.collection (0.1.0) was installed successfully" + assert mock_display.mock_calls[1][1][0] == "ansible_namespace.collection:0.1.0 was installed successfully" assert mock_download.call_count == 1 - assert mock_download.mock_calls[0][1][0] == 'https://downloadme.com' - assert mock_download.mock_calls[0][1][1] == temp_path - assert mock_download.mock_calls[0][1][2] == 'myhash' - assert mock_download.mock_calls[0][1][3] is True + assert mock_download.mock_calls[0][1][0].src == 'https://downloadme.com' + assert mock_download.mock_calls[0][1][0].type == 'galaxy' def test_install_collections_from_tar(collection_artifact, monkeypatch): @@ -702,8 +801,10 @@ def test_install_collections_from_tar(collection_artifact, monkeypatch): mock_display = MagicMock() monkeypatch.setattr(Display, 'display', mock_display) - collection.install_collections([(to_text(collection_tar), '*', None, None)], to_text(temp_path), - [u'https://galaxy.ansible.com'], True, False, False, False, False) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) + + requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file')] + collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, concrete_artifact_cm) assert os.path.isdir(collection_path) @@ -734,9 +835,12 @@ def test_install_collections_existing_without_force(collection_artifact, monkeyp mock_display = MagicMock() monkeypatch.setattr(Display, 'display', mock_display) - # If we don't delete collection_path it will think the original build skeleton is installed so we expect a skip - collection.install_collections([(to_text(collection_tar), '*', None, None)], to_text(temp_path), - [u'https://galaxy.ansible.com'], True, False, False, False, False) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) + + assert os.path.isdir(collection_path) + + requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file')] + collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, concrete_artifact_cm) assert os.path.isdir(collection_path) @@ -746,11 +850,9 @@ def test_install_collections_existing_without_force(collection_artifact, monkeyp # Filter out the progress cursor display calls. display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1] - assert len(display_msgs) == 3 + assert len(display_msgs) == 1 - assert display_msgs[0] == "Process install dependency map" - assert display_msgs[1] == "Starting collection install process" - assert display_msgs[2] == "Skipping 'ansible_namespace.collection' as it is already installed" + assert display_msgs[0] == 'Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`.' for msg in display_msgs: assert 'WARNING' not in msg @@ -768,8 +870,9 @@ def test_install_missing_metadata_warning(collection_artifact, monkeypatch): if os.path.isfile(b_path): os.unlink(b_path) - collection.install_collections([(to_text(collection_tar), '*', None, None)], to_text(temp_path), - [u'https://galaxy.ansible.com'], True, False, False, False, False) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) + requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file')] + collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, concrete_artifact_cm) display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1] @@ -788,8 +891,9 @@ def test_install_collection_with_circular_dependency(collection_artifact, monkey mock_display = MagicMock() monkeypatch.setattr(Display, 'display', mock_display) - collection.install_collections([(to_text(collection_tar), '*', None, None)], to_text(temp_path), - [u'https://galaxy.ansible.com'], True, False, False, False, False) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) + requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file')] + collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, concrete_artifact_cm) assert os.path.isdir(collection_path) @@ -811,4 +915,4 @@ def test_install_collection_with_circular_dependency(collection_artifact, monkey assert display_msgs[0] == "Process install dependency map" assert display_msgs[1] == "Starting collection install process" assert display_msgs[2] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" % to_text(collection_path) - assert display_msgs[3] == "ansible_namespace.collection (0.1.0) was installed successfully" + assert display_msgs[3] == "ansible_namespace.collection:0.1.0 was installed successfully" |