Skip to content

Commit ce6b3c3

Browse files
author
Zhen Li
committed
Merge pull request #51 from neo4j/1.0-tck-tests
Adds TCK: -o Error reporting -o StatementResult navigation -o Auth Fixes in driver code: -o Cast exception if Session already has active transaction when using 'run' or 'beginTransaction' -o Clears transaction from session when recycled -o Changed StatementResult to return tuple of keys, same format as record. -o Some changes in error messages and types.
2 parents aff6860 + 7b512c5 commit ce6b3c3

14 files changed

+456
-141
lines changed

neo4j/v1/connection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
from socket import create_connection, SHUT_RDWR, error as SocketError
3232
from struct import pack as struct_pack, unpack as struct_unpack, unpack_from as struct_unpack_from
3333

34+
import errno
35+
3436
from .constants import DEFAULT_PORT, DEFAULT_USER_AGENT, KNOWN_HOSTS, MAGIC_PREAMBLE, \
3537
TRUST_DEFAULT, TRUST_ON_FIRST_USE
3638
from .compat import hex2
@@ -374,12 +376,14 @@ def connect(host, port=None, ssl_context=None, **config):
374376
"""
375377

376378
# Establish a connection to the host and port specified
379+
# Catches refused connections see:
380+
# https://docs.python.org/2/library/errno.html
377381
port = port or DEFAULT_PORT
378382
if __debug__: log_info("~~ [CONNECT] %s %d", host, port)
379383
try:
380384
s = create_connection((host, port))
381385
except SocketError as error:
382-
if error.errno == 111:
386+
if error.errno == 111 or error.errno == 61:
383387
raise ProtocolError("Unable to connect to %s on port %d - is the server running?" % (host, port))
384388
else:
385389
raise

neo4j/v1/session.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class which can be used to obtain `Driver` instances that are used for
3333
from .compat import integer, string, urlparse
3434
from .connection import connect, Response, RUN, PULL_ALL
3535
from .constants import ENCRYPTED_DEFAULT, TRUST_DEFAULT, TRUST_SIGNED_CERTIFICATES
36-
from .exceptions import CypherError, ResultError
36+
from .exceptions import CypherError, ProtocolError, ResultError
3737
from .ssl_compat import SSL_AVAILABLE, SSLContext, PROTOCOL_SSLv23, OP_NO_SSLv2, CERT_REQUIRED
3838
from .types import hydrated
3939

@@ -107,7 +107,8 @@ def __init__(self, url, **config):
107107
self.host = parsed.hostname
108108
self.port = parsed.port
109109
else:
110-
raise ValueError("Unsupported URL scheme: %s" % parsed.scheme)
110+
raise ProtocolError("Unsupported URI scheme: '%s' in url: '%s'. Currently only supported 'bolt'." %
111+
(parsed.scheme, url))
111112
self.config = config
112113
self.max_pool_size = config.get("max_pool_size", DEFAULT_MAX_POOL_SIZE)
113114
self.session_pool = deque()
@@ -239,7 +240,7 @@ def keys(self):
239240
# Fetch messages until we have the header or a failure
240241
while self._keys is None and not self._consumed:
241242
self.connection.fetch()
242-
return self._keys
243+
return tuple(self._keys)
243244

244245
def buffer(self):
245246
if self.connection and not self.connection.closed:
@@ -262,9 +263,9 @@ def single(self):
262263
records = list(self)
263264
num_records = len(records)
264265
if num_records == 0:
265-
raise ResultError("No records found in stream")
266+
raise ResultError("Cannot retrieve a single record, because this result is empty.")
266267
elif num_records != 1:
267-
raise ResultError("Multiple records found in stream")
268+
raise ResultError("Expected a result with a single record, but this result contains at least one more.")
268269
else:
269270
return records[0]
270271

@@ -396,7 +397,6 @@ def contains_updates(self):
396397
#: a list of sub-plans
397398
Plan = namedtuple("Plan", ("operator_type", "identifiers", "arguments", "children"))
398399

399-
400400
#: A profiled plan describes how the database executed your statement.
401401
#:
402402
#: db_hits:
@@ -484,7 +484,12 @@ def run(self, statement, parameters=None):
484484
:return: Cypher result
485485
:rtype: :class:`.StatementResult`
486486
"""
487+
if self.transaction:
488+
raise ProtocolError("Statements cannot be run directly on a session with an open transaction;"
489+
" either run from within the transaction or use a different session.")
490+
return self._run(statement, parameters)
487491

