Skip to content

Commit 23f59ed

Browse files
authored
Improved Testkit integration
* Support for more testkit messages and parameters - StartTest + SkipTest + RunTest - NewDriver.userAgent - NewDriver.resolverRegistered + ResolverResolutionRequired + ResolverResolutionCompleted - ResultConsume Made backend fail on receiving unexpected message parameters * Testkitbackend sends neo4j error code * Testkitbackend supports more NewDriver options connectionTimeoutMs domainNameResolverRegistered if it's False * Testkitbackend supports VerifyConnectivity message * More debug logging in testkitbackend
1 parent 59e1b42 commit 23f59ed

File tree

4 files changed

+232
-72
lines changed

4 files changed

+232
-72
lines changed

testkitbackend/backend.py

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,78 @@
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-
from json import loads, dumps
1817
from inspect import getmembers, isfunction
19-
import testkitbackend.requests as requests
18+
from json import loads, dumps
19+
import logging
20+
import sys
21+
import traceback
22+
2023
from neo4j.exceptions import Neo4jError, DriverError, ServiceUnavailable
2124

25+
import testkitbackend.requests as requests
26+
27+
handler = logging.StreamHandler(sys.stdout)
28+
handler.setLevel(logging.DEBUG)
29+
logging.getLogger("neo4j").addHandler(handler)
30+
logging.getLogger("neo4j").setLevel(logging.DEBUG)
31+
32+
log = logging.getLogger("testkitbackend")
33+
log.addHandler(handler)
34+
log.setLevel(logging.DEBUG)
35+
36+
37+
class Request(dict):
38+
def __init__(self, *args, **kwargs):
39+
super().__init__(*args, **kwargs)
40+
self._seen_keys = set()
41+
42+
def __getitem__(self, item):
43+
self._seen_keys.add(item)
44+
return super().__getitem__(item)
45+
46+
def get(self, item, default=None):
47+
self._seen_keys.add(item)
48+
return super(Request, self).get(item, default)
49+
50+
def mark_all_as_read(self, recursive=False):
51+
self._seen_keys = set(self.keys())
52+
if recursive:
53+
for val in self.values():
54+
if isinstance(val, Request):
55+
val.mark_all_as_read(recursive=True)
56+
57+
def mark_item_as_read(self, item, recursive=False):
58+
self._seen_keys.add(item)
59+
if recursive:
60+
value = super().__getitem__(item)
61+
if isinstance(value, Request):
62+
value.mark_all_as_read(recursive=True)
63+
64+
def mark_item_as_read_if_equals(self, item, value, recursive=False):
65+
if super().__getitem__(item) == value:
66+
self.mark_item_as_read(item, recursive=recursive)
67+
68+
@property
69+
def unseen_keys(self):
70+
assert not any(isinstance(v, dict) and not isinstance(v, Request)
71+
for v in self.values())
72+
unseen = set(self.keys()) - self._seen_keys
73+
for k, v in self.items():
74+
if isinstance(v, Request):
75+
unseen.update(k + "." + u for u in v.unseen_keys)
76+
return unseen
77+
78+
@property
79+
def seen_all_keys(self):
80+
return not self.unseen_keys
81+
2282

2383
class Backend:
2484
def __init__(self, rd, wr):
2585
self._rd = rd
2686
self._wr = wr
2787
self.drivers = {}
88+
self.address_resolutions = {}
2889
self.sessions = {}
2990
self.results = {}
3091
self.errors = {}
@@ -61,30 +122,47 @@ def _process(self, request):
61122
""" Process a received request by retrieving handler that
62123
corresponds to the request name.
63124
"""
64-
request = loads(request)
65-
if not isinstance(request, dict):
66-
raise Exception("Request is not an object")
67-
name = request.get('name', 'invalid')
68-
handler = self._requestHandlers.get(name)
69-
if not handler:
70-
raise Exception("No request handler for " + name)
71-
data = request["data"]
72125
try:
126+
request = loads(request, object_pairs_hook=Request)
127+
if not isinstance(request, Request):
128+
raise Exception("Request is not an object")
129+
name = request.get('name', 'invalid')
130+
handler = self._requestHandlers.get(name)
131+
if not handler:
132+
raise Exception("No request handler for " + name)
133+
data = request["data"]
134+
log.info("<<< " + name + dumps(data))
73135
handler(self, data)
74-
except Neo4jError or DriverError or ServiceUnavailable as e:
136+
unsused_keys = request.unseen_keys
137+
if unsused_keys:
138+
raise NotImplementedError(
139+
"Backend does not support some properties of the " + name +
140+
" request: " + ", ".join(unsused_keys)
141+
)
142+
except (Neo4jError, DriverError) as e:
143+
log.debug(traceback.format_exc())
144+
if isinstance(e, Neo4jError):
145+
msg = "" if e.message is None else str(e.message)
146+
else:
147+
msg = str(e.args[0]) if e.args else ""
148+
75149
key = self.next_key()
76150
self.errors[key] = e
77-
self.send_response("DriverError", {"id": key})
151+
payload = {"id": key, "errorType": str(type(e)), "msg": msg}
152+
if isinstance(e, Neo4jError):
153+
payload["code"] = e.code
154+
self.send_response("DriverError", payload)
78155
except Exception as e:
79-
from traceback import print_exception
80-
print_exception(type(e), e, e.__traceback__)
81-
self.send_response("BackendError", {"msg": "%s: %s" % (e.__class__.__name__, e)})
156+
traceback.print_exception(type(e), e, e.__traceback__)
157+
self.send_response("BackendError",
158+
{"msg": "%s: %s" % (type(e), e)})
82159

83160
def send_response(self, name, data):
84161
""" Sends a response to backend.
85162
"""
86163
response = {"name": name, "data": data}
87164
response = dumps(response)
165+
log.info(">>> " + name + dumps(data))
88166
self._wr.write(b"#response begin\n")
89167
self._wr.write(bytes(response+"\n", "utf-8"))
90168
self._wr.write(b"#response end\n")

testkitbackend/fromtestkit.py

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

18-
def toCypherAndParams(data):
19-
cypher = data["cypher"]
18+
from neo4j.work.simple import Query
19+
20+
21+
def to_cypher_and_params(data):
22+
from .backend import Request
2023
params = data["params"]
2124
# Optional
2225
if params is None:
23-
return cypher, None
26+
return data["cypher"], None
2427
# Transform the params to Python native
25-
for p in params:
26-
params[p] = toParam(params[p])
27-
return cypher, params
28+
params_dict = {p: to_param(params[p]) for p in params}
29+
return data["cypher"], params_dict
30+
31+
32+
def to_meta_and_timeout(data):
33+
from .backend import Request
34+
metadata = data.get('txMeta', None)
35+
if isinstance(metadata, Request):
36+
metadata.mark_all_as_read()
37+
timeout = data.get('timeout', None)
38+
if timeout:
39+
timeout = float(timeout) / 1000
40+
return metadata, timeout
41+
42+
43+
def to_query_and_params(data):
44+
cypher, param = to_cypher_and_params(data)
45+
metadata, timeout = to_meta_and_timeout(data)
46+
query = Query(cypher, metadata=metadata, timeout=timeout)
47+
return query, param
2848

2949

30-
def toParam(m):
50+
def to_param(m):
3151
""" Converts testkit parameter format to driver (python) parameter
3252
"""
33-
data = m["data"]
53+
value = m["data"]["value"]
3454
name = m["name"]
3555
if name == "CypherString":
36-
return str(data["value"])
56+
return str(value)
3757
if name == "CypherBool":
38-
return bool(data["value"])
58+
return bool(value)
3959
if name == "CypherInt":
40-
return int(data["value"])
60+
return int(value)
4161
if name == "CypherFloat":
42-
return float(data["value"])
62+
return float(value)
4363
if name == "CypherString":
44-
return str(data["value"])
64+
return str(value)
4565
if name == "CypherNull":
4666
return None
4767
if name == "CypherList":
48-
ls = []
49-
for x in data["value"]:
50-
ls.append(toParam(x))
51-
return ls
68+
return [to_param(v) for v in value]
5269
if name == "CypherMap":
53-
mp = {}
54-
for k, v in data["value"].items():
55-
mp[k] = toParam(v)
56-
return mp
70+
return {k: to_param(value[k]) for k in value}
5771
raise Exception("Unknown param type " + name)

0 commit comments

Comments
 (0)