Skip to content

Commit ecb2908

Browse files
committed
Add GitHub actions: windows unit tests
* Adding GitHub Actions CI to run unit tests under Windows * Work around pytest on windows not liking long test ids * Fix old numpy and pandas versions treating `int` as `int32` on windows.
1 parent 5e234e8 commit ecb2908

File tree

8 files changed

+152
-13
lines changed

8 files changed

+152
-13
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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- '5.0'
7+
pull_request:
8+
branches:
9+
- '5.0'
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.7'
19+
tox-factor: 'py37'
20+
- semver: '3.8'
21+
tox-factor: 'py38'
22+
- semver: '3.9'
23+
tox-factor: 'py39'
24+
- semver: '3.10'
25+
tox-factor: 'py310'
26+
- semver: '3.11'
27+
tox-factor: 'py311'
28+
- semver: '3.12'
29+
tox-factor: 'py312'
30+
- semver: '3.13'
31+
tox-factor: 'py313'
32+
steps:
33+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
34+
35+
- name: Set up Python ${{ matrix.python-version.semver }}
36+
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
37+
with:
38+
python-version: ${{ matrix.python-version.semver }}
39+
cache: 'pip'
40+
- name: Install tox
41+
run: python -m pip install -U tox>=4.8.0
42+
- name: Run unit tests
43+
run: python -m tox -vv -f unit ${{ matrix.python-version.tox-factor }}
44+
45+
gha-conclusion:
46+
name: gha-conclusion
47+
needs: win-unit-tests
48+
runs-on: ubuntu-latest
49+
steps:
50+
- name: Signal failure
51+
if: ${{ cancelled() || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }}
52+
run: |
53+
echo "Some workflows have failed!"
54+
exit 1
55+
- name: Signal success
56+
if: ${{ !cancelled() && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
57+
run: echo "All done!"

src/neo4j/_codec/hydration/v1/temporal.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -216,17 +216,18 @@ def dehydrate_np_datetime(value):
216216
"""
217217
if np.isnat(value):
218218
return None
219-
year = value.astype("datetime64[Y]").astype(int) + 1970
219+
year = value.astype("datetime64[Y]").astype(np.int64) + 1970
220220
if not 0 < year <= 9999:
221221
# while we could encode years outside the range, they would fail
222222
# when retrieved from the database.
223223
raise ValueError(
224224
f"Year out of range ({MIN_YEAR:d}..{MAX_YEAR:d}) "
225225
f"found {year}"
226226
)
227-
seconds = value.astype(np.dtype("datetime64[s]")).astype(int)
227+
seconds = value.astype(np.dtype("datetime64[s]")).astype(np.int64)
228228
nanoseconds = (
229-
value.astype(np.dtype("datetime64[ns]")).astype(int) % NANO_SECONDS
229+
value.astype(np.dtype("datetime64[ns]")).astype(np.int64)
230+
% NANO_SECONDS
230231
)
231232
return Structure(b"d", seconds, nanoseconds)
232233

@@ -299,6 +300,7 @@ def dehydrate_timedelta(value):
299300

300301

301302
if np is not None:
303+
_NUMPY_DURATION_NS_FALLBACK = object()
302304
_NUMPY_DURATION_UNITS = {
303305
"Y": "years",
304306
"M": "months",
@@ -310,6 +312,9 @@ def dehydrate_timedelta(value):
310312
"ms": "milliseconds",
311313
"us": "microseconds",
312314
"ns": "nanoseconds",
315+
"ps": _NUMPY_DURATION_NS_FALLBACK,
316+
"fs": _NUMPY_DURATION_NS_FALLBACK,
317+
"as": _NUMPY_DURATION_NS_FALLBACK,
313318
}
314319

315320
def dehydrate_np_timedelta(value):
@@ -323,14 +328,17 @@ def dehydrate_np_timedelta(value):
323328
if np.isnat(value):
324329
return None
325330
unit, step_size = np.datetime_data(value)
326-
numer = int(value.astype(int))
327-
# raise RuntimeError((type(numer), type(step_size)))
328-
kwarg = _NUMPY_DURATION_UNITS.get(unit)
329-
if kwarg is not None:
330-
return dehydrate_duration(Duration(**{kwarg: numer * step_size}))
331-
return dehydrate_duration(
332-
Duration(nanoseconds=value.astype("timedelta64[ns]").astype(int))
333-
)
331+
numer = int(value.astype(np.int64))
332+
try:
333+
kwarg = _NUMPY_DURATION_UNITS[unit]
334+
except KeyError:
335+
raise TypeError(
336+
f"Unsupported numpy.timedelta64 unit: {unit!r}"
337+
) from None
338+
if kwarg is _NUMPY_DURATION_NS_FALLBACK:
339+
nanoseconds = value.astype("timedelta64[ns]").astype(np.int64)
340+
return dehydrate_duration(Duration(nanoseconds=nanoseconds))
341+
return dehydrate_duration(Duration(**{kwarg: numer * step_size}))
334342

335343

336344
if pd is not None:

tests/unit/async_/io/test_neo4j_pool.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616

1717
import inspect
18+
import time
1819

1920
import pytest
2021

@@ -28,6 +29,7 @@
2829
AsyncBolt,
2930
AsyncNeo4jPool,
3031
)
32+
from neo4j._async_compat import async_sleep
3133
from neo4j._async_compat.util import AsyncUtil
3234
from neo4j._conf import (
3335
RoutingConfig,
@@ -45,6 +47,8 @@
4547
from ...._async_compat import mark_async_test
4648

4749

50+
MONOTONIC_TIME_RESOLUTION = time.get_clock_info("monotonic").resolution
51+
4852
ROUTER1_ADDRESS = ResolvedAddress(("1.2.3.1", 9000), host_name="host")
4953
ROUTER2_ADDRESS = ResolvedAddress(("1.2.3.1", 9001), host_name="host")
5054
ROUTER3_ADDRESS = ResolvedAddress(("1.2.3.1", 9002), host_name="host")
@@ -193,6 +197,8 @@ async def test_acquires_new_routing_table_if_stale(
193197
old_value = pool.routing_tables[db.name].last_updated_time
194198
pool.routing_tables[db.name].ttl = 0
195199

200+
await async_sleep(MONOTONIC_TIME_RESOLUTION * 2)
201+
196202
cx = await pool.acquire(READ_ACCESS, 30, db, None, None, None)
197203
await pool.release(cx)
198204
assert pool.routing_tables[db.name].last_updated_time > old_value
@@ -214,6 +220,8 @@ async def test_removes_old_routing_table(opener):
214220
db2_rt = pool.routing_tables[TEST_DB2.name]
215221
db2_rt.ttl = -RoutingConfig.routing_table_purge_delay
216222

223+
await async_sleep(MONOTONIC_TIME_RESOLUTION * 2)
224+
217225
cx = await pool.acquire(READ_ACCESS, 30, TEST_DB1, None, None, None)
218226
await pool.release(cx)
219227
assert pool.routing_tables[TEST_DB1.name].last_updated_time > old_value

tests/unit/async_/test_addressing.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,12 @@ async def test_address_resolve_with_custom_resolver_none() -> None:
5353
@pytest.mark.parametrize(
5454
("test_input", "expected"),
5555
[
56+
(Address(("example.invalid", "7687")), ValueError),
57+
(Address(("example.invalid", 7687)), ValueError),
58+
(Address(("example.invalid", None)), ValueError),
5659
(Address(("127.0.0.1", "abcd")), ValueError),
5760
(Address((None, None)), ValueError),
61+
(Address((1234, "7687")), TypeError),
5862
],
5963
)
6064
@mark_async_test
@@ -67,6 +71,21 @@ async def test_address_resolve_with_unresolvable_address(
6771
)
6872

6973

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

tests/unit/common/codec/packstream/v1/test_packstream.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616

1717
import struct
18+
import sys
1819
from io import BytesIO
1920
from math import (
2021
isnan,
@@ -37,6 +38,11 @@
3738

3839
standard_ascii = [chr(i) for i in range(128)]
3940
not_ascii = "♥O◘♦♥O◘♦"
41+
SKIP_PANDAS_INT_AS_INT64 = (
42+
pd.Series([], dtype=int).dtype.itemsize < 8
43+
and sys.version_info < (3, 8)
44+
and sys.platform == "win32"
45+
)
4046

4147

4248
@pytest.fixture
@@ -289,7 +295,13 @@ def test_positive_int64(self, int_type, assert_packable):
289295
@pytest.mark.parametrize(
290296
"dtype",
291297
(
292-
int,
298+
pytest.param(
299+
int,
300+
marks=pytest.mark.skipif(
301+
SKIP_PANDAS_INT_AS_INT64,
302+
reason="Legacy pandas treating int as int32",
303+
),
304+
),
293305
pd.Int64Dtype(),
294306
pd.UInt64Dtype(),
295307
np.int64,
@@ -317,7 +329,13 @@ def test_negative_int64(self, int_type, assert_packable):
317329
@pytest.mark.parametrize(
318330
"dtype",
319331
(
320-
int,
332+
pytest.param(
333+
int,
334+
marks=pytest.mark.skipif(
335+
SKIP_PANDAS_INT_AS_INT64,
336+
reason="Legacy pandas treating int as int32",
337+
),
338+
),
321339
pd.Int64Dtype(),
322340
np.int64,
323341
np.longlong,

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.

tests/unit/sync/test_addressing.py

Lines changed: 19 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)