Skip to content

Commit 0e838f3

Browse files
committed
Merge branch '1.0' into 1.0-tck-tests
2 parents 28bf075 + aff6860 commit 0e838f3

File tree

10 files changed

+205
-45
lines changed

10 files changed

+205
-45
lines changed

neo4j/meta.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919
# limitations under the License.
2020

2121

22-
version = "1.0.0b3"
22+
version = "1.0.0rc2"

neo4j/v1/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# See the License for the specific language governing permissions and
1919
# limitations under the License.
2020

21+
from .connection import ProtocolError
2122
from .constants import *
2223
from .session import *
2324
from .types import *

neo4j/v1/exceptions.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ class ProtocolError(Exception):
2323
""" Raised when an unexpected or unsupported protocol event occurs.
2424
"""
2525

26-
pass
27-
2826

2927
class CypherError(Exception):
3028
""" Raised when the Cypher engine returns an error to the client.
@@ -38,3 +36,8 @@ def __init__(self, data):
3836
for key, value in data.items():
3937
if not key.startswith("_"):
4038
setattr(self, key, value)
39+
40+
41+
class ResultError(Exception):
42+
""" Raised when an error occurs while consuming a result.
43+
"""

neo4j/v1/session.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ class which can be used to obtain `Driver` instances that are used for
2525
managing sessions.
2626
"""
2727

28+
2829
from __future__ import division
2930

3031
from collections import deque, namedtuple
3132

3233
from .compat import integer, string, urlparse
3334
from .connection import connect, Response, RUN, PULL_ALL
3435
from .constants import ENCRYPTED_DEFAULT, TRUST_DEFAULT, TRUST_SIGNED_CERTIFICATES
35-
from .exceptions import CypherError, ProtocolError
36+
from .exceptions import CypherError, ProtocolError, ResultError
3637
from .ssl_compat import SSL_AVAILABLE, SSLContext, PROTOCOL_SSLv23, OP_NO_SSLv2, CERT_REQUIRED
3738
from .types import hydrated
3839

@@ -257,6 +258,32 @@ def consume(self):
257258
self.connection = None
258259
return self._summary
259260

261+
def single(self):
262+
""" Return the next record, failing if none or more than one remain.
263+
"""
264+
records = list(self)
265+
num_records = len(records)
266+
if num_records == 0:
267+
raise ResultError("No records found in stream")
268+
elif num_records != 1:
269+
raise ResultError("Multiple records found in stream")
270+
else:
271+
return records[0]
272+
273+
def peek(self):
274+
""" Return the next record without advancing the cursor. Fails
275+
if no records remain.
276+
"""
277+
if self._buffer:
278+
values = self._buffer[0]
279+
return Record(self.keys(), tuple(map(hydrated, values)))
280+
while not self._buffer and not self._consumed:
281+
self.connection.fetch()
282+
if self._buffer:
283+
values = self._buffer[0]
284+
return Record(self.keys(), tuple(map(hydrated, values)))
285+
raise ResultError("End of stream")
286+
260287