492+
def _run(self, statement, parameters=None):
488493
# Ensure the statement is a Unicode value
489494
if isinstance(statement, bytes):
490495
statement = statement.decode("UTF-8")
@@ -517,14 +522,18 @@ def close(self):
517522
"""
518523
if self.last_result:
519524
self.last_result.buffer()
525+
if self.transaction:
526+
self.transaction.close()
520527
self.driver.recycle(self)
521528

522529
def begin_transaction(self):
523530
""" Create a new :class:`.Transaction` within this session.
524531
525532
:return: new :class:`.Transaction` instance.
526533
"""
527-
assert not self.transaction
534+
if self.transaction:
535+
raise ProtocolError("You cannot begin a transaction on a session with an open transaction;"
536+
" either run from within the transaction or use a different session.")
528537
self.transaction = Transaction(self)
529538
return self.transaction
530539

@@ -552,7 +561,7 @@ class Transaction(object):
552561

553562
def __init__(self, session):
554563
self.session = session
555-
self.session.run("BEGIN")
564+
self.session._run("BEGIN")
556565

557566
def __enter__(self):
558567
return self
@@ -570,7 +579,7 @@ def run(self, statement, parameters=None):
570579
:return:
571580
"""
572581
assert not self.closed
573-
return self.session.run(statement, parameters)
582+
return self.session._run(statement, parameters)
574583

575584
def commit(self):
576585
""" Mark this transaction as successful and close in order to
@@ -591,9 +600,9 @@ def close(self):
591600
"""
592601
assert not self.closed
593602
if self.success:
594-
self.session.run("COMMIT")
603+
self.session._run("COMMIT")
595604
else:
596-
self.session.run("ROLLBACK")
605+
self.session._run("ROLLBACK")
597606
self.closed = True
598607
self.session.transaction = None
599608

runtests.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ echo ""
8383

8484
TEST_RUNNER="coverage run -m ${UNITTEST} discover -vfs ${TEST}"
8585
EXAMPLES_RUNNER="coverage run -m ${UNITTEST} discover -vfs examples"
86-
BEHAVE_RUNNER="behave --tags=-db --tags=-in_dev test/tck"
86+
BEHAVE_RUNNER="behave --tags=-db --tags=-tls --tags=-fixed_session_pool test/tck"
8787

8888
if [ ${RUNNING} -eq 1 ]
8989
then
@@ -112,4 +112,4 @@ else
112112

113113
fi
114114

115-
fi
115+
fi

test/tck/environment.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ def after_all(context):
5959

6060

6161
def after_scenario(context, scenario):
62-
pass
6362
for runner in tck_util.runners:
6463
runner.close()
6564

test/tck/resultparser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import json
2222
import re
2323
from neo4j.v1 import Node, Relationship, Path
24-
from tck_util import TestValue
24+
from test_value import TestValue
2525

2626

2727
def parse_values_to_comparable(row):

test/tck/steps/cypher_compability_steps.py

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121
from behave import *
2222

2323
from test.tck import tck_util
24-
from test.tck.tck_util import TestValue
25-
from test.tck.resultparser import parse_values, parse_values_to_comparable
24+
from test.tck.resultparser import parse_values
2625

2726
use_step_matcher("re")
2827

@@ -54,51 +53,10 @@ def step_impl(context, statement):
5453

5554
@then("result")
5655
def step_impl(context):
57-
expected = table_to_comparable_result(context.table)
56+
expected = tck_util.table_to_comparable_result(context.table)
5857
assert(len(context.results) > 0)
5958
for result in context.results:
6059
records = list(result)
61-
given = driver_result_to_comparable_result(records)
62-
if not unordered_equal(given, expected):
60+
given = tck_util.driver_result_to_comparable_result(records)
61+
if not tck_util.unordered_equal(given, expected):
6362
raise Exception("Does not match given: \n%s expected: \n%s" % (given, expected))
64-
65-
66-
def _driver_value_to_comparable(val):
67-
if isinstance(val, list):
68-
l = [_driver_value_to_comparable(v) for v in val]
69-
return l
70-
else:
71-
return TestValue(val)
72-
73-
74-
def table_to_comparable_result(table):
75-
result = []
76-
keys = table.headings
77-
for row in table:
78-
result.append(
79-
{keys[i]: parse_values_to_comparable(row[i]) for i in range(len(row))})
80-
return result
81-
82-
83-
def driver_result_to_comparable_result(result):
84-
records = []
85-
for record in result:
86-
records.append({key: _driver_value_to_comparable(record[key]) for key in record})
87-
return records
88-
89-
90-
def unordered_equal(given, expected):
91-
l1 = given[:]
92-
l2 = expected[:]
93-
assert isinstance(l1, list)
94-
assert isinstance(l2, list)
95-
assert len(l1) == len(l2)
96-
for d1 in l1:
97-
size = len(l2)
98-
for d2 in l2:
99-
if d1 == d2:
100-
l2.remove(d2)
101-
break
102-
if size == len(l2):
103-
return False
104-
return True

