diff --git a/runtests.sh b/runtests.sh
index 8dfff7e74..b109a8b2c 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -62,6 +62,7 @@ echo "Running tests with $(python --version)"
pip install --upgrade -r ${DRIVER_HOME}/test_requirements.txt
echo ""
TEST_RUNNER="coverage run -m ${UNITTEST} discover -vfs ${TEST}"
+BEHAVE_RUNNER="behave test/tck"
if [ ${RUNNING} -eq 1 ]
then
${TEST_RUNNER}
@@ -73,6 +74,11 @@ else
then
coverage report --show-missing
fi
+ python -c 'from test.tck.configure_feature_files import *; set_up()'
+ echo "Feature files downloaded"
+ neokit/neorun ${NEORUN_OPTIONS} "${BEHAVE_RUNNER}" ${VERSIONS}
+ python -c 'from test.tck.configure_feature_files import *; clean_up()'
+ echo "Feature files removed"
fi
# Exit correctly
diff --git a/test/tck/__init__.py b/test/tck/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/tck/configure_feature_files.py b/test/tck/configure_feature_files.py
new file mode 100644
index 000000000..9face3062
--- /dev/null
+++ b/test/tck/configure_feature_files.py
@@ -0,0 +1,38 @@
+import os
+import tarfile
+
+def clean_up():
+ dir_path = (os.path.dirname(os.path.realpath(__file__)))
+ files = os.listdir(dir_path)
+ for f in files:
+ if not os.path.isdir(f) and ".feature" in f:
+ os.remove(os.path.join(dir_path, f))
+
+
+def set_up():
+ dir_path = (os.path.dirname(os.path.realpath(__file__)))
+ url = "https://s3-eu-west-1.amazonaws.com/remoting.neotechnology.com/driver-compliance/tck.tar.gz"
+ file_name = url.split('/')[-1]
+ _download_tar(url,file_name)
+
+ tar = tarfile.open(file_name)
+ tar.extractall(dir_path)
+ tar.close()
+ os.remove(file_name)
+
+
+def _download_tar(url, file_name):
+ try:
+ import urllib2
+ tar = open(file_name, 'w')
+ response = urllib2.urlopen(url)
+ block_sz = 1024
+ while True:
+ buffer = response.read(block_sz)
+ if not buffer:
+ break
+ tar.write(buffer)
+ tar.close()
+ except ImportError:
+ from urllib import request
+ request.urlretrieve(url, file_name)
\ No newline at end of file
diff --git a/test/tck/environment.py b/test/tck/environment.py
new file mode 100644
index 000000000..3ce81c9d1
--- /dev/null
+++ b/test/tck/environment.py
@@ -0,0 +1,23 @@
+import logging
+
+from test.tck import tck_util
+from behave.log_capture import capture
+
+
+def before_all(context):
+ # -- SET LOG LEVEL: behave --logging-level=ERROR ...
+ # on behave command-line or in "behave.ini".
+ context.config.setup_logging()
+
+
+@capture
+def after_scenario(context, scenario):
+ for step in scenario.steps:
+ if step.status == 'failed':
+ logging.error("Scenario :'%s' at step: '%s' failed! ", scenario.name, step.name)
+ logging.debug("Expected result: %s", tck_util.as_cypher_text(context.expected))
+ logging.debug("Actual result: %s", tck_util.as_cypher_text(context.results))
+ if step.status == 'skipped':
+ logging.warn("Scenario :'%s' at step: '%s' was skipped! ", scenario.name, step.name)
+ if step.status == 'passed':
+ logging.debug("Scenario :'%s' at step: '%s' was passed! ", scenario.name, step.name)
diff --git a/test/tck/steps/bolt_type_steps.py b/test/tck/steps/bolt_type_steps.py
new file mode 100644
index 000000000..8b67f758c
--- /dev/null
+++ b/test/tck/steps/bolt_type_steps.py
@@ -0,0 +1,124 @@
+from behave import *
+
+from test.tck import tck_util
+
+use_step_matcher("re")
+
+
+@given("A running database")
+def step_impl(context):
+ return None
+ # check if running
+
+
+@given("a value (?P.+) of type (?P.+)")
+def step_impl(context, input, bolt_type):
+ context.expected = tck_util.get_bolt_value(bolt_type, input)
+
+
+@given("a value of type (?P.+)")
+def step_impl(context, bolt_type):
+ context.expected = tck_util.get_bolt_value(bolt_type, u' ')
+
+
+@given("a list value (?P.+) of type (?P.+)")
+def step_impl(context, input, bolt_type):
+ context.expected = tck_util.get_list_from_feature_file(input, bolt_type)
+
+
+@given("an empty list L")
+def step_impl(context):
+ context.L = []
+
+
+@given("an empty map M")
+def step_impl(context):
+ context.M = {}
+
+
+@given("a String of size (?P\d+)")
+def step_impl(context, size):
+ context.expected = tck_util.get_random_string(int(size))
+
+
+@given("a List of size (?P\d+) and type (?P.+)")
+def step_impl(context, size, type):
+ context.expected = tck_util.get_list_of_random_type(int(size), type)
+
+
+@given("a Map of size (?P\d+) and type (?P.+)")
+def step_impl(context, size, type):
+ context.expected = tck_util.get_dict_of_random_type(int(size), type)
+
+
+@step("adding a table of lists to the list L")
+def step_impl(context):
+ for row in context.table:
+ context.L.append(tck_util.get_list_from_feature_file(row[1], row[0]))
+
+
+@step("adding a table of values to the list L")
+def step_impl(context):
+ for row in context.table:
+ context.L.append(tck_util.get_bolt_value(row[0], row[1]))
+
+
+@step("adding a table of values to the map M")
+def step_impl(context):
+ for row in context.table:
+ context.M['a%d' % len(context.M)] = tck_util.get_bolt_value(row[0], row[1])
+
+
+@step("adding map M to list L")
+def step_impl(context):
+ context.L.append(context.M)
+
+
+@when("adding a table of lists to the map M")
+def step_impl(context):
+ for row in context.table:
+ context.M['a%d' % len(context.M)] = tck_util.get_list_from_feature_file(row[1], row[0])
+
+
+@step("adding a copy of map M to map M")
+def step_impl(context):
+ context.M['a%d' % len(context.M)] = context.M.copy()
+
+
+@when("the driver asks the server to echo this value back")
+def step_impl(context):
+ context.results = {}
+ context.results["as_string"] = tck_util.send_string("RETURN " + tck_util.as_cypher_text(context.expected))
+ context.results["as_parameters"] = tck_util.send_parameters("RETURN {input}", {'input': context.expected})
+
+
+@when("the driver asks the server to echo this list back")
+def step_impl(context):
+ context.expected = context.L
+ context.results = {}
+ context.results["as_string"] = tck_util.send_string("RETURN " + tck_util.as_cypher_text(context.expected))
+ context.results["as_parameters"] = tck_util.send_parameters("RETURN {input}", {'input': context.expected})
+
+
+@when("the driver asks the server to echo this map back")
+def step_impl(context):
+ context.expected = context.M
+ context.results = {}
+ context.results["as_string"] = tck_util.send_string("RETURN " + tck_util.as_cypher_text(context.expected))
+ context.results["as_parameters"] = tck_util.send_parameters("RETURN {input}", {'input': context.expected})
+
+
+@then("the result returned from the server should be a single record with a single value")
+def step_impl(context):
+ assert context.results
+ for result in context.results.values():
+ assert len(result) == 1
+ assert len(result[0]) == 1
+
+
+@step("the value given in the result should be the same as what was sent")
+def step_impl(context):
+ assert len(context.results) > 0
+ for result in context.results.values():
+ result_value = result[0].values()[0]
+ assert result_value == context.expected
\ No newline at end of file
diff --git a/test/tck/tck_util.py b/test/tck/tck_util.py
new file mode 100644
index 000000000..155ec08b6
--- /dev/null
+++ b/test/tck/tck_util.py
@@ -0,0 +1,123 @@
+import string
+import random
+from neo4j.v1 import compat
+
+from neo4j.v1 import GraphDatabase
+
+driver = GraphDatabase.driver("bolt://localhost")
+
+
+def send_string(text):
+ session = driver.session()
+ result = session.run(text)
+ session.close()
+ return result
+
+
+def send_parameters(statement, parameters):
+ session = driver.session()
+ result = session.run(statement, parameters)
+ session.close()
+ return result
+
+
+def get_bolt_value(type, value):
+ if type == 'Integer':
+ return int(value)
+ if type == 'Float':
+ return float(value)
+ if type == 'String':
+ return to_unicode(value)
+ if type == 'Null':
+ return None
+ if type == 'Boolean':
+ return bool(value)
+ raise ValueError('No such type : %s' % type)
+
+
+def as_cypher_text(expected):
+ if expected is None:
+ return "Null"
+ if isinstance(expected, (str, compat.string)):
+ return '"' + expected + '"'
+ if isinstance(expected, float):
+ return repr(expected).replace('+', '')
+ if isinstance(expected, list):
+ l = u'['
+ for i, val in enumerate(expected):
+ l += as_cypher_text(val)
+ if i < len(expected)-1:
+ l+= u','
+ l += u']'
+ return l
+ if isinstance(expected, dict):
+ d = u'{'
+ for i, (key, val) in enumerate(expected.items()):
+ d += to_unicode(key) + ':'
+ d += as_cypher_text(val)
+ if i < len(expected.items())-1:
+ d+= u','
+ d += u'}'
+ return d
+ else:
+ return to_unicode(expected)
+
+
+def get_list_from_feature_file(string_list, bolt_type):
+ inputs = string_list.strip('[]')
+ inputs = inputs.split(',')
+ list_to_return = []
+ for value in inputs:
+ list_to_return.append(get_bolt_value(bolt_type, value))
+ return list_to_return
+
+
+def get_random_string(size):
+ return u''.join(
+ random.SystemRandom().choice(list(string.ascii_uppercase + string.digits + string.ascii_lowercase)) for _ in
+ range(size))
+
+
+def get_random_bool():
+ return bool(random.randint(0, 1))
+
+
+def _get_random_func(type):
+ def get_none():
+ return None
+
+ if type == 'Integer':
+ fu = random.randint
+ args = [-9223372036854775808, 9223372036854775808]
+ elif type == 'Float':
+ fu = random.random
+ args = []
+ elif type == 'String':
+ fu = get_random_string
+ args = [3]
+ elif type == 'Null':
+ fu = get_none
+ args = []
+ elif type == 'Boolean':
+ fu = get_random_bool
+ args = []
+ else:
+ raise ValueError('No such type : %s' % type)
+ return (fu, args)
+
+
+def get_list_of_random_type(size, type):
+ fu, args = _get_random_func(type)
+ return [fu(*args) for _ in range(size)]
+
+
+def get_dict_of_random_type(size, type):
+ fu, args = _get_random_func(type)
+ return {'a%d' % i: fu(*args) for i in range(size)}
+
+def to_unicode(val):
+ try:
+ return unicode(val)
+ except NameError:
+ return str(val)
+
diff --git a/test_requirements.txt b/test_requirements.txt
index 1e90d7db5..85278c304 100644
--- a/test_requirements.txt
+++ b/test_requirements.txt
@@ -1,2 +1,3 @@
+behave
coverage
teamcity-messages