|
| 1 | +"""Test process utilities.""" |
| 2 | + |
| 3 | +import socket |
| 4 | +import threading |
| 5 | +import time |
| 6 | +from contextlib import closing |
| 7 | +from unittest import mock |
| 8 | + |
| 9 | +import pytest |
| 10 | + |
| 11 | +from reflex.utils.processes import is_process_on_port |
| 12 | + |
| 13 | + |
| 14 | +def test_is_process_on_port_free_port(): |
| 15 | + """Test is_process_on_port returns False when port is free.""" |
| 16 | + # Find a free port |
| 17 | + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: |
| 18 | + sock.bind(("127.0.0.1", 0)) |
| 19 | + free_port = sock.getsockname()[1] |
| 20 | + |
| 21 | + # Port should be free after socket is closed |
| 22 | + assert not is_process_on_port(free_port) |
| 23 | + |
| 24 | + |
| 25 | +def test_is_process_on_port_occupied_port(): |
| 26 | + """Test is_process_on_port returns True when port is occupied.""" |
| 27 | + # Create a server socket to occupy a port |
| 28 | + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 29 | + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 30 | + server_socket.bind(("127.0.0.1", 0)) |
| 31 | + server_socket.listen(1) |
| 32 | + |
| 33 | + occupied_port = server_socket.getsockname()[1] |
| 34 | + |
| 35 | + try: |
| 36 | + # Port should be occupied |
| 37 | + assert is_process_on_port(occupied_port) |
| 38 | + finally: |
| 39 | + server_socket.close() |
| 40 | + |
| 41 | + |
| 42 | +def test_is_process_on_port_ipv6(): |
| 43 | + """Test is_process_on_port works with IPv6.""" |
| 44 | + # Test with IPv6 socket |
| 45 | + try: |
| 46 | + server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) |
| 47 | + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 48 | + server_socket.bind(("::1", 0)) |
| 49 | + server_socket.listen(1) |
| 50 | + |
| 51 | + occupied_port = server_socket.getsockname()[1] |
| 52 | + |
| 53 | + try: |
| 54 | + # Port should be occupied on IPv6 |
| 55 | + assert is_process_on_port(occupied_port) |
| 56 | + finally: |
| 57 | + server_socket.close() |
| 58 | + except OSError: |
| 59 | + # IPv6 might not be available on some systems |
| 60 | + pytest.skip("IPv6 not available on this system") |
| 61 | + |
| 62 | + |
| 63 | +def test_is_process_on_port_both_protocols(): |
| 64 | + """Test is_process_on_port detects occupation on either IPv4 or IPv6.""" |
| 65 | + # Create IPv4 server |
| 66 | + ipv4_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 67 | + ipv4_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 68 | + ipv4_socket.bind(("127.0.0.1", 0)) |
| 69 | + ipv4_socket.listen(1) |
| 70 | + |
| 71 | + port = ipv4_socket.getsockname()[1] |
| 72 | + |
| 73 | + try: |
| 74 | + # Should detect IPv4 occupation |
| 75 | + assert is_process_on_port(port) |
| 76 | + finally: |
| 77 | + ipv4_socket.close() |
| 78 | + |
| 79 | + |
| 80 | +@pytest.mark.parametrize("port", [0, 1, 80, 443, 8000, 3000, 65535]) |
| 81 | +def test_is_process_on_port_various_ports(port): |
| 82 | + """Test is_process_on_port with various port numbers. |
| 83 | +
|
| 84 | + Args: |
| 85 | + port: The port number to test. |
| 86 | + """ |
| 87 | + # This test just ensures the function doesn't crash with different port numbers |
| 88 | + # The actual result depends on what's running on the system |
| 89 | + result = is_process_on_port(port) |
| 90 | + assert isinstance(result, bool) |
| 91 | + |
| 92 | + |
| 93 | +def test_is_process_on_port_mock_socket_error(): |
| 94 | + """Test is_process_on_port handles socket errors gracefully.""" |
| 95 | + with mock.patch("socket.socket") as mock_socket: |
| 96 | + mock_socket_instance = mock.MagicMock() |
| 97 | + mock_socket.return_value = mock_socket_instance |
| 98 | + mock_socket_instance.__enter__.return_value = mock_socket_instance |
| 99 | + mock_socket_instance.bind.side_effect = OSError("Mock socket error") |
| 100 | + |
| 101 | + # Should return True when socket operations fail |
| 102 | + result = is_process_on_port(8080) |
| 103 | + assert result is True |
| 104 | + |
| 105 | + |
| 106 | +def test_is_process_on_port_permission_error(): |
| 107 | + """Test is_process_on_port handles permission errors.""" |
| 108 | + with mock.patch("socket.socket") as mock_socket: |
| 109 | + mock_socket_instance = mock.MagicMock() |
| 110 | + mock_socket.return_value = mock_socket_instance |
| 111 | + mock_socket_instance.__enter__.return_value = mock_socket_instance |
| 112 | + mock_socket_instance.bind.side_effect = PermissionError("Permission denied") |
| 113 | + |
| 114 | + # Should return True when permission is denied (can't bind = port is "occupied") |
| 115 | + result = is_process_on_port(80) |
| 116 | + assert result is True |
| 117 | + |
| 118 | + |
| 119 | +@pytest.mark.parametrize("should_listen", [True, False]) |
| 120 | +def test_is_process_on_port_concurrent_access(should_listen): |
| 121 | + """Test is_process_on_port works correctly with concurrent access. |
| 122 | +
|
| 123 | + Args: |
| 124 | + should_listen: Whether the server socket should call listen() or just bind(). |
| 125 | + """ |
| 126 | + |
| 127 | + def create_server_and_test(port_holder, listen): |
| 128 | + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 129 | + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 130 | + server.bind(("127.0.0.1", 0)) |
| 131 | + |
| 132 | + if listen: |
| 133 | + server.listen(1) |
| 134 | + |
| 135 | + port = server.getsockname()[1] |
| 136 | + port_holder[0] = port |
| 137 | + |
| 138 | + # Small delay to ensure the test runs while server is active |
| 139 | + time.sleep(0.1) |
| 140 | + server.close() |
| 141 | + |
| 142 | + port_holder = [None] |
| 143 | + thread = threading.Thread( |
| 144 | + target=create_server_and_test, args=(port_holder, should_listen) |
| 145 | + ) |
| 146 | + thread.start() |
| 147 | + |
| 148 | + # Wait a bit for the server to start |
| 149 | + time.sleep(0.05) |
| 150 | + |
| 151 | + if port_holder[0] is not None: |
| 152 | + # Port should be occupied while server is running (both bound-only and listening) |
| 153 | + assert is_process_on_port(port_holder[0]) |
| 154 | + |
| 155 | + thread.join() |
| 156 | + |
| 157 | + # After thread ends and server closes, port should be free |
| 158 | + if port_holder[0] is not None: |
| 159 | + # Give it a moment for the socket to be fully released |
| 160 | + time.sleep(0.1) |
| 161 | + assert not is_process_on_port(port_holder[0]) |
0 commit comments