mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
Python 3 should be available everywhere at this point. Try dropping the wrapper. Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
309 lines
10 KiB
Python
Executable File
309 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import print_function, absolute_import, unicode_literals
|
|
|
|
from argparse import Action, ArgumentParser, RawTextHelpFormatter, SUPPRESS
|
|
import itertools
|
|
import re
|
|
import sys
|
|
import sys
|
|
import unittest
|
|
|
|
# PY2 is true when we're running under Python 2.x It is used for appropriate
|
|
# return value selection of __str__ and __repr_ methods, which must both
|
|
# return str, not unicode (in Python 2) and str (in Python 3). In both cases
|
|
# the return type annotation is exactly the same, but due to unicode_literals
|
|
# being in effect, and the fact we often use a format string (which is an
|
|
# unicode string in Python 2), we must encode the it to byte string when
|
|
# running under Python 2.
|
|
PY2 = sys.version_info[0] == 2
|
|
|
|
# Define MYPY as False and use it as a conditional for typing import. Despite
|
|
# this declaration mypy will really treat MYPY as True when type-checking.
|
|
# This is required so that we can import typing on Python 2.x without the
|
|
# typing module installed. For more details see:
|
|
# https://mypy.readthedocs.io/en/latest/common_issues.html#import-cycles
|
|
MYPY = False
|
|
if MYPY:
|
|
from typing import Any, Text, Tuple, Optional, Union, Sequence
|
|
from argparse import Namespace
|
|
|
|
|
|
class _UnitTestAction(Action):
|
|
def __init__(
|
|
self,
|
|
option_strings,
|
|
dest=SUPPRESS,
|
|
default=SUPPRESS,
|
|
help="run program's unit test suite and exit",
|
|
):
|
|
# type: (Text, Text, Text, Text) -> None
|
|
super(_UnitTestAction, self).__init__(
|
|
option_strings=option_strings,
|
|
dest=dest,
|
|
default=default,
|
|
nargs="...",
|
|
help=help,
|
|
)
|
|
|
|
def __call__(self, parser, ns, values, option_string=None):
|
|
# type: (ArgumentParser, Namespace, Union[str, Sequence[Any], None], Optional[Text]) -> None
|
|
# We allow the caller to provide the test to invoke by giving
|
|
# --run-unit-tests a set of arguments.
|
|
argv = [sys.argv[0]]
|
|
if isinstance(values, list):
|
|
argv += values
|
|
unittest.main(argv=argv)
|
|
parser.exit()
|
|
|
|
|
|
def consistent_relation(rel_op, delta):
|
|
# type: (Text, int) -> bool
|
|
"""
|
|
consistent_relation returns true if the relation is consistent with delta.
|
|
|
|
The relation operator is one of ==, !=, <, <=, > or >=.
|
|
Delta is either 0, a positive or a negative number.
|
|
"""
|
|
if rel_op == "==":
|
|
return delta == 0
|
|
elif rel_op == "!=":
|
|
return delta != 0
|
|
elif rel_op == ">":
|
|
return delta > 0
|
|
elif rel_op == ">=":
|
|
return delta >= 0
|
|
elif rel_op == "<":
|
|
return delta < 0
|
|
elif rel_op == "<=":
|
|
return delta <= 0
|
|
raise ValueError("unexpected relational operator " + rel_op)
|
|
|
|
|
|
class ConsistentRelationTests(unittest.TestCase):
|
|
def test_eq(self):
|
|
# type: () -> None
|
|
self.assertFalse(consistent_relation("==", -1))
|
|
self.assertTrue(consistent_relation("==", 0))
|
|
self.assertFalse(consistent_relation("==", +1))
|
|
|
|
def test_ne(self):
|
|
# type: () -> None
|
|
self.assertTrue(consistent_relation("!=", -1))
|
|
self.assertFalse(consistent_relation("!=", 0))
|
|
self.assertTrue(consistent_relation("!=", +1))
|
|
|
|
def test_gt(self):
|
|
# type: () -> None
|
|
self.assertFalse(consistent_relation(">", -1))
|
|
self.assertFalse(consistent_relation(">", 0))
|
|
self.assertTrue(consistent_relation(">", +1))
|
|
|
|
def test_ge(self):
|
|
# type: () -> None
|
|
self.assertFalse(consistent_relation(">=", -1))
|
|
self.assertTrue(consistent_relation(">=", 0))
|
|
self.assertTrue(consistent_relation(">=", +1))
|
|
|
|
def test_lt(self):
|
|
# type: () -> None
|
|
self.assertTrue(consistent_relation("<", -1))
|
|
self.assertFalse(consistent_relation("<", 0))
|
|
self.assertFalse(consistent_relation("<", +1))
|
|
|
|
def test_le(self):
|
|
# type: () -> None
|
|
self.assertTrue(consistent_relation("<=", -1))
|
|
self.assertTrue(consistent_relation("<=", 0))
|
|
self.assertFalse(consistent_relation("<=", +1))
|
|
|
|
def test_unknown(self):
|
|
# type: () -> None
|
|
with self.assertRaises(ValueError):
|
|
consistent_relation("???", 0)
|
|
|
|
|
|
def strict_version_cmp(a, b):
|
|
# type: (Text, Text) -> int
|
|
"""
|
|
strictly_version_cmp compares two version numbers without leeway.
|
|
|
|
The algorithm considers each version to be a tuple of integers. Non,
|
|
integer elements or element fragments are regarded as an error and raised
|
|
as ValueError.
|
|
|
|
Comparison is performed on by considering the leftmost element in each
|
|
tuple. First pair of numbers that are not equal determine the result of the
|
|
comparison. Tuples have unequal length then missing elements are
|
|
substituted with zero.
|
|
|
|
The return value is 0 if the version strings are equal, -1 if version a is
|
|
smaller or +1 if version b is smaller.
|
|
"""
|
|
try:
|
|
a_items = [int(item, 10) for item in a.split(".")]
|
|
except ValueError:
|
|
raise ValueError("version {} is not purely numeric".format(a))
|
|
try:
|
|
b_items = [int(item, 10) for item in b.split(".")]
|
|
except ValueError:
|
|
raise ValueError("version {} is not purely numeric".format(b))
|
|
if PY2:
|
|
zip_longest_fn = itertools.izip_longest
|
|
else:
|
|
zip_longest_fn = itertools.zip_longest
|
|
for a_val, b_val in zip_longest_fn(a_items, b_items, fillvalue=0):
|
|
delta = a_val - b_val
|
|
if delta != 0:
|
|
return 1 if delta > 0 else -1
|
|
return 0
|
|
|
|
|
|
class StrictVersionCmpTests(unittest.TestCase):
|
|
def test_simple(self):
|
|
# type: () -> None
|
|
self.assertEqual(strict_version_cmp("10", "10"), 0)
|
|
self.assertEqual(strict_version_cmp("10", "20"), -1)
|
|
self.assertEqual(strict_version_cmp("20", "10"), +1)
|
|
|
|
def test_many_segments(self):
|
|
# type: () -> None
|
|
self.assertEqual(strict_version_cmp("1.2.3", "1.2.3"), 0)
|
|
self.assertEqual(strict_version_cmp("1.2.3", "1.3.4"), -1)
|
|
self.assertEqual(strict_version_cmp("1.4.3", "1.2.3"), +1)
|
|
self.assertEqual(strict_version_cmp("1.0.0", "1.1.0"), -1)
|
|
self.assertEqual(strict_version_cmp("0.1.2", "1.1.2"), -1)
|
|
|
|
def test_unequal_length(self):
|
|
# type: () -> None
|
|
self.assertEqual(strict_version_cmp("1", "1.0"), 0)
|
|
self.assertEqual(strict_version_cmp("1", "1.2"), -1)
|
|
self.assertEqual(strict_version_cmp("1.2", "1"), +1)
|
|
self.assertEqual(strict_version_cmp("1", "1.0.1"), -1)
|
|
self.assertEqual(strict_version_cmp("1.1", "1.0.1"), +1)
|
|
|
|
def test_version_with_text(self):
|
|
# type: () -> None
|
|
with self.assertRaises(ValueError) as cm:
|
|
strict_version_cmp("1-foo", "1")
|
|
self.assertEqual(cm.exception.args, ("version 1-foo is not purely numeric",))
|
|
with self.assertRaises(ValueError) as cm:
|
|
strict_version_cmp("1.2-foo", "1.2")
|
|
self.assertEqual(cm.exception.args, ("version 1.2-foo is not purely numeric",))
|
|
|
|
|
|
def _make_parser():
|
|
# type: () -> ArgumentParser
|
|
parser = ArgumentParser(
|
|
epilog="""
|
|
Relational operator is one of:
|
|
|
|
-eq -ne -gt -ge -lt -le
|
|
|
|
Version comparison is performed using the selected algorithm.
|
|
|
|
strict:
|
|
The algorithm considers each version to be a tuple of integers.
|
|
Non-integer elements are considered to be invalid version.
|
|
|
|
Comparison is performed on by considering the leftmost element in each
|
|
tuple. First pair of numbers that are not equal determine the result of the
|
|
comparison. Tuples have unequal length then missing elements are
|
|
substituted with zero.
|
|
""",
|
|
formatter_class=RawTextHelpFormatter,
|
|
)
|
|
parser.register("action", "unit-test", _UnitTestAction)
|
|
parser.add_argument("-v", "--version", action="version", version="1.0")
|
|
parser.add_argument(
|
|
"--verbose", action="store_true", help="describe comparison process"
|
|
)
|
|
|
|
parser.add_argument("version_a", metavar="VERSION-A")
|
|
parser.add_argument("version_b", metavar="VERSION-B")
|
|
|
|
# algorithm selection
|
|
alg_grp = parser.add_mutually_exclusive_group(required=True)
|
|
alg_grp.add_argument(
|
|
"--strict",
|
|
dest="algorithm",
|
|
action="store_const",
|
|
const=strict_version_cmp,
|
|
help="select the strict version comparison",
|
|
)
|
|
# relation selection
|
|
rel_op_grp = parser.add_mutually_exclusive_group(required=True)
|
|
rel_op_grp.add_argument(
|
|
"-eq",
|
|
action="store_const",
|
|
const="==",
|
|
dest="rel_op",
|
|
help="test that versions are equal",
|
|
)
|
|
rel_op_grp.add_argument(
|
|
"-ne",
|
|
action="store_const",
|
|
const="!=",
|
|
dest="rel_op",
|
|
help="test that versions are not equal",
|
|
)
|
|
rel_op_grp.add_argument(
|
|
"-gt",
|
|
action="store_const",
|
|
const=">",
|
|
dest="rel_op",
|
|
help="test that version-a is greater than version-b",
|
|
)
|
|
rel_op_grp.add_argument(
|
|
"-ge",
|
|
action="store_const",
|
|
const=">=",
|
|
dest="rel_op",
|
|
help="test that version-a is greater than or equal to version-b",
|
|
)
|
|
rel_op_grp.add_argument(
|
|
"-lt",
|
|
action="store_const",
|
|
const="<",
|
|
dest="rel_op",
|
|
help="test that version-a is less than version-b",
|
|
)
|
|
rel_op_grp.add_argument(
|
|
"-le",
|
|
action="store_const",
|
|
const="<=",
|
|
dest="rel_op",
|
|
help="test that version-a is less than or equal to version-b",
|
|
)
|
|
|
|
# maintenance commands
|
|
maint_grp = parser.add_argument_group("maintenance commands")
|
|
maint_grp.add_argument("--run-unit-tests", action="unit-test", help=SUPPRESS)
|
|
return parser
|
|
|
|
|
|
def main():
|
|
# type: () -> None
|
|
opts = _make_parser().parse_args()
|
|
try:
|
|
delta = opts.algorithm(opts.version_a, opts.version_b)
|
|
except ValueError as exc:
|
|
print("error: {}".format(exc), file=sys.stderr)
|
|
raise SystemExit(2)
|
|
else:
|
|
is_consistent = consistent_relation(opts.rel_op, delta)
|
|
if opts.verbose:
|
|
print(
|
|
"delta between {} and {} is: {}".format(
|
|
opts.version_a, opts.version_b, delta
|
|
)
|
|
)
|
|
if is_consistent:
|
|
print("delta {} is consistent with {}".format(delta, opts.rel_op))
|
|
else:
|
|
print("delta {} is inconsistent with {}".format(delta, opts.rel_op))
|
|
raise SystemExit(0 if is_consistent else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|