Add transitive dependency resolution and lock file support
Implements two major features for project dependency management: Transitive Dependency Resolution: - DependencyGraph/DependencyNode for structured dependency tracking - DependencyGraphBuilder with recursive resolution and cycle detection - Topological sorting for correct install order (dependencies first) - Version conflict detection with provenance tracking - Full semver support: ^, ~, >=, <=, >, <, = constraints - New `cmdforge deps tree` command to visualize dependency tree - Install flags: --dry-run, --force, --verbose Lock File Support (cmdforge.lock): - Lockfile/LockedPackage/LockfileMetadata dataclasses - Records exact versions of all dependencies (direct + transitive) - Integrity verification via SHA256 hashes - `cmdforge lock` to generate/update lock file - `cmdforge verify` to check installed tools match lock - Install flags: --frozen, --strict-frozen, --ignore-lock 62 new tests for version, dependency_graph, and lockfile modules. Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user