1+ import pytest
2+ from pallet_patcher .solver import _parse_rust_specifier , solve_dependency
3+
4+ # Test code generated with LLM help
5+ @pytest .mark .parametrize ("rust_input, expected_matches, expected_non_matches" , [
6+ # 1. Explicit Equals (=)
7+ ("=1.2.3" , ["1.2.3" ], ["1.2.4" , "1.2.2" ]),
8+
9+ # 2. Tilde Requirements (~)
10+ # ~1.2.3 := >=1.2.3, <1.3.0
11+ ("~1.2.3" , ["1.2.3" , "1.2.9" ], ["1.3.0" , "1.1.0" ]),
12+ # ~1.2 := >=1.2, <1.3.0
13+ ("~1.2" , ["1.2.0" , "1.2.5" ], ["1.3.0" , "1.1.0" ]),
14+ # ~1 := >=1, <2.0.0
15+ ("~1" , ["1.0.0" , "1.5.0" , "1.9.9" ], ["2.0.0" , "0.9.0" ]),
16+
17+ # 3. Caret Requirements (^) - Major > 0
18+ # ^1.2.3 := >=1.2.3, <2.0.0
19+ ("^1.2.3" , ["1.2.3" , "1.9.9" ], ["2.0.0" , "1.2.2" ]),
20+ # Bare 1.2.3 (same as ^)
21+ ("1.2.3" , ["1.2.3" , "1.5.0" , "1.9.9" ], ["2.0.0" , "1.2.2" ]),
22+
23+ # 4. Caret Requirements (^) - Major == 0 (The tricky ones)
24+
25+ # Case B.1: ^0 := >=0.0.0, <1.0.0
26+ ("^0" , ["0.0.0" , "0.1.0" , "0.9.9" ], ["1.0.0" ]),
27+ ("0" , ["0.1.5" ], ["1.0.0" ]),
28+
29+ # Case B.2: ^0.2.3 (Minor > 0) := >=0.2.3, <0.3.0
30+ ("^0.2.3" , ["0.2.3" , "0.2.9" ], ["0.3.0" , "0.2.2" ]),
31+ ("0.2.3" , ["0.2.3" , "0.2.9" ], ["0.3.0" ]),
32+
33+ # Case B.3: ^0.0.3 (Minor == 0, Patch specified) := >=0.0.3, <0.0.4
34+ ("^0.0.3" , ["0.0.3" ], ["0.0.4" , "0.0.2" , "0.1.0" ]),
35+ ("0.0.3" , ["0.0.3" ], ["0.0.4" ]),
36+
37+ # Case B.4: ^0.0 (Minor == 0, Patch missing) := >=0.0, <0.1.0
38+ ("^0.0" , ["0.0.0" , "0.0.5" ], ["0.1.0" ]),
39+ ("0.0" , ["0.0.1" ], ["0.1.0" ]),
40+ ])
41+ def test_rust_specifier_logic (rust_input , expected_matches , expected_non_matches ):
42+ """
43+ Tests that the converted Python SpecifierSet correctly matches
44+ and excludes the versions dictated by Rust SemVer logic.
45+ """
46+ spec_set = _parse_rust_specifier (rust_input )
47+
48+ for version in expected_matches :
49+ assert version in spec_set , \
50+ f"Rust spec '{ rust_input } ' should MATCH { version } , but Python spec { spec_set } did not."
51+
52+ for version in expected_non_matches :
53+ assert version not in spec_set , \
54+ f"Rust spec '{ rust_input } ' should NOT match { version } , but Python spec { spec_set } did."
55+
56+
57+ @pytest .mark .parametrize ("input_str, expected_str_repr" , [
58+ (">=1.5" , ">=1.5" ), # Passthrough standard python
59+ ("<2.0" , "<2.0" ), # Passthrough standard python
60+ ("==1.2.3" , "==1.2.3" ), # Passthrough explicit equality
61+ ("" , "" ), # Empty string handling
62+ ])
63+ def test_standard_python_fallback (input_str , expected_str_repr ):
64+ """
65+ Tests that standard Python specifiers or other strings
66+ are passed through to SpecifierSet mostly unchanged.
67+ """
68+ spec = _parse_rust_specifier (input_str )
69+ # Note: SpecifierSet normalization might change string spacing,
70+ # but the logic checks if it parses without crashing.
71+ assert str (spec ) == expected_str_repr or not input_str
72+
73+ def test_invalid_version_strings ():
74+ """
75+ Ensure the function handles malformed strings gracefully
76+ (falling back to SpecifierSet which might raise its own error or handle it).
77+ """
78+ # This falls through to "Bare" logic, fails try/except, hits fallback
79+ # SpecifierSet("invalid") is technically valid in packaging but matches nothing usually
80+ # or raises InvalidSpecifier depending on version.
81+ # Here we just want to ensure your code doesn't crash internally.
82+ try :
83+ _parse_rust_specifier ("invalid_string_with_char" )
84+ except Exception as e :
85+ # It is acceptable if SpecifierSet raises, but your parsing logic shouldn't
86+ assert "Invalid specifier" in str (e ) or isinstance (e , ValueError )
87+
88+
89+ @pytest .mark .parametrize ("spec, available, expected" , [
90+ # 1. Basic Caret Priority
91+ # ^1.2.0 allows >=1.2.0, <2.0.0.
92+ # It should pick 1.9.9 (highest), ignore 2.0.0 (too high) and 1.1.0 (too low).
93+ ("^1.2.0" , ["1.1.0" , "1.2.0" , "1.2.5" , "1.9.9" , "2.0.0" ], "1.9.9" ),
94+
95+ # 2. Basic Tilde Priority
96+ # ~1.2.0 allows >=1.2.0, <1.3.0.
97+ # Should pick 1.2.9, ignore 1.3.0.
98+ ("~1.2.0" , ["1.2.0" , "1.2.5" , "1.2.9" , "1.3.0" , "1.4.0" ], "1.2.9" ),
99+
100+ # 3. Rust "0.x.y" Semantics (Major 0 breaking changes)
101+ # ^0.2.0 allows >=0.2.0, <0.3.0.
102+ # Should ignore 0.3.0 even though it's higher.
103+ ("^0.2.0" , ["0.1.9" , "0.2.0" , "0.2.5" , "0.3.0" ], "0.2.5" ),
104+
105+ # 4. Rust "0.0.x" Semantics (Patch 0 breaking changes)
106+ # ^0.0.3 allows >=0.0.3, <0.0.4.
107+ # Should strictly match 0.0.3.
108+ ("^0.0.3" , ["0.0.2" , "0.0.3" , "0.0.4" , "0.1.0" ], "0.0.3" ),
109+
110+ # 5. Exact Match
111+ ("=1.5.0" , ["1.4.0" , "1.5.0" , "1.6.0" ], "1.5.0" ),
112+
113+ # 6. No Match Found
114+ # Range is >=1.0.0, <2.0.0. Only have 0.9 and 2.1.
115+ ("^1.0.0" , ["0.9.0" , "2.1.0" , "3.0.0" ], None ),
116+
117+ # 7. Empty List
118+ ("^1.0.0" , [], None ),
119+
120+ # 8. Handling "Bare" versions (Implies ^)
121+ ("1.2.0" , ["1.2.0" , "1.5.0" , "2.0.0" ], "1.5.0" ),
122+ ])
123+ def test_solve_dependency_logic (spec , available , expected ):
124+ """
125+ Verifies that the solver picks the highest version that satisfies
126+ the Rust-style specifier.
127+ """
128+ result = solve_dependency (spec , available )
129+ assert result == expected , \
130+ f"For spec '{ spec } ' and versions { available } , expected '{ expected } ' but got '{ result } '"
131+
132+
133+ def test_solve_dependency_sorting_correctness ():
134+ """
135+ Specifically tests that 1.10.0 is considered greater than 1.2.0.
136+ String sorting would say "1.2" > "1.10", but numeric sorting says 10 > 2.
137+ """
138+ spec = "^1.0.0"
139+ # If sorting was string-based, it might encounter 1.2.0 first (descending).
140+ # Correct numeric sort: 1.10.0 is highest.
141+ available = ["1.1.0" , "1.2.0" , "1.9.0" , "1.10.0" ]
142+
143+ result = solve_dependency (spec , available )
144+ assert result == "1.10.0"
145+
146+
147+ def test_solve_dependency_invalid_input_handling ():
148+ """
149+ Test how the function handles non-integer version strings
150+ given the lambda sorting logic provided.
151+ """
152+ # NOTE: The provided function uses `int(part)` which will crash on "beta".
153+ # This test documents that behavior. If you want to support prereleases,
154+ # the sort key in the function needs to change.
155+ spec = "^1.0.0"
156+ available = ["1.0.0" , "1.1.0-beta" ]
157+
158+ with pytest .raises (ValueError ) as excinfo :
159+ solve_dependency (spec , available )
160+
161+ # Confirm it failed where we expected (in the sort lambda)
162+ assert "invalid literal for int()" in str (excinfo .value )
0 commit comments