11"""Tests for requirement_resolver module."""
22
3+ from unittest .mock import MagicMock , patch
4+
35import pytest
46from packaging .requirements import Requirement
57from packaging .utils import canonicalize_name
@@ -248,8 +250,8 @@ def test_resolve_from_graph_no_previous_graph(tmp_context: WorkContext) -> None:
248250 )
249251
250252
251- def test_resolve_rejects_git_urls (tmp_context : WorkContext ) -> None :
252- """RequirementResolver.resolve() rejects git URLs."""
253+ def test_resolve_rejects_git_urls_for_source (tmp_context : WorkContext ) -> None :
254+ """RequirementResolver.resolve() rejects git URLs when pre_built=False ."""
253255 resolver = RequirementResolver (tmp_context )
254256
255257 with pytest .raises (
@@ -261,3 +263,170 @@ def test_resolve_rejects_git_urls(tmp_context: WorkContext) -> None:
261263 pre_built = False ,
262264 parent_req = None ,
263265 )
266+
267+
268+ @patch ("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel" )
269+ @patch ("fromager.requirement_resolver.wheels.get_wheel_server_urls" )
270+ def test_resolve_allows_git_urls_for_prebuilt (
271+ mock_get_servers : MagicMock ,
272+ mock_resolve_wheel : MagicMock ,
273+ tmp_context : WorkContext ,
274+ ) -> None :
275+ """RequirementResolver.resolve() allows git URLs when pre_built=True (test mode fallback)."""
276+ resolver = RequirementResolver (tmp_context )
277+ req = Requirement ("mypkg @ git+https://github.com/example/repo.git" )
278+
279+ # Mock wheel resolution to return expected result
280+ mock_get_servers .return_value = ["https://pypi.org/simple" ]
281+ mock_resolve_wheel .return_value = (
282+ "https://files.pythonhosted.org/mypkg-1.0-py3-none-any.whl" ,
283+ Version ("1.0" ),
284+ )
285+
286+ # Should NOT raise - git URLs are allowed when explicitly requesting prebuilt
287+ url , version = resolver .resolve (
288+ req = req ,
289+ req_type = RequirementType .INSTALL ,
290+ pre_built = True ,
291+ parent_req = None ,
292+ )
293+
294+ # Verify it routed to wheel resolution
295+ mock_resolve_wheel .assert_called_once ()
296+ assert url == "https://files.pythonhosted.org/mypkg-1.0-py3-none-any.whl"
297+ assert version == Version ("1.0" )
298+
299+
300+ @patch ("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel" )
301+ @patch ("fromager.requirement_resolver.wheels.get_wheel_server_urls" )
302+ def test_resolve_auto_routes_to_prebuilt (
303+ mock_get_servers : MagicMock ,
304+ mock_resolve_wheel : MagicMock ,
305+ tmp_context : WorkContext ,
306+ ) -> None :
307+ """resolve(pre_built=None) with pbi.pre_built=True routes to wheels.resolve_prebuilt_wheel."""
308+ req = Requirement ("setuptools>=40" )
309+
310+ # Mock package build info to return pre_built=True
311+ mock_pbi = MagicMock ()
312+ mock_pbi .pre_built = True
313+
314+ with patch .object (tmp_context , "package_build_info" , return_value = mock_pbi ):
315+ resolver = RequirementResolver (tmp_context )
316+
317+ # Mock wheel resolution to return expected result
318+ mock_get_servers .return_value = ["https://pypi.org/simple" ]
319+ mock_resolve_wheel .return_value = (
320+ "https://files.pythonhosted.org/setuptools-1.0-py3-none-any.whl" ,
321+ Version ("1.0" ),
322+ )
323+
324+ # Call resolve with pre_built=None (should auto-detect)
325+ url , version = resolver .resolve (
326+ req = req ,
327+ req_type = RequirementType .INSTALL ,
328+ parent_req = None ,
329+ pre_built = None ,
330+ )
331+
332+ # Verify it routed to wheel resolution
333+ mock_resolve_wheel .assert_called_once ()
334+ assert url == "https://files.pythonhosted.org/setuptools-1.0-py3-none-any.whl"
335+ assert version == Version ("1.0" )
336+
337+
338+ @patch ("fromager.requirement_resolver.sources.resolve_source" )
339+ def test_resolve_auto_routes_to_source (
340+ mock_resolve_source : MagicMock ,
341+ tmp_context : WorkContext ,
342+ ) -> None :
343+ """resolve(pre_built=None) with pbi.pre_built=False routes to sources.resolve_source."""
344+ req = Requirement ("mypackage>=1.0" )
345+
346+ # Mock package build info to return pre_built=False
347+ mock_pbi = MagicMock ()
348+ mock_pbi .pre_built = False
349+
350+ with patch .object (tmp_context , "package_build_info" , return_value = mock_pbi ):
351+ resolver = RequirementResolver (tmp_context )
352+
353+ # Mock source resolution to return expected result
354+ mock_resolve_source .return_value = (
355+ "https://files.pythonhosted.org/mypackage-2.0.tar.gz" ,
356+ Version ("2.0" ),
357+ )
358+
359+ # Call resolve with pre_built=None (should auto-detect)
360+ url , version = resolver .resolve (
361+ req = req ,
362+ req_type = RequirementType .INSTALL ,
363+ parent_req = None ,
364+ pre_built = None ,
365+ )
366+
367+ # Verify it routed to source resolution
368+ mock_resolve_source .assert_called_once ()
369+ assert url == "https://files.pythonhosted.org/mypackage-2.0.tar.gz"
370+ assert version == Version ("2.0" )
371+
372+
373+ @patch ("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel" )
374+ @patch ("fromager.requirement_resolver.wheels.get_wheel_server_urls" )
375+ @patch ("fromager.requirement_resolver.sources.resolve_source" )
376+ def test_resolve_prebuilt_after_source_uses_separate_cache (
377+ mock_resolve_source : MagicMock ,
378+ mock_get_servers : MagicMock ,
379+ mock_resolve_wheel : MagicMock ,
380+ tmp_context : WorkContext ,
381+ ) -> None :
382+ """resolve(pre_built=True) after same req resolved as source uses separate cache."""
383+ req = Requirement ("testpkg==1.5" )
384+
385+ # Mock package build info to return pre_built=False initially
386+ mock_pbi = MagicMock ()
387+ mock_pbi .pre_built = False
388+
389+ with patch .object (tmp_context , "package_build_info" , return_value = mock_pbi ):
390+ resolver = RequirementResolver (tmp_context )
391+
392+ # Mock source resolution
393+ mock_resolve_source .return_value = (
394+ "https://files.pythonhosted.org/testpkg-1.5.tar.gz" ,
395+ Version ("1.5" ),
396+ )
397+
398+ # First call: resolve as source (pre_built=None, auto-detects to False)
399+ url1 , version1 = resolver .resolve (
400+ req = req ,
401+ req_type = RequirementType .INSTALL ,
402+ parent_req = None ,
403+ pre_built = None ,
404+ )
405+
406+ assert url1 == "https://files.pythonhosted.org/testpkg-1.5.tar.gz"
407+ assert version1 == Version ("1.5" )
408+ assert mock_resolve_source .call_count == 1
409+
410+ # Mock wheel resolution for second call
411+ mock_get_servers .return_value = ["https://pypi.org/simple" ]
412+ mock_resolve_wheel .return_value = (
413+ "https://files.pythonhosted.org/testpkg-1.5-py3-none-any.whl" ,
414+ Version ("1.5" ),
415+ )
416+
417+ # Second call: resolve same req as prebuilt (explicit pre_built=True)
418+ # This should NOT return the cached source result
419+ url2 , version2 = resolver .resolve (
420+ req = req ,
421+ req_type = RequirementType .INSTALL ,
422+ parent_req = None ,
423+ pre_built = True ,
424+ )
425+
426+ # Verify it called wheel resolution (not cached)
427+ assert mock_resolve_wheel .call_count == 1
428+ assert url2 == "https://files.pythonhosted.org/testpkg-1.5-py3-none-any.whl"
429+ assert version2 == Version ("1.5" )
430+
431+ # Verify source was only called once (first time, not second)
432+ assert mock_resolve_source .call_count == 1
0 commit comments