From 2d37518c00be97128def9e7881d6b9bf08b42e39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Sep 2025 20:16:30 -0500 Subject: [PATCH] fix, cover --- esphome/resolver.py | 2 +- tests/unit_tests/test_helpers.py | 62 ++++++++++++++++++++----------- tests/unit_tests/test_resolver.py | 24 +++++++----- 3 files changed, 55 insertions(+), 33 deletions(-) diff --git a/esphome/resolver.py b/esphome/resolver.py index f70ecec357..dff0ca32d7 100644 --- a/esphome/resolver.py +++ b/esphome/resolver.py @@ -34,7 +34,7 @@ class AsyncResolver: self.result = await hr.async_resolve_host( hosts, port, timeout=RESOLVE_TIMEOUT ) - except Exception as e: + except (ResolveAPIError, ResolveTimeoutAPIError, OSError) as e: self.exception = e finally: self.event.set() diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 706acdd359..867e19a604 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -285,7 +285,7 @@ def test_sort_ip_addresses(text: list[str], expected: list[str]) -> None: # DNS resolution tests -def test_is_ip_address_ipv4(): +def test_is_ip_address_ipv4() -> None: """Test is_ip_address with IPv4 addresses.""" assert helpers.is_ip_address("192.168.1.1") is True assert helpers.is_ip_address("127.0.0.1") is True @@ -293,7 +293,7 @@ def test_is_ip_address_ipv4(): assert helpers.is_ip_address("0.0.0.0") is True -def test_is_ip_address_ipv6(): +def test_is_ip_address_ipv6() -> None: """Test is_ip_address with IPv6 addresses.""" assert helpers.is_ip_address("::1") is True assert helpers.is_ip_address("2001:db8::1") is True @@ -301,7 +301,7 @@ def test_is_ip_address_ipv6(): assert helpers.is_ip_address("::") is True -def test_is_ip_address_invalid(): +def test_is_ip_address_invalid() -> None: """Test is_ip_address with non-IP strings.""" assert helpers.is_ip_address("hostname") is False assert helpers.is_ip_address("hostname.local") is False @@ -310,26 +310,38 @@ def test_is_ip_address_invalid(): assert helpers.is_ip_address("") is False -def test_resolve_ip_address_single_ipv4(): +def test_resolve_ip_address_single_ipv4() -> None: """Test resolving a single IPv4 address (fast path).""" result = helpers.resolve_ip_address("192.168.1.100", 6053) assert len(result) == 1 assert result[0][0] == socket.AF_INET # family - assert result[0][1] == socket.SOCK_STREAM # type - assert result[0][2] == socket.IPPROTO_TCP # proto + assert result[0][1] in ( + 0, + socket.SOCK_STREAM, + ) # type (0 on Windows with AI_NUMERICHOST) + assert result[0][2] in ( + 0, + socket.IPPROTO_TCP, + ) # proto (0 on Windows with AI_NUMERICHOST) assert result[0][3] == "" # canonname assert result[0][4] == ("192.168.1.100", 6053) # sockaddr -def test_resolve_ip_address_single_ipv6(): +def test_resolve_ip_address_single_ipv6() -> None: """Test resolving a single IPv6 address (fast path).""" result = helpers.resolve_ip_address("::1", 6053) assert len(result) == 1 assert result[0][0] == socket.AF_INET6 # family - assert result[0][1] == socket.SOCK_STREAM # type - assert result[0][2] == socket.IPPROTO_TCP # proto + assert result[0][1] in ( + 0, + socket.SOCK_STREAM, + ) # type (0 on Windows with AI_NUMERICHOST) + assert result[0][2] in ( + 0, + socket.IPPROTO_TCP, + ) # proto (0 on Windows with AI_NUMERICHOST) assert result[0][3] == "" # canonname # IPv6 sockaddr has 4 elements assert len(result[0][4]) == 4 @@ -337,7 +349,7 @@ def test_resolve_ip_address_single_ipv6(): assert result[0][4][1] == 6053 # port -def test_resolve_ip_address_list_of_ips(): +def test_resolve_ip_address_list_of_ips() -> None: """Test resolving a list of IP addresses (fast path).""" ips = ["192.168.1.100", "10.0.0.1", "::1"] result = helpers.resolve_ip_address(ips, 6053) @@ -348,12 +360,18 @@ def test_resolve_ip_address_list_of_ips(): # Check that results are properly formatted for addr_info in result: assert addr_info[0] in (socket.AF_INET, socket.AF_INET6) - assert addr_info[1] == socket.SOCK_STREAM - assert addr_info[2] == socket.IPPROTO_TCP + assert addr_info[1] in ( + 0, + socket.SOCK_STREAM, + ) # 0 on Windows with AI_NUMERICHOST + assert addr_info[2] in ( + 0, + socket.IPPROTO_TCP, + ) # 0 on Windows with AI_NUMERICHOST assert addr_info[3] == "" -def test_resolve_ip_address_hostname(): +def test_resolve_ip_address_hostname() -> None: """Test resolving a hostname (async resolver path).""" mock_addr_info = AddrInfo( family=socket.AF_INET, @@ -374,7 +392,7 @@ def test_resolve_ip_address_hostname(): mock_resolver.run.assert_called_once_with(["test.local"], 6053) -def test_resolve_ip_address_mixed_list(): +def test_resolve_ip_address_mixed_list() -> None: """Test resolving a mix of IPs and hostnames.""" mock_addr_info = AddrInfo( family=socket.AF_INET, @@ -395,7 +413,7 @@ def test_resolve_ip_address_mixed_list(): mock_resolver.run.assert_called_once_with(["192.168.1.100", "test.local"], 6053) -def test_resolve_ip_address_url(): +def test_resolve_ip_address_url() -> None: """Test extracting hostname from URL.""" mock_addr_info = AddrInfo( family=socket.AF_INET, @@ -414,7 +432,7 @@ def test_resolve_ip_address_url(): mock_resolver.run.assert_called_once_with(["test.local"], 6053) -def test_resolve_ip_address_ipv6_conversion(): +def test_resolve_ip_address_ipv6_conversion() -> None: """Test proper IPv6 address info conversion.""" mock_addr_info = AddrInfo( family=socket.AF_INET6, @@ -434,7 +452,7 @@ def test_resolve_ip_address_ipv6_conversion(): assert result[0][4] == ("2001:db8::1", 6053, 1, 2) -def test_resolve_ip_address_error_handling(): +def test_resolve_ip_address_error_handling() -> None: """Test error handling from AsyncResolver.""" with patch("esphome.resolver.AsyncResolver") as MockResolver: mock_resolver = MockResolver.return_value @@ -444,7 +462,7 @@ def test_resolve_ip_address_error_handling(): helpers.resolve_ip_address("test.local", 6053) -def test_addr_preference_ipv4(): +def test_addr_preference_ipv4() -> None: """Test address preference for IPv4.""" addr_info = ( socket.AF_INET, @@ -456,7 +474,7 @@ def test_addr_preference_ipv4(): assert helpers.addr_preference_(addr_info) == 2 -def test_addr_preference_ipv6(): +def test_addr_preference_ipv6() -> None: """Test address preference for regular IPv6.""" addr_info = ( socket.AF_INET6, @@ -468,7 +486,7 @@ def test_addr_preference_ipv6(): assert helpers.addr_preference_(addr_info) == 1 -def test_addr_preference_ipv6_link_local_no_scope(): +def test_addr_preference_ipv6_link_local_no_scope() -> None: """Test address preference for link-local IPv6 without scope.""" addr_info = ( socket.AF_INET6, @@ -480,7 +498,7 @@ def test_addr_preference_ipv6_link_local_no_scope(): assert helpers.addr_preference_(addr_info) == 3 -def test_addr_preference_ipv6_link_local_with_scope(): +def test_addr_preference_ipv6_link_local_with_scope() -> None: """Test address preference for link-local IPv6 with scope.""" addr_info = ( socket.AF_INET6, @@ -492,7 +510,7 @@ def test_addr_preference_ipv6_link_local_with_scope(): assert helpers.addr_preference_(addr_info) == 1 # Has scope, so it's usable -def test_resolve_ip_address_sorting(): +def test_resolve_ip_address_sorting() -> None: """Test that results are sorted by preference.""" # Create multiple address infos with different preferences mock_addr_infos = [ diff --git a/tests/unit_tests/test_resolver.py b/tests/unit_tests/test_resolver.py index d49a367085..2044443ddd 100644 --- a/tests/unit_tests/test_resolver.py +++ b/tests/unit_tests/test_resolver.py @@ -15,7 +15,7 @@ from esphome.resolver import RESOLVE_TIMEOUT, AsyncResolver @pytest.fixture -def mock_addr_info_ipv4(): +def mock_addr_info_ipv4() -> AddrInfo: """Create a mock IPv4 AddrInfo.""" return AddrInfo( family=socket.AF_INET, @@ -26,7 +26,7 @@ def mock_addr_info_ipv4(): @pytest.fixture -def mock_addr_info_ipv6(): +def mock_addr_info_ipv6() -> AddrInfo: """Create a mock IPv6 AddrInfo.""" return AddrInfo( family=socket.AF_INET6, @@ -36,7 +36,7 @@ def mock_addr_info_ipv6(): ) -def test_async_resolver_successful_resolution(mock_addr_info_ipv4): +def test_async_resolver_successful_resolution(mock_addr_info_ipv4: AddrInfo) -> None: """Test successful DNS resolution.""" with patch( "esphome.resolver.hr.async_resolve_host", @@ -51,7 +51,9 @@ def test_async_resolver_successful_resolution(mock_addr_info_ipv4): ) -def test_async_resolver_multiple_hosts(mock_addr_info_ipv4, mock_addr_info_ipv6): +def test_async_resolver_multiple_hosts( + mock_addr_info_ipv4: AddrInfo, mock_addr_info_ipv6: AddrInfo +) -> None: """Test resolving multiple hosts.""" mock_results = [mock_addr_info_ipv4, mock_addr_info_ipv6] @@ -68,7 +70,7 @@ def test_async_resolver_multiple_hosts(mock_addr_info_ipv4, mock_addr_info_ipv6) ) -def test_async_resolver_resolve_api_error(): +def test_async_resolver_resolve_api_error() -> None: """Test handling of ResolveAPIError.""" error_msg = "Failed to resolve" with patch( @@ -82,7 +84,7 @@ def test_async_resolver_resolve_api_error(): resolver.run(["test.local"], 6053) -def test_async_resolver_timeout_error(): +def test_async_resolver_timeout_error() -> None: """Test handling of ResolveTimeoutAPIError.""" error_msg = "Resolution timed out" with patch( @@ -96,7 +98,7 @@ def test_async_resolver_timeout_error(): resolver.run(["test.local"], 6053) -def test_async_resolver_generic_exception(): +def test_async_resolver_generic_exception() -> None: """Test handling of generic exceptions.""" error = RuntimeError("Unexpected error") with patch( @@ -108,7 +110,7 @@ def test_async_resolver_generic_exception(): resolver.run(["test.local"], 6053) -def test_async_resolver_thread_timeout(): +def test_async_resolver_thread_timeout() -> None: """Test timeout when thread doesn't complete in time.""" async def slow_resolve(hosts, port, timeout): @@ -125,7 +127,7 @@ def test_async_resolver_thread_timeout(): resolver.run(["test.local"], 6053) -def test_async_resolver_ip_addresses(mock_addr_info_ipv4): +def test_async_resolver_ip_addresses(mock_addr_info_ipv4: AddrInfo) -> None: """Test resolving IP addresses.""" with patch( "esphome.resolver.hr.async_resolve_host", @@ -140,7 +142,9 @@ def test_async_resolver_ip_addresses(mock_addr_info_ipv4): ) -def test_async_resolver_mixed_addresses(mock_addr_info_ipv4, mock_addr_info_ipv6): +def test_async_resolver_mixed_addresses( + mock_addr_info_ipv4: AddrInfo, mock_addr_info_ipv6: AddrInfo +) -> None: """Test resolving mix of hostnames and IP addresses.""" mock_results = [mock_addr_info_ipv4, mock_addr_info_ipv6]