"""Tests for version.py - Semantic version parsing and constraint matching.""" import pytest from cmdforge.version import ( Version, matches_constraint, find_best_match, is_valid_version, compare_versions ) class TestVersion: """Tests for Version dataclass.""" def test_parse_basic(self): v = Version.parse("1.2.3") assert v is not None assert v.major == 1 assert v.minor == 2 assert v.patch == 3 assert v.prerelease is None assert v.build is None def test_parse_with_prerelease(self): v = Version.parse("1.0.0-alpha.1") assert v is not None assert v.major == 1 assert v.minor == 0 assert v.patch == 0 assert v.prerelease == "alpha.1" def test_parse_with_build(self): v = Version.parse("1.0.0+build.123") assert v is not None assert v.build == "build.123" def test_parse_full(self): v = Version.parse("2.1.0-beta.2+20240115") assert v is not None assert v.major == 2 assert v.minor == 1 assert v.patch == 0 assert v.prerelease == "beta.2" assert v.build == "20240115" def test_parse_invalid(self): assert Version.parse("") is None assert Version.parse("invalid") is None assert Version.parse("1.2") is None assert Version.parse("1.2.3.4") is None def test_str(self): assert str(Version(1, 2, 3)) == "1.2.3" assert str(Version(1, 0, 0, prerelease="alpha")) == "1.0.0-alpha" assert str(Version(1, 0, 0, build="123")) == "1.0.0+123" assert str(Version(1, 0, 0, prerelease="rc.1", build="build")) == "1.0.0-rc.1+build" def test_tuple(self): v = Version(1, 2, 3) assert v.tuple == (1, 2, 3) def test_equality(self): v1 = Version(1, 2, 3) v2 = Version(1, 2, 3) v3 = Version(1, 2, 4) assert v1 == v2 assert v1 != v3 def test_comparison(self): assert Version(1, 0, 0) < Version(2, 0, 0) assert Version(1, 0, 0) < Version(1, 1, 0) assert Version(1, 0, 0) < Version(1, 0, 1) assert Version(2, 0, 0) > Version(1, 9, 9) def test_prerelease_comparison(self): # Prerelease < release assert Version(1, 0, 0, prerelease="alpha") < Version(1, 0, 0) assert Version(1, 0, 0, prerelease="beta") < Version(1, 0, 0) # Alphabetic ordering for prerelease assert Version(1, 0, 0, prerelease="alpha") < Version(1, 0, 0, prerelease="beta") def test_hash(self): v1 = Version(1, 2, 3) v2 = Version(1, 2, 3) assert hash(v1) == hash(v2) # Can be used in sets/dicts s = {v1, v2} assert len(s) == 1 class TestMatchesConstraint: """Tests for matches_constraint function.""" def test_any_version(self): assert matches_constraint("1.0.0", "*") assert matches_constraint("2.3.4", "*") assert matches_constraint("1.0.0", "latest") assert matches_constraint("1.0.0", "") def test_exact_match(self): assert matches_constraint("1.2.3", "1.2.3") assert not matches_constraint("1.2.4", "1.2.3") assert matches_constraint("1.0.0", "=1.0.0") def test_caret_major(self): # ^1.2.3 allows >=1.2.3 and <2.0.0 assert matches_constraint("1.2.3", "^1.2.3") assert matches_constraint("1.2.4", "^1.2.3") assert matches_constraint("1.9.9", "^1.2.3") assert not matches_constraint("1.2.2", "^1.2.3") assert not matches_constraint("2.0.0", "^1.2.3") def test_caret_zero_minor(self): # ^0.2.3 allows >=0.2.3 and <0.3.0 assert matches_constraint("0.2.3", "^0.2.3") assert matches_constraint("0.2.9", "^0.2.3") assert not matches_constraint("0.3.0", "^0.2.3") assert not matches_constraint("0.2.2", "^0.2.3") def test_caret_zero_zero(self): # ^0.0.3 allows ONLY 0.0.3 assert matches_constraint("0.0.3", "^0.0.3") assert not matches_constraint("0.0.4", "^0.0.3") assert not matches_constraint("0.0.2", "^0.0.3") def test_tilde(self): # ~1.2.3 allows >=1.2.3 and <1.3.0 assert matches_constraint("1.2.3", "~1.2.3") assert matches_constraint("1.2.9", "~1.2.3") assert not matches_constraint("1.3.0", "~1.2.3") assert not matches_constraint("1.2.2", "~1.2.3") def test_greater_than_or_equal(self): assert matches_constraint("1.0.0", ">=1.0.0") assert matches_constraint("2.0.0", ">=1.0.0") assert not matches_constraint("0.9.9", ">=1.0.0") def test_less_than_or_equal(self): assert matches_constraint("1.0.0", "<=1.0.0") assert matches_constraint("0.9.0", "<=1.0.0") assert not matches_constraint("1.0.1", "<=1.0.0") def test_greater_than(self): assert matches_constraint("1.0.1", ">1.0.0") assert not matches_constraint("1.0.0", ">1.0.0") def test_less_than(self): assert matches_constraint("0.9.9", "<1.0.0") assert not matches_constraint("1.0.0", "<1.0.0") def test_prerelease_excluded_by_default(self): # Prerelease versions should NOT match ranges without prerelease specifier assert not matches_constraint("1.0.0-alpha", "^1.0.0") assert not matches_constraint("1.0.0-beta", ">=1.0.0") def test_invalid_version(self): assert not matches_constraint("invalid", "^1.0.0") class TestFindBestMatch: """Tests for find_best_match function.""" def test_finds_highest(self): versions = ["1.0.0", "1.1.0", "1.2.0", "2.0.0"] assert find_best_match(versions, "*") == "2.0.0" def test_respects_constraint(self): versions = ["1.0.0", "1.1.0", "1.2.0", "2.0.0"] assert find_best_match(versions, "^1.0.0") == "1.2.0" def test_no_match(self): versions = ["1.0.0", "1.1.0"] assert find_best_match(versions, ">=2.0.0") is None def test_exact_match(self): versions = ["1.0.0", "1.1.0", "1.2.0"] assert find_best_match(versions, "1.1.0") == "1.1.0" def test_empty_versions(self): assert find_best_match([], "*") is None class TestIsValidVersion: """Tests for is_valid_version function.""" def test_valid(self): assert is_valid_version("1.0.0") assert is_valid_version("0.1.0") assert is_valid_version("1.0.0-alpha") assert is_valid_version("1.0.0+build") def test_invalid(self): assert not is_valid_version("") assert not is_valid_version("1.0") assert not is_valid_version("invalid") class TestCompareVersions: """Tests for compare_versions function.""" def test_less_than(self): assert compare_versions("1.0.0", "2.0.0") == -1 assert compare_versions("1.0.0", "1.1.0") == -1 def test_equal(self): assert compare_versions("1.0.0", "1.0.0") == 0 def test_greater_than(self): assert compare_versions("2.0.0", "1.0.0") == 1 assert compare_versions("1.1.0", "1.0.0") == 1 def test_invalid_raises(self): with pytest.raises(ValueError): compare_versions("invalid", "1.0.0") with pytest.raises(ValueError): compare_versions("1.0.0", "invalid")