diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..ff2c165e2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +/.github/CODEOWNERS @neo4j/drivers +/.github/workflows/ @neo4j/drivers diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 000000000..230bfdf2f --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,52 @@ +name: Tests + +on: + push: + branches: + - '6.x' + pull_request: + branches: + - '6.x' + +jobs: + win-unit-tests: + name: Windows Unit Tests + runs-on: windows-latest + strategy: + matrix: + python-version: + - semver: '3.10' + tox-factor: 'py310' + - semver: '3.11' + tox-factor: 'py311' + - semver: '3.12' + tox-factor: 'py312' + - semver: '3.13' + tox-factor: 'py313' + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Set up Python ${{ matrix.python-version.semver }} + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: ${{ matrix.python-version.semver }} + cache: 'pip' + + - name: Run install tox + run: python -m pip install -U --group tox + - name: Run unit tests + run: python -m tox -vv -f unit ${{ matrix.python-version.tox-factor }} + + gha-conclusion: + name: gha-conclusion + needs: win-unit-tests + runs-on: ubuntu-latest + steps: + - name: Signal failure + if: ${{ cancelled() || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }} + run: | + echo "Some workflows have failed!" + exit 1 + - name: Signal success + if: ${{ !cancelled() && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }} + run: echo "All done!" diff --git a/src/neo4j/_async_compat/network/_util.py b/src/neo4j/_async_compat/network/_util.py index 54206c253..6b0361321 100644 --- a/src/neo4j/_async_compat/network/_util.py +++ b/src/neo4j/_async_compat/network/_util.py @@ -14,6 +14,8 @@ # limitations under the License. +from __future__ import annotations + import asyncio import contextlib import logging @@ -88,14 +90,17 @@ async def _dns_resolver(address, family=0): type=socket.SOCK_STREAM, ) except OSError as e: - if e.errno in _RETRYABLE_DNS_ERRNOS or ( - e.errno in _EAI_NONAME - and (address.host is not None or address.port is not None) + # note: on some systems like Windows, EAI_NONAME and EAI_NODATA + # have the same error-code. + if e.errno in _EAI_NONAME and ( + address.host is None and address.port is None ): - raise ServiceUnavailable( - f"Failed to DNS resolve address {address}: {e}" - ) from e - raise ValueError( + err_cls = ValueError + elif e.errno in _RETRYABLE_DNS_ERRNOS or e.errno in _EAI_NONAME: + err_cls = ServiceUnavailable + else: + err_cls = ValueError + raise err_cls( f"Failed to DNS resolve address {address}: {e}" ) from e return list(_resolved_addresses_from_info(info, address._host_name)) @@ -179,14 +184,17 @@ def _dns_resolver(address, family=0): type=socket.SOCK_STREAM, ) except OSError as e: - if e.errno in _RETRYABLE_DNS_ERRNOS or ( - e.errno in _EAI_NONAME - and (address.host is not None or address.port is not None) + # note: on some systems like Windows, EAI_NONAME and EAI_NODATA + # have the same error-code. + if e.errno in _EAI_NONAME and ( + address.host is None and address.port is None ): - raise ServiceUnavailable( - f"Failed to DNS resolve address {address}: {e}" - ) from e - raise ValueError( + err_cls = ValueError + elif e.errno in _RETRYABLE_DNS_ERRNOS or e.errno in _EAI_NONAME: + err_cls = ServiceUnavailable + else: + err_cls = ValueError + raise err_cls( f"Failed to DNS resolve address {address}: {e}" ) from e return _resolved_addresses_from_info(info, address._host_name) diff --git a/tests/unit/async_/io/test_neo4j_pool.py b/tests/unit/async_/io/test_neo4j_pool.py index d74ab7684..7b2a73df7 100644 --- a/tests/unit/async_/io/test_neo4j_pool.py +++ b/tests/unit/async_/io/test_neo4j_pool.py @@ -17,6 +17,7 @@ import contextlib import inspect import sys +import time from copy import deepcopy import pytest @@ -32,6 +33,7 @@ AsyncBolt, AsyncNeo4jPool, ) +from neo4j._async_compat import async_sleep from neo4j._async_compat.util import AsyncUtil from neo4j._conf import ( RoutingConfig, @@ -49,6 +51,8 @@ from ...._async_compat import mark_async_test +MONOTONIC_TIME_RESOLUTION = time.get_clock_info("monotonic").resolution + ROUTER1_ADDRESS = ResolvedAddress(("1.2.3.1", 9000), host_name="host") ROUTER2_ADDRESS = ResolvedAddress(("1.2.3.1", 9001), host_name="host") ROUTER3_ADDRESS = ResolvedAddress(("1.2.3.1", 9002), host_name="host") @@ -197,6 +201,8 @@ async def test_acquires_new_routing_table_if_stale( old_value = pool.routing_tables[db.name].last_updated_time pool.routing_tables[db.name].ttl = 0 + await async_sleep(MONOTONIC_TIME_RESOLUTION * 2) + cx = await pool.acquire(READ_ACCESS, 30, db, None, None, None) await pool.release(cx) assert pool.routing_tables[db.name].last_updated_time > old_value @@ -218,6 +224,8 @@ async def test_removes_old_routing_table(opener): db2_rt = pool.routing_tables[TEST_DB2.name] db2_rt.ttl = -RoutingConfig.routing_table_purge_delay + await async_sleep(MONOTONIC_TIME_RESOLUTION * 2) + cx = await pool.acquire(READ_ACCESS, 30, TEST_DB1, None, None, None) await pool.release(cx) assert pool.routing_tables[TEST_DB1.name].last_updated_time > old_value diff --git a/tests/unit/async_/test_addressing.py b/tests/unit/async_/test_addressing.py index b65a855ff..c66fda3f7 100644 --- a/tests/unit/async_/test_addressing.py +++ b/tests/unit/async_/test_addressing.py @@ -56,6 +56,7 @@ async def test_address_resolve_with_custom_resolver_none() -> None: [ (Address(("example.invalid", "7687")), ServiceUnavailable), (Address(("example.invalid", 7687)), ServiceUnavailable), + (Address(("example.invalid", None)), ServiceUnavailable), (Address(("127.0.0.1", "abcd")), ValueError), (Address((None, None)), ValueError), (Address((1234, "7687")), TypeError), @@ -65,14 +66,27 @@ async def test_address_resolve_with_custom_resolver_none() -> None: async def test_address_resolve_with_unresolvable_address( test_input, expected ) -> None: - # import contextlib - # with contextlib.suppress(Exception): with pytest.raises(expected): await AsyncUtil.list( AsyncNetworkUtil.resolve_address(test_input, resolver=None) ) +@pytest.mark.parametrize( + "test_input", + [ + Address((None, 7687)), + Address(("example.com", None)), + ], +) +@mark_async_test +async def test_address_resolves_with_none(test_input) -> None: + resolved = await AsyncUtil.list( + AsyncNetworkUtil.resolve_address(test_input, resolver=None) + ) + assert resolved + + @mark_async_test @pytest.mark.parametrize("resolver_type", ("sync", "async")) async def test_address_resolve_with_custom_resolver(resolver_type) -> None: diff --git a/tests/unit/common/vector/test_vector.py b/tests/unit/common/vector/test_vector.py index 496710338..7844517d8 100644 --- a/tests/unit/common/vector/test_vector.py +++ b/tests/unit/common/vector/test_vector.py @@ -275,37 +275,114 @@ def test_swap_endian_unhandled_size(mocker, ext, type_size): @pytest.mark.parametrize( ("dtype", "data"), ( - ("i8", b""), - ("i8", b"\x01"), - ("i8", b"\x01\x02\x03\x04"), - ("i8", _max_value_be_bytes(1, 4096)), - ("i16", b""), - ("i16", b"\x00\x01"), - ("i16", b"\x00\x01\x00\x02"), - ("i16", _max_value_be_bytes(2, 4096)), - ("i32", b""), - ("i32", b"\x00\x00\x00\x01"), - ("i32", b"\x00\x00\x00\x01\x00\x00\x00\x02"), - ("i32", _max_value_be_bytes(4, 4096)), - ("i64", b""), - ("i64", b"\x00\x00\x00\x00\x00\x00\x00\x01"), - ( + pytest.param( + "i8", + b"", + id="i8-empty", + ), + pytest.param( + "i8", + b"\x01", + id="i8-single", + ), + pytest.param( + "i8", + b"\x01\x02\x03\x04", + id="i8-some", + ), + pytest.param( + "i8", + _max_value_be_bytes(1, 4096), + id="i8-limit", + ), + pytest.param( + "i16", + b"", + id="i16-empty", + ), + pytest.param( + "i16", + b"\x00\x01", + id="i16-single", + ), + pytest.param( + "i16", + b"\x00\x01\x00\x02", + id="i16-some", + ), + pytest.param( + "i16", + _max_value_be_bytes(2, 4096), + id="i16-limit", + ), + pytest.param( + "i32", + b"", + id="i32-empty", + ), + pytest.param( + "i32", + b"\x00\x00\x00\x01", + id="i32-single", + ), + pytest.param( + "i32", + b"\x00\x00\x00\x01\x00\x00\x00\x02", + id="i32-some", + ), + pytest.param( + "i32", + _max_value_be_bytes(4, 4096), + id="i32-limit", + ), + pytest.param( + "i64", + b"", + id="i64-empty", + ), + pytest.param( + "i64", + b"\x00\x00\x00\x00\x00\x00\x00\x01", + id="i64-single", + ), + pytest.param( "i64", ( b"\x00\x00\x00\x00\x00\x00\x00\x01" b"\x00\x00\x00\x00\x00\x00\x00\x02" ), + id="i64-some", + ), + pytest.param( + "i64", + _max_value_be_bytes(8, 4096), + id="i64-limit", + ), + pytest.param( + "f32", + b"", + id="f32-empty", + ), + pytest.param( + "f32", + _random_value_be_bytes(4, 4096), + id="f32-limit", + ), + pytest.param( + "f64", + b"", + id="f64-empty", + ), + pytest.param( + "f64", + _random_value_be_bytes(8, 4096), + id="f64-limit", ), - ("i64", _max_value_be_bytes(8, 4096)), - ("f32", b""), - ("f32", _random_value_be_bytes(4, 4096)), - ("f64", b""), - ("f64", _random_value_be_bytes(8, 4096)), ), ) @pytest.mark.parametrize("input_endian", (None, *ENDIAN_LITERALS)) @pytest.mark.parametrize("as_bytearray", (False, True)) -def test_raw_data( +def test_raw_data_limits( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], data: bytes, input_endian: T_ENDIAN_LITERAL | None, diff --git a/tests/unit/sync/io/test_neo4j_pool.py b/tests/unit/sync/io/test_neo4j_pool.py index d2e455676..b84d9bb2a 100644 --- a/tests/unit/sync/io/test_neo4j_pool.py +++ b/tests/unit/sync/io/test_neo4j_pool.py @@ -17,6 +17,7 @@ import contextlib import inspect import sys +import time from copy import deepcopy import pytest @@ -26,6 +27,7 @@ WRITE_ACCESS, ) from neo4j._addressing import ResolvedAddress +from neo4j._async_compat import sleep from neo4j._async_compat.util import Util from neo4j._conf import ( RoutingConfig, @@ -49,6 +51,8 @@ from ...._async_compat import mark_sync_test +MONOTONIC_TIME_RESOLUTION = time.get_clock_info("monotonic").resolution + ROUTER1_ADDRESS = ResolvedAddress(("1.2.3.1", 9000), host_name="host") ROUTER2_ADDRESS = ResolvedAddress(("1.2.3.1", 9001), host_name="host") ROUTER3_ADDRESS = ResolvedAddress(("1.2.3.1", 9002), host_name="host") @@ -197,6 +201,8 @@ def test_acquires_new_routing_table_if_stale( old_value = pool.routing_tables[db.name].last_updated_time pool.routing_tables[db.name].ttl = 0 + sleep(MONOTONIC_TIME_RESOLUTION * 2) + cx = pool.acquire(READ_ACCESS, 30, db, None, None, None) pool.release(cx) assert pool.routing_tables[db.name].last_updated_time > old_value @@ -218,6 +224,8 @@ def test_removes_old_routing_table(opener): db2_rt = pool.routing_tables[TEST_DB2.name] db2_rt.ttl = -RoutingConfig.routing_table_purge_delay + sleep(MONOTONIC_TIME_RESOLUTION * 2) + cx = pool.acquire(READ_ACCESS, 30, TEST_DB1, None, None, None) pool.release(cx) assert pool.routing_tables[TEST_DB1.name].last_updated_time > old_value diff --git a/tests/unit/sync/test_addressing.py b/tests/unit/sync/test_addressing.py index 0b3e497c5..0101fd2a0 100644 --- a/tests/unit/sync/test_addressing.py +++ b/tests/unit/sync/test_addressing.py @@ -56,6 +56,7 @@ def test_address_resolve_with_custom_resolver_none() -> None: [ (Address(("example.invalid", "7687")), ServiceUnavailable), (Address(("example.invalid", 7687)), ServiceUnavailable), + (Address(("example.invalid", None)), ServiceUnavailable), (Address(("127.0.0.1", "abcd")), ValueError), (Address((None, None)), ValueError), (Address((1234, "7687")), TypeError), @@ -65,14 +66,27 @@ def test_address_resolve_with_custom_resolver_none() -> None: def test_address_resolve_with_unresolvable_address( test_input, expected ) -> None: - # import contextlib - # with contextlib.suppress(Exception): with pytest.raises(expected): Util.list( NetworkUtil.resolve_address(test_input, resolver=None) ) +@pytest.mark.parametrize( + "test_input", + [ + Address((None, 7687)), + Address(("example.com", None)), + ], +) +@mark_sync_test +def test_address_resolves_with_none(test_input) -> None: + resolved = Util.list( + NetworkUtil.resolve_address(test_input, resolver=None) + ) + assert resolved + + @mark_sync_test @pytest.mark.parametrize("resolver_type", ("sync", "async")) def test_address_resolve_with_custom_resolver(resolver_type) -> None: