Skip to content
Snippets Groups Projects
Unverified Commit 391f0c9d authored by Yee's avatar Yee Committed by GitHub
Browse files

Support csv data load steps for bdd test framework (#475)

* Add more bdd steps

* Fix session release

* Fix failed cases

* Add error check step

* Fix CI failure
parent 686d355b
No related branches found
No related tags found
No related merge requests found
Showing
with 404 additions and 294 deletions
...@@ -33,8 +33,13 @@ target/ ...@@ -33,8 +33,13 @@ target/
cluster.id cluster.id
pids/ pids/
modules/ modules/
nebula-python/
# tests
!tests/Makefile !tests/Makefile
reformat-gherkin
nebula-python
.pytest
.pytest_cache
# IDE # IDE
.idea/ .idea/
......
.pytest
.pytest_cache
reformat-gherkin
nebula-python
...@@ -4,33 +4,36 @@ ...@@ -4,33 +4,36 @@
# attached with Common Clause Condition 1.0, found in the LICENSES directory. # attached with Common Clause Condition 1.0, found in the LICENSES directory.
import math import math
from functools import reduce
from nebula2.data.DataObject import ( from nebula2.data.DataObject import (
DataSetWrapper,
Node, Node,
PathWrapper,
Record, Record,
Relationship, Relationship,
PathWrapper,
DataSetWrapper,
ValueWrapper, ValueWrapper,
) )
class DataSetWrapperComparator: class DataSetWrapperComparator:
def __init__(self, strict=True, order=False): def __init__(self, strict=True, order=False, included=False):
self._strict = strict self._strict = strict
self._order = order self._order = order
self._included = included
def __call__(self, lhs: DataSetWrapper, rhs: DataSetWrapper): def __call__(self, resp: DataSetWrapper, expect: DataSetWrapper):
return self.compare(lhs, rhs) return self.compare(resp, expect)
def compare(self, lhs: DataSetWrapper, rhs: DataSetWrapper): def compare(self, resp: DataSetWrapper, expect: DataSetWrapper):
if lhs.get_row_size() != rhs.get_row_size(): if resp.get_row_size() < expect.get_row_size():
return False return False
if not lhs.get_col_names() == rhs.get_col_names(): if not resp.get_col_names() == expect.get_col_names():
return False return False
if self._order: if self._order:
return all(self.compare_row(l, r) for (l, r) in zip(lhs, rhs)) return all(self.compare_row(l, r) for (l, r) in zip(resp, expect))
return self._compare_list(lhs, rhs, self.compare_row) return self._compare_list(resp, expect, self.compare_row,
self._included)
def compare_value(self, lhs: ValueWrapper, rhs: ValueWrapper): def compare_value(self, lhs: ValueWrapper, rhs: ValueWrapper):
""" """
...@@ -90,21 +93,40 @@ class DataSetWrapperComparator: ...@@ -90,21 +93,40 @@ class DataSetWrapperComparator:
for (l, r) in zip(lhs, rhs)) for (l, r) in zip(lhs, rhs))
def compare_edge(self, lhs: Relationship, rhs: Relationship): def compare_edge(self, lhs: Relationship, rhs: Relationship):
if not lhs == rhs: if self._strict:
return False if not lhs == rhs:
if not self._strict: return False
return True 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()) return self.compare_map(lhs.propertys(), rhs.propertys())
def compare_node(self, lhs: Node, rhs: Node): def compare_node(self, lhs: Node, rhs: Node):
if lhs.get_id() != rhs.get_id(): if self._strict:
return False if lhs.get_id() != rhs.get_id():
if not self._strict: return False
return True if len(lhs.tags()) != len(rhs.tags()):
if len(lhs.tags()) != len(rhs.tags()): return False
return False else:
for tag in lhs.tags(): if rhs._value.vid is not None and lhs.get_id() != rhs.get_id():
if not rhs.has_tag(tag): return False
if len(lhs.tags()) < len(rhs.tags()):
return False
for tag in rhs.tags():
if not lhs.has_tag(tag):
return False return False
lprops = lhs.propertys(tag) lprops = lhs.propertys(tag)
rprops = rhs.propertys(tag) rprops = rhs.propertys(tag)
...@@ -124,29 +146,28 @@ class DataSetWrapperComparator: ...@@ -124,29 +146,28 @@ class DataSetWrapperComparator:
return True return True
def compare_list(self, lhs, rhs): def compare_list(self, lhs, rhs):
if len(lhs) != len(rhs): return len(lhs) == len(rhs) and \
return False self._compare_list(lhs, rhs, self.compare_value)
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): def compare_row(self, lrecord: Record, rrecord: Record):
if not lrecord.size() == rrecord.size(): if not lrecord.size() == rrecord.size():
return False return False
return all(self.compare_value(l, r) return all(
for (l, r) in zip(lrecord, rrecord)) 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 = [] visited = []
size = 0 for rr in rhs:
for lr in lhs:
size += 1
found = False found = False
for i, rr in enumerate(rhs): for i, lr in enumerate(lhs):
if i not in visited and cmp_fn(lr, rr): if i not in visited and cmp_fn(lr, rr):
visited.append(i) visited.append(i)
found = True found = True
break break
if not found: if not found:
return False 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 return len(visited) == size
...@@ -5,6 +5,14 @@ ...@@ -5,6 +5,14 @@
# This source code is licensed under Apache 2.0 License, # This source code is licensed under Apache 2.0 License,
# attached with Common Clause Condition 1.0, found in the LICENSES directory. # 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'], all_configs = {'--address' : ['address', '', 'Address of the Nebula'],
'--user' : ['user', 'root', 'The user of Nebula'], '--user' : ['user', 'root', 'The user of Nebula'],
'--password' : ['password', 'nebula', 'The password of Nebula'], '--password' : ['password', 'nebula', 'The password of Nebula'],
......
...@@ -173,16 +173,20 @@ class NebulaService(object): ...@@ -173,16 +173,20 @@ class NebulaService(object):
# wait nebula start # wait nebula start
start_time = time.time() start_time = time.time()
if not self._check_servers_status(server_ports): if not self._check_servers_status(server_ports):
raise Exception( self._collect_pids()
f'nebula servers not ready in {time.time() - start_time}s') self.kill_all(signal.SIGKILL)
print(f'nebula servers start ready in {time.time() - start_time}s') 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'): for pf in glob.glob(self.work_dir + '/pids/*.pid'):
with open(pf) as f: with open(pf) as f:
self.pids[f.name] = int(f.readline()) self.pids[f.name] = int(f.readline())
return graph_port
def stop(self): def stop(self):
print("try to stop nebula services...") print("try to stop nebula services...")
self.kill_all(signal.SIGTERM) self.kill_all(signal.SIGTERM)
......
...@@ -4,6 +4,28 @@ ...@@ -4,6 +4,28 @@
# attached with Common Clause Condition 1.0, found in the LICENSES directory. # 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: class Column:
def __init__(self, index: int): def __init__(self, index: int):
if index < 0: if index < 0:
......
...@@ -5,11 +5,18 @@ ...@@ -5,11 +5,18 @@
# This source code is licensed under Apache 2.0 License, # This source code is licensed under Apache 2.0 License,
# attached with Common Clause Condition 1.0, found in the LICENSES directory. # 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 typing import Pattern
from pathlib import Path
from nebula2.gclient.net import Session
from nebula2.common import ttypes as CommonTtypes from nebula2.common import ttypes as CommonTtypes
from tests.common.path_value import PathVal from tests.common.path_value import PathVal
from tests.common.csv_import import CSVImporter
from tests.common.types import SpaceDesc
def utf8b(s: str): def utf8b(s: str):
...@@ -34,7 +41,6 @@ def _compare_values_by_pattern(real, expect): ...@@ -34,7 +41,6 @@ def _compare_values_by_pattern(real, expect):
def _compare_list(rvalues, evalues): def _compare_list(rvalues, evalues):
if len(rvalues) != len(evalues): if len(rvalues) != len(evalues):
pdb.set_trace()
return False return False
for rval in rvalues: for rval in rvalues:
...@@ -44,7 +50,6 @@ def _compare_list(rvalues, evalues): ...@@ -44,7 +50,6 @@ def _compare_list(rvalues, evalues):
found = True found = True
break break
if not found: if not found:
pdb.set_trace()
return False return False
return True return True
...@@ -53,11 +58,9 @@ def _compare_map(rvalues, evalues): ...@@ -53,11 +58,9 @@ def _compare_map(rvalues, evalues):
for key in rvalues: for key in rvalues:
ev = evalues.get(key) ev = evalues.get(key)
if ev is None: if ev is None:
pdb.set_trace()
return False return False
rv = rvalues.get(key) rv = rvalues.get(key)
if not compare_value(rv, ev): if not compare_value(rv, ev):
pdb.set_trace()
return False return False
return True return True
...@@ -249,7 +252,8 @@ def path_to_string(path): ...@@ -249,7 +252,8 @@ def path_to_string(path):
def dataset_to_string(dataset): 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)) rows = '\n'.join(map(row_to_string, dataset.rows))
return '\n'.join([column_names, rows]) return '\n'.join([column_names, rows])
...@@ -310,3 +314,44 @@ def find_in_rows(row, rows): ...@@ -310,3 +314,44 @@ def find_in_rows(row, rows):
if found: if found:
return True return True
return False 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()
...@@ -5,20 +5,20 @@ ...@@ -5,20 +5,20 @@
# This source code is licensed under Apache 2.0 License, # This source code is licensed under Apache 2.0 License,
# attached with Common Clause Condition 1.0, found in the LICENSES directory. # attached with Common Clause Condition 1.0, found in the LICENSES directory.
import pytest import json
import logging
import os import os
import time import time
import logging
import json
import pytest
from filelock import FileLock from filelock import FileLock
from pathlib import Path
from nebula2.gclient.net import ConnectionPool
from nebula2.Config import Config from nebula2.Config import Config
from nebula2.gclient.net import ConnectionPool
from tests.common.configs import all_configs from tests.common.configs import all_configs
from tests.common.nebula_service import NebulaService 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_collected = set()
tests_executed = set() tests_executed = set()
...@@ -26,6 +26,7 @@ data_dir = os.getenv('NEBULA_DATA_DIR') ...@@ -26,6 +26,7 @@ data_dir = os.getenv('NEBULA_DATA_DIR')
CURR_PATH = os.path.dirname(os.path.abspath(__file__)) CURR_PATH = os.path.dirname(os.path.abspath(__file__))
# pytest hook to handle test collection when xdist is used (parallel tests) # 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) # https://github.com/pytest-dev/pytest-xdist/pull/35/commits (No official documentation available)
def pytest_xdist_node_collection_finished(node, ids): def pytest_xdist_node_collection_finished(node, ids):
...@@ -113,7 +114,8 @@ def conn_pool(pytestconfig, worker_id, tmp_path_factory): ...@@ -113,7 +114,8 @@ def conn_pool(pytestconfig, worker_id, tmp_path_factory):
data["num_workers"] += 1 data["num_workers"] += 1
fn.write_text(json.dumps(data)) fn.write_text(json.dumps(data))
else: else:
nb = NebulaService(build_dir, project_dir, rm_dir.lower() == "true") nb = NebulaService(build_dir, project_dir,
rm_dir.lower() == "true")
nb.install() nb.install()
port = nb.start() port = nb.start()
pool = get_conn_pool("localhost", port) pool = get_conn_pool("localhost", port)
...@@ -141,7 +143,7 @@ def conn_pool(pytestconfig, worker_id, tmp_path_factory): ...@@ -141,7 +143,7 @@ def conn_pool(pytestconfig, worker_id, tmp_path_factory):
os.remove(str(fn)) os.remove(str(fn))
@pytest.fixture(scope="session") @pytest.fixture(scope="class")
def session(conn_pool, pytestconfig): def session(conn_pool, pytestconfig):
user = pytestconfig.getoption("user") user = pytestconfig.getoption("user")
password = pytestconfig.getoption("password") password = pytestconfig.getoption("password")
...@@ -150,70 +152,43 @@ def session(conn_pool, pytestconfig): ...@@ -150,70 +152,43 @@ def session(conn_pool, pytestconfig):
sess.release() sess.release()
def load_csv_data(pytestconfig, conn_pool, folder: str): def load_csv_data_once(tmp_path_factory, pytestconfig, worker_id, conn_pool,
data_dir = os.path.join(CURR_PATH, 'data', folder) space_desc: SpaceDesc):
schema_path = os.path.join(data_dir, 'schema.ngql') space_name = space_desc.name
root_tmp_dir = tmp_path_factory.getbasetemp().parent
user = pytestconfig.getoption("user") fn = root_tmp_dir / f"csv-data-{space_name}"
password = pytestconfig.getoption("password") with FileLock(str(fn) + ".lock"):
sess = conn_pool.get_session(user, password) if fn.is_file():
logging.info(
with open(schema_path, 'r') as f: f"session-{worker_id} need not to load {space_name} csv data")
stmts = [] yield space_desc
for line in f.readlines(): return
ln = line.strip() data_dir = os.path.join(CURR_PATH, 'data', space_name)
if ln.startswith('--'): user = pytestconfig.getoption("user")
continue password = pytestconfig.getoption("password")
stmts.append(ln) sess = conn_pool.get_session(user, password)
rs = sess.execute(' '.join(stmts)) create_space(space_desc, sess)
assert rs.is_succeeded() load_csv_data(pytestconfig, sess, data_dir)
sess.release()
time.sleep(3) fn.write_text(space_name)
logging.info(f"session-{worker_id} load {space_name} csv data")
for path in Path(data_dir).rglob('*.csv'): yield space_desc
for stmt in CSVImporter(path): os.remove(str(fn))
rs = sess.execute(stmt)
assert rs.is_succeeded()
sess.release()
# TODO(yee): optimize data load fixtures # TODO(yee): optimize data load fixtures
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def load_nba_data(conn_pool, pytestconfig, tmp_path_factory, worker_id): def load_nba_data(conn_pool, pytestconfig, tmp_path_factory, worker_id):
root_tmp_dir = tmp_path_factory.getbasetemp().parent space_desc = SpaceDesc(name="nba", vid_type="FIXED_STRING(30)")
fn = root_tmp_dir / "csv-data-nba" yield from load_csv_data_once(tmp_path_factory, pytestconfig, worker_id,
load = False conn_pool, space_desc)
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") @pytest.fixture(scope="session")
def load_student_data(conn_pool, pytestconfig, tmp_path_factory, worker_id): def load_student_data(conn_pool, pytestconfig, tmp_path_factory, worker_id):
root_tmp_dir = tmp_path_factory.getbasetemp().parent space_desc = SpaceDesc(name="student", vid_type="FIXED_STRING(8)")
fn = root_tmp_dir / "csv-data-student" yield from load_csv_data_once(tmp_path_factory, pytestconfig, worker_id,
load = False conn_pool, space_desc)
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 # TODO(yee): Delete this when we migrate all test cases
......
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 player(name string, age int);
CREATE TAG IF NOT EXISTS team(name string); CREATE TAG IF NOT EXISTS team(name string);
CREATE TAG IF NOT EXISTS bachelor(name string, speciality string); CREATE TAG IF NOT EXISTS bachelor(name string, speciality string);
......
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 person(name string, age int, gender string);
CREATE TAG IF NOT EXISTS teacher(grade int, subject string); CREATE TAG IF NOT EXISTS teacher(grade int, subject string);
CREATE TAG IF NOT EXISTS student(grade int, hobby string DEFAULT ""); CREATE TAG IF NOT EXISTS student(grade int, hobby string DEFAULT "");
......
...@@ -4,52 +4,137 @@ ...@@ -4,52 +4,137 @@
# attached with Common Clause Condition 1.0, found in the LICENSES directory. # attached with Common Clause Condition 1.0, found in the LICENSES directory.
import functools 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 nebula2.data.DataObject import DataSetWrapper
from tests.tck.utils.table import table, dataset from nebula2.graph.ttypes import ErrorCode
from tests.tck.utils.comparator import DataSetWrapperComparator 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) parse = functools.partial(parsers.parse)
@given( @pytest.fixture
parse('a graph with space named "nba"'), def graph_spaces():
target_fixture="nba_space", return dict(result_set=None)
)
def nba_space(load_nba_data, session):
rs = session.execute('USE nba;') @given(parse('a graph with space named "{space}"'))
assert rs.is_succeeded() def preload_space(space, load_nba_data, load_student_data, session,
return {"result_set": None} 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}")) @when(parse("executing query:\n{query}"))
def executing_query(query, nba_space, session): def executing_query(query, graph_spaces, session):
ngql = " ".join(query.splitlines()) 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}")) @given(parse("wait {secs:d} seconds"))
def result_should_be(result, nba_space): @when(parse("wait {secs:d} seconds"))
rs = nba_space['result_set'] def wait(secs):
assert rs.is_succeeded() 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))) ds = DataSetWrapper(dataset(table(result)))
dscmp = DataSetWrapperComparator(strict=True, order=False) dscmp = DataSetWrapperComparator(strict=strict,
assert dscmp(rs._data_set_wrapper, ds) 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( @then(
parse( parse(
"the result should be, in any order, with relax comparision:\n{result}" "the result should be, in any order, with relax comparision:\n{result}"
)) ))
def result_should_be_relax_cmp(result, nba_space): def result_should_be_relax_cmp(result, graph_spaces):
rs = nba_space['result_set'] cmp_dataset(graph_spaces, result, order=False, strict=False)
assert rs.is_succeeded()
ds = dataset(table(result))
dsw = DataSetWrapper(ds) @then(parse("the result should include:\n{result}"))
dscmp = DataSetWrapperComparator(strict=False, order=False) def result_should_include(result, graph_spaces):
assert dscmp(rs._data_set_wrapper, dsw), \ cmp_dataset(graph_spaces, result, order=False, strict=True, included=True)
f"Response: {str(rs)} vs. Expected: {str(dsw)}"
@then("no side effects") @then("no side effects")
...@@ -57,6 +142,31 @@ def no_side_effects(): ...@@ -57,6 +142,31 @@ def no_side_effects():
pass pass
@then(parse("a TypeError should be raised at runtime: InvalidArgumentValue")) @then("the execution should be successful")
def raised_type_error(): def execution_should_be_succ(graph_spaces):
pass 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}"
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 |
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
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
...@@ -20,7 +20,6 @@ Feature: Basic match ...@@ -20,7 +20,6 @@ Feature: Basic match
| 'Ray Allen' | 43 | | 'Ray Allen' | 43 |
| 'David West' | 38 | | 'David West' | 38 |
| 'Tracy McGrady' | 39 | | 'Tracy McGrady' | 39 |
And no side effects
When executing query: When executing query:
""" """
MATCH (v:player {age: 29}) MATCH (v:player {age: 29})
...@@ -32,4 +31,3 @@ Feature: Basic match ...@@ -32,4 +31,3 @@ Feature: Basic match
| 'Jonathon Simmons' | | 'Jonathon Simmons' |
| 'Klay Thompson' | | 'Klay Thompson' |
| 'Dejounte Murray' | | 'Dejounte Murray' |
And no side effects
Feature: Variable length relationship match (m to n) Feature: Variable length Pattern match (m to n)
Background: Background:
Given a graph with space named "nba" Given a graph with space named "nba"
......
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
...@@ -3,26 +3,27 @@ ...@@ -3,26 +3,27 @@
# This source code is licensed under Apache 2.0 License, # This source code is licensed under Apache 2.0 License,
# attached with Common Clause Condition 1.0, found in the LICENSES directory. # attached with Common Clause Condition 1.0, found in the LICENSES directory.
import csv
import io
from tests.tck.utils.nbv import parse 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): def table(text):
lines = text.splitlines() lines = list(csv.reader(io.StringIO(text), delimiter="|"))
assert len(lines) >= 1 header = lines[0][1:-1]
column_names = list(map(lambda x: bytes(x.strip(), 'utf8'), header))
def parse_line(line): table = [{
return list( column_name: cell.strip()
map(lambda x: x.strip(), filter(lambda x: x, line.split('|')))) for (column_name, cell) in zip(column_names, line[1:-1])
} for line in lines[1:]]
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 { return {
"column_names": column_names, "column_names": column_names,
...@@ -33,14 +34,8 @@ def table(text): ...@@ -33,14 +34,8 @@ def table(text):
def dataset(string_table): def dataset(string_table):
ds = DataSet() ds = DataSet()
ds.column_names = string_table['column_names'] ds.column_names = string_table['column_names']
ds.rows = [] ds.rows = [
for row in string_table['rows']: Row(values=[_parse_value(row[column]) for column in ds.column_names])
nrow = Row() for row in string_table['rows']
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 return ds
# 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)
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment