Skip to content

Commit 3319865

Browse files
committed
Enable TestKit tests for temporal types
1 parent 364c001 commit 3319865

File tree

8 files changed

+275
-57
lines changed

8 files changed

+275
-57
lines changed

testkit/build.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
"""
2-
Executed in Go driver container.
2+
Executed in driver container.
33
Responsible for building driver and test backend.
44
"""
5+
6+
57
import subprocess
8+
import sys
69

710

811
def run(args, env=None):
9-
subprocess.run(args, universal_newlines=True, stderr=subprocess.STDOUT,
10-
check=True, env=env)
12+
subprocess.run(args, universal_newlines=True, stdout=sys.stdout,
13+
stderr=sys.stderr, check=True, env=env)
1114

1215

1316
if __name__ == "__main__":
1417
run(["python", "setup.py", "build"])
18+
run(["python", "-m", "pip", "install", "-U", "pip"])
19+
run(["python", "-m", "pip", "install", "-Ur",
20+
"testkitbackend/requirements.txt"])

testkitbackend/_driver_logger.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [http://neo4j.com]
3+
#
4+
# This file is part of Neo4j.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
19+
import io
20+
import logging
21+
import sys
22+
23+
24+
buffer_handler = logging.StreamHandler(io.StringIO())
25+
buffer_handler.setLevel(logging.DEBUG)
26+
27+
handler = logging.StreamHandler(sys.stdout)
28+
handler.setLevel(logging.DEBUG)
29+
logging.getLogger("neo4j").addHandler(handler)
30+
logging.getLogger("neo4j").addHandler(buffer_handler)
31+
logging.getLogger("neo4j").setLevel(logging.DEBUG)
32+
33+
log = logging.getLogger("testkitbackend")
34+
log.addHandler(handler)
35+
log.setLevel(logging.DEBUG)
36+
37+
38+
__all__ = [
39+
"buffer_handler",
40+
"log",
41+
]

testkitbackend/backend.py

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,37 @@
1414
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1515
# See the License for the specific language governing permissions and
1616
# limitations under the License.
17+
18+
19+
import asyncio
20+
import traceback
1721
from inspect import (
1822
getmembers,
1923
isfunction,
2024
)
21-
import io
22-
from json import loads, dumps
23-
import logging
24-
import sys
25-
import traceback
26-
27-
from neo4j._exceptions import (
28-
BoltError
25+
from json import (
26+
dumps,
27+
loads,
2928
)
29+
from pathlib import Path
30+
31+
from neo4j._exceptions import BoltError
3032
from neo4j.exceptions import (
3133
DriverError,
3234
Neo4jError,
3335
UnsupportedServerProduct,
3436
)
3537

36-
import testkitbackend.requests as requests
37-
38-
buffer_handler = logging.StreamHandler(io.StringIO())
39-
buffer_handler.setLevel(logging.DEBUG)
38+
from ._driver_logger import (
39+
buffer_handler,
40+
log,
41+
)
42+
from .exceptions import MarkdAsDriverException
43+
from . import requests
4044

41-
handler = logging.StreamHandler(sys.stdout)
42-
handler.setLevel(logging.DEBUG)
43-
logging.getLogger("neo4j").addHandler(handler)
44-
logging.getLogger("neo4j").addHandler(buffer_handler)
45-
logging.getLogger("neo4j").setLevel(logging.DEBUG)
4645

47-
log = logging.getLogger("testkitbackend")
48-
log.addHandler(handler)
49-
log.setLevel(logging.DEBUG)
46+
TESTKIT_BACKEND_PATH = Path(__file__).absolute().resolve().parent
47+
DRIVER_PATH = TESTKIT_BACKEND_PATH.parent / "neo4j"
5048

5149

5250
class Request(dict):
@@ -134,6 +132,41 @@ def process_request(self):
134132
request = request + line
135133
return False
136134

135+
@staticmethod
136+
def _exc_stems_from_driver(exc):
137+
stack = traceback.extract_tb(exc.__traceback__)
138+
for frame in stack[-1:1:-1]:
139+
p = Path(frame.filename)
140+
if TESTKIT_BACKEND_PATH in p.parents:
141+
return False
142+
if DRIVER_PATH in p.parents:
143+
return True
144+
145+
def write_driver_exc(self, exc):
146+
log.debug(traceback.format_exc())
147+
148+
key = self.next_key()
149+
self.errors[key] = exc
150+
151+
payload = {"id": key, "msg": ""}
152+
153+
if isinstance(exc, MarkdAsDriverException):
154+
wrapped_exc = exc.wrapped_exc
155+
payload["errorType"] = str(type(wrapped_exc))
156+
if wrapped_exc.args:
157+
payload["msg"] = str(wrapped_exc.args[0])
158+
else:
159+
payload["errorType"] = str(type(exc))
160+
if isinstance(exc, Neo4jError) and exc.message is not None:
161+
payload["msg"] = str(exc.message)
162+
elif exc.args:
163+
payload["msg"] = str(exc.args[0])
164+
165+
if isinstance(exc, Neo4jError):
166+
payload["code"] = exc.code
167+
168+
self.send_response("DriverError", payload)
169+
137170
def _process(self, request):
138171
""" Process a received request by retrieving handler that
139172
corresponds to the request name.
@@ -156,34 +189,25 @@ def _process(self, request):
156189
" request: " + ", ".join(unsused_keys)
157190
)
158191
except (Neo4jError, DriverError, UnsupportedServerProduct,
159-
BoltError) as e:
160-
log.debug(traceback.format_exc())
161-
if isinstance(e, Neo4jError):
162-
msg = "" if e.message is None else str(e.message)
163-
else:
164-
msg = str(e.args[0]) if e.args else ""
165-
166-
key = self.next_key()
167-
self.errors[key] = e
168-
payload = {"id": key, "errorType": str(type(e)), "msg": msg}
169-
if isinstance(e, Neo4jError):
170-
payload["code"] = e.code
171-
self.send_response("DriverError", payload)
192+
BoltError, MarkdAsDriverException) as e:
193+
self.write_driver_exc(e)
172194
except requests.FrontendError as e:
173195
self.send_response("FrontendError", {"msg": str(e)})
174-
except Exception:
175-
tb = traceback.format_exc()
176-
log.error(tb)
177-
self.send_response("BackendError", {"msg": tb})
196+
except Exception as e:
197+
if self._exc_stems_from_driver(e):
198+
self.write_driver_exc(e)
199+
else:
200+
tb = traceback.format_exc()
201+
log.error(tb)
202+
self.send_response("BackendError", {"msg": tb})
178203

179204
def send_response(self, name, data):
180205
""" Sends a response to backend.
181206
"""
182-
buffer_handler.acquire()
183-
log_output = buffer_handler.stream.getvalue()
184-
buffer_handler.stream.truncate(0)
185-
buffer_handler.stream.seek(0)
186-
buffer_handler.release()
207+
with buffer_handler.lock:
208+
log_output = buffer_handler.stream.getvalue()
209+
buffer_handler.stream.truncate(0)
210+
buffer_handler.stream.seek(0)
187211
if not log_output.endswith("\n"):
188212
log_output += "\n"
189213
self._wr.write(log_output.encode("utf-8"))
@@ -193,4 +217,7 @@ def send_response(self, name, data):
193217
self._wr.write(b"#response begin\n")
194218
self._wr.write(bytes(response+"\n", "utf-8"))
195219
self._wr.write(b"#response end\n")
196-
self._wr.flush()
220+
if isinstance(self._wr, asyncio.StreamWriter):
221+
self._wr.drain()
222+
else:
223+
self._wr.flush()

testkitbackend/exceptions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [http://neo4j.com]
3+
#
4+
# This file is part of Neo4j.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
19+
class MarkdAsDriverException(Exception):
20+
"""
21+
Wrap any error as DriverException
22+
"""
23+
def __init__(self, wrapped_exc):
24+
super().__init__()
25+
self.wrapped_exc = wrapped_exc

testkitbackend/fromtestkit.py

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,18 @@
1515
# See the License for the specific language governing permissions and
1616
# limitations under the License.
1717

18-
from neo4j.work.simple import Query
18+
19+
from datetime import timedelta
20+
21+
import pytz
22+
23+
from neo4j import Query
24+
from neo4j.time import (
25+
Date,
26+
DateTime,
27+
Duration,
28+
Time,
29+
)
1930

2031

2132
def to_cypher_and_params(data):
@@ -50,24 +61,72 @@ def to_query_and_params(data):
5061
def to_param(m):
5162
""" Converts testkit parameter format to driver (python) parameter
5263
"""
53-
value = m["data"]["value"]
64+
data = m["data"]
5465
name = m["name"]
5566
if name == "CypherNull":
67+
if data["value"] is not None:
68+
raise ValueError("CypherNull should be None")
5669
return None
5770
if name == "CypherString":
58-
return str(value)
71+
return str(data["value"])
5972
if name == "CypherBool":
60-
return bool(value)
73+
return bool(data["value"])
6174
if name == "CypherInt":
62-
return int(value)
75+
return int(data["value"])
6376
if name == "CypherFloat":
64-
return float(value)
77+
return float(data["value"])
6578
if name == "CypherString":
66-
return str(value)
79+
return str(data["value"])
6780
if name == "CypherBytes":
68-
return bytearray([int(byte, 16) for byte in value.split()])
81+
return bytearray([int(byte, 16) for byte in data["value"].split()])
6982
if name == "CypherList":
70-
return [to_param(v) for v in value]
83+
return [to_param(v) for v in data["value"]]
7184
if name == "CypherMap":
72-
return {k: to_param(value[k]) for k in value}
73-
raise Exception("Unknown param type " + name)
85+
return {k: to_param(data["value"][k]) for k in data["value"]}
86+
if name == "CypherDate":
87+
return Date(data["year"], data["month"], data["day"])
88+
if name == "CypherTime":
89+
tz = None
90+
utc_offset_s = data.get("utc_offset_s")
91+
if utc_offset_s is not None:
92+
utc_offset_m = utc_offset_s // 60
93+
if utc_offset_m * 60 != utc_offset_s:
94+
raise ValueError("the used timezone library only supports "
95+
"UTC offsets by minutes")
96+
tz = pytz.FixedOffset(utc_offset_m)
97+
return Time(data["hour"], data["minute"], data["second"],
98+
data["nanosecond"], tzinfo=tz)
99+
if name == "CypherDateTime":
100+
datetime = DateTime(
101+
data["year"], data["month"], data["day"],
102+
data["hour"], data["minute"], data["second"], data["nanosecond"]
103+
)
104+
utc_offset_s = data["utc_offset_s"]
105+
timezone_id = data["timezone_id"]
106+
if timezone_id is not None:
107+
utc_offset = timedelta(seconds=utc_offset_s)
108+
tz = pytz.timezone(timezone_id)
109+
localized_datetime = tz.localize(datetime, is_dst=False)
110+
if localized_datetime.utcoffset() == utc_offset:
111+
return localized_datetime
112+
localized_datetime = tz.localize(datetime, is_dst=True)
113+
if localized_datetime.utcoffset() == utc_offset:
114+
return localized_datetime
115+
raise ValueError(
116+
"cannot localize datetime %s to timezone %s with UTC "
117+
"offset %s" % (datetime, timezone_id, utc_offset)
118+
)
119+
elif utc_offset_s is not None:
120+
utc_offset_m = utc_offset_s // 60
121+
if utc_offset_m * 60 != utc_offset_s:
122+
raise ValueError("the used timezone library only supports "
123+
"UTC offsets by minutes")
124+
tz = pytz.FixedOffset(utc_offset_m)
125+
return tz.localize(datetime)
126+
return datetime
127+
if name == "CypherDuration":
128+
return Duration(
129+
months=data["months"], days=data["days"],
130+
seconds=data["seconds"], nanoseconds=data["nanoseconds"]
131+
)
132+
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

testkitbackend/test_config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"Feature:API:Result.Peek": true,
3333
"Feature:API:Result.Single": "Does not raise error when not exactly one record is available. To be fixed in 5.0.",
3434
"Feature:API:SessionConnectionTimeout": true,
35+
"Feature:API:Type.Temporal": true,
3536
"Feature:API:UpdateRoutingTableTimeout": true,
3637
"Feature:Auth:Bearer": true,
3738
"Feature:Auth:Custom": true,

0 commit comments

Comments
 (0)