Skip to content

Commit b2187f3

Browse files
authored
7th round of migrating integration tests to TestKit (#685)
* Migrate temporal and spatial ITs to TestKit * Introduce RegExps for skipping TestKit tests * Add TestKit protocol messages for subtests PR in TestKit repo: neo4j-drivers/testkit#435
1 parent 0cca381 commit b2187f3

28 files changed

+783
-682
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@
8787
does not offer the `commit`, `rollback`, `close`, and `closed` methods.
8888
Those methods would have caused a hard to interpreted error previously. Hence,
8989
they have been removed.
90+
- Deprecated Nodes' and Relationships' `id` property (`int`) in favor of
91+
`element_id` (`str`).
92+
This also affects `Graph` objects as `graph.nodes[...]` and
93+
`graph.relationships[...]` now prefers strings over integers.
94+
- `ServerInfo.connection_id` has been deprecated and will be removed in a
95+
future release. There is no replacement as this is considered internal
96+
information.
9097

9198

9299
## Version 4.4

neo4j/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ def agent(self):
308308
return self._metadata.get("server")
309309

310310
@property
311+
@deprecated("The connection id is considered internal information "
312+
"and will no longer be exposed in future versions.")
311313
def connection_id(self):
312314
""" Unique identifier for the remote server connection.
313315
"""

neo4j/graph/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131

3232
from collections.abc import Mapping
3333

34-
from ..meta import deprecated
34+
from ..meta import (
35+
deprecated,
36+
deprecation_warn,
37+
)
3538

3639

3740
class Graph:
@@ -261,6 +264,20 @@ def __init__(self, entity_dict):
261264
self._entity_dict = entity_dict
262265

263266
def __getitem__(self, e_id):
267+
# TODO: 6.0 - remove this compatibility shim
268+
if isinstance(e_id, (int, float, complex)):
269+
deprecation_warn(
270+
"Accessing entities by an integer id is deprecated, "
271+
"use the new style element_id (str) instead"
272+
)
273+
if isinstance(e_id, float) and int(e_id) == e_id:
274+
# Non-int floats would always fail for legacy IDs
275+
e_id = int(e_id)
276+
elif isinstance(e_id, complex) and int(e_id.real) == e_id:
277+
# complex numbers with imaginary parts or non-integer real
278+
# parts would always fail for legacy IDs
279+
e_id = int(e_id.real)
280+
e_id = str(e_id)
264281
return self._entity_dict[e_id]
265282

266283
def __len__(self):

neo4j/time/hydration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def dehydrate_datetime(value):
136136
""" Dehydrator for `datetime` values.
137137
138138
:param value:
139-
:type value: datetime
139+
:type value: datetime or DateTime
140140
:return:
141141
"""
142142

testkit/build.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,22 @@
1919

2020

2121
"""
22-
Executed in Go driver container.
22+
Executed in driver container.
2323
Responsible for building driver and test backend.
2424
"""
2525

2626

2727
import subprocess
28+
import sys
2829

2930

3031
def run(args, env=None):
31-
subprocess.run(args, universal_newlines=True, stderr=subprocess.STDOUT,
32-
check=True, env=env)
32+
subprocess.run(args, universal_newlines=True, stdout=sys.stdout,
33+
stderr=sys.stderr, check=True, env=env)
3334

3435

3536
if __name__ == "__main__":
3637
run(["python", "setup.py", "build"])
38+
run(["python", "-m", "pip", "install", "-U", "pip"])
39+
run(["python", "-m", "pip", "install", "-Ur",
40+
"testkitbackend/requirements.txt"])

testkitbackend/_async/requests.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818

1919
import json
2020
from os import path
21+
import re
22+
import warnings
2123

2224
import neo4j
2325
from neo4j._async_compat.util import AsyncUtil
2426

2527
from .. import (
2628
fromtestkit,
29+
test_subtest_skips,
2730
totestkit,
2831
)
2932
from ..exceptions import MarkdAsDriverException
@@ -48,11 +51,38 @@ def load_config():
4851
SKIPPED_TESTS, FEATURES = load_config()
4952

5053

54+
def _get_skip_reason(test_name):
55+
for skip_pattern, reason in SKIPPED_TESTS.items():
56+
if skip_pattern[0] == skip_pattern[-1] == "'":
57+
match = skip_pattern[1:-1] == test_name
58+
else:
59+
match = re.match(skip_pattern, test_name)
60+
if match:
61+
return reason
62+
63+
5164
async def StartTest(backend, data):
52-
if data["testName"] in SKIPPED_TESTS:
53-
await backend.send_response("SkipTest", {
54-
"reason": SKIPPED_TESTS[data["testName"]]
55-
})
65+
test_name = data["testName"]
66+
reason = _get_skip_reason(test_name)
67+
if reason is not None:
68+
if reason.startswith("test_subtest_skips."):
69+
await backend.send_response("RunSubTests", {})
70+
else:
71+
await backend.send_response("SkipTest", {"reason": reason})
72+
else:
73+
await backend.send_response("RunTest", {})
74+
75+
76+
async def StartSubTest(backend, data):
77+
test_name = data["testName"]
78+
subtest_args = data["subtestArguments"]
79+
subtest_args.mark_all_as_read(recursive=True)
80+
reason = _get_skip_reason(test_name)
81+
assert reason and reason.startswith("test_subtest_skips.") or print(reason)
82+
func = getattr(test_subtest_skips, reason[19:])
83+
reason = func(**subtest_args)
84+
if reason is not None:
85+
await backend.send_response("SkipTest", {"reason": reason})
5686
else:
5787
await backend.send_response("RunTest", {})
5888

@@ -412,6 +442,17 @@ async def ResultSingle(backend, data):
412442
))
413443

