Skip to content

Commit e0dd9c2

Browse files
Peter Wilhelmsson2hdddg
authored andcommitted
Initial testkit support
Datatypes tests passing in testkit.
1 parent 9e51578 commit e0dd9c2

File tree

7 files changed

+325
-0
lines changed

7 files changed

+325
-0
lines changed

testkitbackend/__init__.py

Whitespace-only changes.

testkitbackend/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .server import Server
2+
3+
if __name__ == "__main__":
4+
server = Server(("0.0.0.0", 9876))
5+
while True:
6+
server.handle_request()

testkitbackend/backend.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright (c) 2002-2020 "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+
from json import loads, dumps
18+
from inspect import getmembers, isfunction
19+
import testkitbackend.requests as requests
20+
21+
22+
class Backend:
23+
def __init__(self, rd, wr):
24+
self._rd = rd
25+
self._wr = wr
26+
self.drivers = {}
27+
self.sessions = {}
28+
self.results = {}
29+
self.key = 0
30+
# Collect all request handlers
31+
self._requestHandlers = dict(
32+
[m for m in getmembers(requests, isfunction)])
33+
34+
def next_key(self):
35+
self.key = self.key + 1
36+
return self.key
37+
38+
def process_request(self):
39+
""" Reads next request from the stream and processes it.
40+
"""
41+
in_request = False
42+
request = ""
43+
for line in self._rd:
44+
# Remove trailing newline
45+
line = line.decode('UTF-8').rstrip()
46+
if line == "#request begin":
47+
in_request = True
48+
elif line == "#request end":
49+
self._process(request)
50+
return True
51+
else:
52+
if in_request:
53+
request = request + line
54+
return False
55+
56+
def _process(self, request):
57+
""" Process a received request by retrieving handler that
58+
corresponds to the request name.
59+
"""
60+
request = loads(request)
61+
if not isinstance(request, dict):
62+
raise Exception("Request is not an object")
63+
name = request.get('name', 'invalid')
64+
handler = self._requestHandlers.get(name)
65+
if not handler:
66+
raise Exception("No request handler for " + name)
67+
data = request["data"]
68+
handler(self, data)
69+
70+
def send_response(self, name, data):
71+
""" Sends a response to backend.
72+
"""
73+
response = {"name": name, "data": data}
74+
response = dumps(response)
75+
self._wr.write(b"#response begin\n")
76+
self._wr.write(bytes(response+"\n", "utf-8"))
77+
self._wr.write(b"#response end\n")
78+
self._wr.flush()

testkitbackend/fromtestkit.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) 2002-2020 "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+
def toCypherAndParams(data):
19+
cypher = data["cypher"]
20+
params = data["params"]
21+
# Optional
22+
if params is None:
23+
return cypher, None
24+
# Transform the params to Python native
25+
for p in params:
26+
params[p] = toParam(params[p])
27+
return cypher, params
28+
29+
30+
def toParam(m):
31+
""" Converts testkit parameter format to driver (python) parameter
32+
"""
33+
data = m["data"]
34+
name = m["name"]
35+
if name == "CypherString":
36+
return str(data["value"])
37+
if name == "CypherBool":
38+
return bool(data["value"])
39+
if name == "CypherInt":
40+
return int(data["value"])
41+
if name == "CypherFloat":
42+
return float(data["value"])
43+
if name == "CypherString":
44+
return str(data["value"])
45+
if name == "CypherNull":
46+
return None
47+
if name == "CypherList":
48+
ls = []
49+
for x in data["value"]:
50+
ls.append(toParam(x))
51+
return ls
52+
if name == "CypherMap":
53+
mp = {}
54+
for k, v in data["value"].items():
55+
mp[k] = toParam(v)
56+
return mp
57+
raise Exception("Unknown param type " + name)

