Support pre-releases via new SemanticVersion (#68258)

* Support pre-releases via new SemanticVersion. Fixes #64905

* Don't treat buildmeta as prerelease

* Don't inherit from str and int

* Add helper method to try and construct a SemanticVersion from a LooseVersion

* Don't count major 0 as pre-release, it's different

* Guard against invalid or no version in LooseVersion

* return a bool

* Add integration tests for pre-release

* Fix up lingering issues with comparisons

* typo fix

* Always allow pre-releases in verify

* Move pre-release filtering into CollectionRequirement, add messaging when a collection only contains pre-releases

* Update changelog

* If explicit requirement allow pre releases

* Enable pre-releases for tar installs, and collections already installed when they are pre-releases

* Drop --pre-release alias, make arg name more clear

* Simplify code into a single line

* Remove build metadata precedence, add some comments, and is_stable helper

* Improve from_loose_version

* Increase test coverage

* linting fix

* Update changelog
pull/68417/head
Matt Martz 6 years ago committed by GitHub
parent ed9de94ad9
commit d3ec31f8d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,4 @@
minor_changes:
- Add ``--pre`` flag to ``ansible-galaxy collection install``
to allow pulling in the most recent pre-release version of a collection
(https://github.com/ansible/ansible/issues/64905)

@ -339,6 +339,8 @@ class GalaxyCLI(CLI):
help='The path to the directory containing your collections.') help='The path to the directory containing your collections.')
install_parser.add_argument('-r', '--requirements-file', dest='requirements', install_parser.add_argument('-r', '--requirements-file', dest='requirements',
help='A file containing a list of collections to be installed.') help='A file containing a list of collections to be installed.')
install_parser.add_argument('--pre', dest='allow_pre_release', action='store_true',
help='Include pre-release versions. Semantic versioning pre-releases are ignored by default')
else: else:
install_parser.add_argument('-r', '--role-file', dest='role_file', install_parser.add_argument('-r', '--role-file', dest='role_file',
help='A file containing a list of roles to be imported.') help='A file containing a list of roles to be imported.')
@ -897,7 +899,8 @@ class GalaxyCLI(CLI):
resolved_paths = [validate_collection_path(GalaxyCLI._resolve_path(path)) for path in search_paths] resolved_paths = [validate_collection_path(GalaxyCLI._resolve_path(path)) for path in search_paths]
verify_collections(requirements, resolved_paths, self.api_servers, (not ignore_certs), ignore_errors) verify_collections(requirements, resolved_paths, self.api_servers, (not ignore_certs), ignore_errors,
allow_pre_release=True)
return 0 return 0
@ -941,7 +944,7 @@ class GalaxyCLI(CLI):
os.makedirs(b_output_path) os.makedirs(b_output_path)
install_collections(requirements, output_path, self.api_servers, (not ignore_certs), ignore_errors, install_collections(requirements, output_path, self.api_servers, (not ignore_certs), ignore_errors,
no_deps, force, force_deps) no_deps, force, force_deps, context.CLIARGS['allow_pre_release'])
return 0 return 0

