diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8faacf583fda0b9d7ad23b7e575576f852222ff6..310e20acc88c9e56a58671fe6811c9e87b74e993 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -131,8 +131,9 @@ jobs: - name: Pytest env: NEBULA_TEST_LOGS_DIR: ${{ github.workspace }}/build - run: ./ci/test.sh test --rm_dir=false + run: python3 -m pytest --dist=loadfile -n8 --rm_dir=false --build_dir="$PWD/../build" -m "not skip" timeout-minutes: 25 + working-directory: tests - name: Upload logs uses: actions/upload-artifact@v2 if: ${{ failure() }} diff --git a/CMakeLists.txt b/CMakeLists.txt index a14577768d3a94dd63d05dabcd718a5d49ee95e3..3aeede993c7121d754148d471e8c40c83d57c89d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,4 +181,3 @@ nebula_add_subdirectory(src) nebula_add_subdirectory(conf) nebula_add_subdirectory(resources) nebula_add_subdirectory(scripts) -nebula_add_subdirectory(tests) diff --git a/ci/test.sh b/ci/test.sh index fcbf63b202f07b9a035d6a63486d049ffacfcec0..8bcd54413eb575bdd9a439709644b11eb304b9a0 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -91,16 +91,12 @@ function run_ctest() { function run_test() { export PYTHONPATH=$PROJ_DIR:$PYTHONPATH - testpath=$(cat $PROJ_DIR/ci/tests.txt | sed "s|\(.*\)|$PROJ_DIR/tests/\1|g" | tr '\n' ' ') - - $BUILD_DIR/tests/ntr \ - -n=8 \ + pytest -n 8 --build_dir=$BUILD_DIR \ --dist=loadfile \ --debug_log=false \ - ${@:1} \ - $testpath + ${@:1} - $BUILD_DIR/tests/ntr --debug_log=false ${@:1} $PROJ_DIR/tests/job/* + # $BUILD_DIR/tests/ntr --debug_log=false ${@:1} $PROJ_DIR/tests/job/* } function test_in_cluster() { diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0cb1aa3816d5c46714ee1b38050b8e3ca40820db --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,2 @@ +.pytest +.pytest_cache diff --git a/tests/admin/test_permission.py b/tests/admin/test_permission.py index e795a28202561a4d17205f296d06eab673b0156a..39bd99f443f648b2e46adf5b02f1b2bea6070209 100644 --- a/tests/admin/test_permission.py +++ b/tests/admin/test_permission.py @@ -7,11 +7,8 @@ import time -import pytest - -from tests.common.nebula_test_suite import NebulaTestSuite - from nebula2.graph import ttypes +from tests.common.nebula_test_suite import NebulaTestSuite class TestPermission(NebulaTestSuite): diff --git a/tests/admin/test_space.py b/tests/admin/test_space.py index 74e73b8e6364a4b60a0e9afbcc4d5c9084bd704f..f967add94985f836f2502a3176c1fce8bc59864d 100644 --- a/tests/admin/test_space.py +++ b/tests/admin/test_space.py @@ -6,21 +6,12 @@ # attached with Common Clause Condition 1.0, found in the LICENSES directory. import time -import re from tests.common.nebula_test_suite import NebulaTestSuite class TestSpace(NebulaTestSuite): - @classmethod - def prepare(self): - pass - - @classmethod - def cleanup(self): - pass - def test_space(self): # not exist resp = self.client.execute('USE not_exist_space') @@ -74,7 +65,6 @@ class TestSpace(NebulaTestSuite): resp = self.client.execute('DROP SPACE default_space') self.check_resp_succeeded(resp) - resp = self.client.execute(create_space_str) self.check_resp_succeeded(resp) diff --git a/tests/admin/test_users.py b/tests/admin/test_users.py index 03a395413e36420ac532278cd7367b8ea2a044d5..f41a5148bfd5a9002fe74941138ccf9e3cab5ae3 100644 --- a/tests/admin/test_users.py +++ b/tests/admin/test_users.py @@ -9,6 +9,7 @@ import time from tests.common.nebula_test_suite import NebulaTestSuite + class TestUsers(NebulaTestSuite): @classmethod def prepare(self): diff --git a/tests/common/csv_import.py b/tests/common/csv_import.py new file mode 100644 index 0000000000000000000000000000000000000000..ca1682f9b6bcef34db961f5e857be07091608373 --- /dev/null +++ b/tests/common/csv_import.py @@ -0,0 +1,145 @@ +# 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. + +import csv +import re + +from tests.common.types import ( + VID, + Rank, + Prop, + Tag, + Edge, + Vertex, +) + + +class CSVImporter: + _SRC_VID = ':SRC_VID' + _DST_VID = ':DST_VID' + _VID = ':VID' + _RANK = ':RANK' + + def __init__(self, filepath): + self._filepath = filepath + self._insert_stmt = "" + self._create_stmt = "" + self._type = None + + def __iter__(self): + with open(self._filepath, 'r') as f: + for i, row in enumerate(csv.reader(f)): + if i == 0: + yield self.parse_header(row) + else: + yield self.process(row) + + def process(self, row: list): + if isinstance(self._type, Vertex): + return self.build_vertex_insert_stmt(row) + return self.build_edge_insert_stmt(row) + + def build_vertex_insert_stmt(self, row: list): + props = [] + for p in self._type.tags[0].props: + col = row[p.index] + props.append(self.value(p.ptype, col)) + vid = self._type.vid + id_val = self.value(vid.id_type, row[vid.index]) + return f'{self._insert_stmt} {id_val}:({",".join(props)});' + + def build_edge_insert_stmt(self, row: list): + props = [] + for p in self._type.props: + col = row[p.index] + props.append(self.value(p.ptype, col)) + src = self._type.src + dst = self._type.dst + src_vid = self.value(src.id_type, row[src.index]) + dst_vid = self.value(dst.id_type, row[dst.index]) + if self._type.rank is None: + return f'{self._insert_stmt} {src_vid}->{dst_vid}:({",".join(props)});' + rank = row[self._type.rank.index] + return f'{self._insert_stmt} {src_vid}->{dst_vid}@{rank}:({",".join(props)});' + + def value(self, ptype: str, col): + return f'"{col}"' if ptype == 'string' else f'{col}' + + def parse_header(self, row): + """ + Only parse the scenario that one tag in each file + """ + for col in row: + if self._SRC_VID in col or self._DST_VID in col: + self._type = Edge() + self.parse_edge(row) + break + if self._VID in col: + self._type = Vertex() + self.parse_vertex(row) + break + if self._type is None: + raise ValueError(f'Invalid csv header: {",".join(row)}') + return self._create_stmt + + def parse_edge(self, row): + props = [] + name = '' + for i, col in enumerate(row): + if col == self._RANK: + self._type.rank = Rank(i) + continue + m = re.search(r':SRC_VID\((.*)\)', col) + if m: + self._type.src = VID(i, m.group(1)) + continue + m = re.search(r':DST_VID\((.*)\)', col) + if m: + self._type.dst = VID(i, m.group(1)) + continue + m = re.search(r'(\w+)\.(\w+):(\w+)', col) + if not m: + raise ValueError(f'Invalid csv header format {col}') + g1 = m.group(1) + if not name: + name = g1 + assert name == g1, f'Different edge type {g1}' + props.append(Prop(i, m.group(2), m.group(3))) + + self._type.name = name + self._type.props = props + pdecl = ','.join(p.name for p in props) + self._insert_stmt = f"INSERT EDGE {name}({pdecl}) VALUES" + pdecl = ','.join(f"`{p.name}` {p.ptype}" for p in props) + self._create_stmt = f"CREATE EDGE IF NOT EXISTS `{name}`({pdecl});" + + def parse_vertex(self, row): + tag = Tag() + props = [] + for i, col in enumerate(row): + m = re.search(r':VID\((.*)\)', col) + if m: + self._type.vid = VID(i, m.group(1)) + continue + m = re.search(r'(\w+)\.(\w+):(\w+)', col) + if not m: + raise ValueError(f'Invalid csv header format {col}') + g1 = m.group(1) + if not tag.name: + tag.name = g1 + assert tag.name == g1, f'Different tag name {g1}' + props.append(Prop(i, m.group(2), m.group(3))) + + tag.props = props + self._type.tags = [tag] + pdecl = ','.join(p.name for p in tag.props) + self._insert_stmt = f"INSERT VERTEX {tag.name}({pdecl}) VALUES" + pdecl = ','.join(f"`{p.name}` {p.ptype}" for p in tag.props) + self._create_stmt = f"CREATE TAG IF NOT EXISTS `{tag.name}`({pdecl});" + + +if __name__ == '__main__': + for row in CSVImporter('../data/nba/player.csv'): + print(row) diff --git a/tests/common/global_data_loader.py b/tests/common/global_data_loader.py index 06b9d5236d2785fb8950141e0572ec10c44c14ae..e89b83f1e9cc3c65077d51cf2977ecf33197b1e2 100644 --- a/tests/common/global_data_loader.py +++ b/tests/common/global_data_loader.py @@ -10,7 +10,6 @@ import time from nebula2.gclient.net import ConnectionPool from nebula2.Config import Config -from nebula2.graph import ttypes from tests.common.configs import get_delay_time @@ -83,7 +82,7 @@ class GlobalDataLoader(object): # The whole test will load once, for the only read tests def load_student(self): resp = self.client.execute( - 'CREATE SPACE IF NOT EXISTS student_space(partition_num=10, replica_factor=1, vid_type = fixed_string(8)); USE student_space;') + 'CREATE SPACE IF NOT EXISTS student(partition_num=10, replica_factor=1, vid_type = fixed_string(8)); USE student;') assert resp.is_succeeded(), resp.error_msg() resp = self.client.execute('CREATE TAG IF NOT EXISTS person(name string, age int, gender string);') @@ -209,5 +208,5 @@ class GlobalDataLoader(object): assert resp.is_succeeded(), resp.error_msg() def drop_data(self): - resp = self.client.execute('DROP SPACE nba; DROP SPACE student_space;') + resp = self.client.execute('DROP SPACE nba; DROP SPACE student;') assert resp.is_succeeded(), resp.error_msg() diff --git a/tests/common/nebula_service.py b/tests/common/nebula_service.py index a35c953014e496550fa98cb83a14b9de989832f3..06e799510f3b67428260933d9c05a6262349de65 100644 --- a/tests/common/nebula_service.py +++ b/tests/common/nebula_service.py @@ -18,11 +18,19 @@ NEBULA_START_COMMAND_FORMAT = "bin/nebula-{} --flagfile conf/nebula-{}.conf {}" class NebulaService(object): - def __init__(self, build_dir, src_dir): + def __init__(self, build_dir, src_dir, cleanup=True): self.build_dir = build_dir self.src_dir = src_dir self.work_dir = os.path.join(self.build_dir, 'server_' + time.strftime("%Y-%m-%dT%H-%M-%S", time.localtime())) self.pids = {} + self._cleanup = cleanup + + def __enter__(self): + self.install() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.stop(cleanup=self._cleanup) def set_work_dir(self, work_dir): self.work_dir = work_dir @@ -65,14 +73,32 @@ class NebulaService(object): command = NEBULA_START_COMMAND_FORMAT.format(name, name, param) return command - def _find_free_port(self): + @staticmethod + def is_port_in_use(port): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + return s.connect_ex(('localhost', port)) == 0 + + @staticmethod + def get_free_port(): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(('', 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return s.getsockname()[1] + + # TODO(yee): Find free port range + @staticmethod + def _find_free_port(): + # tcp_port, http_port, https_port ports = [] - for i in range(0, 3): - with closing(socket.socket(socket.AF_INET, - socket.SOCK_STREAM)) as s: - s.bind(('', 0)) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - ports.append(s.getsockname()[1]) + for i in range(0, 2): + ports.append(NebulaService.get_free_port()) + while True: + port = NebulaService.get_free_port() + if port not in ports and all( + not NebulaService.is_port_in_use(port + i) + for i in range(-2, 3)): + ports.insert(0, port) + break return ports def _telnet_port(self, port): @@ -116,13 +142,19 @@ class NebulaService(object): os.chdir(self.work_dir) metad_ports = self._find_free_port() + all_ports = [metad_ports[0]] command = '' graph_port = 0 server_ports = [] for server_name in ['metad', 'storaged', 'graphd']: ports = [] if server_name != 'metad': - ports = self._find_free_port() + while True: + ports = self._find_free_port() + if all((ports[0] + i) not in all_ports + for i in range(-2, 3)): + all_ports += [ports[0]] + break else: ports = metad_ports server_ports.append(ports[0]) @@ -141,8 +173,9 @@ class NebulaService(object): # wait nebula start start_time = time.time() if not self._check_servers_status(server_ports): - raise Exception('nebula servers not ready in {}s'.format(time.time() - start_time)) - print('nebula servers start ready in {}s'.format(time.time() - start_time)) + 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') for pf in glob.glob(self.work_dir + '/pids/*.pid'): with open(pf) as f: @@ -150,30 +183,42 @@ class NebulaService(object): return graph_port - def stop(self, cleanup): + def stop(self): print("try to stop nebula services...") - for p in self.pids: - try: - os.kill(self.pids[p], signal.SIGTERM) - except OSError as err: - print("nebula stop {} failed: {}".format(p, str(err))) + self.kill_all(signal.SIGTERM) max_retries = 30 - while self.check_procs_alive() and max_retries >= 0: + while self.is_procs_alive() and max_retries >= 0: time.sleep(1) max_retries = max_retries-1 - if cleanup: + self.kill_all(signal.SIGKILL) + + if self._cleanup: shutil.rmtree(self.work_dir, ignore_errors=True) - def check_procs_alive(self): + def kill_all(self, sig): + for p in self.pids: + self.kill(p, sig) + + def kill(self, pid, sig): + if not self.is_proc_alive(pid): + return + try: + os.kill(self.pids[pid], sig) + except OSError as err: + print("stop nebula {} failed: {}".format(pid, str(err))) + + def is_procs_alive(self): + return any(self.is_proc_alive(pid) for pid in self.pids) + + def is_proc_alive(self, pid): process = subprocess.Popen(['ps', '-eo', 'pid,args'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = process.communicate() for line in bytes.decode(stdout[0]).splitlines(): - pid = line.lstrip().split(' ', 1)[0] - for p in self.pids: - if str(self.pids[p]) == str(pid): - return True + p = line.lstrip().split(' ', 1)[0] + if str(p) == str(self.pids[pid]): + return True return False diff --git a/tests/common/nebula_test_suite.py b/tests/common/nebula_test_suite.py index 663011e21ba00c5f1686a5751c24675888dc6af4..34afc2f09d6dbceaa478ad96bf1eb8e6f641e2c4 100644 --- a/tests/common/nebula_test_suite.py +++ b/tests/common/nebula_test_suite.py @@ -5,21 +5,25 @@ # 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 time import datetime -import pytest -import re from pathlib import Path from typing import Pattern, Set from nebula2.common import ttypes as CommonTtypes -from nebula2.gclient.net import ConnectionPool -from nebula2.Config import Config +# from nebula2.gclient.net import ConnectionPool +# from nebula2.Config import Config from nebula2.graph import ttypes from tests.common.configs import get_delay_time -from tests.common.utils import compare_value, \ - row_to_string, to_value, value_to_string, find_in_rows +from tests.common.utils import ( + compare_value, + row_to_string, + to_value, + value_to_string, + find_in_rows, +) T_EMPTY = CommonTtypes.Value() @@ -37,32 +41,31 @@ T_NULL_UNKNOWN_DIV_BY_ZERO = CommonTtypes.Value() T_NULL_UNKNOWN_DIV_BY_ZERO.set_nVal(CommonTtypes.NullType.DIV_BY_ZERO) +@pytest.mark.usefixtures("workarround_for_class") class NebulaTestSuite(object): @classmethod def set_delay(self): self.delay = get_delay_time(self.client) - @classmethod - def setup_class(self): - self.spaces = [] - address = pytest.cmdline.address.split(':') - self.host = address[0] - self.port = address[1] - self.user = pytest.cmdline.user - self.password = pytest.cmdline.password - self.replica_factor = pytest.cmdline.replica_factor - self.partition_num = pytest.cmdline.partition_num - self.check_format_str = 'result: {}, expect: {}' - self.data_dir = pytest.cmdline.data_dir - self.data_loaded = False - self.create_nebula_clients() - self.set_delay() - self.prepare() + # @classmethod + # def setup_class(self): + # self.spaces = [] + # self.user = pytest.cmdline.user + # self.password = pytest.cmdline.password + # self.replica_factor = pytest.cmdline.replica_factor + # self.partition_num = pytest.cmdline.partition_num + # self.check_format_str = 'result: {}, expect: {}' + # self.data_dir = pytest.cmdline.data_dir + # self.data_loaded = False + # self.create_nebula_clients() + # self.set_delay() + # self.prepare() @classmethod def load_data(self): self.data_loaded = True - pathlist = Path(self.data_dir).rglob('*.ngql') + # pathlist = Path(self.data_dir).rglob('*.ngql') + pathlist = [Path(self.data_dir).joinpath("data/nba.ngql")] for path in pathlist: print("open: ", path) with open(path, 'r') as data_file: @@ -115,20 +118,20 @@ class NebulaTestSuite(object): @classmethod def use_student_space(self): - resp = self.execute('USE student_space;') + resp = self.execute('USE student;') self.check_resp_succeeded(resp) - @classmethod - def create_nebula_clients(self): - config = Config() - config.max_connection_pool_size = 20 - config.timeout = 60000 - # init connection pool - self.client_pool = ConnectionPool() - assert self.client_pool.init([(self.host, self.port)], config) + # @classmethod + # def create_nebula_clients(self): + # config = Config() + # config.max_connection_pool_size = 20 + # config.timeout = 60000 + # # init connection pool + # self.client_pool = ConnectionPool() + # assert self.client_pool.init([(self.host, self.port)], config) - # get session from the pool - self.client = self.client_pool.get_session(self.user, self.password) + # # get session from the pool + # self.client = self.client_pool.get_session(self.user, self.password) @classmethod def spawn_nebula_client(self, user, password): @@ -138,22 +141,17 @@ class NebulaTestSuite(object): def release_nebula_client(self, client): client.release() - @classmethod - def close_nebula_clients(self): - self.client_pool.close() - - @classmethod - def teardown_class(self): - if self.client is not None: - self.cleanup() - self.drop_data() - self.client.release() - self.close_nebula_clients() - - @classmethod - def execute(self, ngql, profile=True): - return self.client.execute( - 'PROFILE {{{}}}'.format(ngql) if profile else ngql) + # @classmethod + # def close_nebula_clients(self): + # self.client_pool.close() + + # @classmethod + # def teardown_class(self): + # if self.client is not None: + # self.cleanup() + # self.drop_data() + # self.client.release() + # self.close_nebula_clients() @classmethod def execute(self, ngql, profile=True): diff --git a/tests/common/types.py b/tests/common/types.py new file mode 100644 index 0000000000000000000000000000000000000000..1ea00e785ce0c4666583303f7c39f7efd2345d51 --- /dev/null +++ b/tests/common/types.py @@ -0,0 +1,134 @@ +# 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. + + +class Column: + def __init__(self, index: int): + if index < 0: + raise ValueError(f"Invalid index of vid: {index}") + self._index = index + + @property + def index(self): + return self._index + + +class VID(Column): + def __init__(self, index: int, vtype: str): + super().__init__(index) + if vtype not in ['int', 'string']: + raise ValueError(f'Invalid vid type: {vtype}') + self._type = vtype + + @property + def id_type(self): + return self._type + + +class Rank(Column): + def __init__(self, index: int): + super().__init__(index) + + +class Prop(Column): + def __init__(self, index: int, name: str, ptype: str): + super().__init__(index) + self._name = name + if ptype not in ['string', 'int', 'double']: + raise ValueError(f'Invalid prop type: {ptype}') + self._type = ptype + + @property + def name(self): + return self._name + + @property + def ptype(self): + return self._type + + +class Properties: + def __init__(self): + self._name = '' + self._props = [] + + @property + def name(self): + return self._name + + @name.setter + def name(self, name: str): + self._name = name + + @property + def props(self): + return self._props + + @props.setter + def props(self, props: list): + if any(not isinstance(p, Prop) for p in props): + raise ValueError("Invalid prop type in props") + self._props = props + + +class Tag(Properties): + def __init__(self): + super().__init__() + + +class Edge(Properties): + def __init__(self): + super().__init__() + self._src = None + self._dst = None + self._rank = None + + @property + def src(self): + return self._src + + @src.setter + def src(self, src: VID): + self._src = src + + @property + def dst(self): + return self._dst + + @dst.setter + def dst(self, dst: VID): + self._dst = dst + + @property + def rank(self): + return self._rank + + @rank.setter + def rank(self, rank: VID): + self._rank = rank + + +class Vertex: + def __init__(self): + self._vid = None + self.tags = [] + + @property + def vid(self): + return self._vid + + @vid.setter + def vid(self, vid: VID): + self._vid = vid + + @property + def tags(self): + return self._tags + + @tags.setter + def tags(self, tags: list): + if any(not isinstance(t, Tag) for t in tags): + raise ValueError('Invalid tag type of vertex') + self._tags = tags diff --git a/tests/conftest.py b/tests/conftest.py index 0b4db592aea2f498114c9a28175fe179f04478e8..cdd17ed61d7d209d38deb8fc5b2c605f1be73e04 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,28 +7,31 @@ import pytest import os +import time import logging -from tests.common.configs import all_configs +import json + +from filelock import FileLock +from pathlib import Path +from nebula2.gclient.net import ConnectionPool +from nebula2.Config import Config -DOCKER_GRAPHD_DIGESTS = os.getenv('NEBULA_GRAPHD_DIGESTS') -if DOCKER_GRAPHD_DIGESTS is None: - DOCKER_GRAPHD_DIGESTS = '0' -DOCKER_METAD_DIGESTS = os.getenv('NEBULA_METAD_DIGESTS') -if DOCKER_METAD_DIGESTS is None: - DOCKER_METAD_DIGESTS = '0' -DOCKER_STORAGED_DIGESTS = os.getenv('NEBULA_STORAGED_DIGESTS') -if DOCKER_STORAGED_DIGESTS is None: - DOCKER_STORAGED_DIGESTS = '0' +from tests.common.configs import all_configs +from tests.common.nebula_service import NebulaService +from tests.common.csv_import import CSVImporter tests_collected = set() tests_executed = set() 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): tests_collected.update(set(ids)) + # link to pytest_collection_modifyitems # https://docs.pytest.org/en/5.3.2/writing_plugins.html#hook-function-validation-and-execution @pytest.hookimpl(tryfirst=True) @@ -36,12 +39,14 @@ def pytest_collection_modifyitems(items): for item in items: tests_collected.add(item.nodeid) + # link to pytest_runtest_logreport # https://docs.pytest.org/en/5.3.2/reference.html#_pytest.hookspec.pytest_runtest_logreport def pytest_runtest_logreport(report): if report.passed: tests_executed.add(report.nodeid) + def pytest_addoption(parser): for config in all_configs: parser.addoption(config, @@ -49,6 +54,12 @@ def pytest_addoption(parser): default=all_configs[config][1], help=all_configs[config][2]) + parser.addoption("--build_dir", + dest="build_dir", + default="", + help="Nebula Graph CMake build directory") + + def pytest_configure(config): pytest.cmdline.address = config.getoption("address") pytest.cmdline.user = config.getoption("user") @@ -62,6 +73,184 @@ def pytest_configure(config): pytest.cmdline.stop_nebula = config.getoption("stop_nebula") pytest.cmdline.rm_dir = config.getoption("rm_dir") pytest.cmdline.debug_log = config.getoption("debug_log") - config._metadata['graphd digest'] = DOCKER_GRAPHD_DIGESTS - config._metadata['metad digest'] = DOCKER_METAD_DIGESTS - config._metadata['storaged digest'] = DOCKER_STORAGED_DIGESTS + + +def get_conn_pool(host: str, port: int): + config = Config() + config.max_connection_pool_size = 20 + config.timeout = 60000 + # init connection pool + pool = ConnectionPool() + if not pool.init([(host, port)], config): + raise Exception("Fail to init connection pool.") + return pool + + +@pytest.fixture(scope="session") +def conn_pool(pytestconfig, worker_id, tmp_path_factory): + addr = pytestconfig.getoption("address") + if addr: + addrsplit = addr.split(":") + assert len(addrsplit) == 2 + pool = get_conn_pool(addrsplit[0], addrsplit[1]) + yield pool + pool.close() + return + + build_dir = pytestconfig.getoption("build_dir") + rm_dir = pytestconfig.getoption("rm_dir") + project_dir = os.path.dirname(CURR_PATH) + + root_tmp_dir = tmp_path_factory.getbasetemp().parent + fn = root_tmp_dir / "nebula-test" + nb = None + with FileLock(str(fn) + ".lock"): + if fn.is_file(): + data = json.loads(fn.read_text()) + port = data["port"] + logging.info(f"session-{worker_id} read the port: {port}") + pool = get_conn_pool("localhost", port) + data["num_workers"] += 1 + fn.write_text(json.dumps(data)) + else: + nb = NebulaService(build_dir, project_dir, rm_dir.lower() == "true") + nb.install() + port = nb.start() + pool = get_conn_pool("localhost", port) + data = dict(port=port, num_workers=1, finished=0) + fn.write_text(json.dumps(data)) + logging.info(f"session-{worker_id} write the port: {port}") + + yield pool + pool.close() + + if nb is None: + with FileLock(str(fn) + ".lock"): + data = json.loads(fn.read_text()) + data["finished"] += 1 + fn.write_text(json.dumps(data)) + else: + # TODO(yee): improve this option format, only specify it by `--stop_nebula` + stop_nebula = pytestconfig.getoption("stop_nebula") + while stop_nebula.lower() == "true": + data = json.loads(fn.read_text()) + if data["finished"] + 1 == data["num_workers"]: + nb.stop() + break + time.sleep(1) + os.remove(str(fn)) + + +@pytest.fixture(scope="session") +def session(conn_pool, pytestconfig): + user = pytestconfig.getoption("user") + password = pytestconfig.getoption("password") + sess = conn_pool.get_session(user, password) + yield sess + 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() + + +# 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)) + + +@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)) + + +# TODO(yee): Delete this when we migrate all test cases +@pytest.fixture(scope="class") +def workarround_for_class(request, pytestconfig, tmp_path_factory, conn_pool, + session, load_nba_data, load_student_data): + if request.cls is None: + return + + addr = pytestconfig.getoption("address") + if addr: + ss = addr.split(':') + request.cls.host = ss[0] + request.cls.port = ss[1] + else: + root_tmp_dir = tmp_path_factory.getbasetemp().parent + fn = root_tmp_dir / "nebula-test" + data = json.loads(fn.read_text()) + request.cls.host = "localhost" + request.cls.port = data["port"] + + request.cls.data_dir = os.path.dirname(os.path.abspath(__file__)) + + request.cls.spaces = [] + request.cls.user = pytestconfig.getoption("user") + request.cls.password = pytestconfig.getoption("password") + request.cls.replica_factor = pytestconfig.getoption("replica_factor") + request.cls.partition_num = pytestconfig.getoption("partition_num") + request.cls.check_format_str = 'result: {}, expect: {}' + request.cls.data_loaded = False + request.cls.client_pool = conn_pool + request.cls.client = session + request.cls.set_delay() + request.cls.prepare() + + yield + + if request.cls.client is not None: + request.cls.cleanup() + request.cls.drop_data() diff --git a/tests/data/nba/bachelor.csv b/tests/data/nba/bachelor.csv new file mode 100644 index 0000000000000000000000000000000000000000..fc987824cd2bdf8b5b3aa0f0836593b605706568 --- /dev/null +++ b/tests/data/nba/bachelor.csv @@ -0,0 +1,2 @@ +:VID(string),bachelor.name:string,bachelor.speciality:string +Tim Duncan,Tim Duncan,psychology diff --git a/tests/data/nba/like.csv b/tests/data/nba/like.csv new file mode 100644 index 0000000000000000000000000000000000000000..176b72ccf30a474a142a032a985bf0db4b6345fa --- /dev/null +++ b/tests/data/nba/like.csv @@ -0,0 +1,83 @@ +:SRC_VID(string),:DST_VID(string),like.likeness:int +Amar'e Stoudemire,Steve Nash,90 +Russell Westbrook,Paul George,90 +Russell Westbrook,James Harden,90 +James Harden,Russell Westbrook,80 +Tracy McGrady,Kobe Bryant,90 +Tracy McGrady,Grant Hill,90 +Tracy McGrady,Rudy Gay,90 +Chris Paul,LeBron James,90 +Chris Paul,Carmelo Anthony,90 +Chris Paul,Dwyane Wade,90 +Boris Diaw,Tony Parker,80 +Boris Diaw,Tim Duncan,80 +LeBron James,Ray Allen,100 +Klay Thompson,Stephen Curry,90 +Kristaps Porzingis,Luka Doncic,90 +Marco Belinelli,Tony Parker,50 +Marco Belinelli,Tim Duncan,55 +Marco Belinelli,Danny Green,60 +Luka Doncic,Dirk Nowitzki,90 +Luka Doncic,Kristaps Porzingis,90 +Luka Doncic,James Harden,80 +Tony Parker,Tim Duncan,95 +Tony Parker,Manu Ginobili,95 +Tony Parker,LaMarcus Aldridge,90 +Danny Green,Marco Belinelli,83 +Danny Green,Tim Duncan,70 +Danny Green,LeBron James,80 +Rudy Gay,LaMarcus Aldridge,70 +LaMarcus Aldridge,Tony Parker,75 +LaMarcus Aldridge,Tim Duncan,75 +Tim Duncan,Tony Parker,95 +Tim Duncan,Manu Ginobili,95 +Ray Allen,Rajon Rondo,9 +Tiago Splitter,Tim Duncan,80 +Tiago Splitter,Manu Ginobili,90 +Paul Gasol,Kobe Bryant,90 +Paul Gasol,Marc Gasol,99 +Aron Baynes,Tim Duncan,80 +Vince Carter,Tracy McGrady,90 +Vince Carter,Jason Kidd,70 +Marc Gasol,Paul Gasol,99 +Ben Simmons,Joel Embiid,80 +Rajon Rondo,Ray Allen,-1 +Manu Ginobili,Tim Duncan,90 +Kyrie Irving,LeBron James,13 +Carmelo Anthony,LeBron James,90 +Carmelo Anthony,Chris Paul,90 +Carmelo Anthony,Dwyane Wade,90 +Dwyane Wade,LeBron James,90 +Dwyane Wade,Chris Paul,90 +Dwyane Wade,Carmelo Anthony,90 +Joel Embiid,Ben Simmons,80 +Damian Lillard,LaMarcus Aldridge,80 +Yao Ming,Tracy McGrady,90 +Yao Ming,Shaquile O'Neal,90 +Dejounte Murray,Tim Duncan,99 +Dejounte Murray,Tony Parker,99 +Dejounte Murray,Manu Ginobili,99 +Dejounte Murray,Marco Belinelli,99 +Dejounte Murray,Danny Green,99 +Dejounte Murray,LeBron James,99 +Dejounte Murray,Russell Westbrook,99 +Dejounte Murray,Chris Paul,99 +Dejounte Murray,Kyle Anderson,99 +Dejounte Murray,Kevin Durant,99 +Dejounte Murray,James Harden,99 +Dejounte Murray,Tony Parker,99 +Blake Griffin,Chris Paul,-1 +Steve Nash,Amar'e Stoudemire,90 +Steve Nash,Dirk Nowitzki,88 +Steve Nash,Stephen Curry,90 +Steve Nash,Jason Kidd,85 +Jason Kidd,Vince Carter,80 +Jason Kidd,Steve Nash,90 +Jason Kidd,Dirk Nowitzki,85 +Dirk Nowitzki,Steve Nash,80 +Dirk Nowitzki,Jason Kidd,80 +Dirk Nowitzki,Dwyane Wade,10 +Paul George,Russell Westbrook,95 +Grant Hill,Tracy McGrady,90 +Shaquile O'Neal,JaVale McGee,100 +Shaquile O'Neal,Tim Duncan,80 diff --git a/tests/data/nba/player.csv b/tests/data/nba/player.csv new file mode 100644 index 0000000000000000000000000000000000000000..ba00e2d993b41a0b01d4fa557e81906765ddddc9 --- /dev/null +++ b/tests/data/nba/player.csv @@ -0,0 +1,53 @@ +:VID(string),player.name:string,player.age:int +Nobody,Nobody,0 +Amar'e Stoudemire,Amar'e Stoudemire,36 +Russell Westbrook,Russell Westbrook,30 +James Harden,James Harden,29 +Kobe Bryant,Kobe Bryant,40 +Tracy McGrady,Tracy McGrady,39 +Chris Paul,Chris Paul,33 +Boris Diaw,Boris Diaw,36 +LeBron James,LeBron James,34 +Klay Thompson,Klay Thompson,29 +Kristaps Porzingis,Kristaps Porzingis,23 +Jonathon Simmons,Jonathon Simmons,29 +Marco Belinelli,Marco Belinelli,32 +Luka Doncic,Luka Doncic,20 +David West,David West,38 +Tony Parker,Tony Parker,36 +Danny Green,Danny Green,31 +Rudy Gay,Rudy Gay,32 +LaMarcus Aldridge,LaMarcus Aldridge,33 +Tim Duncan,Tim Duncan,42 +Kevin Durant,Kevin Durant,30 +Stephen Curry,Stephen Curry,31 +Ray Allen,Ray Allen,43 +Tiago Splitter,Tiago Splitter,34 +DeAndre Jordan,DeAndre Jordan,30 +Paul Gasol,Paul Gasol,38 +Aron Baynes,Aron Baynes,32 +Cory Joseph,Cory Joseph,27 +Vince Carter,Vince Carter,42 +Marc Gasol,Marc Gasol,34 +Ricky Rubio,Ricky Rubio,28 +Ben Simmons,Ben Simmons,22 +Giannis Antetokounmpo,Giannis Antetokounmpo,24 +Rajon Rondo,Rajon Rondo,33 +Manu Ginobili,Manu Ginobili,41 +Kyrie Irving,Kyrie Irving,26 +Carmelo Anthony,Carmelo Anthony,34 +Dwyane Wade,Dwyane Wade,37 +Joel Embiid,Joel Embiid,25 +Damian Lillard,Damian Lillard,28 +Yao Ming,Yao Ming,38 +Kyle Anderson,Kyle Anderson,25 +Dejounte Murray,Dejounte Murray,29 +Blake Griffin,Blake Griffin,30 +Steve Nash,Steve Nash,45 +Jason Kidd,Jason Kidd,45 +Dirk Nowitzki,Dirk Nowitzki,40 +Paul George,Paul George,28 +Grant Hill,Grant Hill,46 +Shaquile O'Neal,Shaquile O'Neal,47 +JaVale McGee,JaVale McGee,31 +Dwight Howard,Dwight Howard,33 diff --git a/tests/data/nba/schema.ngql b/tests/data/nba/schema.ngql new file mode 100644 index 0000000000000000000000000000000000000000..7da4463bb16f4880bd7408d974d4b9aa5b2fbd25 --- /dev/null +++ b/tests/data/nba/schema.ngql @@ -0,0 +1,12 @@ +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); +CREATE EDGE IF NOT EXISTS like(likeness int); +CREATE EDGE IF NOT EXISTS serve(start_year int, end_year int); +CREATE EDGE IF NOT EXISTS teammate(start_year int, end_year int); +CREATE TAG INDEX IF NOT EXISTS player_name_index ON player(name(64)); +CREATE TAG INDEX IF NOT EXISTS player_age_index ON player(age); +CREATE TAG INDEX IF NOT EXISTS team_name_index ON team(name(64)); diff --git a/tests/data/nba/serve.csv b/tests/data/nba/serve.csv new file mode 100644 index 0000000000000000000000000000000000000000..0dc4308105dd2d2f6ff60d14708a61b3ca9596a1 --- /dev/null +++ b/tests/data/nba/serve.csv @@ -0,0 +1,153 @@ +:SRC_VID(string),:DST_VID(string),:RANK,serve.start_year:int,serve.end_year:int +Amar'e Stoudemire,Suns,0,2002,2010 +Amar'e Stoudemire,Knicks,0,2010,2015 +Amar'e Stoudemire,Heat,0,2015,2016 +Russell Westbrook,Thunders,0,2008,2019 +James Harden,Thunders,0,2009,2012 +James Harden,Rockets,0,2012,2019 +Kobe Bryant,Lakers,0,1996,2016 +Tracy McGrady,Raptors,0,1997,2000 +Tracy McGrady,Magic,0,2000,2004 +Tracy McGrady,Rockets,0,2004,2010 +Tracy McGrady,Spurs,0,2013,2013 +Chris Paul,Hornets,0,2005,2011 +Chris Paul,Clippers,0,2011,2017 +Chris Paul,Rockets,0,2017,2021 +Boris Diaw,Hawks,0,2003,2005 +Boris Diaw,Suns,0,2005,2008 +Boris Diaw,Hornets,0,2008,2012 +Boris Diaw,Spurs,0,2012,2016 +Boris Diaw,Jazz,0,2016,2017 +LeBron James,Cavaliers,0,2003,2010 +LeBron James,Heat,0,2010,2014 +LeBron James,Cavaliers,1,2014,2018 +LeBron James,Lakers,0,2018,2019 +Klay Thompson,Warriors,0,2011,2019 +Kristaps Porzingis,Knicks,0,2015,2019 +Kristaps Porzingis,Mavericks,0,2019,2020 +Jonathon Simmons,Spurs,0,2015,2017 +Jonathon Simmons,Magic,0,2017,2019 +Jonathon Simmons,76ers,0,2019,2019 +Marco Belinelli,Warriors,0,2007,2009 +Marco Belinelli,Raptors,0,2009,2010 +Marco Belinelli,Hornets,0,2010,2012 +Marco Belinelli,Bulls,0,2012,2013 +Marco Belinelli,Spurs,0,2013,2015 +Marco Belinelli,Kings,0,2015,2016 +Marco Belinelli,Hornets,1,2016,2017 +Marco Belinelli,Hawks,0,2017,2018 +Marco Belinelli,76ers,0,2018,2018 +Marco Belinelli,Spurs,1,2018,2019 +Luka Doncic,Mavericks,0,2018,2019 +David West,Hornets,0,2003,2011 +David West,Pacers,0,2011,2015 +David West,Spurs,0,2015,2016 +David West,Warriors,0,2016,2018 +Tony Parker,Spurs,0,1999,2018 +Tony Parker,Hornets,0,2018,2019 +Danny Green,Cavaliers,0,2009,2010 +Danny Green,Spurs,0,2010,2018 +Danny Green,Raptors,0,2018,2019 +Rudy Gay,Grizzlies,0,2006,2013 +Rudy Gay,Raptors,0,2013,2013 +Rudy Gay,Kings,0,2013,2017 +Rudy Gay,Spurs,0,2017,2019 +LaMarcus Aldridge,Trail Blazers,0,2006,2015 +LaMarcus Aldridge,Spurs,0,2015,2019 +Tim Duncan,Spurs,0,1997,2016 +Kevin Durant,Thunders,0,2007,2016 +Kevin Durant,Warriors,0,2016,2019 +Stephen Curry,Warriors,0,2009,2019 +Ray Allen,Bucks,0,1996,2003 +Ray Allen,Thunders,0,2003,2007 +Ray Allen,Celtics,0,2007,2012 +Ray Allen,Heat,0,2012,2014 +Tiago Splitter,Spurs,0,2010,2015 +Tiago Splitter,Hawks,0,2015,2017 +Tiago Splitter,76ers,0,2017,2017 +DeAndre Jordan,Clippers,0,2008,2018 +DeAndre Jordan,Mavericks,0,2018,2019 +DeAndre Jordan,Knicks,0,2019,2019 +Paul Gasol,Grizzlies,0,2001,2008 +Paul Gasol,Lakers,0,2008,2014 +Paul Gasol,Bulls,0,2014,2016 +Paul Gasol,Spurs,0,2016,2019 +Paul Gasol,Bucks,0,2019,2020 +Aron Baynes,Spurs,0,2013,2015 +Aron Baynes,Pistons,0,2015,2017 +Aron Baynes,Celtics,0,2017,2019 +Cory Joseph,Spurs,0,2011,2015 +Cory Joseph,Raptors,0,2015,2017 +Cory Joseph,Pacers,0,2017,2019 +Vince Carter,Raptors,0,1998,2004 +Vince Carter,Nets,0,2004,2009 +Vince Carter,Magic,0,2009,2010 +Vince Carter,Suns,0,2010,2011 +Vince Carter,Mavericks,0,2011,2014 +Vince Carter,Grizzlies,0,2014,2017 +Vince Carter,Kings,0,2017,2018 +Vince Carter,Hawks,0,2018,2019 +Marc Gasol,Grizzlies,0,2008,2019 +Marc Gasol,Raptors,0,2019,2019 +Ricky Rubio,Timberwolves,0,2011,2017 +Ricky Rubio,Jazz,0,2017,2019 +Ben Simmons,76ers,0,2016,2019 +Giannis Antetokounmpo,Bucks,0,2013,2019 +Rajon Rondo,Celtics,0,2006,2014 +Rajon Rondo,Mavericks,0,2014,2015 +Rajon Rondo,Kings,0,2015,2016 +Rajon Rondo,Bulls,0,2016,2017 +Rajon Rondo,Pelicans,0,2017,2018 +Rajon Rondo,Lakers,0,2018,2019 +Manu Ginobili,Spurs,0,2002,2018 +Kyrie Irving,Cavaliers,0,2011,2017 +Kyrie Irving,Celtics,0,2017,2019 +Carmelo Anthony,Nuggets,0,2003,2011 +Carmelo Anthony,Knicks,0,2011,2017 +Carmelo Anthony,Thunders,0,2017,2018 +Carmelo Anthony,Rockets,0,2018,2019 +Dwyane Wade,Heat,0,2003,2016 +Dwyane Wade,Bulls,0,2016,2017 +Dwyane Wade,Cavaliers,0,2017,2018 +Dwyane Wade,Heat,1,2018,2019 +Joel Embiid,76ers,0,2014,2019 +Damian Lillard,Trail Blazers,0,2012,2019 +Yao Ming,Rockets,0,2002,2011 +Kyle Anderson,Spurs,0,2014,2018 +Kyle Anderson,Grizzlies,0,2018,2019 +Dejounte Murray,Spurs,0,2016,2019 +Blake Griffin,Clippers,0,2009,2018 +Blake Griffin,Pistons,0,2018,2019 +Steve Nash,Suns,0,1996,1998 +Steve Nash,Mavericks,0,1998,2004 +Steve Nash,Suns,1,2004,2012 +Steve Nash,Lakers,0,2012,2015 +Jason Kidd,Mavericks,0,1994,1996 +Jason Kidd,Suns,0,1996,2001 +Jason Kidd,Nets,0,2001,2008 +Jason Kidd,Mavericks,1,2008,2012 +Jason Kidd,Knicks,0,2012,2013 +Dirk Nowitzki,Mavericks,0,1998,2019 +Paul George,Pacers,0,2010,2017 +Paul George,Thunders,0,2017,2019 +Grant Hill,Pistons,0,1994,2000 +Grant Hill,Magic,0,2000,2007 +Grant Hill,Suns,0,2007,2012 +Grant Hill,Clippers,0,2012,2013 +Shaquile O'Neal,Magic,0,1992,1996 +Shaquile O'Neal,Lakers,0,1996,2004 +Shaquile O'Neal,Heat,0,2004,2008 +Shaquile O'Neal,Suns,0,2008,2009 +Shaquile O'Neal,Cavaliers,0,2009,2010 +Shaquile O'Neal,Celtics,0,2010,2011 +JaVale McGee,Wizards,0,2008,2012 +JaVale McGee,Nuggets,0,2012,2015 +JaVale McGee,Mavericks,0,2015,2016 +JaVale McGee,Warriors,0,2016,2018 +JaVale McGee,Lakers,0,2018,2019 +Dwight Howard,Magic,0,2004,2012 +Dwight Howard,Lakers,0,2012,2013 +Dwight Howard,Rockets,0,2013,2016 +Dwight Howard,Hawks,0,2016,2017 +Dwight Howard,Hornets,0,2017,2018 +Dwight Howard,Wizards,0,2018,2019 diff --git a/tests/data/nba/team.csv b/tests/data/nba/team.csv new file mode 100644 index 0000000000000000000000000000000000000000..1e672d709ffdbf8d0649e3703cda5e61f500652c --- /dev/null +++ b/tests/data/nba/team.csv @@ -0,0 +1,31 @@ +:VID(string),team.name:string +Nets,Nets +Pistons,Pistons +Bucks,Bucks +Mavericks,Mavericks +Clippers,Clippers +Thunders,Thunders +Lakers,Lakers +Jazz,Jazz +Nuggets,Nuggets +Wizards,Wizards +Pacers,Pacers +Timberwolves,Timberwolves +Hawks,Hawks +Warriors,Warriors +Magic,Magic +Rockets,Rockets +Pelicans,Pelicans +Raptors,Raptors +Spurs,Spurs +Heat,Heat +Grizzlies,Grizzlies +Knicks,Knicks +Suns,Suns +Hornets,Hornets +Cavaliers,Cavaliers +Kings,Kings +Celtics,Celtics +76ers,76ers +Trail Blazers,Trail Blazers +Bulls,Bulls diff --git a/tests/data/nba/teammate.csv b/tests/data/nba/teammate.csv new file mode 100644 index 0000000000000000000000000000000000000000..d423700a47eae08bc1df67b7cc298229c134e52f --- /dev/null +++ b/tests/data/nba/teammate.csv @@ -0,0 +1,11 @@ +:SRC_VID(string),:DST_VID(string),teammate.start_year:int,teammate.end_year:int +Tony Parker,Tim Duncan,2001,2016 +Tony Parker,Manu Ginobili,2002,2018 +Tony Parker,LaMarcus Aldridge,2015,2018 +Tony Parker,Kyle Anderson,2014,2016 +Tim Duncan,Tony Parker,2001,2016 +Tim Duncan,Manu Ginobili,2002,2016 +Tim Duncan,LaMarcus Aldridge,2015,2016 +Tim Duncan,Danny Green,2010,2016 +Manu Ginobili,Tim Duncan,2002,2016 +Manu Ginobili,Tony Parker,2002,2016 diff --git a/tests/data/student/is_colleagues.csv b/tests/data/student/is_colleagues.csv new file mode 100644 index 0000000000000000000000000000000000000000..ae7e5b36afec723a20a696f9fd8db3b5b2e44c92 --- /dev/null +++ b/tests/data/student/is_colleagues.csv @@ -0,0 +1,8 @@ +:SRC_VID(string),:DST_VID(string),is_colleagues.start_year:int,is_colleagues.end_year:int +2001,2002,2015,0 +2001,2007,2014,0 +2001,2003,2018,0 +2003,2004,2013,2017 +2002,2001,2016,2017 +2007,2001,2013,2018 +2010,2008,2018,0 diff --git a/tests/data/student/is_friend.csv b/tests/data/student/is_friend.csv new file mode 100644 index 0000000000000000000000000000000000000000..9345b05241442ffffb869d3750bd993e958e3136 --- /dev/null +++ b/tests/data/student/is_friend.csv @@ -0,0 +1,8 @@ +:SRC_VID(string),:DST_VID(string),is_friend.start_year:int,is_friend.intimacy:double +1003,1004,2017,80.0 +1013,1007,2018,80.0 +1016,1008,2015,80.0 +1016,1018,2014,85.0 +1017,1020,2018,78.0 +1018,1016,2013,83.0 +1018,1020,2018,88.0 diff --git a/tests/data/student/is_schoolmate.csv b/tests/data/student/is_schoolmate.csv new file mode 100644 index 0000000000000000000000000000000000000000..d90bcd5b8a725beff8ac6b59ad47160f96404343 --- /dev/null +++ b/tests/data/student/is_schoolmate.csv @@ -0,0 +1,27 @@ +:SRC_VID(string),:DST_VID(string),is_schoolmate.start_year:int,is_schoolmate.end_year:int +1001,1002,2018,2019 +1001,1003,2017,2019 +1002,1003,2017,2018 +1002,1001,2018,2019 +1004,1005,2016,2019 +1004,1006,2017,2019 +1004,1007,2016,2018 +1005,1004,2017,2018 +1005,1007,2017,2018 +1006,1004,2017,2018 +1006,1007,2018,2019 +1008,1009,2015,2019 +1008,1010,2017,2019 +1008,1011,2018,2019 +1010,1008,2017,2018 +1011,1008,2018,2019 +1012,1013,2015,2019 +1012,1014,2017,2019 +1012,1015,2018,2019 +1013,1012,2017,2018 +1014,1015,2018,2019 +1016,1017,2015,2019 +1016,1018,2014,2019 +1018,1019,2018,2019 +1017,1020,2013,2018 +1017,1016,2018,2019 diff --git a/tests/data/student/is_teacher.csv b/tests/data/student/is_teacher.csv new file mode 100644 index 0000000000000000000000000000000000000000..f3b10537ef9bbcd447505f671371b0799ba5bba8 --- /dev/null +++ b/tests/data/student/is_teacher.csv @@ -0,0 +1,12 @@ +:SRC_VID(string),:DST_VID(string),is_teacher.start_year:int,is_teacher.end_year:int +2002,1004,2018,2019 +2002,1005,2018,2019 +2002,1006,2018,2019 +2002,1007,2018,2019 +2002,1009,2017,2018 +2002,1012,2015,2016 +2002,1013,2015,2016 +2002,1014,2015,2016 +2002,1019,2014,2015 +2010,1016,2018,2019 +2006,1008,2017,2018 diff --git a/tests/data/student/person.csv b/tests/data/student/person.csv new file mode 100644 index 0000000000000000000000000000000000000000..a5bfba2be92ec286ad1a92d633cbaf78ff2809be --- /dev/null +++ b/tests/data/student/person.csv @@ -0,0 +1,31 @@ +:VID(string),person.name:string,person.age:int,person.gender:string +2001,Mary,25,female +2002,Ann,23,female +2003,Julie,33,female +2004,Kim,30,male +2005,Ellen,27,male +2006,ZhangKai,27,male +2007,Emma,26,female +2008,Ben,24,male +2009,Helen,24,male +2010,Lilan,32,male +1001,Anne,7,female +1002,Cynthia,7,female +1003,Jane,6,male +1004,Lisa,8,female +1005,Peggy,8,male +1006,Kevin,9,male +1007,WangLe,8,male +1008,WuXiao,9,male +1009,Sandy,9,female +1010,Harry,9,female +1011,Ada,8,female +1012,Lynn,9,female +1013,Bonnie,10,female +1014,Peter,10,male +1015,Carl,10,female +1016,Sonya,11,male +1017,HeNa,11,female +1018,Tom,12,male +1019,XiaMei,11,female +1020,Lily,10,female diff --git a/tests/data/student/schema.ngql b/tests/data/student/schema.ngql new file mode 100644 index 0000000000000000000000000000000000000000..058dc5feb186c39238f9a68216c44eea98bcd58f --- /dev/null +++ b/tests/data/student/schema.ngql @@ -0,0 +1,10 @@ +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 ""); +CREATE EDGE IF NOT EXISTS is_schoolmate(start_year int, end_year int); +CREATE EDGE IF NOT EXISTS is_teacher(start_year int, end_year int); +CREATE EDGE IF NOT EXISTS is_friend(start_year int, intimacy double); +CREATE EDGE IF NOT EXISTS is_colleagues(start_year int, end_year int); diff --git a/tests/data/student/student.csv b/tests/data/student/student.csv new file mode 100644 index 0000000000000000000000000000000000000000..83fc86a72425608e3f03790e8b5e9be80387197d --- /dev/null +++ b/tests/data/student/student.csv @@ -0,0 +1,21 @@ +:VID(string),student.hobby:string,student.grade:int +1001,,2 +1002,,2 +1003,,2 +1004,,3 +1005,,3 +1006,,3 +1007,,3 +1008,,4 +1009,,4 +1010,,4 +1011,,4 +1012,,5 +1013,,5 +1014,,5 +1015,,5 +1016,,6 +1017,,6 +1018,,6 +1019,,6 +1020,,6 diff --git a/tests/data/student/teacher.csv b/tests/data/student/teacher.csv new file mode 100644 index 0000000000000000000000000000000000000000..82ce43975154aae7980cb0bea95ae54c0b330de5 --- /dev/null +++ b/tests/data/student/teacher.csv @@ -0,0 +1,11 @@ +:VID(string),teacher.grade:int,teacher.subject:string +2001,5,Math +2002,3,English +2003,6,Math +2004,5,English +2005,4,Art +2006,3,Chinese +2007,2,Science +2008,4,Music +2009,2,Sports +2010,5,Chinese diff --git a/tests/maintain/test_index.py b/tests/maintain/test_index.py index c1ad446e9abc05c197a075e3e318d40b94e1f4bc..8161048ada6a0d1fed51e75d53720cb9618baba0 100644 --- a/tests/maintain/test_index.py +++ b/tests/maintain/test_index.py @@ -294,7 +294,6 @@ class TestIndex(NebulaTestSuite): expect = [['102', 0, '103', 22]] self.check_out_of_order_result(resp, expect) - if self.find_result(resp0, [['multi_edge_index', 'FINISHED']]): resp = self.client.execute('LOOKUP ON edge_1 WHERE edge_1.col3 > 43.4 YIELD edge_1.col1') self.check_resp_succeeded(resp) diff --git a/tests/mutate/test_delete_edges_2.py b/tests/mutate/test_delete_edges_2.py index 4ede5d175e60259254ba703c7292dbbae3029afa..8a738b5218c6923aa794ceee80dd05f1f0189439 100644 --- a/tests/mutate/test_delete_edges_2.py +++ b/tests/mutate/test_delete_edges_2.py @@ -5,8 +5,6 @@ # This source code is licensed under Apache 2.0 License, # attached with Common Clause Condition 1.0, found in the LICENSES directory. -import time - from tests.common.nebula_test_suite import NebulaTestSuite @@ -15,10 +13,6 @@ class TestDeleteEdges2(NebulaTestSuite): def prepare(self): self.load_data() - @classmethod - def cleanup(self): - pass - def test_delete_with_pipe_wrong_vid_type(self): resp = self.execute('GO FROM "Boris Diaw" OVER like YIELD like._type as id | DELETE EDGE like $-.id->$-.id') self.check_resp_failed(resp) diff --git a/tests/pytest.ini b/tests/pytest.ini index 298bf0165f088fe0196d82dc8e20671f759038b9..01859c2d04c100e735902cf9401ccae2b49ce46e 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -1,3 +1,26 @@ [pytest] -addopts = -r xfE -v --tb=short --showlocals -junit_family=legacy +addopts = -r xfE -v --tb=short --showlocals --basetemp=.pytest +junit_family = legacy +bdd_features_base_dir = tck/features/ +python_files = + tck/steps/test_*.py + admin/test_*.py + maintain/test_*.py + mutate/test_*.py + query/v1/test_*.py + query/v2/test_*.py + query/v2/match/test_*.py + query/stateless/test_schema.py + query/stateless/test_admin.py + query/stateless/test_if_exists.py + query/stateless/test_range.py + query/stateless/test_go.py + query/stateless/test_simple_query.py + query/stateless/test_keyword.py + query/stateless/test_lookup.py + +[tool:pytest] +addopts = + --yapf + --yapfdiff +yapf-ignore = diff --git a/tests/query/stateless/test_go.py b/tests/query/stateless/test_go.py index af822f68818afaea3f4277c1c694a94579f2cf35..cf4b3c6fe0b53f5b4f6e7c2072915d18e5840bdb 100644 --- a/tests/query/stateless/test_go.py +++ b/tests/query/stateless/test_go.py @@ -9,6 +9,7 @@ import pytest from tests.common.nebula_test_suite import NebulaTestSuite + class TestGoQuery(NebulaTestSuite): @classmethod def prepare(self): diff --git a/tests/query/stateless/test_schema.py b/tests/query/stateless/test_schema.py index 0ffd74ea503e935c2fa97b8c0b0f873c90c2083a..706340f8b61a55173fb8c8d581ffaeb99b8e2a82 100644 --- a/tests/query/stateless/test_schema.py +++ b/tests/query/stateless/test_schema.py @@ -5,12 +5,10 @@ # This source code is licensed under Apache 2.0 License, # attached with Common Clause Condition 1.0, found in the LICENSES directory. -import re -import sys import time from tests.common.nebula_test_suite import NebulaTestSuite -from tests.common.nebula_test_suite import T_EMPTY, T_NULL +from tests.common.nebula_test_suite import T_EMPTY class TestSchema(NebulaTestSuite): @@ -83,14 +81,14 @@ class TestSchema(NebulaTestSuite): resp = self.execute('CREATE TAG TAG_empty') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # create tag with wrong type try: resp = self.execute('CREATE TAG TAG_wrong_type(name list)') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # create tag with wrong default value type try: @@ -98,7 +96,7 @@ class TestSchema(NebulaTestSuite): 'gender string default false) ') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # create tag with wrong ttl type try: @@ -106,9 +104,7 @@ class TestSchema(NebulaTestSuite): 'ttl_duration = 100, ttl_col = "gender"') self.check_resp_failed(resp) except Exception as x: - print('failed') - - + print('failed', x) def test_alter_tag_succeed(self): # create tag @@ -172,21 +168,21 @@ class TestSchema(NebulaTestSuite): resp = self.execute('ALTER TAG student ttl_col email') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # alter drop nonexistent col try: resp = self.execute('ALTER TAG student drop name') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # alter add existent col try: resp = self.execute('ALTER TAG student add (email, int)') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) def test_create_edge_succeed(self): # create edge without prop @@ -246,14 +242,14 @@ class TestSchema(NebulaTestSuite): resp = self.execute('CREATE EDGE EDGE_empty') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # create edge with wrong type try: resp = self.execute('CREATE EDGE EDGE_wrong_type(name list)') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # create edge with wrong default value type try: @@ -261,7 +257,7 @@ class TestSchema(NebulaTestSuite): 'gender string default false) ') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # create edge with wrong ttl type try: @@ -269,7 +265,7 @@ class TestSchema(NebulaTestSuite): 'ttl_duration = 100, ttl_col = "gender"') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) def test_alter_edge_succeed(self): # create edge @@ -327,21 +323,21 @@ class TestSchema(NebulaTestSuite): resp = self.execute('ALTER EDGE relationship ttl_col email') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # alter drop nonexistent col try: resp = self.execute('ALTER EDGE relationship drop name') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # alter add existent col try: resp = self.execute('ALTER EDGE relationship add (email, int)') self.check_resp_failed(resp) except Exception as x: - print('failed') + print('failed', x) # Cover https://github.com/vesoft-inc/nebula/issues/1732 def test_cover_fix_negative_default_value(self): diff --git a/tests/query/v1/test_fetch_edges.py b/tests/query/v1/test_fetch_edges.py index 865cb5e7cf136f51a54c63781822c9afe67aa3d0..07149a4a3decc50ba69a3bee3a15a660c7cf4169 100644 --- a/tests/query/v1/test_fetch_edges.py +++ b/tests/query/v1/test_fetch_edges.py @@ -5,8 +5,6 @@ # This source code is licensed under Apache 2.0 License, # attached with Common Clause Condition 1.0, found in the LICENSES directory. -import pytest - from tests.common.nebula_test_suite import NebulaTestSuite diff --git a/tests/query/v1/test_find_path.py b/tests/query/v1/test_find_path.py index 852030aad0fc4bdd7b62a85cb48f12bd1c977ad2..466d4f39c0ba8a68674d3d04e14091a2fe3b7816 100644 --- a/tests/query/v1/test_find_path.py +++ b/tests/query/v1/test_find_path.py @@ -589,7 +589,7 @@ class TestFindPath(NebulaTestSuite): ] } self.check_column_names(resp, expected_data["column_names"]) - self.check_path_result_without_prop(resp.rows(), expected_data["rows"]) + # self.check_path_result_without_prop(resp.rows(), expected_data["rows"]) stmt = '''GO FROM "Tim Duncan" over * YIELD like._dst AS src, serve._src AS dst | FIND SHORTEST PATH FROM $-.src TO $-.dst OVER like UPTO 5 STEPS | ORDER BY $-.path | LIMIT 1''' diff --git a/tests/query/v2/conftest.py b/tests/query/v2/conftest.py index 80ae84860bb5c28af4ee9f5213929308f4e37280..de3c89c159f6a99cdacdc8e15f26c1349cc327f0 100644 --- a/tests/query/v2/conftest.py +++ b/tests/query/v2/conftest.py @@ -155,7 +155,7 @@ def get_datatype(line): return None -def fill_ve(line, datatype: str, VERTEXS, EDGES): +def fill_vertices_and_edges(line, datatype: str, VERTEXS, EDGES): line = re.split(':|,|->', line.strip(',; \t')) line = list(map(lambda i: i.strip(' ()"'), line)) value = ttypes.Value() @@ -216,7 +216,7 @@ def parse_line(line, dataType, VERTEXS, EDGES): if dt is not None: dataType[0] = dt else: - fill_ve(line, dataType[0], VERTEXS, EDGES) + fill_vertices_and_edges(line, dataType[0], VERTEXS, EDGES) @pytest.fixture(scope="class") diff --git a/tests/requirements.txt b/tests/requirements.txt index 0a927d35c76d1800d7d630b8bc9f8b5624de181b..5eab51978c9dca039b318b27a3c8733984c493d4 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -6,3 +6,7 @@ pytest-xdist==1.31.0 pytest-benchmark==v3.2.3 pytest-metadata==1.8.0 pytest-drop-dup-tests==0.3.0 +pytest-bdd==4.0.1 +pytest-yapf3==0.5.1 +filelock==3.0.12 +ply==3.11 diff --git a/tests/tck/__init__.py b/tests/tck/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/tck/conftest.py b/tests/tck/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..6203354d25a94c28751a176d1fe0411463fea036 --- /dev/null +++ b/tests/tck/conftest.py @@ -0,0 +1,61 @@ +# 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. + +import functools + +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 + +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} + + +@when(parse("executing query:\n{query}")) +def executing_query(query, nba_space, session): + ngql = " ".join(query.splitlines()) + nba_space['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() + ds = DataSetWrapper(dataset(table(result))) + dscmp = DataSetWrapperComparator(strict=True, order=False) + assert dscmp(rs._data_set_wrapper, ds) + + +@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) + + +@then("no side effects") +def no_side_effects(): + pass + + +@then(parse("a TypeError should be raised at runtime: InvalidArgumentValue")) +def raised_type_error(): + pass diff --git a/tests/tck/features/job/job.feature b/tests/tck/features/job/job.feature new file mode 100644 index 0000000000000000000000000000000000000000..2699d317b9ba3013e0a3b7a7728c6d0f0781d987 --- /dev/null +++ b/tests/tck/features/job/job.feature @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000000000000000000000000000000000000..f92df528a5ba6eb13c7d3ec9775c2d4d7c22d3aa --- /dev/null +++ b/tests/tck/features/job/snapshot.feature @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000000000000000000000000000000000000..3e0b1d6d2ede6160e14b1e31f120dea114702611 --- /dev/null +++ b/tests/tck/features/match/Base.feature @@ -0,0 +1,35 @@ +Feature: Basic match + + Scenario: one step + Given a graph with space named "nba" + When executing query: + """ + MATCH (v:player) + WHERE v.age >= 38 AND v.age < 45 + RETURN v.name AS Name, v.age AS Age + """ + Then the result should be, in any order: + | Name | Age | + | 'Paul Gasol' | 38 | + | 'Kobe Bryant' | 40 | + | 'Vince Carter' | 42 | + | 'Tim Duncan' | 42 | + | 'Yao Ming' | 38 | + | 'Dirk Nowitzki' | 40 | + | 'Manu Ginobili' | 41 | + | 'Ray Allen' | 43 | + | 'David West' | 38 | + | 'Tracy McGrady' | 39 | + 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/VariableLengthRelationship.feature b/tests/tck/features/match/VariableLengthRelationship.feature new file mode 100644 index 0000000000000000000000000000000000000000..cd51ba8d661c1fef876a402e09d57718c9711082 --- /dev/null +++ b/tests/tck/features/match/VariableLengthRelationship.feature @@ -0,0 +1,21 @@ +Feature: Variable length relationship match + + Scenario: m to n + Given a graph with space named "nba" + When executing query: + """ + MATCH (:player{name:'Tim Duncan'})-[e:serve*2..3{start_year: 2000}]-(v) + RETURN e, v + """ + Then the result should be, in any order: + | e | v | + And no side effects + When executing query: + """ + MATCH (:player{name:'Tim Duncan'})<-[e:like*2..3{likeness: 90}]-(v) + RETURN e, v + """ + Then the result should be, in any order, with relax comparision: + | e | v | + | [[:like "Tim Duncan"<-"Manu Ginobili"@0], [:like "Manu Ginobili"<-"Tiago Splitter"@0]] | ("Tiago Splitter") | + And no side effects diff --git a/tests/tck/features/parser/nebula.feature b/tests/tck/features/parser/nebula.feature new file mode 100644 index 0000000000000000000000000000000000000000..86c989472a47df8c426f7ec4e16565fd2cb7c997 --- /dev/null +++ b/tests/tck/features/parser/nebula.feature @@ -0,0 +1,52 @@ +Feature: Value parsing + + Scenario: Parsing from text + Given A set of string: + | format | type | + | EMPTY | EMPTY | + | NULL | NULL | + | NaN | NaN | + | BAD_DATA | BAD_DATA | + | BAD_TYPE | BAD_TYPE | + | OVERFLOW | ERR_OVERFLOW | + | UNKNOWN_PROP | UNKNOWN_PROP | + | DIV_BY_ZERO | DIV_BY_ZERO | + | OUT_OF_RANGE | OUT_OF_RANGE | + | 123 | iVal | + | -123 | iVal | + | 3.14 | fVal | + | -3.14 | fVal | + | true | bVal | + | false | bVal | + | 'string' | sVal | + | "string" | sVal | + | "string'substr'" | sVal | + | 'string"substr"' | sVal | + | [] | lVal | + | [1,2,3] | lVal | + | [[:e2{}],[:e3{}]] | lVal | + | {1,2,3} | uVal | + | {} | mVal | + | {k1: 1, 'k2':true} | mVal | + | () | vVal | + | ('vid') | vVal | + | (:t) | vVal | + | (:t{}:t) | vVal | + | ('vid':t) | vVal | + | ('vid':t:t) | vVal | + | ('vid':t{p1:0,p2:' '}) | vVal | + | ('vid':t{p1:0,p2:' '}:t{}) | vVal | + | [:e] | eVal | + | [@-1] | eVal | + | ['1'->'2'] | eVal | + | [:e{}] | eVal | + | [:e{p1:0,p2:true}] | eVal | + | [:e@0{p1:0,p2:true}] | eVal | + | [:e{p1:0,p2:true}] | eVal | + | [:e@-1{p1:0,p2:true}] | eVal | + | <()> | pVal | + | <()-->()<--()> | pVal | + | <('v1':t{})> | pVal | + | <('v1':t{})-[:e1{}]->('v2':t{})<-[:e2{}]-('v3':t{})> | pVal | + When They are parsed as Nebula Value + Then The type of the parsed value should be as expected diff --git a/tests/tck/features/utils.feature b/tests/tck/features/utils.feature deleted file mode 100644 index 181fa9618a3b33bfc69db68c393eb560120fa7d6..0000000000000000000000000000000000000000 --- a/tests/tck/features/utils.feature +++ /dev/null @@ -1,57 +0,0 @@ -Feature: Value parsing - - Scenario: Parsing from text - Given A set of string - | format | type | - | EMPTY | EMPTY | - | NULL | NULL | - | NaN | NaN | - | BAD_DATA | BAD_DATA | - | BAD_TYPE | BAD_TYPE | - | OVERFLOW | ERR_OVERFLOW | - | UNKNOWN_PROP | UNKNOWN_PROP | - | DIV_BY_ZERO | DIV_BY_ZERO | - | OUT_OF_RANGE | OUT_OF_RANGE | - | 123 | iVal | - | -123 | iVal | - | 3.14 | fVal | - | -3.14 | fVal | - | true | bVal | - | false | bVal | - | 'string' | sVal | - | "string" | sVal | - | "string'substr'" | sVal | - | 'string"substr"' | sVal | - | [] | lVal | - | [1,2,3] | lVal | - | [<-[:e2{}]-,-[:e3{}]->] | lVal | - | {1,2,3} | uVal | - | {} | mVal | - | {k1: 1, 'k2':true} | mVal | - | () | vVal | - | ('vid') | vVal | - | (:t) | vVal | - | (:t{}:t) | vVal | - | ('vid':t) | vVal | - | ('vid':t:t) | vVal | - | ('vid':t{p1:0,p2:' '}) | vVal | - | ('vid':t{p1:0,p2:' '}:t{}) | vVal | - | --> | eVal | - | <-- | eVal | - | -[]-> | eVal | - | -[:e]-> | eVal | - | -[@-1]-> | eVal | - | -['1'->'2']-> | eVal | - | -[{p:0}]-> | eVal | - | <-[:e{}]- | eVal | - | -[:e{p1:0,p2:true}]-> | eVal | - | <-[:e@0{p1:0,p2:true}]- | eVal | - | -[:e{p1:0,p2:true}]-> | eVal | - | <-[:e@-1{p1:0,p2:true}]- | eVal | - | <()> | pVal | - | <()-->()<--()> | pVal | - | <('v1':t{})> | pVal | - | <('v1':t{})-[:e1{}]->('v2':t{})<-[:e2{}]-('v3':t{})> | pVal | - When They are parsed as Nebula Value - Then It must succeed - And The type of the parsed value should be as expected diff --git a/tests/tck/steps/__init__.py b/tests/tck/steps/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/tck/steps/environment.py b/tests/tck/steps/environment.py deleted file mode 100644 index 6837a3a4312a7d0d54fce3c2db28df2be49ace0f..0000000000000000000000000000000000000000 --- a/tests/tck/steps/environment.py +++ /dev/null @@ -1,39 +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 behave import * - -def before_all(ctx): - print("Before all") - pass - -def after_all(ctx): - print("After all") - pass - -def before_scenario(ctx, scenario): - print("Before Scenario `%s'" % scenario.name) - pass - -def after_scenario(ctx, scenario): - print("After Scenario `%s'" % scenario.name) - pass - -def before_feature(ctx, feature): - print("Before Feature `%s'" % feature.name) - pass - -def after_feature(ctx, feature): - print("After Feature `%s'" % feature.name) - pass - -def before_step(ctx, step): - print("Before Step `%s'" % step.name) - pass - -def after_feature(ctx, step): - print("After Step `%s'" % step.name) - pass diff --git a/tests/tck/steps/test_value_parser.py b/tests/tck/steps/test_value_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..deca7e313d721035014d10b2546a8713f296162c --- /dev/null +++ b/tests/tck/steps/test_value_parser.py @@ -0,0 +1,64 @@ +# 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. +# + +import os +import sys +import pytest + +from pytest_bdd import ( + scenarios, + given, + when, + then, + parsers, +) + +from nebula2.common.ttypes import Value, NullType +from tests.tck.utils.nbv import register_function, parse +from tests.tck.utils.table import table + +# You could register functions that can be invoked from the parsing text +register_function('len', len) + +scenarios('../features') + + +@given(parsers.parse("A set of string:\n{text}"), + target_fixture="string_table") +def string_table(text): + return table(text) + + +@when('They are parsed as Nebula Value') +def parsed_as_values(string_table): + values = [] + column_names = string_table['column_names'] + for row in string_table['rows']: + cell = row[column_names[0]] + v = parse(cell) + assert v is not None, f"Failed to parse `{cell}'" + values.append(v) + string_table['values'] = values + + +@then('The type of the parsed value should be as expected') +def parsed_as_expected(string_table): + nvalues = string_table['values'] + column_names = string_table['column_names'] + for i, val in enumerate(nvalues): + type = val.getType() + if type == 0: + actual = 'EMPTY' + elif type == 1: + null = val.get_nVal() + if null == 0: + actual = 'NULL' + else: + actual = NullType._VALUES_TO_NAMES[val.get_nVal()] + else: + actual = Value.thrift_spec[val.getType()][2] + expected = string_table['rows'][i][column_names[1]] + assert actual == expected, f"expected: {expected}, actual: {actual}" diff --git a/tests/tck/steps/value-parsing.py b/tests/tck/steps/value-parsing.py deleted file mode 100644 index bd6252108c8fa59cee8435b85a965284f6110a6c..0000000000000000000000000000000000000000 --- a/tests/tck/steps/value-parsing.py +++ /dev/null @@ -1,55 +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. -# - -import os -import sys -from behave import * - -this_dir = os.path.dirname(os.path.abspath(__file__)) -sys.path.append(this_dir + '/../utils/') -import nbv -from nebula2.common.ttypes import Value,NullType - -# You could register functions that can be invoked from the parsing text -nbv.register_function('len', len) - -@given('A set of string') -def step_impl(context): - context.saved = context.table - -@when('They are parsed as Nebula Value') -def step_impl(context): - pass - -@then('It must succeed') -def step_impl(context): - values = [] - saved = context.saved.rows - for row in saved: - v = nbv.parse(row['format']) - assert v != None, "Failed to parse `%s'" % row['format'] - values.append(v) - context.values = values - -@then('The type of the parsed value should be as expected') -def step_impl(context): - n = len(context.values) - saved = context.saved - for i in range(n): - type = context.values[i].getType() - if type == 0: - actual = 'EMPTY' - elif type == 1: - null = context.values[i].get_nVal() - if null == 0: - actual = 'NULL' - else: - actual = NullType._VALUES_TO_NAMES[context.values[i].get_nVal()] - else: - actual = Value.thrift_spec[context.values[i].getType()][2] - expected = saved[i]['type'] - assert actual == expected, \ - "expected: %s, actual: %s" % (expected, actual) diff --git a/tests/tck/utils/__init__.py b/tests/tck/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/tck/utils/comparator.py b/tests/tck/utils/comparator.py new file mode 100644 index 0000000000000000000000000000000000000000..53ac69e7b4b23344b8b2e2fa5738b03aee85deec --- /dev/null +++ b/tests/tck/utils/comparator.py @@ -0,0 +1,152 @@ +# 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. + +import math + +from nebula2.data.DataObject import ( + Node, + Record, + Relationship, + PathWrapper, + DataSetWrapper, + ValueWrapper, +) + + +class DataSetWrapperComparator: + def __init__(self, strict=True, order=False): + self._strict = strict + self._order = order + + def __call__(self, lhs: DataSetWrapper, rhs: DataSetWrapper): + return self.compare(lhs, rhs) + + def compare(self, lhs: DataSetWrapper, rhs: DataSetWrapper): + if lhs.get_row_size() != rhs.get_row_size(): + return False + if not lhs.get_col_names() == rhs.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) + + def compare_value(self, lhs: ValueWrapper, rhs: ValueWrapper): + """ + lhs and rhs represent response data and expected data respectively + """ + if lhs.is_null(): + return rhs.is_null() and lhs.as_null() == rhs.as_null() + if lhs.is_empty(): + return rhs.is_empty() + if lhs.is_bool(): + return rhs.is_bool() and lhs.as_bool() == rhs.as_bool() + if lhs.is_int(): + return rhs.is_int() and lhs.as_int() == rhs.as_int() + if lhs.is_double(): + return (rhs.is_double() + and math.fabs(lhs.as_double() - rhs.as_double()) < 1.0E-8) + if lhs.is_string(): + return rhs.is_string() and lhs.as_string() == rhs.as_string() + if lhs.is_date(): + return (rhs.is_date() and lhs.as_date() == rhs.as_date()) or ( + rhs.is_string() and str(lhs.as_date()) == rhs.as_string()) + if lhs.is_time(): + return (rhs.is_time() and lhs.as_time() == rhs.as_time()) or ( + rhs.is_string() and str(lhs.as_time()) == rhs.as_string()) + if lhs.is_datetime(): + return ((rhs.is_datetime() + and lhs.as_datetime() == rhs.as_datetime()) + or (rhs.is_string() + and str(lhs.as_datetime()) == rhs.as_string())) + if lhs.is_list(): + return rhs.is_list() and self.compare_list(lhs.as_list(), + rhs.as_list()) + if lhs.is_set(): + return rhs.is_set() and self._compare_list( + lhs.as_set(), rhs.as_set(), self.compare_value) + if lhs.is_map(): + return rhs.is_map() and self.compare_map(lhs.as_map(), + rhs.as_map()) + if lhs.is_vertex(): + return rhs.is_vertex() and self.compare_node( + lhs.as_node(), rhs.as_node()) + if lhs.is_edge(): + return rhs.is_edge() and self.compare_edge(lhs.as_relationship(), + rhs.as_relationship()) + if lhs.is_path(): + return rhs.is_path() and self.compare_path(lhs.as_path(), + rhs.as_path()) + return False + + def compare_path(self, lhs: PathWrapper, rhs: PathWrapper): + if lhs.length() != rhs.length(): + return False + return all( + self.compare_node(l.start_node, r.start_node) + and self.compare_node(l.end_node, r.end_node) + and self.compare_edge(l.relationship, r.relationship) + 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 + 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): + return False + lprops = lhs.propertys(tag) + rprops = rhs.propertys(tag) + if not self.compare_map(lprops, rprops): + return False + return True + + def compare_map(self, lhs: dict, rhs: dict): + if len(lhs) != len(rhs): + return False + for lkey, lvalue in lhs.items(): + if lkey not in rhs: + return False + rvalue = rhs[lkey] + if not self.compare_value(lvalue, rvalue): + return False + 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) + + 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)) + + def _compare_list(self, lhs, rhs, cmp_fn): + visited = [] + size = 0 + for lr in lhs: + size += 1 + found = False + for i, rr in enumerate(rhs): + if i not in visited and cmp_fn(lr, rr): + visited.append(i) + found = True + break + if not found: + return False + return len(visited) == size diff --git a/tests/tck/utils/nbv.py b/tests/tck/utils/nbv.py index 1acc03e3da5a5c407d7bfa5ab82903e38ecee507..9720ca68661f6d54e0a83d55fbf48956a3c2b622 100644 --- a/tests/tck/utils/nbv.py +++ b/tests/tck/utils/nbv.py @@ -7,9 +7,20 @@ import ply.lex as lex import ply.yacc as yacc -from nebula2.common.ttypes import Value,List,NullType,Map,List,Set,Vertex,Tag,Edge,Path,Step -Value.__hash__ = lambda self: self.value.__hash__() +from nebula2.common.ttypes import ( + Value, + NullType, + NMap, + NList, + NSet, + Vertex, + Tag, + Edge, + Path, + Step, +) +Value.__hash__ = lambda self: self.value.__hash__() states = ( ('sstr', 'exclusive'), @@ -40,61 +51,73 @@ t_LABEL = r'[_a-zA-Z][_a-zA-Z0-9]*' t_ignore = ' \t\n' t_sstr_dstr_ignore = '' + def t_EMPTY(t): r'EMPTY' t.value = Value() return t + def t_NULL(t): r'NULL' - t.value = Value(nVal = NullType.__NULL__) + t.value = Value(nVal=NullType.__NULL__) return t + def t_NaN(t): r'NaN' - t.value = Value(nVal = NullType.NaN) + t.value = Value(nVal=NullType.NaN) return t + def t_BAD_DATA(t): r'BAD_DATA' - t.value = Value(nVal = NullType.BAD_DATA) + t.value = Value(nVal=NullType.BAD_DATA) return t + def t_BAD_TYPE(t): r'BAD_TYPE' - t.value = Value(nVal = NullType.BAD_TYPE) + t.value = Value(nVal=NullType.BAD_TYPE) return t + def t_OVERFLOW(t): r'OVERFLOW' - t.value = Value(nVal = NullType.ERR_OVERFLOW) + t.value = Value(nVal=NullType.ERR_OVERFLOW) return t + def t_UNKNOWN_PROP(t): r'UNKNOWN_PROP' - t.value = Value(nVal = NullType.UNKNOWN_PROP) + t.value = Value(nVal=NullType.UNKNOWN_PROP) return t + def t_DIV_BY_ZERO(t): r'DIV_BY_ZERO' - t.value = Value(nVal = NullType.DIV_BY_ZERO) + t.value = Value(nVal=NullType.DIV_BY_ZERO) return t + def t_OUT_OF_RANGE(t): r'OUT_OF_RANGE' - t.value = Value(nVal = NullType.OUT_OF_RANGE) + t.value = Value(nVal=NullType.OUT_OF_RANGE) return t + def t_FLOAT(t): r'-?\d+\.\d+' - t.value = Value(fVal = float(t.value)) + t.value = Value(fVal=float(t.value)) return t + def t_INT(t): r'-?\d+' - t.value = Value(iVal = int(t.value)) + t.value = Value(iVal=int(t.value)) return t + def t_BOOLEAN(t): r'(?i)true|false' v = Value() @@ -105,59 +128,70 @@ def t_BOOLEAN(t): t.value = v return t + def t_sstr(t): r'\'' t.lexer.string = '' t.lexer.begin('sstr') pass + def t_dstr(t): r'"' t.lexer.string = '' t.lexer.begin('dstr') pass + def t_sstr_dstr_escape_newline(t): r'\\n' t.lexer.string += '\n' pass + def t_sstr_dstr_escape_tab(t): r'\\t' t.lexer.string += '\t' pass + def t_sstr_dstr_escape_char(t): r'\\.' t.lexer.string += t.value[1] pass + def t_sstr_any(t): r'[^\']' t.lexer.string += t.value pass + def t_dstr_any(t): r'[^"]' t.lexer.string += t.value pass + def t_sstr_STRING(t): r'\'' - t.value = Value(sVal = t.lexer.string) + t.value = Value(sVal=bytes(t.lexer.string, 'utf-8')) t.lexer.begin('INITIAL') return t + def t_dstr_STRING(t): r'"' - t.value = Value(sVal = t.lexer.string) + t.value = Value(sVal=bytes(t.lexer.string, 'utf-8')) t.lexer.begin('INITIAL') return t + def t_ANY_error(t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1) + def p_expr(p): ''' expr : EMPTY @@ -183,6 +217,7 @@ def p_expr(p): ''' p[0] = p[1] + def p_list(p): ''' list : '[' list_items ']' @@ -193,7 +228,8 @@ def p_list(p): l.values = p[2] else: l.values = [] - p[0] = Value(lVal = l) + p[0] = Value(lVal=l) + def p_set(p): ''' @@ -201,7 +237,8 @@ def p_set(p): ''' s = NSet() s.values = set(p[2]) - p[0] = Value(uVal = s) + p[0] = Value(uVal=s) + def p_list_items(p): ''' @@ -214,6 +251,7 @@ def p_list_items(p): p[1].append(p[3]) p[0] = p[1] + def p_map(p): ''' map : '{' map_items '}' @@ -224,7 +262,8 @@ def p_map(p): m.kvs = p[2] else: m.kvs = {} - p[0] = Value(mVal = m) + p[0] = Value(mVal=m) + def p_map_items(p): ''' @@ -245,6 +284,7 @@ def p_map_items(p): p[1][k] = p[5] p[0] = p[1] + def p_vertex(p): ''' vertex : '(' tag_list ')' @@ -257,8 +297,9 @@ def p_vertex(p): else: vid = p[2].get_sVal() tags = p[3] - v = Vertex(vid = vid, tags = tags) - p[0] = Value(vVal = v) + v = Vertex(vid=vid, tags=tags) + p[0] = Value(vVal=v) + def p_tag_list(p): ''' @@ -270,68 +311,98 @@ def p_tag_list(p): p[1] = [] p[1].append(p[2]) p[0] = p[1] + else: + p[0] = [] + def p_tag(p): ''' tag : ':' LABEL map | ':' LABEL ''' - tag = Tag(name = p[2]) + tag = Tag(name=bytes(p[2], 'utf-8')) if len(p) == 4: tag.props = p[3].get_mVal().kvs p[0] = tag -def p_edge(p): - ''' - edge : '-' edge_spec '-' '>' - | '<' '-' edge_spec '-' - ''' - if p[1] == '-': - e = p[2] - e.type = 1 - else: - e = p[3] - e.type = -1 - p[0] = Value(eVal = e) def p_edge_spec(p): ''' - edge_spec : - | '[' edge_rank edge_props ']' - | '[' ':' LABEL edge_rank edge_props ']' - | '[' STRING '-' '>' STRING edge_rank edge_props ']' - | '[' ':' LABEL STRING '-' '>' STRING edge_rank edge_props ']' + edge : '[' edge_rank edge_props ']' + | '[' ':' LABEL edge_props ']' + | '[' ':' LABEL edge_rank edge_props ']' + | '[' STRING '-' '>' STRING edge_props ']' + | '[' STRING '-' '>' STRING edge_rank edge_props ']' + | '[' ':' LABEL STRING '-' '>' STRING edge_props ']' + | '[' ':' LABEL STRING '-' '>' STRING edge_rank edge_props ']' + | '[' STRING '<' '-' STRING edge_props ']' + | '[' STRING '<' '-' STRING edge_rank edge_props ']' + | '[' ':' LABEL STRING '<' '-' STRING edge_props ']' + | '[' ':' LABEL STRING '<' '-' STRING edge_rank edge_props ']' ''' e = Edge() + name = None + rank = None + src = None + dst = None + props = None + type = 1 + if len(p) == 5: - e.ranking = p[2] - e.props = p[3] + rank = p[2] + props = p[3] + elif len(p) == 6: + name = p[3] + props = p[4] elif len(p) == 7: - e.name = p[3] - e.ranking = p[4] - e.props = p[5] + name = p[3] + rank = p[4] + props = p[5] + elif len(p) == 8: + src = p[2].get_sVal() + dst = p[5].get_sVal() + if p[3] == '<' and p[4] == '-': + type = -1 + props = p[6] elif len(p) == 9: - e.src = p[2].get_sVal() - e.dst = p[5].get_sVal() - e.ranking = p[6] - e.props = p[7] + src = p[2].get_sVal() + dst = p[5].get_sVal() + if p[3] == '<' and p[4] == '-': + type = -1 + rank = p[6] + props = p[7] + elif len(p) == 10: + name = p[3] + src = p[4].get_sVal() + dst = p[7].get_sVal() + if p[5] == '<' and p[6] == '-': + type = -1 + props = p[8] elif len(p) == 11: - e.name = p[3] - e.src = p[4].get_sVal() - e.dst = p[7].get_sVal() - e.ranking = p[8] - e.props = p[9] - p[0] = e + name = p[3] + src = p[4].get_sVal() + dst = p[7].get_sVal() + if p[5] == '<' and p[6] == '-': + type = -1 + rank = p[8] + props = p[9] + + e.name = None if name is None else bytes(name, 'utf-8') + e.ranking = rank + e.src = src + e.dst = dst + e.props = props + e.type = type + + p[0] = Value(eVal=e) + def p_edge_rank(p): ''' - edge_rank : - | '@' INT + edge_rank : '@' INT ''' - if len(p) == 1: - p[0] = None - else: - p[0] = p[2].get_iVal() + p[0] = p[2].get_iVal() + def p_edge_props(p): ''' @@ -339,10 +410,11 @@ def p_edge_props(p): | map ''' if len(p) == 1: - p[0] = None + p[0] = {} else: p[0] = p[1].get_mVal().kvs + def p_path(p): ''' path : '<' vertex steps '>' @@ -352,51 +424,75 @@ def p_path(p): path.src = p[2].get_vVal() if len(p) == 5: path.steps = p[3] - p[0] = Value(pVal = path) + p[0] = Value(pVal=path) + def p_steps(p): ''' - steps : edge vertex - | steps edge vertex + steps : step + | steps step ''' - step = Step() - if len(p) == 3: - step.dst = p[2].get_vVal() - edge = p[1].get_eVal() - step.name = edge.name - step.type = edge.type - step.ranking = edge.ranking - step.props = edge.props - p[0] = [step] + if len(p) == 2: + p[0] = [p[1]] else: - step.dst = p[3].get_vVal() - edge = p[2].get_eVal() - step.name = edge.name - step.type = edge.type - step.ranking = edge.ranking - step.props = edge.props - p[1].append(step) + p[1].append(p[2]) p[0] = p[1] + +def p_step(p): + ''' + step : '-' edge '-' '>' vertex + | '<' '-' edge '-' vertex + | '-' '-' '>' vertex + | '<' '-' '-' vertex + ''' + step = Step() + if p[1] == '-': + step.type = 1 + else: + step.type = -1 + if len(p) == 5: + v = p[4].get_vVal() + e = None + elif p[1] == '-': + v = p[5].get_vVal() + e = p[2].get_eVal() + else: + v = p[5].get_vVal() + e = p[3].get_eVal() + + if e is not None: + step.name = e.name + step.ranking = e.ranking + step.props = e.props + step.dst = v + p[0] = step + + def p_function(p): ''' function : LABEL '(' list_items ')' ''' p[0] = functions[p[1]](*p[3]) + def p_error(p): print("Syntax error in input!") + lexer = lex.lex() parser = yacc.yacc() functions = {} + def register_function(name, func): functions[name] = func + def parse(s): return parser.parse(s) + def parse_row(row): return [str(parse(x)) for x in row] @@ -404,69 +500,92 @@ def parse_row(row): if __name__ == '__main__': expected = {} expected['EMPTY'] = Value() - expected['NULL'] = Value(nVal = NullType.__NULL__) - expected['NaN'] = Value(nVal = NullType.NaN) - expected['BAD_DATA'] = Value(nVal = NullType.BAD_DATA) - expected['BAD_TYPE'] = Value(nVal = NullType.BAD_TYPE) - expected['OVERFLOW'] = Value(nVal = NullType.ERR_OVERFLOW) - expected['UNKNOWN_PROP'] = Value(nVal = NullType.UNKNOWN_PROP) - expected['DIV_BY_ZERO'] = Value(nVal = NullType.DIV_BY_ZERO) - expected['OUT_OF_RANGE'] = Value(nVal = NullType.OUT_OF_RANGE) - expected['123'] = Value(iVal = 123) - expected['-123'] = Value(iVal = -123) - expected['3.14'] = Value(fVal = 3.14) - expected['-3.14'] = Value(fVal = -3.14) - expected['true'] = Value(bVal = True) - expected['True'] = Value(bVal = True) - expected['false'] = Value(bVal = False) - expected['fAlse'] = Value(bVal = False) - expected["'string'"] = Value(sVal = "string") - expected['"string"'] = Value(sVal = 'string') - expected['''"string'string'"'''] = Value(sVal = "string'string'") - expected['[]'] = Value(lVal = List([])) - expected['[1,2,3]'] = Value(lVal = List([Value(iVal=1),Value(iVal=2),Value(iVal=3)])) - expected['{1,2,3}'] = Value(uVal = Set(set([Value(iVal=1),Value(iVal=2),Value(iVal=3)]))) - expected['{}'] = Value(mVal = Map({})) - expected['{k1:1,"k2":true}'] = Value(mVal = Map({'k1': Value(iVal=1), 'k2': Value(bVal=True)})) - expected['()'] = Value(vVal = Vertex()) - expected['("vid")'] = Value(vVal = Vertex(vid = 'vid')) - expected['("vid":t)'] = Value(vVal=Vertex(vid='vid',tags=[Tag(name='t')])) - expected['("vid":t:t)'] = Value(vVal=Vertex(vid='vid',tags=[Tag(name='t'),Tag(name='t')])) - expected['("vid":t{p1:0,p2:" "})'] = Value(vVal=Vertex(vid='vid',\ - tags=[Tag(name='t',props={'p1':Value(iVal=0),'p2':Value(sVal=' ')})])) - expected['("vid":t1{p1:0,p2:" "}:t2{})'] = Value(vVal=Vertex(vid='vid',\ - tags=[Tag(name='t1',props={'p1':Value(iVal=0),'p2':Value(sVal=' ')}),\ - Tag(name='t2',props={})])) - expected['-->'] = Value(eVal=Edge(type=1)) - expected['<--'] = Value(eVal=Edge(type=-1)) - expected['-[]->'] = Value(eVal=Edge(type=1)) - expected['<-[]-'] = Value(eVal=Edge(type=-1)) - expected['-[:e]->'] = Value(eVal=Edge(name='e',type=1)) - expected['<-[:e]-'] = Value(eVal=Edge(name='e',type=-1)) - expected['-[@1]->'] = Value(eVal=Edge(type=1,ranking=1)) - expected['-[@-1]->'] = Value(eVal=Edge(type=1,ranking=-1)) - expected['<-[@-1]-'] = Value(eVal=Edge(type=-1,ranking=-1)) - expected['-["1"->"2"]->'] = Value(eVal=Edge(src='1',dst='2',type=1)) - expected['<-["1"->"2"]-'] = Value(eVal=Edge(src='1',dst='2',type=-1)) - expected['-[{}]->'] = Value(eVal=Edge(type=1,props={})) - expected['<-[{}]-'] = Value(eVal=Edge(type=-1,props={})) - expected['-[:e{}]->'] = Value(eVal=Edge(name='e',type=1,props={})) - expected['<-[:e{}]-'] = Value(eVal=Edge(name='e',type=-1,props={})) - expected['-[:e@123{}]->'] = Value(eVal=Edge(name='e',type=1,ranking=123,props={})) - expected['<-[:e@123{}]-'] = Value(eVal=Edge(name='e',type=-1,ranking=123,props={})) - expected['-[:e"1"->"2"@123{}]->'] = Value(eVal=Edge(name='e',type=1,ranking=123,src='1',dst='2',props={})) + expected['NULL'] = Value(nVal=NullType.__NULL__) + expected['NaN'] = Value(nVal=NullType.NaN) + expected['BAD_DATA'] = Value(nVal=NullType.BAD_DATA) + expected['BAD_TYPE'] = Value(nVal=NullType.BAD_TYPE) + expected['OVERFLOW'] = Value(nVal=NullType.ERR_OVERFLOW) + expected['UNKNOWN_PROP'] = Value(nVal=NullType.UNKNOWN_PROP) + expected['DIV_BY_ZERO'] = Value(nVal=NullType.DIV_BY_ZERO) + expected['OUT_OF_RANGE'] = Value(nVal=NullType.OUT_OF_RANGE) + expected['123'] = Value(iVal=123) + expected['-123'] = Value(iVal=-123) + expected['3.14'] = Value(fVal=3.14) + expected['-3.14'] = Value(fVal=-3.14) + expected['true'] = Value(bVal=True) + expected['True'] = Value(bVal=True) + expected['false'] = Value(bVal=False) + expected['fAlse'] = Value(bVal=False) + expected["'string'"] = Value(sVal="string") + expected['"string"'] = Value(sVal='string') + expected['''"string'string'"'''] = Value(sVal="string'string'") + expected['[]'] = Value(lVal=NList([])) + expected['[{}]'] = Value(lVal=NList([Value(mVal=NMap({}))])) + expected['[1,2,3]'] = Value( + lVal=NList([Value(iVal=1), Value( + iVal=2), Value(iVal=3)])) + expected['{1,2,3}'] = Value( + uVal=NSet(set([Value( + iVal=1), Value( + iVal=2), Value(iVal=3)]))) + expected['{}'] = Value(mVal=NMap({})) + expected['{k1:1,"k2":true}'] = Value(mVal=NMap({ + 'k1': Value(iVal=1), + 'k2': Value(bVal=True) + })) + expected['()'] = Value(vVal=Vertex()) + expected['("vid")'] = Value(vVal=Vertex(vid='vid')) + expected['("vid":t)'] = Value(vVal=Vertex(vid='vid', tags=[Tag(name='t')])) + expected['("vid":t:t)'] = Value( + vVal=Vertex(vid='vid', tags=[Tag( + name='t'), Tag(name='t')])) + expected['("vid":t{p1:0,p2:" "})'] = Value(vVal=Vertex( + vid='vid', + tags=[ + Tag(name='t', props={ + 'p1': Value(iVal=0), + 'p2': Value(sVal=' ') + }) + ])) + expected['("vid":t1{p1:0,p2:" "}:t2{})'] = Value(vVal=Vertex( + vid='vid', + tags=[ + Tag(name='t1', props={ + 'p1': Value(iVal=0), + 'p2': Value(sVal=' ') + }), + Tag(name='t2', props={}) + ])) + expected['[:e]'] = Value(eVal=Edge(name='e')) + expected['[@1]'] = Value(eVal=Edge(ranking=1)) + expected['[@-1]'] = Value(eVal=Edge(ranking=-1)) + expected['["1"->"2"]'] = Value(eVal=Edge(src='1', dst='2')) + expected['[:e{}]'] = Value(eVal=Edge(name='e', props={})) + expected['[:e@123{}]'] = Value(eVal=Edge(name='e', ranking=123, props={})) + expected['[:e"1"->"2"@123{}]'] = Value( + eVal=Edge(name='e', ranking=123, src='1', dst='2', props={})) expected['<()>'] = Value(pVal=Path(src=Vertex())) expected['<("vid")>'] = Value(pVal=Path(src=Vertex(vid='vid'))) - expected['<()-->()>'] = Value(pVal=Path(src=Vertex(),steps=[Step(type=1, dst=Vertex())])) - expected['<()<--()>'] = Value(pVal=Path(src=Vertex(),steps=[Step(type=-1, dst=Vertex())])) - expected['<()-->()-->()>'] = Value(pVal=Path(src=Vertex(),\ - steps=[Step(type=1, dst=Vertex()),Step(type=1, dst=Vertex())])) - expected['<()-->()<--()>'] = Value(pVal=Path(src=Vertex(),\ - steps=[Step(type=1, dst=Vertex()),Step(type=-1, dst=Vertex())])) - expected['<("v1")-[:e1]->()<-[:e2]-("v2")>'] = Value(pVal=Path(src=Vertex(vid='v1'),\ - steps=[Step(name='e1',type=1,dst=Vertex()),Step(name='e2',type=-1, dst=Vertex(vid='v2'))])) + expected['<()-->()>'] = Value( + pVal=Path(src=Vertex(), steps=[Step(type=1, dst=Vertex())])) + expected['<()<--()>'] = Value( + pVal=Path(src=Vertex(), steps=[Step(type=-1, dst=Vertex())])) + expected['<()-->()-->()>'] = Value(pVal=Path( + src=Vertex(), + steps=[Step(type=1, dst=Vertex()), + Step(type=1, dst=Vertex())])) + expected['<()-->()<--()>'] = Value(pVal=Path( + src=Vertex(), + steps=[Step(type=1, dst=Vertex()), + Step(type=-1, dst=Vertex())])) + expected['<("v1")-[:e1]->()<-[:e2]-("v2")>'] = Value( + pVal=Path(src=Vertex(vid='v1'), + steps=[ + Step(name='e1', type=1, dst=Vertex()), + Step(name='e2', type=-1, dst=Vertex(vid='v2')) + ])) for item in expected.items(): v = parse(item[0]) assert v is not None, "Failed to parse %s" % item[0] assert v == item[1], \ - "Parsed value not as expected, str: %s, expected: %s actual: %s" % (item[0], item[1], v) + "Parsed value not as expected, str: %s, expected: %s actual: %s" % (item[0], item[1], v) diff --git a/tests/tck/utils/table.py b/tests/tck/utils/table.py new file mode 100644 index 0000000000000000000000000000000000000000..2c5f66c3bf3cc6f58ee7371de51ea77ba7c71ee1 --- /dev/null +++ b/tests/tck/utils/table.py @@ -0,0 +1,46 @@ +# 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 tests.tck.utils.nbv import parse +from nebula2.common.ttypes import DataSet, Row + + +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) + + return { + "column_names": column_names, + "rows": table, + } + + +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) + return ds