414444

445+
async def ResultSingleOptional(backend, data):
446+
result = backend.results[data["resultId"]]
447+
with warnings.catch_warnings(record=True) as warning_list:
448+
record = await result.single(strict=False)
449+
if record:
450+
record = totestkit.record(record)
451+
await backend.send_response("RecordOptional", {
452+
"record": record, "warnings": list(map(str, warning_list))
453+
})
454+
455+
415456
async def ResultPeek(backend, data):
416457
result = backend.results[data["resultId"]]
417458
record = await result.peek()

testkitbackend/_sync/requests.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818

1919
import json
2020
from os import path
21+
import re
22+
import warnings
2123

2224
import neo4j
2325
from neo4j._async_compat.util import Util
2426

2527
from .. import (
2628
fromtestkit,
29+
test_subtest_skips,
2730
totestkit,
2831
)
2932
from ..exceptions import MarkdAsDriverException
@@ -48,11 +51,38 @@ def load_config():
4851
SKIPPED_TESTS, FEATURES = load_config()
4952

5053

54+
def _get_skip_reason(test_name):
55+
for skip_pattern, reason in SKIPPED_TESTS.items():
56+
if skip_pattern[0] == skip_pattern[-1] == "'":
57+
match = skip_pattern[1:-1] == test_name
58+
else:
59+
match = re.match(skip_pattern, test_name)
60+
if match:
61+
return reason
62+
63+
5164
def StartTest(backend, data):
52-
if data["testName"] in SKIPPED_TESTS:
53-
backend.send_response("SkipTest", {
54-
"reason": SKIPPED_TESTS[data["testName"]]
55-
})
65+
test_name = data["testName"]
66+
reason = _get_skip_reason(test_name)
67+
if reason is not None:
68+
if reason.startswith("test_subtest_skips."):
69+
backend.send_response("RunSubTests", {})
70+
else:
71+
backend.send_response("SkipTest", {"reason": reason})
72+
else:
73+
backend.send_response("RunTest", {})
74+
75+
76+
def StartSubTest(backend, data):
77+
test_name = data["testName"]
78+
subtest_args = data["subtestArguments"]
79+
subtest_args.mark_all_as_read(recursive=True)
80+
reason = _get_skip_reason(test_name)
81+
assert reason and reason.startswith("test_subtest_skips.") or print(reason)
82+
func = getattr(test_subtest_skips, reason[19:])
83+
reason = func(**subtest_args)
84+
if reason is not None:
85+
backend.send_response("SkipTest", {"reason": reason})
5686
else:
5787
backend.send_response("RunTest", {})
5888

@@ -412,6 +442,17 @@ def ResultSingle(backend, data):
412442
))
413443

414444

445+
def ResultSingleOptional(backend, data):
446+
result = backend.results[data["resultId"]]
447+
with warnings.catch_warnings(record=True) as warning_list:
448+
record = result.single(strict=False)
449+
if record:
450+
record = totestkit.record(record)
451+
backend.send_response("RecordOptional", {
452+
"record": record, "warnings": list(map(str, warning_list))
453+
})
454+
455+
415456
def ResultPeek(backend, data):
416457
result = backend.results[data["resultId"]]
417458
record = result.peek()

testkitbackend/fromtestkit.py

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,21 @@
1616
# limitations under the License.
1717

