Skip to content

Commit 6ea4c12

Browse files
authored
Add GitHub actions: windows unit tests (#1254)
* Adding GitHub Actions CI to run unit tests under Windows * Work around pytest on windows not liking long test ids * Fix test flakiness on systems with low monotonic clock resolution 👀️🪟️ * Fix DNS error re-write on Windows On windows only, the driver would re-write the DNS error when resolving `Address(None, None)` to be retryable, which it shouldn't.
1 parent ed1494a commit 6ea4c12

File tree

8 files changed

+222
-39
lines changed

8 files changed

+222
-39
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/.github/CODEOWNERS @neo4j/drivers
2+
/.github/workflows/ @neo4j/drivers

.github/workflows/tests.yaml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- '6.x'
7+
pull_request:
8+
branches:
9+
- '6.x'
10+
11+
jobs:
12+
win-unit-tests:
13+
name: Windows Unit Tests
14+
runs-on: windows-latest
15+
strategy:
16+
matrix:
17+
python-version:
18+
- semver: '3.10'
19+
tox-factor: 'py310'
20+
- semver: '3.11'
21+
tox-factor: 'py311'
22+
- semver: '3.12'
23+
tox-factor: 'py312'
24+
- semver: '3.13'
25+
tox-factor: 'py313'
26+
steps:
27+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
28+
29+
- name: Set up Python ${{ matrix.python-version.semver }}
30+
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
31+
with:
32+
python-version: ${{ matrix.python-version.semver }}
33+
cache: 'pip'
34+
35+
- name: Run install tox
36+
run: python -m pip install -U --group tox
37+
- name: Run unit tests
38+
run: python -m tox -vv -f unit ${{ matrix.python-version.tox-factor }}
39+
40+
gha-conclusion:
41+
name: gha-conclusion
42+
needs: win-unit-tests
43+
runs-on: ubuntu-latest
44+
steps:
45+
- name: Signal failure
46+
if: ${{ cancelled() || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }}
47+
run: |
48+
echo "Some workflows have failed!"
49+
exit 1
50+
- name: Signal success
51+
if: ${{ !cancelled() && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
52+
run: echo "All done!"

src/neo4j/_async_compat/network/_util.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
# limitations under the License.
1515

1616

17+
from __future__ import annotations
18+
1719
import asyncio
1820
import contextlib
1921
import logging
@@ -88,14 +90,17 @@ async def _dns_resolver(address, family=0):
8890
type=socket.SOCK_STREAM,
8991
)
9092
except OSError as e:
91-
if e.errno in _RETRYABLE_DNS_ERRNOS or (
92-
e.errno in _EAI_NONAME
93-
and (address.host is not None or address.port is not None)
93+
# note: on some systems like Windows, EAI_NONAME and EAI_NODATA
94+
# have the same error-code.
95+
if e.errno in _EAI_NONAME and (
96+
address.host is None and address.port is None
9497
):
95-
raise ServiceUnavailable(
96-
f"Failed to DNS resolve address {address}: {e}"
97-
) from e
98-
raise ValueError(
98+
err_cls = ValueError
99+
elif e.errno in _RETRYABLE_DNS_ERRNOS or e.errno in _EAI_NONAME:
100+
err_cls = ServiceUnavailable
101+
else:
102+
err_cls = ValueError
103+
raise err_cls(
99104
f"Failed to DNS resolve address {address}: {e}"
100105
) from e
101106
return list(_resolved_addresses_from_info(info, address._host_name))
@@ -179,14 +184,17 @@ def _dns_resolver(address, family=0):
179184
type=socket.SOCK_STREAM,
180185
)
181186
except OSError as e:
182-
if e.errno in _RETRYABLE_DNS_ERRNOS or (
183-
e.errno in _EAI_NONAME
184-
and (address.host is not None or address.port is not None)
187+
# note: on some systems like Windows, EAI_NONAME and EAI_NODATA
188+
# have the same error-code.
189+
if e.errno in _EAI_NONAME and (
190+
address.host is None and address.port is None
185191
):
186-
raise ServiceUnavailable(
187-
f"Failed to DNS resolve address {address}: {e}"
188-
) from e
189-
raise ValueError(
192+
err_cls = ValueError
193+
elif e.errno in _RETRYABLE_DNS_ERRNOS or e.errno in _EAI_NONAME:
194+
err_cls = ServiceUnavailable
195+
else:
196+
err_cls = ValueError
197+
raise err_cls(
190198
f"Failed to DNS resolve address {address}: {e}"
191199
) from e
192200
return _resolved_addresses_from_info(info, address._host_name)

tests/unit/async_/io/test_neo4j_pool.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import contextlib
1818
import inspect
1919
import sys
20+
import time
2021
from copy import deepcopy
2122

2223
import pytest
@@ -32,6 +33,7 @@
3233
AsyncBolt,
3334
AsyncNeo4jPool,
3435
)
36+
from neo4j._async_compat import async_sleep
3537
from neo4j._async_compat.util import AsyncUtil
3638
from neo4j._conf import (
3739
RoutingConfig,
@@ -49,6 +51,8 @@
4951
from ...._async_compat import mark_async_test
5052

5153

54+
MONOTONIC_TIME_RESOLUTION = time.get_clock_info("monotonic").resolution
55+
5256
ROUTER1_ADDRESS = ResolvedAddress(("1.2.3.1", 9000), host_name="host")
5357
ROUTER2_ADDRESS = ResolvedAddress(("1.2.3.1", 9001), host_name="host")
5458
ROUTER3_ADDRESS = ResolvedAddress(("1.2.3.1", 9002), host_name="host")
@@ -197,6 +201,8 @@ async def test_acquires_new_routing_table_if_stale(
197201
old_value = pool.routing_tables[db.name].last_updated_time
198202
pool.routing_tables[db.name].ttl = 0
199203

204+
await async_sleep(MONOTONIC_TIME_RESOLUTION * 2)
205+
200206
cx = await pool.acquire(READ_ACCESS, 30, db, None, None, None)
201207
await pool.release(cx)
202208
assert pool.routing_tables[db.name].last_updated_time > old_value
@@ -218,6 +224,8 @@ async def test_removes_old_routing_table(opener):
218224
db2_rt = pool.routing_tables[TEST_DB2.name]
219225
db2_rt.ttl = -RoutingConfig.routing_table_purge_delay
220226

227+
await async_sleep(MONOTONIC_TIME_RESOLUTION * 2)
228+
221229
cx = await pool.acquire(READ_ACCESS, 30, TEST_DB1, None, None, None)
222230
await pool.release(cx)
223231
assert pool.routing_tables[TEST_DB1.name].last_updated_time > old_value

tests/unit/async_/test_addressing.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ async def test_address_resolve_with_custom_resolver_none() -> None:
5656
[
5757
(Address(("example.invalid", "7687")), ServiceUnavailable),
5858
(Address(("example.invalid", 7687)), ServiceUnavailable),
59+
(Address(("example.invalid", None)), ServiceUnavailable),
5960
(Address(("127.0.0.1", "abcd")), ValueError),
6061
(Address((None, None)), ValueError),
6162
(Address((1234, "7687")), TypeError),
@@ -65,14 +66,27 @@ async def test_address_resolve_with_custom_resolver_none() -> None:
6566
async def test_address_resolve_with_unresolvable_address(
6667
test_input, expected
6768
) -> None:
68-
# import contextlib
69-
# with contextlib.suppress(Exception):
7069
with pytest.raises(expected):
7170
await AsyncUtil.list(
7271
AsyncNetworkUtil.resolve_address(test_input, resolver=None)
7372
)
7473

7574

75+
@pytest.mark.parametrize(
76+
"test_input",
77+
[
78+
Address((None, 7687)),
79+
Address(("example.com", None)),
80+
],
81+
)
82+
@mark_async_test
83+
async def test_address_resolves_with_none(test_input) -> None:
84+
resolved = await AsyncUtil.list(
85+
AsyncNetworkUtil.resolve_address(test_input, resolver=None)
86+
)
87+
assert resolved
88+
89+
7690
@mark_async_test
7791
@pytest.mark.parametrize("resolver_type", ("sync", "async"))
7892
async def test_address_resolve_with_custom_resolver(resolver_type) -> None:

tests/unit/common/vector/test_vector.py

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -275,37 +275,114 @@ def test_swap_endian_unhandled_size(mocker, ext, type_size):
275275
@pytest.mark.parametrize(
276276
("dtype", "data"),
277277
(
278-
("i8", b""),
279-
("i8", b"\x01"),
280-
("i8", b"\x01\x02\x03\x04"),
281-
("i8", _max_value_be_bytes(1, 4096)),
282-
("i16", b""),
283-
("i16", b"\x00\x01"),
284-
("i16", b"\x00\x01\x00\x02"),
285-
("i16", _max_value_be_bytes(2, 4096)),
286-
("i32", b""),
287-
("i32", b"\x00\x00\x00\x01"),
288-
("i32", b"\x00\x00\x00\x01\x00\x00\x00\x02"),
289-
("i32", _max_value_be_bytes(4, 4096)),
290-
("i64", b""),
291-
("i64", b"\x00\x00\x00\x00\x00\x00\x00\x01"),
292-
(
278+
pytest.param(
279+
"i8",
280+
b"",
281+
id="i8-empty",
282+
),
283+
pytest.param(
284+
"i8",
285+
b"\x01",
286+
id="i8-single",
287+
),
288+
pytest.param(
289+
"i8",
290+
b"\x01\x02\x03\x04",
291+
id="i8-some",
292+
),
293+
pytest.param(
294+
"i8",
295+
_max_value_be_bytes(1, 4096),
296+
id="i8-limit",
297+
),
298+
pytest.param(
299+
"i16",
300+
b"",
301+
id="i16-empty",
302+
),
303+
pytest.param(
304+
"i16",
305+
b"\x00\x01",
306+
id="i16-single",
307+
),
308+
pytest.param(
309+
"i16",
310+
b"\x00\x01\x00\x02",
311+
id="i16-some",
312+
),
313+
pytest.param(
314+
"i16",
315+
_max_value_be_bytes(2, 4096),
316+
id="i16-limit",
317+
),
318+
pytest.param(
319+
"i32",
320+
b"",
321+
id="i32-empty",
322+
),
323+
pytest.param(
324+
"i32",
325+
b"\x00\x00\x00\x01",
326+
id="i32-single",
327+
),
328+
pytest.param(
329+
"i32",
330+
b"\x00\x00\x00\x01\x00\x00\x00\x02",
331+
id="i32-some",
332+
),
333+
pytest.param(
334+
"i32",
335+
_max_value_be_bytes(4, 4096),
336+
id="i32-limit",
337+
),
338+
pytest.param(
339+
"i64",
340+
b"",
341+
id="i64-empty",
342+
),
343+
pytest.param(
344+
"i64",
345+
b"\x00\x00\x00\x00\x00\x00\x00\x01",
346+
id="i64-single",
347+
),
348+
pytest.param(
293349
"i64",
294350
(
295351
b"\x00\x00\x00\x00\x00\x00\x00\x01"
296352
b"\x00\x00\x00\x00\x00\x00\x00\x02"
297353
),
354+
id="i64-some",
355+
),
356+
pytest.param(
357+
"i64",
358+
_max_value_be_bytes(8, 4096),
359+
id="i64-limit",
360+
),
361+
pytest.param(
362+
"f32",
363+
b"",
364+
id="f32-empty",
365+
),
366+
pytest.param(
367+
"f32",
368+
_random_value_be_bytes(4, 4096),
369+
id="f32-limit",
370+
),
371+
pytest.param(
372+
"f64",
373+
b"",
374+
id="f64-empty",
375+
),
376+
pytest.param(
377+
"f64",
378+
_random_value_be_bytes(8, 4096),
379+
id="f64-limit",
298380
),
299-
("i64", _max_value_be_bytes(8, 4096)),
300-
("f32", b""),
301-
("f32", _random_value_be_bytes(4, 4096)),
302-
("f64", b""),
303-
("f64", _random_value_be_bytes(8, 4096)),
304381
),
305382
)
306383
@pytest.mark.parametrize("input_endian", (None, *ENDIAN_LITERALS))
307384
@pytest.mark.parametrize("as_bytearray", (False, True))
308-
def test_raw_data(
385+
def test_raw_data_limits(
309386
dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"],
310387
data: bytes,
311388
input_endian: T_ENDIAN_LITERAL | None,

tests/unit/sync/io/test_neo4j_pool.py

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)