diff --git a/.gitignore b/.gitignore index 99c59cec93c9d4f54f294ced2cc252c8cd34e9e5..fd915c1b9b0cecb7f4a3fc069e70ad8999ff4a10 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,13 @@ target/ cluster.id pids/ modules/ -nebula-python/ + +# tests !tests/Makefile +reformat-gherkin +nebula-python +.pytest +.pytest_cache # IDE .idea/ diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index 4097c3de5ebe4e07db83b6d8abb1de0ce3ab9f2d..0000000000000000000000000000000000000000 --- a/tests/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.pytest -.pytest_cache -reformat-gherkin -nebula-python diff --git a/tests/tck/utils/comparator.py b/tests/common/comparator.py similarity index 66% rename from tests/tck/utils/comparator.py rename to tests/common/comparator.py index 53ac69e7b4b23344b8b2e2fa5738b03aee85deec..691b5c5948389bf6badd9b426c2710f6769135df 100644 --- a/tests/tck/utils/comparator.py +++ b/tests/common/comparator.py @@ -4,33 +4,36 @@ # attached with Common Clause Condition 1.0, found in the LICENSES directory. import math +from functools import reduce from nebula2.data.DataObject import ( + DataSetWrapper, Node, + PathWrapper, Record, Relationship, - PathWrapper, - DataSetWrapper, ValueWrapper, ) class DataSetWrapperComparator: - def __init__(self, strict=True, order=False): + def __init__(self, strict=True, order=False, included=False): self._strict = strict self._order = order + self._included = included - def __call__(self, lhs: DataSetWrapper, rhs: DataSetWrapper): - return self.compare(lhs, rhs) + def __call__(self, resp: DataSetWrapper, expect: DataSetWrapper): + return self.compare(resp, expect) - def compare(self, lhs: DataSetWrapper, rhs: DataSetWrapper): - if lhs.get_row_size() != rhs.get_row_size(): + def compare(self, resp: DataSetWrapper, expect: DataSetWrapper): + if resp.get_row_size() < expect.get_row_size(): return False - if not lhs.get_col_names() == rhs.get_col_names(): + if not resp.get_col_names() == expect.get_col_names(): return False if self._order: - return all(self.compare_row(l, r) for (l, r) in zip(lhs, rhs)) - return self._compare_list(lhs, rhs, self.compare_row) + return all(self.compare_row(l, r) for (l, r) in zip(resp, expect)) + return self._compare_list(resp, expect, self.compare_row, + self._included) def compare_value(self, lhs: ValueWrapper, rhs: ValueWrapper): """ @@ -90,21 +93,40 @@ class DataSetWrapperComparator: for (l, r) in zip(lhs, rhs)) def compare_edge(self, lhs: Relationship, rhs: Relationship): - if not lhs == rhs: - return False - if not self._strict: - return True + if self._strict: + if not lhs == rhs: + return False + else: + redge = rhs._value + if redge.src is not None and redge.dst is not None: + if lhs.start_vertex_id() != rhs.start_vertex_id(): + return False + if lhs.end_vertex_id() != rhs.end_vertex_id(): + return False + if redge.ranking is not None: + if lhs.ranking() != rhs.ranking(): + return False + if redge.name is not None: + if lhs.edge_name() != rhs.edge_name(): + return False + # FIXME(yee): diff None and {} test cases + if len(rhs.propertys()) == 0: + return True return self.compare_map(lhs.propertys(), rhs.propertys()) def compare_node(self, lhs: Node, rhs: Node): - if lhs.get_id() != rhs.get_id(): - return False - if not self._strict: - return True - if len(lhs.tags()) != len(rhs.tags()): - return False - for tag in lhs.tags(): - if not rhs.has_tag(tag): + if self._strict: + if lhs.get_id() != rhs.get_id(): + return False + if len(lhs.tags()) != len(rhs.tags()): + return False + else: + if rhs._value.vid is not None and lhs.get_id() != rhs.get_id(): + return False + if len(lhs.tags()) < len(rhs.tags()): + return False + for tag in rhs.tags(): + if not lhs.has_tag(tag): return False lprops = lhs.propertys(tag) rprops = rhs.propertys(tag) @@ -124,29 +146,28 @@ class DataSetWrapperComparator: return True def compare_list(self, lhs, rhs): - if len(lhs) != len(rhs): - return False - if self._strict: - return all(self.compare_value(l, r) for (l, r) in zip(lhs, rhs)) - return self._compare_list(lhs, rhs, self.compare_value) + return len(lhs) == len(rhs) and \ + self._compare_list(lhs, rhs, self.compare_value) def compare_row(self, lrecord: Record, rrecord: Record): if not lrecord.size() == rrecord.size(): return False - return all(self.compare_value(l, r) - for (l, r) in zip(lrecord, rrecord)) + return all( + self.compare_value(l, r) for (l, r) in zip(lrecord, rrecord)) - def _compare_list(self, lhs, rhs, cmp_fn): + def _compare_list(self, lhs, rhs, cmp_fn, included=False): visited = [] - size = 0 - for lr in lhs: - size += 1 + for rr in rhs: found = False - for i, rr in enumerate(rhs): + for i, lr in enumerate(lhs): if i not in visited and cmp_fn(lr, rr): visited.append(i) found = True break if not found: return False + lst = [1 for i in lhs] + size = reduce(lambda x, y: x + y, lst) if len(lst) > 0 else 0 + if included: + return len(visited) <= size return len(visited) == size diff --git a/tests/common/configs.py b/tests/common/configs.py index 9dd786e9eae8f5b3324b198bba9175082d62b335..3307d5a9c66282e1b8c0db2d5d1f690a8d68f2cc 100644 --- a/tests/common/configs.py +++ b/tests/common/configs.py @@ -5,6 +5,14 @@ # This source code is licensed under Apache 2.0 License, # attached with Common Clause Condition 1.0, found in the LICENSES directory. +import os + +DATA_DIR = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "data", +) + all_configs = {'--address' : ['address', '', 'Address of the Nebula'], '--user' : ['user', 'root', 'The user of Nebula'], '--password' : ['password', 'nebula', 'The password of Nebula'], diff --git a/tests/common/nebula_service.py b/tests/common/nebula_service.py index 06e799510f3b67428260933d9c05a6262349de65..5ebd671d4b1e5af40690749188f422205fcd0d78 100644 --- a/tests/common/nebula_service.py +++ b/tests/common/nebula_service.py @@ -173,16 +173,20 @@ class NebulaService(object): # wait nebula start start_time = time.time() if not self._check_servers_status(server_ports): - raise Exception( - f'nebula servers not ready in {time.time() - start_time}s') - print(f'nebula servers start ready in {time.time() - start_time}s') + self._collect_pids() + self.kill_all(signal.SIGKILL) + elapse = time.time() - start_time + raise Exception(f'nebula servers not ready in {elapse}s') + self._collect_pids() + + return graph_port + + def _collect_pids(self): for pf in glob.glob(self.work_dir + '/pids/*.pid'): with open(pf) as f: self.pids[f.name] = int(f.readline()) - return graph_port - def stop(self): print("try to stop nebula services...") self.kill_all(signal.SIGTERM) diff --git a/tests/common/types.py b/tests/common/types.py index 1ea00e785ce0c4666583303f7c39f7efd2345d51..f18261522afc5f53c16ffe5c67995a214f0fb69b 100644 --- a/tests/common/types.py +++ b/tests/common/types.py @@ -4,6 +4,28 @@ # attached with Common Clause Condition 1.0, found in the LICENSES directory. +class SpaceDesc: + def __init__(self, + name: str, + vid_type: str = "FIXED_STRING(32)", + partition_num: int = 7, + replica_factor: int = 1): + self.name = name + self.vid_type = vid_type + self.partition_num = partition_num + self.replica_factor = replica_factor + + def create_stmt(self) -> str: + return "CREATE SPACE IF NOT EXISTS `{}`(partition_num={}, replica_factor={}, vid_type={});".format( + self.name, self.partition_num, self.replica_factor, self.vid_type) + + def use_stmt(self) -> str: + return f"USE `{self.name}`;" + + def drop_stmt(self) -> str: + return f"DROP SPACE IF EXISTS `{self.name}`;" + + class Column: def __init__(self, index: int): if index < 0: diff --git a/tests/common/utils.py b/tests/common/utils.py index 664ab5e9218a6dd45a6cc585f405878965c762a1..cb330215a995de7e322dc177fd16fa882ac01951 100644 --- a/tests/common/utils.py +++ b/tests/common/utils.py @@ -5,11 +5,18 @@ # This source code is licensed under Apache 2.0 License, # attached with Common Clause Condition 1.0, found in the LICENSES directory. -import pdb +import string +import random +import time +import os from typing import Pattern +from pathlib import Path +from nebula2.gclient.net import Session from nebula2.common import ttypes as CommonTtypes from tests.common.path_value import PathVal +from tests.common.csv_import import CSVImporter +from tests.common.types import SpaceDesc def utf8b(s: str): @@ -34,7 +41,6 @@ def _compare_values_by_pattern(real, expect): def _compare_list(rvalues, evalues): if len(rvalues) != len(evalues): - pdb.set_trace() return False for rval in rvalues: @@ -44,7 +50,6 @@ def _compare_list(rvalues, evalues): found = True break if not found: - pdb.set_trace() return False return True @@ -53,11 +58,9 @@ def _compare_map(rvalues, evalues): for key in rvalues: ev = evalues.get(key) if ev is None: - pdb.set_trace() return False rv = rvalues.get(key) if not compare_value(rv, ev): - pdb.set_trace() return False return True @@ -249,7 +252,8 @@ def path_to_string(path): def dataset_to_string(dataset): - column_names = ','.join(map(lambda x: x.decode('utf-8'), dataset.column_names)) + column_names = ','.join( + map(lambda x: x.decode('utf-8'), dataset.column_names)) rows = '\n'.join(map(row_to_string, dataset.rows)) return '\n'.join([column_names, rows]) @@ -310,3 +314,44 @@ def find_in_rows(row, rows): if found: return True return False + + +def space_generator(size=6, chars=string.ascii_uppercase + string.digits): + return ''.join(random.choice(chars) for _ in range(size)) + + +def create_space(space_desc: SpaceDesc, sess: Session): + def exec(stmt): + resp = sess.execute(stmt) + assert resp.is_succeeded(), f"Fail to exec: {stmt}, {resp.error_msg()}" + + exec(space_desc.drop_stmt()) + exec(space_desc.create_stmt()) + time.sleep(3) + exec(space_desc.use_stmt()) + + +def load_csv_data(pytestconfig, sess: Session, data_dir: str): + """ + Before loading CSV data files, you must create and select a graph + space. The `schema.ngql' file only create schema about tags and + edges, not include space. + """ + schema_path = os.path.join(data_dir, 'schema.ngql') + + with open(schema_path, 'r') as f: + stmts = [] + for line in f.readlines(): + ln = line.strip() + if ln.startswith('--'): + continue + stmts.append(ln) + rs = sess.execute(' '.join(stmts)) + assert rs.is_succeeded() + + time.sleep(3) + + for path in Path(data_dir).rglob('*.csv'): + for stmt in CSVImporter(path): + rs = sess.execute(stmt) + assert rs.is_succeeded() diff --git a/tests/conftest.py b/tests/conftest.py index 18795fddf8717bd0e6049dccdba065ac4728a035..ffa7e14d079e88090b9082a89f64eb04f9fd08d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,20 +5,20 @@ # This source code is licensed under Apache 2.0 License, # attached with Common Clause Condition 1.0, found in the LICENSES directory. -import pytest +import json +import logging import os import time -import logging -import json +import pytest from filelock import FileLock -from pathlib import Path -from nebula2.gclient.net import ConnectionPool from nebula2.Config import Config +from nebula2.gclient.net import ConnectionPool from tests.common.configs import all_configs from tests.common.nebula_service import NebulaService -from tests.common.csv_import import CSVImporter +from tests.common.types import SpaceDesc +from tests.common.utils import create_space, load_csv_data tests_collected = set() tests_executed = set() @@ -26,6 +26,7 @@ data_dir = os.getenv('NEBULA_DATA_DIR') CURR_PATH = os.path.dirname(os.path.abspath(__file__)) + # pytest hook to handle test collection when xdist is used (parallel tests) # https://github.com/pytest-dev/pytest-xdist/pull/35/commits (No official documentation available) def pytest_xdist_node_collection_finished(node, ids): @@ -113,7 +114,8 @@ def conn_pool(pytestconfig, worker_id, tmp_path_factory): data["num_workers"] += 1 fn.write_text(json.dumps(data)) else: - nb = NebulaService(build_dir, project_dir, rm_dir.lower() == "true") + nb = NebulaService(build_dir, project_dir, + rm_dir.lower() == "true") nb.install() port = nb.start() pool = get_conn_pool("localhost", port) @@ -141,7 +143,7 @@ def conn_pool(pytestconfig, worker_id, tmp_path_factory): os.remove(str(fn)) -@pytest.fixture(scope="session") +@pytest.fixture(scope="class") def session(conn_pool, pytestconfig): user = pytestconfig.getoption("user") password = pytestconfig.getoption("password") @@ -150,70 +152,43 @@ def session(conn_pool, pytestconfig): sess.release() -def load_csv_data(pytestconfig, conn_pool, folder: str): - data_dir = os.path.join(CURR_PATH, 'data', folder) - schema_path = os.path.join(data_dir, 'schema.ngql') - - user = pytestconfig.getoption("user") - password = pytestconfig.getoption("password") - sess = conn_pool.get_session(user, password) - - with open(schema_path, 'r') as f: - stmts = [] - for line in f.readlines(): - ln = line.strip() - if ln.startswith('--'): - continue - stmts.append(ln) - rs = sess.execute(' '.join(stmts)) - assert rs.is_succeeded() - - time.sleep(3) - - for path in Path(data_dir).rglob('*.csv'): - for stmt in CSVImporter(path): - rs = sess.execute(stmt) - assert rs.is_succeeded() - - sess.release() +def load_csv_data_once(tmp_path_factory, pytestconfig, worker_id, conn_pool, + space_desc: SpaceDesc): + space_name = space_desc.name + root_tmp_dir = tmp_path_factory.getbasetemp().parent + fn = root_tmp_dir / f"csv-data-{space_name}" + with FileLock(str(fn) + ".lock"): + if fn.is_file(): + logging.info( + f"session-{worker_id} need not to load {space_name} csv data") + yield space_desc + return + data_dir = os.path.join(CURR_PATH, 'data', space_name) + user = pytestconfig.getoption("user") + password = pytestconfig.getoption("password") + sess = conn_pool.get_session(user, password) + create_space(space_desc, sess) + load_csv_data(pytestconfig, sess, data_dir) + sess.release() + fn.write_text(space_name) + logging.info(f"session-{worker_id} load {space_name} csv data") + yield space_desc + os.remove(str(fn)) # TODO(yee): optimize data load fixtures @pytest.fixture(scope="session") def load_nba_data(conn_pool, pytestconfig, tmp_path_factory, worker_id): - root_tmp_dir = tmp_path_factory.getbasetemp().parent - fn = root_tmp_dir / "csv-data-nba" - load = False - with FileLock(str(fn) + ".lock"): - if not fn.is_file(): - load_csv_data(pytestconfig, conn_pool, "nba") - fn.write_text("nba") - logging.info(f"session-{worker_id} load nba csv data") - load = True - else: - logging.info(f"session-{worker_id} need not to load nba csv data") - yield - if load: - os.remove(str(fn)) + space_desc = SpaceDesc(name="nba", vid_type="FIXED_STRING(30)") + yield from load_csv_data_once(tmp_path_factory, pytestconfig, worker_id, + conn_pool, space_desc) @pytest.fixture(scope="session") def load_student_data(conn_pool, pytestconfig, tmp_path_factory, worker_id): - root_tmp_dir = tmp_path_factory.getbasetemp().parent - fn = root_tmp_dir / "csv-data-student" - load = False - with FileLock(str(fn) + ".lock"): - if not fn.is_file(): - load_csv_data(pytestconfig, conn_pool, "student") - fn.write_text("student") - logging.info(f"session-{worker_id} load student csv data") - load = True - else: - logging.info( - f"session-{worker_id} need not to load student csv data") - yield - if load: - os.remove(str(fn)) + space_desc = SpaceDesc(name="student", vid_type="FIXED_STRING(8)") + yield from load_csv_data_once(tmp_path_factory, pytestconfig, worker_id, + conn_pool, space_desc) # TODO(yee): Delete this when we migrate all test cases diff --git a/tests/data/nba/schema.ngql b/tests/data/nba/schema.ngql index 7da4463bb16f4880bd7408d974d4b9aa5b2fbd25..7ac3137e8c1512102299e4f8b9cc002419c7e48a 100644 --- a/tests/data/nba/schema.ngql +++ b/tests/data/nba/schema.ngql @@ -1,6 +1,3 @@ -DROP SPACE IF EXISTS nba; -CREATE SPACE nba(partition_num=7, replica_factor=1, vid_type=FIXED_STRING(30)); -USE nba; CREATE TAG IF NOT EXISTS player(name string, age int); CREATE TAG IF NOT EXISTS team(name string); CREATE TAG IF NOT EXISTS bachelor(name string, speciality string); diff --git a/tests/data/student/schema.ngql b/tests/data/student/schema.ngql index 058dc5feb186c39238f9a68216c44eea98bcd58f..d019931e3b40b8b34c1663d3364befc7f7b91a22 100644 --- a/tests/data/student/schema.ngql +++ b/tests/data/student/schema.ngql @@ -1,6 +1,3 @@ -DROP SPACE IF EXISTS student; -CREATE SPACE IF NOT EXISTS student(partition_num=7, replica_factor=1, vid_type=FIXED_STRING(8)); -USE student; CREATE TAG IF NOT EXISTS person(name string, age int, gender string); CREATE TAG IF NOT EXISTS teacher(grade int, subject string); CREATE TAG IF NOT EXISTS student(grade int, hobby string DEFAULT ""); diff --git a/tests/tck/conftest.py b/tests/tck/conftest.py index 2bee9e09030bc5432abd160a73074469711f4b7d..11441d20740677847ad09bef3c03b8e667631ece 100644 --- a/tests/tck/conftest.py +++ b/tests/tck/conftest.py @@ -4,52 +4,137 @@ # attached with Common Clause Condition 1.0, found in the LICENSES directory. import functools +import os +import time +import pytest +import io +import csv -from pytest_bdd import given, when, then, parsers from nebula2.data.DataObject import DataSetWrapper -from tests.tck.utils.table import table, dataset -from tests.tck.utils.comparator import DataSetWrapperComparator +from nebula2.graph.ttypes import ErrorCode +from pytest_bdd import given, parsers, then, when + +from tests.common.comparator import DataSetWrapperComparator +from tests.common.configs import DATA_DIR +from tests.common.types import SpaceDesc +from tests.common.utils import create_space, load_csv_data, space_generator +from tests.tck.utils.table import dataset, table parse = functools.partial(parsers.parse) -@given( - parse('a graph with space named "nba"'), - target_fixture="nba_space", -) -def nba_space(load_nba_data, session): - rs = session.execute('USE nba;') - assert rs.is_succeeded() - return {"result_set": None} +@pytest.fixture +def graph_spaces(): + return dict(result_set=None) + + +@given(parse('a graph with space named "{space}"')) +def preload_space(space, load_nba_data, load_student_data, session, + graph_spaces): + if space == "nba": + graph_spaces["space_desc"] = load_nba_data + elif space == "student": + graph_spaces["space_desc"] = load_student_data + else: + raise ValueError(f"Invalid space name given: {space}") + rs = session.execute(f'USE {space};') + assert rs.is_succeeded(), f"Fail to use space `{space}': {rs.error_msg()}" + + +@given("an empty graph") +def empty_graph(session, graph_spaces): + pass + + +@given(parse("having executed:\n{query}")) +def having_executed(query, session): + ngql = " ".join(query.splitlines()) + resp = session.execute(ngql) + assert resp.is_succeeded(), \ + f"Fail to execute {ngql}, error: {resp.error_msg()}" + + +@given(parse("create a space with following options:\n{options}")) +def new_space(options, session, graph_spaces): + lines = csv.reader(io.StringIO(options), delimiter="|") + opts = {line[1]: line[2] for line in lines} + name = "EmptyGraph_" + space_generator() + space_desc = SpaceDesc( + name=name, + partition_num=int(opts.get("partition_num", 7)), + replica_factor=int(opts.get("replica_factor", 1)), + vid_type=opts.get("vid_type", "FIXED_STRING(30)"), + ) + create_space(space_desc, session) + graph_spaces["space_desc"] = space_desc + graph_spaces["drop_space"] = True + + +@given(parse('import "{data}" csv data')) +def import_csv_data(data, graph_spaces, session, pytestconfig): + data_dir = os.path.join(DATA_DIR, data) + space_desc = graph_spaces["space_desc"] + assert space_desc is not None + resp = session.execute(space_desc.use_stmt()) + assert resp.is_succeeded(), \ + f"Fail to use {space_desc.name}, {resp.error_msg()}" + load_csv_data(pytestconfig, session, data_dir) @when(parse("executing query:\n{query}")) -def executing_query(query, nba_space, session): +def executing_query(query, graph_spaces, session): ngql = " ".join(query.splitlines()) - nba_space['result_set'] = session.execute(ngql) + graph_spaces['result_set'] = session.execute(ngql) -@then(parse("the result should be, in any order:\n{result}")) -def result_should_be(result, nba_space): - rs = nba_space['result_set'] - assert rs.is_succeeded() +@given(parse("wait {secs:d} seconds")) +@when(parse("wait {secs:d} seconds")) +def wait(secs): + time.sleep(secs) + + +def cmp_dataset(graph_spaces, + result, + order: bool, + strict: bool, + included=False) -> None: + rs = graph_spaces['result_set'] + assert rs.is_succeeded(), f"Response failed: {rs.error_msg()}" ds = DataSetWrapper(dataset(table(result))) - dscmp = DataSetWrapperComparator(strict=True, order=False) - assert dscmp(rs._data_set_wrapper, ds) + dscmp = DataSetWrapperComparator(strict=strict, + order=order, + included=included) + assert dscmp(rs._data_set_wrapper, ds), \ + f"Response: {str(rs._data_set_wrapper)} vs. Expected: {str(ds)}" + + +@then(parse("the result should be, in order:\n{result}")) +def result_should_be_in_order(result, graph_spaces): + cmp_dataset(graph_spaces, result, order=True, strict=True) + + +@then( + parse("the result should be, in order, with relax comparision:\n{result}")) +def result_should_be_in_order_relax_cmp(result, graph_spaces): + cmp_dataset(graph_spaces, result, order=True, strict=False) + + +@then(parse("the result should be, in any order:\n{result}")) +def result_should_be(result, graph_spaces): + cmp_dataset(graph_spaces, result, order=False, strict=True) @then( parse( "the result should be, in any order, with relax comparision:\n{result}" )) -def result_should_be_relax_cmp(result, nba_space): - rs = nba_space['result_set'] - assert rs.is_succeeded() - ds = dataset(table(result)) - dsw = DataSetWrapper(ds) - dscmp = DataSetWrapperComparator(strict=False, order=False) - assert dscmp(rs._data_set_wrapper, dsw), \ - f"Response: {str(rs)} vs. Expected: {str(dsw)}" +def result_should_be_relax_cmp(result, graph_spaces): + cmp_dataset(graph_spaces, result, order=False, strict=False) + + +@then(parse("the result should include:\n{result}")) +def result_should_include(result, graph_spaces): + cmp_dataset(graph_spaces, result, order=False, strict=True, included=True) @then("no side effects") @@ -57,6 +142,31 @@ def no_side_effects(): pass -@then(parse("a TypeError should be raised at runtime: InvalidArgumentValue")) -def raised_type_error(): - pass +@then("the execution should be successful") +def execution_should_be_succ(graph_spaces): + rs = graph_spaces["result_set"] + assert rs is not None, "Please execute a query at first" + assert rs.is_succeeded(), f"Response failed: {rs.error_msg()}" + + +@then(parse("a {err_type} should be raised at {time}:{msg}")) +def raised_type_error(err_type, time, msg, graph_spaces): + res = graph_spaces["result_set"] + assert not res.is_succeeded(), "Response should be failed" + if res.error_code() == ErrorCode.E_EXECUTION_ERROR: + expect_msg = f"{msg.strip()}" + assert err_type.strip() == "ExecutionError" + else: + expect_msg = f"{err_type.strip()}: {msg.strip()}" + assert res.error_msg() == expect_msg, \ + "Response error msg: {res.error_msg()} vs. Expected: {expect_msg}" + + +@then("drop the used space") +def drop_used_space(session, graph_spaces): + drop_space = graph_spaces.get("drop_space", False) + if not drop_space: + return + space_desc = graph_spaces["space_desc"] + resp = session.execute(space_desc.drop_stmt()) + assert resp.is_succeeded(), f"Fail to drop space {space_desc.name}" diff --git a/tests/tck/features/expression/Yield.feature b/tests/tck/features/expression/Yield.feature new file mode 100644 index 0000000000000000000000000000000000000000..9e3b6cc3972338dc6d0b1d42b72a97d2ad86e674 --- /dev/null +++ b/tests/tck/features/expression/Yield.feature @@ -0,0 +1,12 @@ +Feature: Yield + + @skip + Scenario: yield without chosen space + Given an empty graph + When executing query: + """ + YIELD 1+1 AS sum + """ + Then the result should be, in any order: + | sum | + | 2 | diff --git a/tests/tck/features/job/job.feature b/tests/tck/features/job/job.feature deleted file mode 100644 index a100b4143c3c162df42d10d3fa82ed67daefe03e..0000000000000000000000000000000000000000 --- a/tests/tck/features/job/job.feature +++ /dev/null @@ -1,25 +0,0 @@ -Feature: Job - - @skip - Scenario: submit snapshot job - Given a graph with data "nba" named "nba_snapshot" - When submitting job: - """ - SUBMIT JOB SNAPSHOT - """ - Then the result should be, in any order, with "10" seconds timout: - | Job Id | Command | Status | - | r"\d+" | SNAPSHOT | FINISHED | - And no side effects - When executing query: - """ - MATCH (v:player {age: 29}) - RETURN v.name AS Name - """ - Then the result should be, in any order: - | Name | - | 'James Harden' | - | 'Jonathon Simmons' | - | 'Klay Thompson' | - | 'Dejounte Murray' | - And no side effects diff --git a/tests/tck/features/job/snapshot.feature b/tests/tck/features/job/snapshot.feature deleted file mode 100644 index 9b9b0ecfa7fd5c37fdeac828b4132f98e7b49347..0000000000000000000000000000000000000000 --- a/tests/tck/features/job/snapshot.feature +++ /dev/null @@ -1,25 +0,0 @@ -Feature: Snapshot - - @skip - Scenario: submit snapshot job - Given a graph with data "nba" named "nba_snapshot" - When submitting job: - """ - SUBMIT JOB SNAPSHOT - """ - Then the result should be, in any order, with "10" seconds timeout: - | Job Id | Command | Status | - | r"\d+" | SNAPSHOT | FINISHED | - And no side effects - When executing query: - """ - MATCH (v:player {age: 29}) - RETURN v.name AS Name - """ - Then the result should be, in any order: - | Name | - | 'James Harden' | - | 'Jonathon Simmons' | - | 'Klay Thompson' | - | 'Dejounte Murray' | - And no side effects diff --git a/tests/tck/features/match/Base.feature b/tests/tck/features/match/Base.feature index dc4be60fb4d022884a4a8b8504bd2b29ee22b45b..66a3bc7c6d224dca8575f70b767bf8a6c9871d30 100644 --- a/tests/tck/features/match/Base.feature +++ b/tests/tck/features/match/Base.feature @@ -20,7 +20,6 @@ Feature: Basic match | 'Ray Allen' | 43 | | 'David West' | 38 | | 'Tracy McGrady' | 39 | - And no side effects When executing query: """ MATCH (v:player {age: 29}) @@ -32,4 +31,3 @@ Feature: Basic match | 'Jonathon Simmons' | | 'Klay Thompson' | | 'Dejounte Murray' | - And no side effects diff --git a/tests/tck/features/match/VariableLengthRelationship.feature b/tests/tck/features/match/VariableLengthPattern.feature similarity index 99% rename from tests/tck/features/match/VariableLengthRelationship.feature rename to tests/tck/features/match/VariableLengthPattern.feature index 4e6d3b9146a950c52a9a72c8d9c28af773362a49..c715ceb582e5680ccdbac543164d5dbec964c4b9 100644 --- a/tests/tck/features/match/VariableLengthRelationship.feature +++ b/tests/tck/features/match/VariableLengthPattern.feature @@ -1,4 +1,4 @@ -Feature: Variable length relationship match (m to n) +Feature: Variable length Pattern match (m to n) Background: Given a graph with space named "nba" diff --git a/tests/tck/features/parser/Space.feature b/tests/tck/features/parser/Space.feature new file mode 100644 index 0000000000000000000000000000000000000000..42af115c6dd7aeb365b0d6fa881859d7b9eddf7a --- /dev/null +++ b/tests/tck/features/parser/Space.feature @@ -0,0 +1,41 @@ +Feature: Test space steps + + Scenario: Test space steps + Given an empty graph + And create a space with following options: + | partition_num | 9 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(30) | + And import "nba" csv data + And having executed: + """ + CREATE TAG IF NOT EXISTS `test_tag`(name string) + """ + And wait 3 seconds + When executing query: + """ + MATCH (v:player{name: "Tim Duncan"}) + RETURN v.name AS Name + """ + Then the result should be, in any order: + | Name | + | "Tim Duncan" | + When executing query: + """ + MATCH (v:player{name: "Tim Duncan"}) + RETURN v.name AS Name + """ + Then the result should include: + | Name | + | "Tim Duncan" | + When executing query: + """ + SHOW HOSTS + """ + Then the execution should be successful + When executing query: + """ + YIELD $-.id AS id + """ + Then a SemanticError should be raised at runtime: `$-.id', not exist prop `id' + Then drop the used space diff --git a/tests/tck/utils/table.py b/tests/tck/utils/table.py index 2c5f66c3bf3cc6f58ee7371de51ea77ba7c71ee1..723ba6f188f0851dd6698bf36bdfa1b197e437ca 100644 --- a/tests/tck/utils/table.py +++ b/tests/tck/utils/table.py @@ -3,26 +3,27 @@ # This source code is licensed under Apache 2.0 License, # attached with Common Clause Condition 1.0, found in the LICENSES directory. +import csv +import io + from tests.tck.utils.nbv import parse -from nebula2.common.ttypes import DataSet, Row +from nebula2.common.ttypes import DataSet, Row, Value + + +def _parse_value(cell: str) -> Value: + value = parse(cell) + assert value is not None, f'parse error: column is {cell}' + return value def table(text): - lines = text.splitlines() - assert len(lines) >= 1 - - def parse_line(line): - return list( - map(lambda x: x.strip(), filter(lambda x: x, line.split('|')))) - - table = [] - column_names = list(map(lambda x: bytes(x, 'utf-8'), parse_line(lines[0]))) - for line in lines[1:]: - row = {} - cells = parse_line(line) - for i, cell in enumerate(cells): - row[column_names[i]] = cell - table.append(row) + lines = list(csv.reader(io.StringIO(text), delimiter="|")) + header = lines[0][1:-1] + column_names = list(map(lambda x: bytes(x.strip(), 'utf8'), header)) + table = [{ + column_name: cell.strip() + for (column_name, cell) in zip(column_names, line[1:-1]) + } for line in lines[1:]] return { "column_names": column_names, @@ -33,14 +34,8 @@ def table(text): def dataset(string_table): ds = DataSet() ds.column_names = string_table['column_names'] - ds.rows = [] - for row in string_table['rows']: - nrow = Row() - nrow.values = [] - for column in ds.column_names: - value = parse(row[column]) - assert value is not None, \ - f'parse error: column is {column}:{row[column]}' - nrow.values.append(value) - ds.rows.append(nrow) + ds.rows = [ + Row(values=[_parse_value(row[column]) for column in ds.column_names]) + for row in string_table['rows'] + ] return ds diff --git a/tests/tck/utils/utils.py b/tests/tck/utils/utils.py deleted file mode 100644 index fdf6309d155fe76532fc76ea58955c2aa264bdf6..0000000000000000000000000000000000000000 --- a/tests/tck/utils/utils.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) 2020 vesoft inc. All rights reserved. -# -# This source code is licensed under Apache 2.0 License, -# attached with Common Clause Condition 1.0, found in the LICENSES directory. -# - -from nebula2.common.ttypes import Value,Vertex,Edge,Path,Step,NullType,DataSet,Row,List -import nbv -from behave import model as bh - -def parse(input): - if isinstance(input, bh.Table): - return parse_table(input) - elif isinstance(input, bh.Row): - return parse_row(input) - else: - raise Exception('Unable to parse %s' % str(input)) - -def parse_table(table): - names = table.headings - rows = [] - for row in table: - rows.append(parse_row(row)) - return DataSet(column_names = table.headings, rows = rows) - -def parse_row(row): - list = [] - for cell in row: - v = nbv.parse(cell) - if v is None: - raise Exception('Unable to parse %s' % cell) - list.append(v) - return Row(list) - -if __name__ == '__main__': - headings = ['m', 'r', 'n'] - rows = [ - [ - '1', '2', '3' - ], - [ - '()', '-->', '()' - ], - [ - '("vid")', '<-[:e "1" -> "2" @-1 {p1: 0, p2: [1, 2, 3]}]-', '()' - ], - [ - '<()-->()<--()>', '()', '"prop"' - ], - [ - 'EMPTY', 'NULL', 'BAD_TYPE' - ], - ] - expected = DataSet(column_names = headings,\ - rows = [ - Row([Value(iVal=1), Value(iVal=2), Value(iVal=3)]), - Row([Value(vVal=Vertex()), Value(eVal=Edge(type=1)), Value(vVal=Vertex())]), - Row([Value(vVal=Vertex('vid')), Value(eVal=Edge(name='e',type=-1,src='1',dst='2',ranking=-1,props={'p1': Value(iVal=0), 'p2': Value(lVal=List([Value(iVal=1),Value(iVal=2),Value(iVal=3)]))})), Value(vVal=Vertex())]), - Row([Value(pVal=Path(src=Vertex(),steps=[Step(type=1,dst=Vertex()),Step(type=-1,dst=Vertex())])), Value(vVal=Vertex()), Value(sVal='prop')]), - Row([Value(), Value(nVal=NullType.__NULL__), Value(nVal=NullType.BAD_TYPE)]) - ]) - - table = bh.Table(headings = headings, rows = rows) - dataset = parse(table) - assert dataset == expected,\ - "Parsed DataSet doesn't match, \nexpected: %s, \nactual: %s" % (expected, dataset)