261288
class ResultSummary(object):
262289
""" A summary of execution returned with a :class:`.StatementResult` object.

neokit

runprofile.sh

Lines changed: 0 additions & 21 deletions
This file was deleted.

runtests.sh

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,23 @@ DRIVER_HOME=$(dirname $0)
2121

2222
NEORUN_OPTIONS=""
2323
RUNNING=0
24+
QUICK=0
2425
KNOWN_HOSTS="${HOME}/.neo4j/known_hosts"
2526
KNOWN_HOSTS_BACKUP="${KNOWN_HOSTS}.backup"
2627

2728
FG_BRIGHT_RED='\033[1;31m'
2829
FG_DEFAULT='\033[0m'
2930

3031
# Parse options
31-
while getopts ":dr" OPTION
32+
while getopts ":dqr" OPTION
3233
do
3334
case ${OPTION} in
3435
d)
3536
NEORUN_OPTIONS="-f"
3637
;;
38+
q)
39+
QUICK=1
40+
;;
3741
r)
3842
RUNNING=1
3943
;;
@@ -79,34 +83,33 @@ echo ""
7983

8084
TEST_RUNNER="coverage run -m ${UNITTEST} discover -vfs ${TEST}"
8185
EXAMPLES_RUNNER="coverage run -m ${UNITTEST} discover -vfs examples"
82-
BEHAVE_RUNNER="behave --tags=-db --tags=-in_dev --tags=-streaming_and_cursor_navigation test/tck"
86+
BEHAVE_RUNNER="behave --tags=-db --tags=-in_dev test/tck"
8387

8488
if [ ${RUNNING} -eq 1 ]
8589
then
8690
${TEST_RUNNER}
8791
check_exit_status $?
8892
else
89-
#echo "Updating password"
90-
#mv ${KNOWN_HOSTS} ${KNOWN_HOSTS_BACKUP}
91-
#neokit/neorun ${NEORUN_OPTIONS} "python -m test.auth password" ${VERSIONS}
92-
#EXIT_STATUS=$?
93-
#mv ${KNOWN_HOSTS_BACKUP} ${KNOWN_HOSTS}
94-
#check_exit_status ${EXIT_STATUS}
9593
export NEO4J_PASSWORD="password"
9694

9795
echo "Running unit tests"
9896
neokit/neorun ${NEORUN_OPTIONS} "${TEST_RUNNER}" ${VERSIONS}
9997
check_exit_status $?
10098

101-
echo "Testing example code"
102-
neokit/neorun ${NEORUN_OPTIONS} "${EXAMPLES_RUNNER}" ${VERSIONS}
103-
check_exit_status $?
99+
if [ ${QUICK} -eq 0 ]
100+
then
101+
echo "Testing example code"
102+
neokit/neorun ${NEORUN_OPTIONS} "${EXAMPLES_RUNNER}" ${VERSIONS}
103+
check_exit_status $?
104+
105+
echo "Testing TCK"
106+
coverage report --show-missing
107+
python -c 'from test.tck.configure_feature_files import *; set_up()'
108+
echo "Feature files downloaded"
109+
neokit/neorun ${NEORUN_OPTIONS} "${BEHAVE_RUNNER}" ${VERSIONS}
110+
python -c 'from test.tck.configure_feature_files import *; clean_up()'
111+
echo "Feature files removed"
104112

105-
coverage report --show-missing
106-
python -c 'from test.tck.configure_feature_files import *; set_up()'
107-
echo "Feature files downloaded"
108-
neokit/neorun ${NEORUN_OPTIONS} "${BEHAVE_RUNNER}" ${VERSIONS}
109-
python -c 'from test.tck.configure_feature_files import *; clean_up()'
110-
echo "Feature files removed"
113+
fi
111114

112115
fi

test/test_session.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from mock import patch
2727

2828
from neo4j.v1.constants import TRUST_ON_FIRST_USE
29-
from neo4j.v1.exceptions import CypherError
29+
from neo4j.v1.exceptions import CypherError, ResultError
3030
from neo4j.v1.session import GraphDatabase, basic_auth, Record, SSL_AVAILABLE
3131
from neo4j.v1.types import Node, Relationship, Path
3232

@@ -575,3 +575,74 @@ def test_can_consume_result_after_session_with_error(self):
575575
tx.commit()
576576
session.close()
577577
assert [record[0] for record in result] == [1, 2, 3]
578+
579+
def test_single_with_exactly_one_record(self):
580+
session = self.driver.session()
581+
result = session.run("UNWIND range(1, 1) AS n RETURN n")
582+
record = result.single()
583+
assert list(record.values()) == [1]
584+
585+
def test_single_with_no_records(self):
586+
session = self.driver.session()
587+
result = session.run("CREATE ()")
588+
with self.assertRaises(ResultError):
589+
result.single()
590+
591+
def test_single_with_multiple_records(self):
592+
session = self.driver.session()
593+
result = session.run("UNWIND range(1, 3) AS n RETURN n")
594+
with self.assertRaises(ResultError):
595+
result.single()
596+
597+
def test_single_consumes_entire_result_if_one_record(self):
598+
session = self.driver.session()
599+
result = session.run("UNWIND range(1, 1) AS n RETURN n")
600+
_ = result.single()
601+
assert result._consumed
602+
603+
def test_single_consumes_entire_result_if_multiple_records(self):
604+
session = self.driver.session()
605+
result = session.run("UNWIND range(1, 3) AS n RETURN n")
606+
with self.assertRaises(ResultError):
607+
_ = result.single()
608+
assert result._consumed
609+
610+
def test_peek_can_look_one_ahead(self):
611+
session = self.driver.session()
612+
result = session.run("UNWIND range(1, 3) AS n RETURN n")
613+
record = result.peek()
614+
assert list(record.values()) == [1]
615+
616+
def test_peek_fails_if_nothing_remains(self):
617+
session = self.driver.session()
618+
result = session.run("CREATE ()")
619+
with self.assertRaises(ResultError):
620+
result.peek()
621+
622+
def test_peek_does_not_advance_cursor(self):
623+
session = self.driver.session()
624+
result = session.run("UNWIND range(1, 3) AS n RETURN n")
625+
result.peek()
626+
assert [record[0] for record in result] == [1, 2, 3]
627+
628+
def test_peek_at_different_stages(self):
629+
session = self.driver.session()
630+
result = session.run("UNWIND range(0, 9) AS n RETURN n")
631+
# Peek ahead to the first record
632+
expected_next = 0
633+
upcoming = result.peek()
634+
assert upcoming[0] == expected_next
635+
# Then look through all the other records
636+
for expected, record in enumerate(result):
637+
# Check this record is as expected
638+
assert record[0] == expected
639+
# Check the upcoming record is as expected...
640+
if expected < 9:
641+
# ...when one should follow
642+
expected_next = expected + 1
643+
upcoming = result.peek()
644+
assert upcoming[0] == expected_next
645+
else:
646+
# ...when none should follow
647+
with self.assertRaises(ResultError):
648+
result.peek()

test/test_stability.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
22+
import platform
23+
from unittest import skipIf
24+
25+
from neo4j.v1 import GraphDatabase, basic_auth, ProtocolError
26+
27+
from test.util import ServerTestCase, restart_server
28+
29+
30+
auth_token = basic_auth("neo4j", "password")
31+
32+
33+
class ServerRestartTestCase(ServerTestCase):
34+
35+
@skipIf(platform.system() == "Windows", "restart testing not supported on Windows")
36+
def test_server_shutdown_detection(self):
37+
driver = GraphDatabase.driver("bolt://localhost", auth=auth_token)
38+
session = driver.session()
39+
session.run("RETURN 1").consume()
40+
assert restart_server()
41+
with self.assertRaises(ProtocolError):
42+
session.run("RETURN 1").consume()
43+
session.close()

test/util.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020

2121

2222
import functools
23-
from os import rename
23+
from os import getenv, remove, rename
2424
from os.path import isfile
25+
from socket import create_connection
26+
from subprocess import check_call, CalledProcessError
27+
from time import sleep
2528
from unittest import TestCase
2629

2730
from neo4j.util import Watcher
@@ -48,6 +51,32 @@ def wrapper(*args, **kwargs):
4851
return wrapper
4952

5053

54+
def restart_server(http_port=7474):
55+
try:
56+
check_call("%s/bin/neo4j restart" % getenv("NEO4J_HOME"), shell=True)
57+
except CalledProcessError as error:
58+
if error.returncode == 2:
59+
raise OSError("Another process is listening on the server port")
60+
elif error.returncode == 512:
61+
raise OSError("Another server process is already running")
62+
else:
63+
raise OSError("An error occurred while trying to start "
64+
"the server [%s]" % error.returncode)
65+
else:
66+
running = False
67+
t = 0
68+
while not running and t < 30:
69+
try:
70+
s = create_connection(("localhost", http_port))
71+
except IOError:
72+
sleep(1)
73+
t += 1
74+
else:
75+
s.close()
76+
running = True
77+
return running
78+
79+
5180
class ServerTestCase(TestCase):
5281
""" Base class for test cases that use a remote server.
5382
"""
@@ -57,8 +86,12 @@ class ServerTestCase(TestCase):
5786

5887
def setUp(self):
5988
if isfile(self.known_hosts):
89+
if isfile(self.known_hosts_backup):
90+
remove(self.known_hosts_backup)
6091
rename(self.known_hosts, self.known_hosts_backup)
6192

6293
def tearDown(self):
6394
if isfile(self.known_hosts_backup):
95+
if isfile(self.known_hosts):
96+
remove(self.known_hosts)
6497
rename(self.known_hosts_backup, self.known_hosts)

0 commit comments

Comments
 (0)