blob: 820f90b6abdf2063c5866e318ee0b2a97916b6be [file]
# SPDX-License-Identifier: Apache-2.0
import unittest
from mesonbuild.mesonlib import (
Range, Version, version_compare,
version_compare_many, version_compare_condition_with_min,
search_version,
)
class VersionComparisonTests(unittest.TestCase):
def test_version_ordering(self):
LT = -1
EQ = 0
#GT = 1
# mostly from RPM tests
tests = [
("1.0", "1.0", EQ),
("1_0", "1_0", EQ),
("1_0", "1.0", EQ),
("1.0", "2.0", LT),
("2.0", "2.0.0", LT),
("2.0", "2.0.1", LT),
("2.0.1", "2.0.1", EQ),
("2.0.1", "2.0.1a", LT),
("2.0.1a", "2.0.1a", EQ),
("2.10", "3.111", LT),
("2.456", "2.1000", LT),
("1.2rc1", "1.2.0", LT),
("5.5p1", "5.5p1", EQ),
("5.5p1", "5.5p2", LT),
("5.5p1", "5.5p10", LT),
("10xyz", "10.1xyz", LT),
("xyz10", "xyz10", EQ),
("xyz10", "xyz10.1", LT),
("xyz.4", "8", LT),
("xyz.4", "2", LT),
("5.5p2", "5.6p1", LT),
("5.6p1", "6.5p1", LT),
("6.0", "6.0.rc1", LT),
("10a2", "10b2", LT),
("1.0aa", "1.0aa", EQ),
("1.0a", "1.0aa", LT),
("10.0001", "10.1", EQ),
("10.0001", "10.0039", LT),
("1.05", "1.5", EQ),
("2a", "2.0", LT),
]
for (a, b, result) in tests:
with self.subTest(f'({a!r}, {b!r})'):
ver_a = Version(a)
ver_b = Version(b)
self.assertEqual(ver_a <= ver_b, result <= 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_a < ver_b, result < 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_a > ver_b, result > 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_a >= ver_b, result >= 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_a == ver_b, result == 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_b == ver_a, result == 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_a != ver_b, result != 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_b != ver_a, result != 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_b <= ver_a, -result <= 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_b < ver_a, -result < 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_b > ver_a, -result > 0, f'{ver_a} <= {ver_b}')
self.assertEqual(ver_b >= ver_a, -result >= 0, f'{ver_a} <= {ver_b}')
def test_version_compare(self):
"""Test version_compare with operator prefixes."""
self.assertTrue(version_compare('1.0', '>=1.0'))
self.assertTrue(version_compare('1.1', '>=1.0'))
self.assertFalse(version_compare('0.9', '>=1.0'))
self.assertTrue(version_compare('1.0', '<=1.0'))
self.assertTrue(version_compare('0.9', '<=1.0'))
self.assertFalse(version_compare('1.1', '<=1.0'))
self.assertTrue(version_compare('1.0', '>0.9'))
self.assertFalse(version_compare('1.0', '>1.0'))
self.assertTrue(version_compare('1.0', '<1.1'))
self.assertFalse(version_compare('1.0', '<1.0'))
self.assertTrue(version_compare('1.0', '==1.0'))
self.assertFalse(version_compare('1.0', '==1.1'))
self.assertTrue(version_compare('1.0', '!=1.1'))
self.assertFalse(version_compare('1.0', '!=1.0'))
# bare version and = means ==
self.assertTrue(version_compare('1.0', '1.0'))
self.assertFalse(version_compare('1.0', '1.1'))
self.assertTrue(version_compare('1.0', '=1.0'))
self.assertFalse(version_compare('1.0', '=1.1'))
def test_version_compare_many(self):
result, not_found, found = version_compare_many('1.5', ['>=1.0', '<2.0'])
self.assertTrue(result)
self.assertEqual(not_found, [])
self.assertEqual(found, ['>=1.0', '<2.0'])
result, not_found, found = version_compare_many('0.5', ['>=1.0', '<2.0'])
self.assertFalse(result)
self.assertEqual(not_found, ['>=1.0'])
self.assertEqual(found, ['<2.0'])
result, not_found, found = version_compare_many('2.5', ['>=1.0', '<2.0'])
self.assertFalse(result)
self.assertEqual(not_found, ['<2.0'])
self.assertEqual(found, ['>=1.0'])
# string condition is treated as single-element list
result, not_found, found = version_compare_many('1.0', '>=1.0')
self.assertTrue(result)
self.assertEqual(not_found, [])
self.assertEqual(found, ['>=1.0'])
def test_version_compare_condition_with_min(self):
# >= condition: minimum must be <= condition version
self.assertTrue(version_compare_condition_with_min('>=0.46.0', '0.46.0'))
self.assertTrue(version_compare_condition_with_min('>=0.50.0', '0.46.0'))
self.assertFalse(version_compare_condition_with_min('>=0.40.0', '0.46.0'))
# > condition: minimum must be <= condition version
self.assertTrue(version_compare_condition_with_min('>0.46.0', '0.46.0'))
self.assertTrue(version_compare_condition_with_min('>0.50.0', '0.46.0'))
self.assertFalse(version_compare_condition_with_min('>0.45.0', '0.46.0'))
# == condition: minimum must be <= condition version
self.assertTrue(version_compare_condition_with_min('==0.46.0', '0.46.0'))
self.assertTrue(version_compare_condition_with_min('==0.50.0', '0.46.0'))
self.assertFalse(version_compare_condition_with_min('==0.40.0', '0.46.0'))
# = or bare condition is the same as ==
self.assertTrue(version_compare_condition_with_min('=0.46.0', '0.46.0'))
self.assertTrue(version_compare_condition_with_min('0.46.0', '0.46.0'))
self.assertFalse(version_compare_condition_with_min('0.40.0', '0.46.0'))
# < and <= conditions always return False (includes versions older than minimum)
self.assertFalse(version_compare_condition_with_min('<1.0.0', '0.46.0'))
self.assertFalse(version_compare_condition_with_min('<=1.0.0', '0.46.0'))
self.assertFalse(version_compare_condition_with_min('<0.30.0', '0.46.0'))
self.assertFalse(version_compare_condition_with_min('<=0.30.0', '0.46.0'))
# != condition always returns False
self.assertFalse(version_compare_condition_with_min('!=0.40.0', '0.46.0'))
# test behavior of two-component versions
# '0.46.0' is mangled to '0.46' so that '>=0.46.0' works, but this is
# not done by version_compare_condition_with_min
self.assertFalse(version_compare_condition_with_min('>=0.46', '0.46.0'))
self.assertFalse(version_compare_condition_with_min('>=0.46', '0.46.1'))
self.assertTrue(version_compare_condition_with_min('>=0.46.0', '0.46'))
self.assertTrue(version_compare_condition_with_min('>=0.46.1', '0.46'))
def test_range_contains(self):
"""Test Range.__contains__."""
# unbounded range contains everything
r = Range()
self.assertIn(5, r)
self.assertIn(0, r)
# min only, inclusive
r = Range(min=3, min_eq=True)
self.assertIn(3, r)
self.assertIn(5, r)
self.assertNotIn(2, r)
# min only, exclusive
r = Range(min=3, min_eq=False)
self.assertNotIn(3, r)
self.assertIn(4, r)
self.assertNotIn(2, r)
# max only, inclusive
r = Range(max=7, max_eq=True)
self.assertIn(7, r)
self.assertIn(5, r)
self.assertNotIn(8, r)
# max only, exclusive
r = Range(max=7, max_eq=False)
self.assertNotIn(7, r)
self.assertIn(6, r)
self.assertNotIn(8, r)
# both bounds, inclusive
r = Range(min=3, min_eq=True, max=7, max_eq=True)
self.assertIn(3, r)
self.assertIn(5, r)
self.assertIn(7, r)
self.assertNotIn(2, r)
self.assertNotIn(8, r)
# both bounds, exclusive
r = Range(min=3, min_eq=False, max=7, max_eq=False)
self.assertNotIn(3, r)
self.assertIn(5, r)
self.assertNotIn(7, r)
# empty range contains nothing
r = Range(min=5, min_eq=False, max=5, max_eq=False)
self.assertNotIn(5, r)
# min == max, both inclusive -> single point, not empty
r = Range(min=5, max=5, min_eq=True, max_eq=True)
self.assertIn(5, r)
def test_range_init_trivial(self):
"""Test that __post_init__ detects trivial ranges."""
r = Range()
self.assertFalse(r.is_empty)
# min > max
r = Range(min=7, max=3, min_eq=True, max_eq=True)
self.assertTrue(r.is_empty)
# min == max, not both inclusive -> empty
r = Range(min=5, max=5, min_eq=True, max_eq=False)
self.assertTrue(r.is_empty)
r = Range(min=5, max=5, min_eq=False, max_eq=True)
self.assertTrue(r.is_empty)
r = Range(min=5, max=5, min_eq=False, max_eq=False)
self.assertTrue(r.is_empty)
# min == max, both inclusive -> single point, not empty
r = Range(min=5, max=5, min_eq=True, max_eq=True)
self.assertFalse(r.is_empty)
def test_range_intersect(self):
"""Test Range.intersect."""
# intersect two overlapping ranges
a = Range(min=1, min_eq=True, max=10, max_eq=True)
b = Range(min=5, min_eq=True, max=15, max_eq=True)
r = a.intersect(b)
self.assertIn(5, r)
self.assertIn(10, r)
self.assertNotIn(4, r)
self.assertNotIn(11, r)
# intersect does not mutate original
self.assertIn(4, a)
# intersect narrows unbounded range
a = Range()
b = Range(min=3, min_eq=True, max=7, max_eq=False)
r = a.intersect(b)
self.assertIn(3, r)
self.assertIn(6, r)
self.assertNotIn(2, r)
self.assertNotIn(7, r)
# intersect non-overlapping ranges produces empty
a = Range(min=1, min_eq=True, max=3, max_eq=True)
b = Range(min=5, min_eq=True, max=7, max_eq=True)
r = a.intersect(b)
self.assertTrue(r.is_empty)
# intersect with matching boundary tightens eq
a = Range(min=5, min_eq=True)
b = Range(min=5, min_eq=False)
r = a.intersect(b)
self.assertEqual(5, r.min)
self.assertNotIn(5, r)
a = Range(max=5, max_eq=True)
b = Range(max=5, max_eq=False)
r = a.intersect(b)
self.assertEqual(5, r.max)
self.assertIn(4, r)
# intersecting empty range stays empty
a = Range(min=7, max=3, min_eq=True, max_eq=True)
self.assertTrue(a.is_empty)
b = Range(min=1, min_eq=True, max=10, max_eq=True)
r = a.intersect(b)
self.assertTrue(r.is_empty)
b = Range()
r = a.intersect(b)
self.assertTrue(r.is_empty)
a = Range()
b = Range(min=7, max=3, min_eq=True, max_eq=True)
r = a.intersect(b)
self.assertTrue(r.is_empty)
def test_range_always(self):
"""Range.always checks if inner is always true/false given outer."""
outer = Range(min=5, min_eq=True, max=10, max_eq=True)
unbounded = Range()
empty = Range(min=7, max=3, min_eq=True, max_eq=True)
# inner fully contains outer => true
self.assertTrue(outer.always(Range(min=3, min_eq=True)))
self.assertTrue(outer.always(Range(max=12, max_eq=True)))
self.assertTrue(outer.always(unbounded))
# inner same as outer => true
self.assertTrue(outer.always(Range(min=5, min_eq=True, max=10, max_eq=True)))
# inner doesn't overlap outer => false
self.assertFalse(outer.always(Range(min=11, min_eq=True)))
self.assertFalse(outer.always(Range(max=4, max_eq=True)))
# inner partially overlaps => indeterminate
self.assertIsNone(outer.always(Range(min=7, min_eq=True)))
self.assertIsNone(outer.always(Range(max=8, max_eq=True)))
# exclusive boundaries count as a partial overlap
self.assertIsNone(outer.always(Range(min=5, min_eq=False)))
self.assertIsNone(outer.always(Range(max=10, max_eq=False)))
# outer is unbounded
self.assertIsNone(unbounded.always(Range(min=5, min_eq=True)))
self.assertTrue(unbounded.always(unbounded))
self.assertFalse(unbounded.always(empty))
# outer is empty => always false (intersection is empty)
self.assertFalse(empty.always(Range(min=5, min_eq=True)))
self.assertFalse(empty.always(unbounded))
self.assertFalse(empty.always(empty))
def test_range_str(self):
"""Test Range.__str__."""
self.assertEqual(str(Range()), '(any)')
self.assertEqual(str(Range(min=3, min_eq=True)), '>= 3')
self.assertEqual(str(Range(min=3, min_eq=False)), '> 3')
self.assertEqual(str(Range(max=7, max_eq=True)), '<= 7')
self.assertEqual(str(Range(max=7, max_eq=False)), '< 7')
self.assertEqual(str(Range(min=3, min_eq=True, max=7, max_eq=False)), '>= 3, < 7')
self.assertEqual(str(Range(min=5, max=5, min_eq=True, max_eq=True)), '== 5')
# empty range
r = Range(min=7, max=3, min_eq=True, max_eq=True)
self.assertEqual(str(r), '(empty)')
def test_range_str_version(self):
"""Test Range.__str__ with Version objects."""
r = Range(min=Version('0.46.0'), min_eq=True)
self.assertEqual(str(r), '>= 0.46.0')
r = Range(min=Version('1.0'), min_eq=True, max=Version('2.0'), max_eq=False)
self.assertEqual(str(r), '>= 1.0, < 2.0')
def test_search_version(self):
self.assertEqual(search_version('foo 1.2.3'), '1.2.3')
self.assertEqual(search_version('1.2.3'), '1.2.3')
self.assertEqual(search_version('foo 2026.03.20 1.2.3'), '1.2.3')
self.assertEqual(search_version('2026.03.20 1.2.3'), '1.2.3')
self.assertEqual(search_version('foo 2026.03.20'), '2026.03.20')
self.assertEqual(search_version('2026.03.20'), '2026.03.20')
self.assertEqual(search_version('2026.03'), '2026.03')
self.assertEqual(search_version('2026.03 1.2.3'), '1.2.3')
self.assertEqual(search_version('foo v1.2.3'), '1.2.3')
self.assertEqual(search_version('2026'), 'unknown version')