@ -18,7 +18,7 @@ import yaml
from collections import namedtuple from collections import namedtuple
from contextlib import contextmanager from contextlib import contextmanager
from distutils.version import LooseVersion, StrictVersion from distutils.version import LooseVersion
from hashlib import sha256 from hashlib import sha256
from io import BytesIO from io import BytesIO
from yaml.error import YAMLError from yaml.error import YAMLError
@ -38,6 +38,7 @@ from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.utils.collection_loader import AnsibleCollectionRef from ansible.utils.collection_loader import AnsibleCollectionRef
from ansible.utils.display import Display from ansible.utils.display import Display
from ansible.utils.hashing import secure_hash, secure_hash_s from ansible.utils.hashing import secure_hash, secure_hash_s
from ansible.utils.version import SemanticVersion
from ansible.module_utils.urls import open_url from ansible.module_utils.urls import open_url
urlparse = six.moves.urllib.parse.urlparse urlparse = six.moves.urllib.parse.urlparse
@ -56,7 +57,7 @@ class CollectionRequirement:
_FILE_MAPPING = [(b'MANIFEST.json', 'manifest_file'), (b'FILES.json', 'files_file')] _FILE_MAPPING = [(b'MANIFEST.json', 'manifest_file'), (b'FILES.json', 'files_file')]
def __init__(self, namespace, name, b_path, api, versions, requirement, force, parent=None, metadata=None, def __init__(self, namespace, name, b_path, api, versions, requirement, force, parent=None, metadata=None,
files=None, skip=False): files=None, skip=False, allow_pre_releases=False):
""" """
Represents a collection requirement, the versions that are available to be installed as well as any Represents a collection requirement, the versions that are available to be installed as well as any
dependencies the collection has. dependencies the collection has.
@ -75,15 +76,17 @@ class CollectionRequirement:
collection artifact. collection artifact.
:param skip: Whether to skip installing the collection. Should be set if the collection is already installed :param skip: Whether to skip installing the collection. Should be set if the collection is already installed
and force is not set. and force is not set.
:param allow_pre_releases: Whether to skip pre-release versions of collections.
""" """
self.namespace = namespace self.namespace = namespace
self.name = name self.name = name
self.b_path = b_path self.b_path = b_path
self.api = api self.api = api
self.versions = set(versions) self._versions = set(versions)
self.force = force self.force = force
self.skip = skip self.skip = skip
self.required_by = [] self.required_by = []
self.allow_pre_releases = allow_pre_releases
self._metadata = metadata self._metadata = metadata
self._files = files self._files = files
@ -101,10 +104,24 @@ class CollectionRequirement:
self._get_metadata() self._get_metadata()
return self._metadata return self._metadata
@property
def versions(self):
if self.allow_pre_releases:
return self._versions
return set(v for v in self._versions if v == '*' or not SemanticVersion(v).is_prerelease)
@versions.setter
def versions(self, value):
self._versions = set(value)
@property
def pre_releases(self):
return set(v for v in self._versions if SemanticVersion(v).is_prerelease)
@property @property
def latest_version(self): def latest_version(self):
try: try:
return max([v for v in self.versions if v != '*'], key=LooseVersion) return max([v for v in self.versions if v != '*'], key=SemanticVersion)
except ValueError: # ValueError: max() arg is an empty sequence except ValueError: # ValueError: max() arg is an empty sequence
return '*' return '*'
@ -144,10 +161,18 @@ class CollectionRequirement:
for p, r in self.required_by for p, r in self.required_by
) )
versions = ", ".join(sorted(self.versions, key=LooseVersion)) versions = ", ".join(sorted(self.versions, key=SemanticVersion))
if not self.versions and self.pre_releases:
pre_release_msg = (
'\nThis collection only contains pre-releases. Utilize `--pre` to install pre-releases, or '
'explicitly provide the pre-release version.'
)
else:
pre_release_msg = ''
raise AnsibleError( raise AnsibleError(
"%s from source '%s'. Available versions before last requirement added: %s\nRequirements from:\n%s" "%s from source '%s'. Available versions before last requirement added: %s\nRequirements from:\n%s%s"
% (msg, collection_source, versions, req_by) % (msg, collection_source, versions, req_by, pre_release_msg)
) )
self.versions = new_versions self.versions = new_versions
@ -298,7 +323,7 @@ class CollectionRequirement:
elif requirement == '*' or version == '*': elif requirement == '*' or version == '*':
continue continue
if not op(LooseVersion(version), LooseVersion(requirement)): if not op(SemanticVersion(version), SemanticVersion.from_loose_version(LooseVersion(requirement))):
break break
else: else:
return True return True
@ -336,8 +361,13 @@ class CollectionRequirement:
version = meta['version'] version = meta['version']
meta = CollectionVersionMetadata(namespace, name, version, None, None, meta['dependencies']) meta = CollectionVersionMetadata(namespace, name, version, None, None, meta['dependencies'])
if SemanticVersion(version).is_prerelease:
allow_pre_release = True
else:
allow_pre_release = False
return CollectionRequirement(namespace, name, b_path, None, [version], version, force, parent=parent, return CollectionRequirement(namespace, name, b_path, None, [version], version, force, parent=parent,
metadata=meta, files=files) metadata=meta, files=files, allow_pre_releases=allow_pre_release)
@staticmethod @staticmethod
def from_path(b_path, force, parent=None): def from_path(b_path, force, parent=None):
@ -354,13 +384,19 @@ class CollectionRequirement:
raise AnsibleError("Collection file at '%s' does not contain a valid json string." raise AnsibleError("Collection file at '%s' does not contain a valid json string."
% to_native(b_file_path)) % to_native(b_file_path))
allow_pre_release = False
if 'manifest_file' in info: if 'manifest_file' in info:
manifest = info['manifest_file']['collection_info'] manifest = info['manifest_file']['collection_info']
namespace = manifest['namespace'] namespace = manifest['namespace']
name = manifest['name'] name = manifest['name']
version = to_text(manifest['version'], errors='surrogate_or_strict') version = to_text(manifest['version'], errors='surrogate_or_strict')
if not hasattr(LooseVersion(version), 'version'): try:
_v = SemanticVersion()
_v.parse(version)
if _v.is_prerelease:
allow_pre_release = True
except ValueError:
display.warning("Collection at '%s' does not have a valid version set, falling back to '*'. Found " display.warning("Collection at '%s' does not have a valid version set, falling back to '*'. Found "
"version: '%s'" % (to_text(b_path), version)) "version: '%s'" % (to_text(b_path), version))
version = '*' version = '*'
@ -380,10 +416,10 @@ class CollectionRequirement:
files = info.get('files_file', {}).get('files', {}) files = info.get('files_file', {}).get('files', {})
return CollectionRequirement(namespace, name, b_path, None, [version], version, force, parent=parent, return CollectionRequirement(namespace, name, b_path, None, [version], version, force, parent=parent,
metadata=meta, files=files, skip=True) metadata=meta, files=files, skip=True, allow_pre_releases=allow_pre_release)
@staticmethod @staticmethod
def from_name(collection, apis, requirement, force, parent=None): def from_name(collection, apis, requirement, force, parent=None, allow_pre_release=False):
namespace, name = collection.split('.', 1) namespace, name = collection.split('.', 1)
galaxy_meta = None galaxy_meta = None
@ -391,6 +427,9 @@ class CollectionRequirement:
try: try:
if not (requirement == '*' or requirement.startswith('<') or requirement.startswith('>') or if not (requirement == '*' or requirement.startswith('<') or requirement.startswith('>') or
requirement.startswith('!=')): requirement.startswith('!=')):
# Exact requirement
allow_pre_release = True
if requirement.startswith('='): if requirement.startswith('='):
requirement = requirement.lstrip('=') requirement = requirement.lstrip('=')
@ -399,11 +438,7 @@ class CollectionRequirement:
galaxy_meta = resp galaxy_meta = resp
versions = [resp.version] versions = [resp.version]
else: else:
resp = api.get_collection_versions(namespace, name) versions = api.get_collection_versions(namespace, name)
# Galaxy supports semver but ansible-galaxy does not. We ignore any versions that don't match
# StrictVersion (x.y.z) and only support pre-releases if an explicit version was set (done above).
versions = [v for v in resp if StrictVersion.version_re.match(v)]
except GalaxyError as err: except GalaxyError as err:
if err.http_code == 404: if err.http_code == 404:
display.vvv("Collection '%s' is not available from server %s %s" display.vvv("Collection '%s' is not available from server %s %s"
@ -417,7 +452,7 @@ class CollectionRequirement:
raise AnsibleError("Failed to find collection %s:%s" % (collection, requirement)) raise AnsibleError("Failed to find collection %s:%s" % (collection, requirement))
req = CollectionRequirement(namespace, name, None, api, versions, requirement, force, parent=parent, req = CollectionRequirement(namespace, name, None, api, versions, requirement, force, parent=parent,
metadata=galaxy_meta) metadata=galaxy_meta, allow_pre_releases=allow_pre_release)
return req return req
@ -493,7 +528,8 @@ def publish_collection(collection_path, api, wait, timeout):
% (api.name, api.api_server, import_uri)) % (api.name, api.api_server, import_uri))
def install_collections(collections, output_path, apis, validate_certs, ignore_errors, no_deps, force, force_deps): def install_collections(collections, output_path, apis, validate_certs, ignore_errors, no_deps, force, force_deps,
allow_pre_release=False):
""" """
Install Ansible collections to the path specified. Install Ansible collections to the path specified.
@ -512,7 +548,8 @@ def install_collections(collections, output_path, apis, validate_certs, ignore_e
display.display("Process install dependency map") display.display("Process install dependency map")
with _display_progress(): with _display_progress():
dependency_map = _build_dependency_map(collections, existing_collections, b_temp_path, apis, dependency_map = _build_dependency_map(collections, existing_collections, b_temp_path, apis,
validate_certs, force, force_deps, no_deps) validate_certs, force, force_deps, no_deps,
allow_pre_release=allow_pre_release)
display.display("Starting collection install process") display.display("Starting collection install process")
with _display_progress(): with _display_progress():
@ -557,7 +594,7 @@ def validate_collection_path(collection_path):
return collection_path return collection_path
def verify_collections(collections, search_paths, apis, validate_certs, ignore_errors): def verify_collections(collections, search_paths, apis, validate_certs, ignore_errors, allow_pre_release=False):
with _display_progress(): with _display_progress():
with _tempdir() as b_temp_path: with _tempdir() as b_temp_path:
@ -585,7 +622,8 @@ def verify_collections(collections, search_paths, apis, validate_certs, ignore_e
# Download collection on a galaxy server for comparison # Download collection on a galaxy server for comparison
try: try:
remote_collection = CollectionRequirement.from_name(collection_name, apis, collection_version, False, parent=None) remote_collection = CollectionRequirement.from_name(collection_name, apis, collection_version, False, parent=None,
allow_pre_release=allow_pre_release)
except AnsibleError as e: except AnsibleError as e:
if e.message == 'Failed to find collection %s:%s' % (collection[0], collection[1]): if e.message == 'Failed to find collection %s:%s' % (collection[0], collection[1]):
raise AnsibleError('Failed to find remote collection %s:%s on any of the galaxy servers' % (collection[0], collection[1])) raise AnsibleError('Failed to find remote collection %s:%s on any of the galaxy servers' % (collection[0], collection[1]))
@ -921,13 +959,13 @@ def find_existing_collections(path):
def _build_dependency_map(collections, existing_collections, b_temp_path, apis, validate_certs, force, force_deps, def _build_dependency_map(collections, existing_collections, b_temp_path, apis, validate_certs, force, force_deps,
no_deps): no_deps, allow_pre_release=False):
dependency_map = {} dependency_map = {}
# First build the dependency map on the actual requirements # First build the dependency map on the actual requirements
for name, version, source in collections: for name, version, source in collections:
_get_collection_info(dependency_map, existing_collections, name, version, source, b_temp_path, apis, _get_collection_info(dependency_map, existing_collections, name, version, source, b_temp_path, apis,
validate_certs, (force or force_deps)) validate_certs, (force or force_deps), allow_pre_release=allow_pre_release)
checked_parents = set([to_text(c) for c in dependency_map.values() if c.skip]) checked_parents = set([to_text(c) for c in dependency_map.values() if c.skip])
while len(dependency_map) != len(checked_parents): while len(dependency_map) != len(checked_parents):
@ -943,7 +981,7 @@ def _build_dependency_map(collections, existing_collections, b_temp_path, apis,
for dep_name, dep_requirement in parent_info.dependencies.items(): for dep_name, dep_requirement in parent_info.dependencies.items():
_get_collection_info(dependency_map, existing_collections, dep_name, dep_requirement, _get_collection_info(dependency_map, existing_collections, dep_name, dep_requirement,
parent_info.api, b_temp_path, apis, validate_certs, force_deps, parent_info.api, b_temp_path, apis, validate_certs, force_deps,
parent=parent) parent=parent, allow_pre_release=allow_pre_release)
checked_parents.add(parent) checked_parents.add(parent)
@ -963,7 +1001,7 @@ def _build_dependency_map(collections, existing_collections, b_temp_path, apis,
def _get_collection_info(dep_map, existing_collections, collection, requirement, source, b_temp_path, apis, def _get_collection_info(dep_map, existing_collections, collection, requirement, source, b_temp_path, apis,
validate_certs, force, parent=None): validate_certs, force, parent=None, allow_pre_release=False):
dep_msg = "" dep_msg = ""
if parent: if parent:
dep_msg = " - as dependency of %s" % parent dep_msg = " - as dependency of %s" % parent
@ -999,7 +1037,8 @@ def _get_collection_info(dep_map, existing_collections, collection, requirement,
collection_info.add_requirement(parent, requirement) collection_info.add_requirement(parent, requirement)
else: else:
apis = [source] if source else apis apis = [source] if source else apis
collection_info = CollectionRequirement.from_name(collection, apis, requirement, force, parent=parent) collection_info = CollectionRequirement.from_name(collection, apis, requirement, force, parent=parent,
allow_pre_release=allow_pre_release)
existing = [c for c in existing_collections if to_text(c) == to_text(collection_info)] existing = [c for c in existing_collections if to_text(c) == to_text(collection_info)]
if existing and not collection_info.force: if existing and not collection_info.force:

@ -0,0 +1,272 @@
# Copyright (c) 2020 Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
from distutils.version import LooseVersion, Version
from ansible.module_utils.six import text_type
# Regular expression taken from
# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
SEMVER_RE = re.compile(
r'''
^
(?P<major>0|[1-9]\d*)
\.
(?P<minor>0|[1-9]\d*)
\.
(?P<patch>0|[1-9]\d*)
(?:
-
(?P<prerelease>
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
)
)?
(?:
\+
(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)
)?
$
''',
flags=re.X
)
class _Alpha:
"""Class to easily allow comparing strings
Largely this exists to make comparing an integer and a string on py3
so that it works like py2.
"""
def __init__(self, specifier):
self.specifier = specifier
def __repr__(self):
return repr(self.specifier)
def __eq__(self, other):
if isinstance(other, _Alpha):
return self.specifier == other.specifier
elif isinstance(other, str):
return self.specifier == other
return False
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if isinstance(other, _Alpha):
return self.specifier < other.specifier
elif isinstance(other, str):
return self.specifier < other
elif isinstance(other, _Numeric):
return False
raise ValueError
def __gt__(self, other):
return not self.__lt__(other)
def __le__(self, other):
return self.__lt__(other) or self.__eq__(other)
def __ge__(self, other):
return self.__gt__(other) or self.__eq__(other)
class _Numeric:
"""Class to easily allow comparing numbers
Largely this exists to make comparing an integer and a string on py3
so that it works like py2.
"""
def __init__(self, specifier):
self.specifier = int(specifier)
def __repr__(self):
return repr(self.specifier)
def __eq__(self, other):
if isinstance(other, _Numeric):
return self.specifier == other.specifier
elif isinstance(other, int):
return self.specifier == other
return False
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if isinstance(other, _Numeric):
return self.specifier < other.specifier
elif isinstance(other, int):
return self.specifier < other
elif isinstance(other, _Alpha):
return True
raise ValueError
def __gt__(self, other):
return not self.__lt__(other)
def __le__(self, other):
return self.__lt__(other) or self.__eq__(other)
def __ge__(self, other):
return self.__gt__(other) or self.__eq__(other)
class SemanticVersion(Version):
"""Version comparison class that implements Semantic Versioning 2.0.0
Based off of ``distutils.version.Version``
"""
version_re = SEMVER_RE
def __init__(self, vstring=None):
self.vstring = vstring
self.major = None
self.minor = None
self.patch = None
self.prerelease = ()
self.buildmetadata = ()
if vstring:
self.parse(vstring)
def __repr__(self):
return 'SemanticVersion(%r)' % self.vstring
@staticmethod
def from_loose_version(loose_version):
"""This method is designed to take a ``LooseVersion``
and attempt to construct a ``SemanticVersion`` from it
This is useful where you want to do simple version math
without requiring users to provide a compliant semver.
"""
if not isinstance(loose_version, LooseVersion):
raise ValueError("%r is not a LooseVersion" % loose_version)
try:
version = loose_version.version[:]
except AttributeError:
raise ValueError("%r is not a LooseVersion" % loose_version)
extra_idx = 3
for marker in ('-', '+'):
try:
idx = version.index(marker)
except ValueError:
continue
else:
if idx < extra_idx:
extra_idx = idx
version[:] = version[:extra_idx]
if version and set(type(v) for v in version) != set((int,)):
raise ValueError("Non integer values in %r" % loose_version)
# Extra is everything to the right of the core version
extra = re.search('[+-].+$', loose_version.vstring)
version[:] = version + [0] * (3 - len(version))
return SemanticVersion(
'%s%s' % (
'.'.join(str(v) for v in version),
extra.group(0) if extra else ''
)
)
def parse(self, vstring):
match = SEMVER_RE.match(vstring)
if not match:
raise ValueError("invalid semantic version '%s'" % vstring)
(major, minor, patch, prerelease, buildmetadata) = match.group(1, 2, 3, 4, 5)
self.major = int(major)
self.minor = int(minor)
self.patch = int(patch)
if prerelease:
self.prerelease = tuple(_Numeric(x) if x.isdigit() else _Alpha(x) for x in prerelease.split('.'))
if buildmetadata:
self.buildmetadata = tuple(_Numeric(x) if x.isdigit() else _Alpha(x) for x in buildmetadata.split('.'))
@property
def core(self):
return self.major, self.minor, self.patch
@property
def is_prerelease(self):
return bool(self.prerelease)
@property
def is_stable(self):
# Major version zero (0.y.z) is for initial development. Anything MAY change at any time.
# The public API SHOULD NOT be considered stable.
# https://semver.org/#spec-item-4
return not (self.major == 0 or self.is_prerelease)
def _cmp(self, other):
if isinstance(other, str):
other = SemanticVersion(other)
if self.core != other.core:
# if the core version doesn't match
# prerelease and buildmetadata doesn't matter
if self.core < other.core:
return -1
else:
return 1
if not any((self.prerelease, other.prerelease)):
return 0
if self.prerelease and not other.prerelease:
return -1
elif not self.prerelease and other.prerelease:
return 1
else:
if self.prerelease < other.prerelease:
return -1
elif self.prerelease > other.prerelease:
return 1
# Build metadata MUST be ignored when determining version precedence
# https://semver.org/#spec-item-10
# With the above in mind it is ignored here
# If we have made it here, things should be equal
return 0
# The Py2 and Py3 implementations of distutils.version.Version
# are quite different, this makes the Py2 and Py3 implementations
# the same
def __eq__(self, other):
return self._cmp(other) == 0
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
return self._cmp(other) < 0
def __le__(self, other):
return self._cmp(other) <= 0
def __gt__(self, other):
return self._cmp(other) > 0
def __ge__(self, other):
return self._cmp(other) >= 0

@ -73,6 +73,26 @@
- '"Installing ''namespace1.name1:1.1.0-beta.1'' to" in install_prerelease.stdout' - '"Installing ''namespace1.name1:1.1.0-beta.1'' to" in install_prerelease.stdout'
- (install_prerelease_actual.content | b64decode | from_json).collection_info.version == '1.1.0-beta.1' - (install_prerelease_actual.content | b64decode | from_json).collection_info.version == '1.1.0-beta.1'
- name: Remove beta
file:
path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1'
state: absent
- name: install pre-release version with --pre to custom dir - {{ test_name }}
command: ansible-galaxy collection install --pre 'namespace1.name1' -s '{{ test_server }}' -p '{{ galaxy_dir }}/ansible_collections'
register: install_prerelease
- name: get result of install pre-release version with --pre to custom dir - {{ test_name }}
slurp:
path: '{{ galaxy_dir }}/ansible_collections/namespace1/name1/MANIFEST.json'
register: install_prerelease_actual
- name: assert install pre-release version with --pre to custom dir - {{ test_name }}
assert:
that:
- '"Installing ''namespace1.name1:1.1.0-beta.1'' to" in install_prerelease.stdout'
- (install_prerelease_actual.content | b64decode | from_json).collection_info.version == '1.1.0-beta.1'
- name: install multiple collections with dependencies - {{ test_name }} - name: install multiple collections with dependencies - {{ test_name }}
command: ansible-galaxy collection install parent_dep.parent_collection namespace2.name -s {{ test_name }} command: ansible-galaxy collection install parent_dep.parent_collection namespace2.name -s {{ test_name }}
args: args:

@ -165,7 +165,7 @@ def test_build_requirement_from_path(collection_artifact):
assert actual.dependencies == {} assert actual.dependencies == {}
@pytest.mark.parametrize('version', ['1.1.1', 1.1, 1]) @pytest.mark.parametrize('version', ['1.1.1', '1.1.0', '1.0.0'])
def test_build_requirement_from_path_with_manifest(version, collection_artifact): def test_build_requirement_from_path_with_manifest(version, collection_artifact):
manifest_path = os.path.join(collection_artifact[0], b'MANIFEST.json') manifest_path = os.path.join(collection_artifact[0], b'MANIFEST.json')
manifest_value = json.dumps({ manifest_value = json.dumps({

@ -0,0 +1,285 @@
# -*- coding: utf-8 -*-
# (c) 2020 Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from distutils.version import LooseVersion, StrictVersion
import pytest
from ansible.utils.version import _Alpha, _Numeric, SemanticVersion
EQ = [
('1.0.0', '1.0.0', True),
('1.0.0', '1.0.0-beta', False),
('1.0.0-beta2+build1', '1.0.0-beta.2+build.1', False),
('1.0.0-beta+build', '1.0.0-beta+build', True),
('1.0.0-beta+build1', '1.0.0-beta+build2', True),
('1.0.0-beta+a', '1.0.0-alpha+bar', False),
]
NE = [
('1.0.0', '1.0.0', False),
('1.0.0', '1.0.0-beta', True),
('1.0.0-beta2+build1', '1.0.0-beta.2+build.1', True),
('1.0.0-beta+build', '1.0.0-beta+build', False),
('1.0.0-beta+a', '1.0.0-alpha+bar', True),
]
LT = [
('1.0.0', '2.0.0', True),
('1.0.0-beta', '2.0.0-alpha', True),
('1.0.0-alpha', '2.0.0-beta', True),
('1.0.0-alpha', '1.0.0', True),
('1.0.0-beta', '1.0.0-alpha3', False),
('1.0.0+foo', '1.0.0-alpha', False),
('1.0.0-beta.1', '1.0.0-beta.a', True),
('1.0.0-beta+a', '1.0.0-alpha+bar', False),
]
GT = [
('1.0.0', '2.0.0', False),
('1.0.0-beta', '2.0.0-alpha', False),
('1.0.0-alpha', '2.0.0-beta', False),
('1.0.0-alpha', '1.0.0', False),
('1.0.0-beta', '1.0.0-alpha3', True),
('1.0.0+foo', '1.0.0-alpha', True),
('1.0.0-beta.1', '1.0.0-beta.a', False),
('1.0.0-beta+a', '1.0.0-alpha+bar', True),
]
LE = [
('1.0.0', '1.0.0', True),
('1.0.0', '2.0.0', True),
('1.0.0-alpha', '1.0.0-beta', True),
('1.0.0-beta', '1.0.0-alpha', False),
]
GE = [
('1.0.0', '1.0.0', True),
('1.0.0', '2.0.0', False),
('1.0.0-alpha', '1.0.0-beta', False),
('1.0.0-beta', '1.0.0-alpha', True),
]
VALID = [
"0.0.4",
"1.2.3",
"10.20.30",
"1.1.2-prerelease+meta",
"1.1.2+meta",
"1.1.2+meta-valid",
"1.0.0-alpha",
"1.0.0-beta",
"1.0.0-alpha.beta",
"1.0.0-alpha.beta.1",
"1.0.0-alpha.1",
"1.0.0-alpha0.valid",
"1.0.0-alpha.0valid",
"1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay",
"1.0.0-rc.1+build.1",
"2.0.0-rc.1+build.123",
"1.2.3-beta",
"10.2.3-DEV-SNAPSHOT",
"1.2.3-SNAPSHOT-123",
"1.0.0",
"2.0.0",
"1.1.7",
"2.0.0+build.1848",
"2.0.1-alpha.1227",
"1.0.0-alpha+beta",
"1.2.3----RC-SNAPSHOT.12.9.1--.12+788",
"1.2.3----R-S.12.9.1--.12+meta",
"1.2.3----RC-SNAPSHOT.12.9.1--.12",
"1.0.0+0.build.1-rc.10000aaa-kk-0.1",
"99999999999999999999999.999999999999999999.99999999999999999",
"1.0.0-0A.is.legal",
]
INVALID = [
"1",
"1.2",
"1.2.3-0123",
"1.2.3-0123.0123",
"1.1.2+.123",
"+invalid",
"-invalid",
"-invalid+invalid",
"-invalid.01",
"alpha",
"alpha.beta",
"alpha.beta.1",
"alpha.1",
"alpha+beta",
"alpha_beta",
"alpha.",
"alpha..",
"beta",
"1.0.0-alpha_beta",
"-alpha.",
"1.0.0-alpha..",
"1.0.0-alpha..1",
"1.0.0-alpha...1",
"1.0.0-alpha....1",
"1.0.0-alpha.....1",
"1.0.0-alpha......1",
"1.0.0-alpha.......1",
"01.1.1",
"1.01.1",
"1.1.01",
"1.2",
"1.2.3.DEV",
"1.2-SNAPSHOT",
"1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788",
"1.2-RC-SNAPSHOT",
"-1.0.3-gamma+b7718",
"+justmeta",
"9.8.7+meta+meta",
"9.8.7-whatever+meta+meta",
]
PRERELEASE = [
('1.0.0-alpha', True),
('1.0.0-alpha.1', True),
('1.0.0-0.3.7', True),
('1.0.0-x.7.z.92', True),
('0.1.2', False),
('0.1.2+bob', False),
('1.0.0', False),
]
STABLE = [
('1.0.0-alpha', False),
('1.0.0-alpha.1', False),
('1.0.0-0.3.7', False),
('1.0.0-x.7.z.92', False),
('0.1.2', False),
('0.1.2+bob', False),
('1.0.0', True),
('1.0.0+bob', True),
]
LOOSE_VERSION = [
(LooseVersion('1'), SemanticVersion('1.0.0')),
(LooseVersion('1-alpha'), SemanticVersion('1.0.0-alpha')),
(LooseVersion('1.0.0-alpha+build'), SemanticVersion('1.0.0-alpha+build')),
]
LOOSE_VERSION_INVALID = [
LooseVersion('1.a.3'),
LooseVersion(),
'bar',
StrictVersion('1.2.3'),
]
def test_semanticversion_none():
assert SemanticVersion().major is None
@pytest.mark.parametrize('left,right,expected', EQ)
def test_eq(left, right, expected):
assert (SemanticVersion(left) == SemanticVersion(right)) is expected
@pytest.mark.parametrize('left,right,expected', NE)
def test_ne(left, right, expected):
assert (SemanticVersion(left) != SemanticVersion(right)) is expected
@pytest.mark.parametrize('left,right,expected', LT)
def test_lt(left, right, expected):
assert (SemanticVersion(left) < SemanticVersion(right)) is expected
@pytest.mark.parametrize('left,right,expected', LE)
def test_le(left, right, expected):
assert (SemanticVersion(left) <= SemanticVersion(right)) is expected
@pytest.mark.parametrize('left,right,expected', GT)
def test_gt(left, right, expected):
assert (SemanticVersion(left) > SemanticVersion(right)) is expected
@pytest.mark.parametrize('left,right,expected', GE)
def test_ge(left, right, expected):
assert (SemanticVersion(left) >= SemanticVersion(right)) is expected
@pytest.mark.parametrize('value', VALID)
def test_valid(value):
SemanticVersion(value)
@pytest.mark.parametrize('value', INVALID)
def test_invalid(value):
pytest.raises(ValueError, SemanticVersion, value)
def test_example_precedence():
# https://semver.org/#spec-item-11
sv = SemanticVersion
assert sv('1.0.0') < sv('2.0.0') < sv('2.1.0') < sv('2.1.1')
assert sv('1.0.0-alpha') < sv('1.0.0')
assert sv('1.0.0-alpha') < sv('1.0.0-alpha.1') < sv('1.0.0-alpha.beta')
assert sv('1.0.0-beta') < sv('1.0.0-beta.2') < sv('1.0.0-beta.11') < sv('1.0.0-rc.1') < sv('1.0.0')
@pytest.mark.parametrize('value,expected', PRERELEASE)
def test_prerelease(value, expected):
assert SemanticVersion(value).is_prerelease is expected
@pytest.mark.parametrize('value,expected', STABLE)
def test_stable(value, expected):
assert SemanticVersion(value).is_stable is expected
@pytest.mark.parametrize('value,expected', LOOSE_VERSION)
def test_from_loose_version(value, expected):
assert SemanticVersion.from_loose_version(value) == expected
@pytest.mark.parametrize('value', LOOSE_VERSION_INVALID)
def test_from_loose_version_invalid(value):
pytest.raises((AttributeError, ValueError), SemanticVersion.from_loose_version, value)
def test_comparison_with_string():
assert SemanticVersion('1.0.0') > '0.1.0'
def test_alpha():
assert _Alpha('a') == _Alpha('a')
assert _Alpha('a') == 'a'
assert _Alpha('a') != _Alpha('b')
assert _Alpha('a') != 1
assert _Alpha('a') < _Alpha('b')
assert _Alpha('a') < 'c'
assert _Alpha('a') > _Numeric(1)
with pytest.raises(ValueError):
_Alpha('a') < None
assert _Alpha('a') <= _Alpha('a')
assert _Alpha('a') <= _Alpha('b')
assert _Alpha('b') >= _Alpha('a')
assert _Alpha('b') >= _Alpha('b')
def test_numeric():
assert _Numeric(1) == _Numeric(1)
assert _Numeric(1) == 1
assert _Numeric(1) != _Numeric(2)
assert _Numeric(1) != 'a'
assert _Numeric(1) < _Numeric(2)
assert _Numeric(1) < 3
assert _Numeric(1) < _Alpha('b')
with pytest.raises(ValueError):
_Numeric(1) < None
assert _Numeric(1) <= _Numeric(1)
assert _Numeric(1) <= _Numeric(2)
assert _Numeric(2) >= _Numeric(1)
assert _Numeric(2) >= _Numeric(2)
Loading…
Cancel
Save