1818

19+
from datetime import timedelta
20+
21+
import pytz
22+
1923
from neo4j import Query
24+
from neo4j.spatial import (
25+
CartesianPoint,
26+
WGS84Point,
27+
)
28+
from neo4j.time import (
29+
Date,
30+
DateTime,
31+
Duration,
32+
Time,
33+
)
2034

2135

2236
def to_cypher_and_params(data):
@@ -54,24 +68,81 @@ def to_query_and_params(data):
5468
def to_param(m):
5569
""" Converts testkit parameter format to driver (python) parameter
5670
"""
57-
value = m["data"]["value"]
71+
data = m["data"]
5872
name = m["name"]
5973
if name == "CypherNull":
74+
if data["value"] is not None:
75+
raise ValueError("CypherNull should be None")
6076
return None
6177
if name == "CypherString":
62-
return str(value)
78+
return str(data["value"])
6379
if name == "CypherBool":
64-
return bool(value)
80+
return bool(data["value"])
6581
if name == "CypherInt":
66-
return int(value)
82+
return int(data["value"])
6783
if name == "CypherFloat":
68-
return float(value)
84+
return float(data["value"])
6985
if name == "CypherString":
70-
return str(value)
86+
return str(data["value"])
7187
if name == "CypherBytes":
72-
return bytearray([int(byte, 16) for byte in value.split()])
88+
return bytearray([int(byte, 16) for byte in data["value"].split()])
7389
if name == "CypherList":
74-
return [to_param(v) for v in value]
90+
return [to_param(v) for v in data["value"]]
7591
if name == "CypherMap":
76-
return {k: to_param(value[k]) for k in value}
77-
raise Exception("Unknown param type " + name)
92+
return {k: to_param(data["value"][k]) for k in data["value"]}
93+
if name == "CypherPoint":
94+
coords = [data["x"], data["y"]]
95+
if data.get("z") is not None:
96+
coords.append(data["z"])
97+
if data["system"] == "cartesian":
98+
return CartesianPoint(coords)
99+
if data["system"] == "wgs84":
100+
return WGS84Point(coords)
101+
raise ValueError("Unknown point system: {}".format(data["system"]))
102+
if name == "CypherDate":
103+
return Date(data["year"], data["month"], data["day"])
104+
if name == "CypherTime":
105+
tz = None
106+
utc_offset_s = data.get("utc_offset_s")
107+
if utc_offset_s is not None:
108+
utc_offset_m = utc_offset_s // 60
109+
if utc_offset_m * 60 != utc_offset_s:
110+
raise ValueError("the used timezone library only supports "
111+
"UTC offsets by minutes")
112+
tz = pytz.FixedOffset(utc_offset_m)
113+
return Time(data["hour"], data["minute"], data["second"],
114+
data["nanosecond"], tzinfo=tz)
115+
if name == "CypherDateTime":
116+
datetime = DateTime(
117+
data["year"], data["month"], data["day"],
118+
data["hour"], data["minute"], data["second"], data["nanosecond"]
119+
)
120+
utc_offset_s = data["utc_offset_s"]
121+
timezone_id = data["timezone_id"]
122+
if timezone_id is not None:
123+
utc_offset = timedelta(seconds=utc_offset_s)
124+
tz = pytz.timezone(timezone_id)
125+
localized_datetime = tz.localize(datetime, is_dst=False)
126+
if localized_datetime.utcoffset() == utc_offset:
127+
return localized_datetime
128+
localized_datetime = tz.localize(datetime, is_dst=True)
129+
if localized_datetime.utcoffset() == utc_offset:
130+
return localized_datetime
131+
raise ValueError(
132+
"cannot localize datetime %s to timezone %s with UTC "
133+
"offset %s" % (datetime, timezone_id, utc_offset)
134+
)
135+
elif utc_offset_s is not None:
136+
utc_offset_m = utc_offset_s // 60
137+
if utc_offset_m * 60 != utc_offset_s:
138+
raise ValueError("the used timezone library only supports "
139+
"UTC offsets by minutes")
140+
tz = pytz.FixedOffset(utc_offset_m)
141+
return tz.localize(datetime)
142+
return datetime
143+
if name == "CypherDuration":
144+
return Duration(
145+
months=data["months"], days=data["days"],
146+
seconds=data["seconds"], nanoseconds=data["nanoseconds"]
147+
)
148+
raise ValueError("Unknown param type " + name)

testkitbackend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-r ../requirements.txt

0 commit comments

Comments
 (0)