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