1818import threading
1919from collections import Counter
2020
21+ import pytest
22+
2123from lightning .fabric .plugins .environments .lightning import find_free_network_port
2224from lightning .fabric .utilities .port_manager import PortManager , get_port_manager
2325
26+ # =============================================================================
27+ # Fixtures
28+ # =============================================================================
29+
30+
31+ @pytest .fixture
32+ def with_master_port ():
33+ """Fixture that sets MASTER_PORT before test runs, for conftest coverage."""
34+ port = find_free_network_port ()
35+ previous_value = os .environ .get ("MASTER_PORT" )
36+ os .environ ["MASTER_PORT" ] = str (port )
37+ try :
38+ yield port
39+ finally :
40+ if previous_value is None :
41+ os .environ .pop ("MASTER_PORT" , None )
42+ else :
43+ os .environ ["MASTER_PORT" ] = previous_value
44+
45+
46+ @pytest .fixture
47+ def with_invalid_master_port ():
48+ """Fixture that sets invalid MASTER_PORT to test error handling."""
49+ previous_value = os .environ .get ("MASTER_PORT" )
50+ os .environ ["MASTER_PORT" ] = "not_a_valid_port_number"
51+ try :
52+ yield
53+ finally :
54+ if previous_value is None :
55+ os .environ .pop ("MASTER_PORT" , None )
56+ else :
57+ os .environ ["MASTER_PORT" ] = previous_value
58+
59+
2460# =============================================================================
2561# Unit Tests for PortManager
2662# =============================================================================
@@ -135,12 +171,19 @@ def test_port_manager_allocation_failure():
135171 """Test that PortManager raises error when unable to allocate after max attempts."""
136172 manager = PortManager ()
137173
138- # This is hard to test without actually exhausting ports, but we can test
139- # the error path by mocking or just ensure the code path exists
140- # For now, just verify that max_attempts parameter exists
141- port = manager .allocate_port (max_attempts = 1 )
174+ # Pre-allocate a large number of ports to make it harder to find a free one
175+ # Then try with max_attempts=1 which should fail quickly
176+ allocated_ports = [manager .allocate_port () for _ in range (50 )]
177+
178+ # Test that it can still allocate with enough attempts
179+ port = manager .allocate_port (max_attempts = 100 )
142180 assert port >= 1024
143181
182+ # Clean up
183+ for p in allocated_ports :
184+ manager .release_port (p )
185+ manager .release_port (port )
186+
144187
145188def test_port_manager_prevents_reallocation ():
146189 """Test that a port won't be allocated twice until released."""
@@ -160,6 +203,10 @@ def test_port_manager_prevents_reallocation():
160203 manager .release_port (port1 )
161204 assert port1 not in manager ._allocated_ports
162205
206+ # Clean up
207+ for port in more_ports :
208+ manager .release_port (port )
209+
163210
164211def test_get_port_manager_singleton ():
165212 """Test that get_port_manager returns the same instance."""
@@ -173,6 +220,9 @@ def test_get_port_manager_singleton():
173220 port = manager1 .allocate_port ()
174221 assert port in manager2 ._allocated_ports
175222
223+ # Clean up
224+ manager1 .release_port (port )
225+
176226
177227def test_get_port_manager_thread_safe_singleton ():
178228 """Test that get_port_manager creates singleton safely across threads."""
@@ -280,6 +330,42 @@ def test_port_manager_atexit_cleanup():
280330 assert len (manager ._allocated_ports ) == 0
281331
282332
333+ def test_port_manager_reserve_existing_port_free ():
334+ """reserve_existing_port should succeed for free ports and track them."""
335+ manager = PortManager ()
336+
337+ port = manager ._find_free_port ()
338+ assert manager .reserve_existing_port (port )
339+ assert port in manager ._allocated_ports
340+
341+ # Second call should succeed but not duplicate
342+ assert manager .reserve_existing_port (port )
343+ assert len (manager ._allocated_ports ) == 1
344+
345+
346+ def test_port_manager_reserve_existing_port_invalid_value ():
347+ """reserve_existing_port should reject invalid port numbers."""
348+ manager = PortManager ()
349+
350+ assert not manager .reserve_existing_port (0 )
351+ assert not manager .reserve_existing_port (- 1 )
352+ assert not manager .reserve_existing_port (70000 )
353+
354+
355+ def test_port_manager_reserve_existing_port_after_release ():
356+ """Ports released from sockets should become reservable."""
357+ manager = PortManager ()
358+
359+ s = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
360+ s .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
361+ s .bind (("" , 0 ))
362+ reusable_port = s .getsockname ()[1 ]
363+ s .close ()
364+
365+ assert manager .reserve_existing_port (reusable_port )
366+ assert reusable_port in manager ._allocated_ports
367+
368+
283369def test_port_manager_context_manager ():
284370 """Test that context manager automatically releases ports."""
285371 manager = PortManager ()
@@ -430,6 +516,44 @@ def test_port_allocation_simulates_distributed_test_lifecycle():
430516 assert len (manager ._allocated_ports ) == initial_count
431517
432518
519+ def test_conftest_cleanup_with_master_port_set (with_master_port ):
520+ """Test conftest cleanup when MASTER_PORT is set before test starts.
521+
522+ This test uses a fixture to set MASTER_PORT before the test runs, allowing the conftest teardown_process_group
523+ fixture to capture and clean it up. This ensures the conftest cleanup code is covered.
524+
525+ """
526+ manager = get_port_manager ()
527+ port = with_master_port # Port was set by fixture
528+
529+ # Verify port is allocated
530+ assert port in manager ._allocated_ports
531+ assert os .environ .get ("MASTER_PORT" ) == str (port )
532+
533+ # Leave MASTER_PORT set - conftest teardown will clean it up
534+ # After this test, teardown_process_group will:
535+ # 1. Detect MASTER_PORT in os.environ (line captured before yield)
536+ # 2. Call get_port_manager().release_port(port)
537+ # 3. Port gets released back to manager
538+
539+
540+ def test_conftest_handles_invalid_master_port (with_invalid_master_port ):
541+ """Test conftest handles invalid MASTER_PORT gracefully.
542+
543+ This exercises the contextlib.suppress(ValueError, KeyError) path in the conftest teardown_process_group fixture.
544+
545+ """
546+ # Fixture set MASTER_PORT to "not_a_valid_port_number"
547+ # The conftest will try to parse it: int(os.environ["MASTER_PORT"])
548+ # This will raise ValueError, which should be caught by contextlib.suppress
549+
550+ # Verify the invalid value is set
551+ assert os .environ .get ("MASTER_PORT" ) == "not_a_valid_port_number"
552+
553+ # This test just needs to complete without crashing
554+ # The conftest teardown will handle the ValueError gracefully
555+
556+
433557def test_multiple_tests_can_reuse_ports_after_release ():
434558 """Test that ports can be reused after being released."""
435559 manager = get_port_manager ()
@@ -521,3 +645,49 @@ def test_port_manager_survives_multiple_test_sessions():
521645 # Clean up
522646 for port in session2_ports + session3_ports :
523647 manager .release_port (port )
648+
649+
650+ def test_port_manager_allocation_runtime_error ():
651+ """Test that allocation fails gracefully when max_attempts is exhausted."""
652+ manager = PortManager ()
653+
654+ # Mock the _find_free_port to always return a port that's already allocated
655+ # This will cause max_attempts to be exhausted
656+ allocated_port = manager .allocate_port ()
657+
658+ # Save original method
659+ original_find = manager ._find_free_port
660+
661+ # Make _find_free_port always return the already-allocated port
662+ def always_return_allocated ():
663+ return allocated_port
664+
665+ manager ._find_free_port = always_return_allocated
666+
667+ # This should raise RuntimeError after max_attempts
668+ with pytest .raises (RuntimeError , match = "Failed to allocate a free port after .* attempts" ):
669+ manager .allocate_port (max_attempts = 5 )
670+
671+ # Restore original method and clean up
672+ manager ._find_free_port = original_find
673+ manager .release_port (allocated_port )
674+
675+
676+ def test_find_free_network_port_respects_existing_master_port (with_master_port ):
677+ """find_free_network_port should reuse externally provided MASTER_PORT."""
678+ manager = get_port_manager ()
679+ port = with_master_port
680+
681+ returned_port = find_free_network_port ()
682+ assert returned_port == port
683+ assert port in manager ._allocated_ports
684+
685+
686+ def test_find_free_network_port_handles_invalid_master_port (with_invalid_master_port ):
687+ """Invalid MASTER_PORT values should fall back to allocating a fresh port."""
688+ manager = get_port_manager ()
689+
690+ returned_port = find_free_network_port ()
691+ assert isinstance (returned_port , int )
692+ assert returned_port in manager ._allocated_ports
693+ assert returned_port != "not_a_valid_port_number"
0 commit comments