testkitbackend/requests.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Copyright (c) 2002-2020 "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+
import neo4j
18+
import testkitbackend.fromtestkit as fromtestkit
19+
import testkitbackend.totestkit as totestkit
20+
21+
22+
def NewDriver(backend, data):
23+
authToken = data["authorizationToken"]["data"]
24+
auth = neo4j.Auth(
25+
authToken["scheme"], authToken["principal"],
26+
authToken["credentials"], realm=authToken["realm"])
27+
driver = neo4j.GraphDatabase.driver(data["uri"], auth=auth)
28+
key = backend.next_key()
29+
backend.drivers[key] = driver
30+
backend.send_response("Driver", {"id": key})
31+
32+
33+
def DriverClose(backend, data):
34+
key = data["driverId"]
35+
driver = backend.drivers[key]
36+
driver.close()
37+
backend.send_response("Driver", {"id": key})
38+
39+
40+
class SessionState:
41+
""" Keeps some extra state about the tracked session
42+
"""
43+
44+
def __init__(self, session):
45+
self.session = session
46+
47+
48+
def NewSession(backend, data):
49+
driver = backend.drivers[data["driverId"]]
50+
access_mode = data["accessMode"]
51+
if access_mode == "r":
52+
access_mode = neo4j.READ_ACCESS
53+
elif access_mode == "w":
54+
access_mode = neo4j.WRITE_ACCESS
55+
else:
56+
raise Exception("Unknown access mode:" + access_mode)
57+
config = {
58+
"default_access_mode": access_mode,
59+
"bookmarks": data["bookmarks"],
60+
"database": data["database"],
61+
}
62+
# TODO: fetchSize, database
63+
session = driver.session(**config)
64+
key = backend.next_key()
65+
backend.sessions[key] = SessionState(session)
66+
backend.send_response("Session", {"id": key})
67+
68+
69+
def SessionRun(backend, data):
70+
session = backend.sessions[data["sessionId"]].session
71+
print(data)
72+
cypher, params = fromtestkit.toCypherAndParams(data)
73+
# TODO: txMeta, timeout
74+
result = session.run(cypher, parameters=params)
75+
key = backend.next_key()
76+
backend.results[key] = result
77+
backend.send_response("Result", {"id": key})
78+
79+
80+
def SessionClose(backend, data):
81+
key = data["sessionId"]
82+
session = backend.sessions[key].session
83+
session.close()
84+
del backend.sessions[key]
85+
backend.send_response("Session", {"id": key})
86+
87+
88+
def ResultNext(backend, data):
89+
result = backend.results[data["resultId"]]
90+
try:
91+
record = next(iter(result))
92+
except StopIteration:
93+
backend.send_response("NullRecord", {})
94+
backend.send_response("Record", totestkit.record(record))

testkitbackend/server.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (c) 2002-2020 "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+
from socketserver import TCPServer, StreamRequestHandler
18+
from testkitbackend.backend import Backend
19+
20+
21+
class Server(TCPServer):
22+
allow_reuse_address = True
23+
24+
def __init__(self, address):
25+
class Handler(StreamRequestHandler):
26+
def handle(self):
27+
backend = Backend(self.rfile, self.wfile)
28+
while backend.process_request():
29+
pass
30+
print("Disconnected")
31+
super(Server, self).__init__(address, Handler)

testkitbackend/totestkit.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright (c) 2002-2020 "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+
from neo4j.graph import Node
18+
19+
20+
def record(rec):
21+
fields = []
22+
for f in rec:
23+
fields.append(field(f))
24+
return {"values": fields}
25+
26+
27+
def field(v):
28+
def to(name, val):
29+
return {"name": name, "data": {"value": val}}
30+
31+
if v is None:
32+
return {"name": "CypherNull"}
33+
if isinstance(v, bool):
34+
return to("CypherBool", v)
35+
if isinstance(v, int):
36+
return to("CypherInt", v)
37+
if isinstance(v, float):
38+
return to("CypherFloat", v)
39+
if isinstance(v, str):
40+
return to("CypherString", v)
41+
if isinstance(v, list) or isinstance(v, frozenset) or isinstance(v, set):
42+
ls = []
43+
for x in v:
44+
ls.append(field(x))
45+
return to("CypherList", ls)
46+
if isinstance(v, dict):
47+
mp = {}
48+
for k, v in v.items():
49+
mp[k] = field(v)
50+
return to("CypherMap", mp)
51+
if isinstance(v, Node):
52+
node = {
53+
"id": field(v.id),
54+
"labels": field(v.labels),
55+
"props": field(v._properties),
56+
}
57+
return {"name": "Node", "data": node}
58+
59+
raise Exception("Unhandled type:" + str(type(v)))

0 commit comments

Comments
 (0)