test/tck/steps/driver_result_api_steps.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ def step_impl(context, expected):
6363
def step_impl(context):
6464
for summary in context.summaries:
6565
for row in context.table:
66-
print(row[0].replace(" ","_"))
67-
print(getattr(summary.counters, row[0].replace(" ","_")))
6866
assert getattr(summary.counters, row[0].replace(" ","_")) == parse_values(row[1])
6967

7068

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env python
2+
# -*- encoding: utf-8 -*-
3+
4+
# Copyright (c) 2002-2016 "Neo Technology,"
5+
# Network Engine for Objects in Lund AB [http://neotechnology.com]
6+
#
7+
# This file is part of Neo4j.
8+
#
9+
# Licensed under the Apache License, Version 2.0 (the "License");
10+
# you may not use this file except in compliance with the License.
11+
# You may obtain a copy of the License at
12+
#
13+
# http://www.apache.org/licenses/LICENSE-2.0
14+
#
15+
# Unless required by applicable law or agreed to in writing, software
16+
# distributed under the License is distributed on an "AS IS" BASIS,
17+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
# See the License for the specific language governing permissions and
19+
# limitations under the License.
20+
21+
from behave import *
22+
23+
from neo4j.v1.exceptions import ProtocolError, CypherError
24+
from test.tck import tck_util
25+
26+
from neo4j.v1 import GraphDatabase
27+
28+
use_step_matcher("re")
29+
30+
31+
@given("I have a driver")
32+
def step_impl(context):
33+
context.driver = tck_util.driver
34+
35+
36+
@step("I start a `Transaction` through a session")
37+
def step_impl(context):
38+
context.session = context.driver.session()
39+
context.session.begin_transaction()
40+
41+
42+
@step("`run` a query with that same session without closing the transaction first")
43+
def step_impl(context):
44+
try:
45+
context.session.run("CREATE (:n)")
46+
except Exception as e:
47+
context.exception = e
48+
finally:
49+
context.session.close()
50+
51+
52+
@step("I start a new `Transaction` with the same session before closing the previous")
53+
def step_impl(context):
54+
try:
55+
context.session.begin_transaction()
56+
except Exception as e:
57+
context.exception = e
58+
finally:
59+
context.session.close()
60+
61+
62+
@step("I run a non valid cypher statement")
63+
def step_impl(context):
64+
try:
65+
s = context.driver.session()
66+
print(s.transaction)
67+
s.run("NOT VALID").consume()
68+
except Exception as e:
69+
context.exception = e
70+
71+
72+
@step("I set up a driver to an incorrect port")
73+
def step_impl(context):
74+
try:
75+
context.driver = GraphDatabase.driver("bolt://localhost:7777")
76+
context.driver.session()
77+
except Exception as e:
78+
context.exception = e
79+
80+
81+
@step("I set up a driver with wrong scheme")
82+
def step_impl(context):
83+
try:
84+
context.driver = GraphDatabase.driver("wrong://localhost")
85+
context.driver.session()
86+
except Exception as e:
87+
context.exception = e
88+
89+
90+
@step("it throws a `ClientException`")
91+
def step_impl(context):
92+
print(context.exception)
93+
assert context.exception is not None
94+
assert type(context.exception) == ProtocolError or type(context.exception) == CypherError
95+
assert isinstance(context.exception, ProtocolError) or isinstance(context.exception, CypherError)
96+
assert str(context.exception).startswith(context.table.rows[0][0])

0 commit comments

Comments
 (0)