Compare commits

...

13 Commits

Author SHA1 Message Date
niko4299
83aa71a29f interpreter.cpp 286 line convert vector label strings to vector LabelId, possible changes to LabelPermission 2022-07-08 10:59:54 +02:00
niko4299
c2a1328dcc added Boris class 2022-07-07 16:03:22 +02:00
niko4299
b63db202d6 uncommented 2022-07-07 13:21:05 +02:00
niko4299
1abe8f8bfc Labels defined with colon 2022-07-07 13:15:01 +02:00
niko4299
4366085d89 GRANT, DENY, REVOKE all saving to rocksdb and working 2022-07-07 11:26:43 +02:00
niko4299
9369ae9085 My version with map, will test tomorrow 2022-07-06 14:50:00 +02:00
josipmrden
86a15331d1 Added saving of labels to AuthQuery 2022-07-04 16:49:23 +02:00
josipmrden
0c8b35b151 Added accepting visiting privilege to labels 2022-07-04 14:25:28 +02:00
josipmrden
11d60c203e Updated CypherLexer for LABELS 2022-07-04 14:16:50 +02:00
josipmrden
38c0a08342 Updated case which adds LABELS as Permissions 2022-07-04 14:11:30 +02:00
josipmrden
7e1d39bf86 Updated switch cases with privileges and permissions 2022-07-04 13:59:17 +02:00
josipmrden
dd85b428bf Updated lcp file 2022-07-04 13:54:36 +02:00
josipmrden
2f9ed0146e Updated lexer for adding privileges over labels 2022-07-04 13:49:54 +02:00
89 changed files with 3138 additions and 2646 deletions

2
.gitignore vendored
View File

@@ -9,6 +9,7 @@
*.swn *.swn
*.swo *.swo
*.swp *.swp
*~ *~
.DS_Store .DS_Store
.gdb_history .gdb_history
@@ -26,6 +27,7 @@ src/query/frontend/opencypher/generated/
tags tags
ve/ ve/
ve3/ ve3/
.cache/
perf.data* perf.data*
TAGS TAGS
*.apollo_measurements *.apollo_measurements

View File

@@ -18,14 +18,16 @@ WIDTH = 80
def wrap_text(s, initial_indent="# "): def wrap_text(s, initial_indent="# "):
return "\n#\n".join( return "\n#\n".join(
map(lambda x: textwrap.fill(x, WIDTH, initial_indent=initial_indent, map(
subsequent_indent="# "), s.split("\n"))) lambda x: textwrap.fill(x, WIDTH, initial_indent=initial_indent, subsequent_indent="# "),
s.split("\n"),
)
)
def extract_flags(binary_path): def extract_flags(binary_path):
ret = {} ret = {}
data = subprocess.run([binary_path, "--help-xml"], data = subprocess.run([binary_path, "--help-xml"], stdout=subprocess.PIPE).stdout.decode("utf-8")
stdout=subprocess.PIPE).stdout.decode("utf-8")
root = ET.fromstring(data) root = ET.fromstring(data)
for child in root: for child in root:
if child.tag == "usage" and child.text.lower().count("warning"): if child.tag == "usage" and child.text.lower().count("warning"):
@@ -46,8 +48,7 @@ def apply_config_to_flags(config, flags):
for modification in config["modifications"]: for modification in config["modifications"]:
name = modification["name"] name = modification["name"]
if name not in flags: if name not in flags:
print("WARNING: Flag '" + name + "' missing from binary!", print("WARNING: Flag '" + name + "' missing from binary!", file=sys.stderr)
file=sys.stderr)
continue continue
flags[name]["default"] = modification["value"] flags[name]["default"] = modification["value"]
flags[name]["override"] = modification["override"] flags[name]["override"] = modification["override"]
@@ -75,8 +76,9 @@ def extract_sections(flags):
else: else:
sections.append((current_section, current_flags)) sections.append((current_section, current_flags))
sections.append(("other", other)) sections.append(("other", other))
assert set(sum(map(lambda x: x[1], sections), [])) == set(flags.keys()), \ assert set(sum(map(lambda x: x[1], sections), [])) == set(
"The section extraction algorithm lost some flags!" flags.keys()
), "The section extraction algorithm lost some flags!"
return sections return sections
@@ -89,8 +91,7 @@ def generate_config_file(sections, flags):
helpstr = flag["meaning"] + " [" + flag["type"] + "]" helpstr = flag["meaning"] + " [" + flag["type"] + "]"
ret += wrap_text(helpstr) + "\n" ret += wrap_text(helpstr) + "\n"
prefix = "# " if not flag["override"] else "" prefix = "# " if not flag["override"] else ""
ret += prefix + "--" + flag["name"].replace("_", "-") + \ ret += prefix + "--" + flag["name"].replace("_", "-") + "=" + flag["default"] + "\n\n"
"=" + flag["default"] + "\n\n"
ret += "\n" ret += "\n"
ret += wrap_text(config["footer"]) ret += wrap_text(config["footer"])
return ret.strip() + "\n" return ret.strip() + "\n"
@@ -98,13 +99,16 @@ def generate_config_file(sections, flags):
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("memgraph_binary", parser.add_argument("memgraph_binary", help="path to Memgraph binary")
help="path to Memgraph binary") parser.add_argument(
parser.add_argument("output_file", "output_file",
help="path where to store the generated Memgraph " help="path where to store the generated Memgraph " "configuration file",
"configuration file") )
parser.add_argument("--config-file", default=CONFIG_FILE, parser.add_argument(
help="path to generator configuration file") "--config-file",
default=CONFIG_FILE,
help="path to generator configuration file",
)
args = parser.parse_args() args = parser.parse_args()
flags = extract_flags(args.memgraph_binary) flags = extract_flags(args.memgraph_binary)

View File

@@ -7,13 +7,11 @@ import copy
@mgp.read_proc @mgp.read_proc
def procedure(context: mgp.ProcCtx, def procedure(
required_arg: mgp.Nullable[mgp.Any], context: mgp.ProcCtx,
optional_arg: mgp.Nullable[mgp.Any] = None required_arg: mgp.Nullable[mgp.Any],
) -> mgp.Record(args=list, optional_arg: mgp.Nullable[mgp.Any] = None,
vertex_count=int, ) -> mgp.Record(args=list, vertex_count=int, avg_degree=mgp.Number, props=mgp.Nullable[mgp.Map]):
avg_degree=mgp.Number,
props=mgp.Nullable[mgp.Map]):
""" """
This example procedure returns 4 fields. This example procedure returns 4 fields.
@@ -37,7 +35,7 @@ def procedure(context: mgp.ProcCtx,
if isinstance(required_arg, (mgp.Edge, mgp.Vertex)): if isinstance(required_arg, (mgp.Edge, mgp.Vertex)):
props = dict(required_arg.properties.items()) props = dict(required_arg.properties.items())
elif isinstance(required_arg, mgp.Path): elif isinstance(required_arg, mgp.Path):
start_vertex, = required_arg.vertices (start_vertex,) = required_arg.vertices
props = dict(start_vertex.properties.items()) props = dict(start_vertex.properties.items())
# Count the vertices and edges in the database; this may take a while. # Count the vertices and edges in the database; this may take a while.
vertex_count = 0 vertex_count = 0
@@ -51,15 +49,13 @@ def procedure(context: mgp.ProcCtx,
# Copy the received arguments to make it equivalent to the C example. # Copy the received arguments to make it equivalent to the C example.
args_copy = [copy.deepcopy(required_arg), copy.deepcopy(optional_arg)] args_copy = [copy.deepcopy(required_arg), copy.deepcopy(optional_arg)]
# Multiple rows can be produced by returning an iterable of mgp.Record. # Multiple rows can be produced by returning an iterable of mgp.Record.
return mgp.Record(args=args_copy, vertex_count=vertex_count, return mgp.Record(args=args_copy, vertex_count=vertex_count, avg_degree=avg_degree, props=props)
avg_degree=avg_degree, props=props)
@mgp.write_proc @mgp.write_proc
def write_procedure(context: mgp.ProcCtx, def write_procedure(
property_name: str, context: mgp.ProcCtx, property_name: str, property_value: mgp.Nullable[mgp.Any]
property_value: mgp.Nullable[mgp.Any] ) -> mgp.Record(created_vertex=mgp.Vertex):
) -> mgp.Record(created_vertex=mgp.Vertex):
""" """
This example procedure creates a new vertex with the specified property This example procedure creates a new vertex with the specified property
and connects it to all existing vertex which has the same property with and connects it to all existing vertex which has the same property with

View File

@@ -4,15 +4,17 @@ from collections import OrderedDict
from itertools import chain, repeat from itertools import chain, repeat
from inspect import cleandoc from inspect import cleandoc
from typing import List, Tuple from typing import List, Tuple
try: try:
import networkx as nx import networkx as nx
except ImportError as import_error: except ImportError as import_error:
sys.stderr.write(( sys.stderr.write(
'\n' (
'NOTE: Please install networkx to be able to use graph_analyzer ' "\n"
'module. Using Python:\n' "NOTE: Please install networkx to be able to use graph_analyzer "
+ sys.version + "module. Using Python:\n" + sys.version + "\n"
'\n')) )
)
raise import_error raise import_error
# Imported last because it also depends on networkx. # Imported last because it also depends on networkx.
from mgp_networkx import MemgraphMultiDiGraph # noqa E402 from mgp_networkx import MemgraphMultiDiGraph # noqa E402
@@ -23,16 +25,14 @@ _MAX_LIST_SIZE = 10
@mgp.read_proc @mgp.read_proc
def help() -> mgp.Record(name=str, value=str): def help() -> mgp.Record(name=str, value=str):
'''Shows manual page for graph_analyzer.''' """Shows manual page for graph_analyzer."""
records = [] records = []
def make_records(name, doc): def make_records(name, doc):
return (mgp.Record(name=n, value=v) for n, v in return (mgp.Record(name=n, value=v) for n, v in zip(chain([name], repeat("")), cleandoc(doc).splitlines()))
zip(chain([name], repeat('')), cleandoc(doc).splitlines()))
for func in (help, analyze, analyze_subgraph): for func in (help, analyze, analyze_subgraph):
records.extend(make_records("Procedure '{}'".format(func.__name__), records.extend(make_records("Procedure '{}'".format(func.__name__), func.__doc__))
func.__doc__))
for m, v in _get_analysis_mapping().items(): for m, v in _get_analysis_mapping().items():
records.extend(make_records("Analysis '{}'".format(m), v.__doc__)) records.extend(make_records("Analysis '{}'".format(m), v.__doc__))
@@ -41,10 +41,8 @@ def help() -> mgp.Record(name=str, value=str):
@mgp.read_proc @mgp.read_proc
def analyze(context: mgp.ProcCtx, def analyze(context: mgp.ProcCtx, analyses: mgp.Nullable[List[str]] = None) -> mgp.Record(name=str, value=str):
analyses: mgp.Nullable[List[str]] = None """
) -> mgp.Record(name=str, value=str):
'''
Shows graph information. Shows graph information.
In case of multiple results, only the first 10 will be shown. In case of multiple results, only the first 10 will be shown.
@@ -57,19 +55,20 @@ def analyze(context: mgp.ProcCtx,
Example call (with parameter): Example call (with parameter):
CALL graph_analyzer.analyze(['nodes', 'edges']) YIELD *; CALL graph_analyzer.analyze(['nodes', 'edges']) YIELD *;
''' """
g = MemgraphMultiDiGraph(ctx=context) g = MemgraphMultiDiGraph(ctx=context)
recs = _analyze_graph(context, g, analyses) recs = _analyze_graph(context, g, analyses)
return [mgp.Record(name=name, value=value) for name, value in recs] return [mgp.Record(name=name, value=value) for name, value in recs]
@mgp.read_proc @mgp.read_proc
def analyze_subgraph(context: mgp.ProcCtx, def analyze_subgraph(
vertices: mgp.List[mgp.Vertex], context: mgp.ProcCtx,
edges: mgp.List[mgp.Edge], vertices: mgp.List[mgp.Vertex],
analyses: mgp.Nullable[List[str]] = None edges: mgp.List[mgp.Edge],
) -> mgp.Record(name=str, value=str): analyses: mgp.Nullable[List[str]] = None,
''' ) -> mgp.Record(name=str, value=str):
"""
Shows subgraph information. Shows subgraph information.
In case of multiple results, only the first 10 will be shown. In case of multiple results, only the first 10 will be shown.
@@ -91,36 +90,40 @@ def analyze_subgraph(context: mgp.ProcCtx,
CALL graph_analyzer.analyze_subgraph(nodes, edges, ['nodes', 'edges']) CALL graph_analyzer.analyze_subgraph(nodes, edges, ['nodes', 'edges'])
YIELD * YIELD *
RETURN name, value; RETURN name, value;
''' """
vertices, edges = map(set, [vertices, edges]) vertices, edges = map(set, [vertices, edges])
g = nx.subgraph_view( g = nx.subgraph_view(
MemgraphMultiDiGraph(ctx=context), MemgraphMultiDiGraph(ctx=context),
lambda n: n in vertices, lambda n: n in vertices,
lambda n1, n2, e: e in edges) lambda n1, n2, e: e in edges,
)
recs = _analyze_graph(context, g, analyses) recs = _analyze_graph(context, g, analyses)
return [mgp.Record(name=name, value=value) for name, value in recs] return [mgp.Record(name=name, value=value) for name, value in recs]
def _get_analysis_mapping(): def _get_analysis_mapping():
return OrderedDict([ return OrderedDict(
('nodes', _number_of_nodes), [
('edges', _number_of_edges), ("nodes", _number_of_nodes),
('bridges', _bridges), ("edges", _number_of_edges),
('articulation_points', _articulation_points), ("bridges", _bridges),
('avg_degree', _avg_degree), ("articulation_points", _articulation_points),
('sorted_nodes_degree', _sorted_nodes_degree), ("avg_degree", _avg_degree),
('self_loops', _self_loops), ("sorted_nodes_degree", _sorted_nodes_degree),
('is_bipartite', _is_bipartite), ("self_loops", _self_loops),
('is_planar', _is_planar), ("is_bipartite", _is_bipartite),
('is_biconnected: ', _is_biconnected), ("is_planar", _is_planar),
('is_weakly_connected', _is_weakly_connected), ("is_biconnected: ", _is_biconnected),
('number_of_weakly_components', _weakly_components), ("is_weakly_connected", _is_weakly_connected),
('is_strongly_connected', _is_strongly_connected), ("number_of_weakly_components", _weakly_components),
('strongly_components', _strongly_components), ("is_strongly_connected", _is_strongly_connected),
('is_dag', _is_dag), ("strongly_components", _strongly_components),
('is_eulerian', _is_eulerian), ("is_dag", _is_dag),
('is_forest', _is_forest), ("is_eulerian", _is_eulerian),
('is_tree', _is_tree)]) ("is_forest", _is_forest),
("is_tree", _is_tree),
]
)
def _get_analysis_func(name: str): def _get_analysis_func(name: str):
@@ -132,20 +135,15 @@ def _get_analysis_funcs():
return _get_analysis_mapping().values() return _get_analysis_mapping().values()
def _analyze_graph(context: mgp.ProcCtx, def _analyze_graph(context: mgp.ProcCtx, g: nx.MultiDiGraph, analyses: List[str]) -> List[Tuple[str, str]]:
g: nx.MultiDiGraph,
analyses: List[str]
) -> List[Tuple[str, str]]:
functions = (_get_analysis_funcs() if analyses is None functions = _get_analysis_funcs() if analyses is None else [_get_analysis_func(name) for name in analyses]
else [_get_analysis_func(name) for name in analyses])
records = [] records = []
for index, f in enumerate(functions): for index, f in enumerate(functions):
context.check_must_abort() context.check_must_abort()
if f is None: if f is None:
raise KeyError('Graph analysis is not supported: ' + raise KeyError("Graph analysis is not supported: " + analyses[index])
analyses[index])
name, value = f(g) name, value = f(g)
if isinstance(value, (list, set, tuple)): if isinstance(value, (list, set, tuple)):
value = list(value)[:_MAX_LIST_SIZE] value = list(value)[:_MAX_LIST_SIZE]
@@ -155,126 +153,120 @@ def _analyze_graph(context: mgp.ProcCtx,
def _number_of_nodes(g: nx.MultiDiGraph) -> Tuple[str, int]: def _number_of_nodes(g: nx.MultiDiGraph) -> Tuple[str, int]:
'''Returns number of nodes.''' """Returns number of nodes."""
return 'Number of nodes', nx.number_of_nodes(g) return "Number of nodes", nx.number_of_nodes(g)
def _number_of_edges(g: nx.MultiDiGraph) -> Tuple[str, int]: def _number_of_edges(g: nx.MultiDiGraph) -> Tuple[str, int]:
'''Returns number of edges.''' """Returns number of edges."""
return 'Number of edges', nx.number_of_edges(g) return "Number of edges", nx.number_of_edges(g)
def _avg_degree(g: nx.MultiDiGraph) -> Tuple[str, float]: def _avg_degree(g: nx.MultiDiGraph) -> Tuple[str, float]:
'''Returns average degree.''' """Returns average degree."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
_, number_of_edges = _number_of_edges(g) _, number_of_edges = _number_of_edges(g)
avg_degree = (0 if number_of_nodes == 0 avg_degree = 0 if number_of_nodes == 0 else number_of_edges / number_of_nodes
else number_of_edges / number_of_nodes) return "Average degree", avg_degree
return 'Average degree', avg_degree
def _sorted_nodes_degree(g: nx.MultiDiGraph) -> Tuple[str, List[int]]: def _sorted_nodes_degree(g: nx.MultiDiGraph) -> Tuple[str, List[int]]:
'''Returns list of sorted nodes degree. [(node_id, degree), ...]''' """Returns list of sorted nodes degree. [(node_id, degree), ...]"""
nodes_degree = [(n, g.degree(n)) for n in g.nodes()] nodes_degree = [(n, g.degree(n)) for n in g.nodes()]
nodes_degree.sort(key=lambda x: x[1], reverse=True) nodes_degree.sort(key=lambda x: x[1], reverse=True)
return 'Sorted nodes degree', nodes_degree return "Sorted nodes degree", nodes_degree
def _self_loops(g: nx.MultiDiGraph) -> Tuple[str, int]: def _self_loops(g: nx.MultiDiGraph) -> Tuple[str, int]:
'''Returns number of self loops.''' """Returns number of self loops."""
return 'Self loops', sum((1 if e[0] == e[1] else 0 for e in g.edges())) return "Self loops", sum((1 if e[0] == e[1] else 0 for e in g.edges()))
def _is_bipartite(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_bipartite(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Checks if graph is bipartite.''' """Checks if graph is bipartite."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = (False if number_of_nodes == 0 ret = False if number_of_nodes == 0 else nx.algorithms.bipartite.basic.is_bipartite(g)
else nx.algorithms.bipartite.basic.is_bipartite(g)) return "Is bipartite", ret
return 'Is bipartite', ret
def _is_planar(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_planar(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Checks if graph is planar.''' """Checks if graph is planar."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = (False if number_of_nodes == 0 ret = False if number_of_nodes == 0 else nx.algorithms.planarity.check_planarity(g)[0]
else nx.algorithms.planarity.check_planarity(g)[0]) return "Is planar", ret
return 'Is planar', ret
def _is_biconnected(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_biconnected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Check if graph is biconnected.''' """Check if graph is biconnected."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = (False if number_of_nodes == 0 ret = False if number_of_nodes == 0 else nx.is_biconnected(nx.MultiDiGraph.to_undirected(g))
else nx.is_biconnected(nx.MultiDiGraph.to_undirected(g))) return "Is biconnected", ret
return 'Is biconnected', ret
def _is_weakly_connected(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_weakly_connected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Check if graph is weakly connected.''' """Check if graph is weakly connected."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = False if number_of_nodes == 0 else nx.is_weakly_connected(g) ret = False if number_of_nodes == 0 else nx.is_weakly_connected(g)
return 'Is weakly connected', ret return "Is weakly connected", ret
def _is_strongly_connected(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_strongly_connected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Checks if graph is strongly connected.''' """Checks if graph is strongly connected."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = False if number_of_nodes == 0 else nx.is_strongly_connected(g) ret = False if number_of_nodes == 0 else nx.is_strongly_connected(g)
return 'Is strongly connected', ret return "Is strongly connected", ret
def _is_dag(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_dag(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Check if graph is directed acyclic graph (DAG)''' """Check if graph is directed acyclic graph (DAG)"""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = (False if number_of_nodes == 0 ret = False if number_of_nodes == 0 else nx.algorithms.dag.is_directed_acyclic_graph(g)
else nx.algorithms.dag.is_directed_acyclic_graph(g)) return "Is DAG", ret
return 'Is DAG', ret
def _is_eulerian(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_eulerian(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Checks if graph is Eulerian.''' """Checks if graph is Eulerian."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = (False if number_of_nodes == 0 ret = False if number_of_nodes == 0 else nx.algorithms.euler.is_eulerian(g)
else nx.algorithms.euler.is_eulerian(g)) return "Is eulerian", ret
return 'Is eulerian', ret
def _is_forest(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_forest(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Checks if graph is forest, all components must be trees.''' """Checks if graph is forest, all components must be trees."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = (False if number_of_nodes == 0 ret = False if number_of_nodes == 0 else nx.algorithms.tree.recognition.is_forest(g)
else nx.algorithms.tree.recognition.is_forest(g)) return "Is forest", ret
return 'Is forest', ret
def _is_tree(g: nx.MultiDiGraph) -> Tuple[str, bool]: def _is_tree(g: nx.MultiDiGraph) -> Tuple[str, bool]:
'''Checks if graph is tree.''' """Checks if graph is tree."""
_, number_of_nodes = _number_of_nodes(g) _, number_of_nodes = _number_of_nodes(g)
ret = (False if number_of_nodes == 0 ret = False if number_of_nodes == 0 else nx.algorithms.tree.recognition.is_tree(g)
else nx.algorithms.tree.recognition.is_tree(g)) return "Is tree", ret
return 'Is tree', ret
def _bridges(g: nx.MultiDiGraph) -> Tuple[str, int]: def _bridges(g: nx.MultiDiGraph) -> Tuple[str, int]:
'''Returns number of bridges, multiple edges between same nodes are """Returns number of bridges, multiple edges between same nodes are
mapped to one edge.''' mapped to one edge."""
return 'Number of bridges', sum(1 for _ in nx.bridges(nx.Graph(g))) return "Number of bridges", sum(1 for _ in nx.bridges(nx.Graph(g)))
def _articulation_points(g: nx.MultiDiGraph): def _articulation_points(g: nx.MultiDiGraph):
'''Returns number of articulation points.''' """Returns number of articulation points."""
undirected = nx.MultiDiGraph.to_undirected(g) undirected = nx.MultiDiGraph.to_undirected(g)
return ('Number of articulation points', return (
sum(1 for _ in nx.articulation_points(undirected))) "Number of articulation points",
sum(1 for _ in nx.articulation_points(undirected)),
)
def _weakly_components(g: nx.MultiDiGraph): def _weakly_components(g: nx.MultiDiGraph):
'''Returns number of weakly components.''' """Returns number of weakly components."""
comps = nx.algorithms.components.number_weakly_connected_components(g) comps = nx.algorithms.components.number_weakly_connected_components(g)
return 'Number of weakly connected components', comps return "Number of weakly connected components", comps
def _strongly_components(g: nx.MultiDiGraph): def _strongly_components(g: nx.MultiDiGraph):
'''Returns number of strongly connected components.''' """Returns number of strongly connected components."""
comps = nx.algorithms.components.number_strongly_connected_components(g) comps = nx.algorithms.components.number_strongly_connected_components(g)
return 'Number of strongly connected components', comps return "Number of strongly connected components", comps

View File

@@ -1,20 +1,22 @@
import sys import sys
import mgp import mgp
import collections import collections
try: try:
import networkx as nx import networkx as nx
except ImportError as import_error: except ImportError as import_error:
sys.stderr.write(( sys.stderr.write(
'\n' (
'NOTE: Please install networkx to be able to use Memgraph NetworkX ' "\n"
'wrappers. Using Python:\n' "NOTE: Please install networkx to be able to use Memgraph NetworkX "
+ sys.version + "wrappers. Using Python:\n" + sys.version + "\n"
'\n')) )
)
raise import_error raise import_error
class MemgraphAdjlistOuterDict(collections.abc.Mapping): class MemgraphAdjlistOuterDict(collections.abc.Mapping):
__slots__ = ('_ctx', '_succ', '_multi') __slots__ = ("_ctx", "_succ", "_multi")
def __init__(self, ctx, succ=True, multi=True): def __init__(self, ctx, succ=True, multi=True):
self._ctx = ctx self._ctx = ctx
@@ -24,8 +26,7 @@ class MemgraphAdjlistOuterDict(collections.abc.Mapping):
def __getitem__(self, key): def __getitem__(self, key):
if key not in self: if key not in self:
raise KeyError raise KeyError
return MemgraphAdjlistInnerDict(key, succ=self._succ, return MemgraphAdjlistInnerDict(key, succ=self._succ, multi=self._multi)
multi=self._multi)
def __iter__(self): def __iter__(self):
return iter(self._ctx.graph.vertices) return iter(self._ctx.graph.vertices)
@@ -40,7 +41,7 @@ class MemgraphAdjlistOuterDict(collections.abc.Mapping):
class MemgraphAdjlistInnerDict(collections.abc.Mapping): class MemgraphAdjlistInnerDict(collections.abc.Mapping):
__slots__ = ('_node', '_succ', '_multi', '_neighbors') __slots__ = ("_node", "_succ", "_multi", "_neighbors")
def __init__(self, node, succ=True, multi=True): def __init__(self, node, succ=True, multi=True):
self._node = node self._node = node
@@ -71,31 +72,26 @@ class MemgraphAdjlistInnerDict(collections.abc.Mapping):
def _get_neighbors(self): def _get_neighbors(self):
if not self._neighbors: if not self._neighbors:
if self._succ: if self._succ:
self._neighbors = set( self._neighbors = set(e.to_vertex for e in self._node.out_edges)
e.to_vertex for e in self._node.out_edges)
else: else:
self._neighbors = set( self._neighbors = set(e.from_vertex for e in self._node.in_edges)
e.from_vertex for e in self._node.in_edges)
return self._neighbors return self._neighbors
def _get_edge(self, neighbor): def _get_edge(self, neighbor):
if self._succ: if self._succ:
edge = list(filter(lambda e: e.to_vertex == neighbor, edge = list(filter(lambda e: e.to_vertex == neighbor, self._node.out_edges))
self._node.out_edges))
else: else:
edge = list(filter(lambda e: e.from_vertex == neighbor, edge = list(filter(lambda e: e.from_vertex == neighbor, self._node.in_edges))
self._node.in_edges))
assert len(edge) >= 1 assert len(edge) >= 1
if len(edge) > 1: if len(edge) > 1:
raise RuntimeError('Graph contains multiedges but ' raise RuntimeError("Graph contains multiedges but " "is of non-multigraph type: {}".format(edge))
'is of non-multigraph type: {}'.format(edge))
return edge[0] return edge[0]
class MemgraphEdgeKeyDict(collections.abc.Mapping): class MemgraphEdgeKeyDict(collections.abc.Mapping):
__slots__ = ('_node', '_neighbor', '_succ', '_edges') __slots__ = ("_node", "_neighbor", "_succ", "_edges")
def __init__(self, node, neighbor, succ=True): def __init__(self, node, neighbor, succ=True):
self._node = node self._node = node
@@ -122,18 +118,14 @@ class MemgraphEdgeKeyDict(collections.abc.Mapping):
def _get_edges(self): def _get_edges(self):
if not self._edges: if not self._edges:
if self._succ: if self._succ:
self._edges = list(filter( self._edges = list(filter(lambda e: e.to_vertex == self._neighbor, self._node.out_edges))
lambda e: e.to_vertex == self._neighbor,
self._node.out_edges))
else: else:
self._edges = list(filter( self._edges = list(filter(lambda e: e.from_vertex == self._neighbor, self._node.in_edges))
lambda e: e.from_vertex == self._neighbor,
self._node.in_edges))
return self._edges return self._edges
class UnhashableProperties(collections.abc.Mapping): class UnhashableProperties(collections.abc.Mapping):
__slots__ = ('_properties') __slots__ = "_properties"
def __init__(self, properties): def __init__(self, properties):
self._properties = properties self._properties = properties
@@ -155,7 +147,7 @@ class UnhashableProperties(collections.abc.Mapping):
class MemgraphNodeDict(collections.abc.Mapping): class MemgraphNodeDict(collections.abc.Mapping):
__slots__ = ('_ctx',) __slots__ = ("_ctx",)
def __init__(self, ctx): def __init__(self, ctx):
self._ctx = ctx self._ctx = ctx
@@ -187,8 +179,7 @@ class MemgraphNodeDict(collections.abc.Mapping):
class MemgraphDiGraphBase: class MemgraphDiGraphBase:
def __init__(self, incoming_graph_data=None, ctx=None, multi=True, def __init__(self, incoming_graph_data=None, ctx=None, multi=True, **kwargs):
**kwargs):
# NOTE: We assume that our graph will never be given any initial data # NOTE: We assume that our graph will never be given any initial data
# because we already pull our data from the Memgraph database. This # because we already pull our data from the Memgraph database. This
# assert is triggered by certain NetworkX procedures because they # assert is triggered by certain NetworkX procedures because they
@@ -201,23 +192,30 @@ class MemgraphDiGraphBase:
# modify the graph's internal attributes and don't try to populate it # modify the graph's internal attributes and don't try to populate it
# with initial data or modify it. # with initial data or modify it.
self.node_dict_factory = lambda: MemgraphNodeDict(ctx) \ self.node_dict_factory = lambda: MemgraphNodeDict(ctx) if ctx else self._error
if ctx else self._error
self.node_attr_dict_factory = self._error self.node_attr_dict_factory = self._error
self.adjlist_outer_dict_factory = \ self.adjlist_outer_dict_factory = lambda: MemgraphAdjlistOuterDict(ctx, multi=multi) if ctx else self._error
lambda: MemgraphAdjlistOuterDict(ctx, multi=multi) \
if ctx else self._error
self.adjlist_inner_dict_factory = self._error self.adjlist_inner_dict_factory = self._error
self.edge_key_dict_factory = self._error self.edge_key_dict_factory = self._error
self.edge_attr_dict_factory = self._error self.edge_attr_dict_factory = self._error
# NOTE: We forbid any mutating operations because our graph is # NOTE: We forbid any mutating operations because our graph is
# immutable and pulls its data from the Memgraph database. # immutable and pulls its data from the Memgraph database.
for f in ['add_node', 'add_nodes_from', 'remove_node', for f in [
'remove_nodes_from', 'add_edge', 'add_edges_from', "add_node",
'add_weighted_edges_from', 'new_edge_key', 'remove_edge', "add_nodes_from",
'remove_edges_from', 'update', 'clear']: "remove_node",
"remove_nodes_from",
"add_edge",
"add_edges_from",
"add_weighted_edges_from",
"new_edge_key",
"remove_edge",
"remove_edges_from",
"update",
"clear",
]:
setattr(self, f, lambda *args, **kwargs: self._error()) setattr(self, f, lambda *args, **kwargs: self._error())
super().__init__(None, **kwargs) super().__init__(None, **kwargs)
@@ -231,33 +229,29 @@ class MemgraphDiGraphBase:
self._pred = MemgraphAdjlistOuterDict(ctx, succ=False, multi=multi) self._pred = MemgraphAdjlistOuterDict(ctx, succ=False, multi=multi)
def _error(self): def _error(self):
raise RuntimeError('Modification operations are not supported') raise RuntimeError("Modification operations are not supported")
class MemgraphMultiDiGraph(MemgraphDiGraphBase, nx.MultiDiGraph): class MemgraphMultiDiGraph(MemgraphDiGraphBase, nx.MultiDiGraph):
def __init__(self, incoming_graph_data=None, ctx=None, **kwargs): def __init__(self, incoming_graph_data=None, ctx=None, **kwargs):
super().__init__(incoming_graph_data=incoming_graph_data, super().__init__(incoming_graph_data=incoming_graph_data, ctx=ctx, multi=True, **kwargs)
ctx=ctx, multi=True, **kwargs)
def MemgraphMultiGraph(incoming_graph_data=None, ctx=None, **kwargs): def MemgraphMultiGraph(incoming_graph_data=None, ctx=None, **kwargs):
return MemgraphMultiDiGraph(incoming_graph_data=incoming_graph_data, return MemgraphMultiDiGraph(incoming_graph_data=incoming_graph_data, ctx=ctx, **kwargs).to_undirected(as_view=True)
ctx=ctx, **kwargs).to_undirected(as_view=True)
class MemgraphDiGraph(MemgraphDiGraphBase, nx.DiGraph): class MemgraphDiGraph(MemgraphDiGraphBase, nx.DiGraph):
def __init__(self, incoming_graph_data=None, ctx=None, **kwargs): def __init__(self, incoming_graph_data=None, ctx=None, **kwargs):
super().__init__(incoming_graph_data=incoming_graph_data, super().__init__(incoming_graph_data=incoming_graph_data, ctx=ctx, multi=False, **kwargs)
ctx=ctx, multi=False, **kwargs)
def MemgraphGraph(incoming_graph_data=None, ctx=None, **kwargs): def MemgraphGraph(incoming_graph_data=None, ctx=None, **kwargs):
return MemgraphDiGraph(incoming_graph_data=incoming_graph_data, return MemgraphDiGraph(incoming_graph_data=incoming_graph_data, ctx=ctx, **kwargs).to_undirected(as_view=True)
ctx=ctx, **kwargs).to_undirected(as_view=True)
class PropertiesDictionary(collections.abc.Mapping): class PropertiesDictionary(collections.abc.Mapping):
__slots__ = ('_ctx', '_prop', '_len') __slots__ = ("_ctx", "_prop", "_len")
def __init__(self, ctx, prop): def __init__(self, ctx, prop):
self._ctx = ctx self._ctx = ctx
@@ -270,8 +264,7 @@ class PropertiesDictionary(collections.abc.Mapping):
try: try:
return vertex.properties[self._prop] return vertex.properties[self._prop]
except KeyError: except KeyError:
raise KeyError(("{} doesn\t have the required " + raise KeyError(("{} doesn\t have the required " + "property '{}'").format(vertex, self._prop))
"property '{}'").format(vertex, self._prop))
def __iter__(self): def __iter__(self):
for v in self._ctx.graph.vertices: for v in self._ctx.graph.vertices:

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,20 @@
import sys import sys
import mgp import mgp
try: try:
import networkx as nx import networkx as nx
except ImportError as import_error: except ImportError as import_error:
sys.stderr.write( sys.stderr.write(
'\n' "\n" "NOTE: Please install networkx to be able to use wcc module.\n" "Using Python:\n" + sys.version + "\n"
'NOTE: Please install networkx to be able to use wcc module.\n' )
'Using Python:\n'
+ sys.version +
'\n')
raise import_error raise import_error
@mgp.read_proc @mgp.read_proc
def get_components(vertices: mgp.List[mgp.Vertex], def get_components(
edges: mgp.List[mgp.Edge] vertices: mgp.List[mgp.Vertex], edges: mgp.List[mgp.Edge]
) -> mgp.Record(n_components=int, ) -> mgp.Record(n_components=int, components=mgp.List[mgp.List[mgp.Vertex]]):
components=mgp.List[mgp.List[mgp.Vertex]]): """
'''
This procedure finds weakly connected components of a given subgraph of a This procedure finds weakly connected components of a given subgraph of a
directed graph. directed graph.
@@ -41,7 +38,7 @@ def get_components(vertices: mgp.List[mgp.Vertex],
WITH collect(n) AS nodes, collect(e) AS edges WITH collect(n) AS nodes, collect(e) AS edges
CALL wcc.get_components(nodes, edges) YIELD * CALL wcc.get_components(nodes, edges) YIELD *
RETURN n_components, components; RETURN n_components, components;
''' """
g = nx.DiGraph() g = nx.DiGraph()
g.add_nodes_from(vertices) g.add_nodes_from(vertices)
g.add_edges_from([(edge.from_vertex, edge.to_vertex) for edge in edges]) g.add_edges_from([(edge.from_vertex, edge.to_vertex) for edge in edges])

View File

@@ -104,7 +104,9 @@ def retry(retry_limit, timeout=100):
except Exception: except Exception:
time.sleep(timeout) time.sleep(timeout)
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
return inner_func return inner_func
@@ -163,8 +165,15 @@ def format_version(variant, version, offering, distance=None, shorthash=None, su
# Parse arguments. # Parse arguments.
parser = argparse.ArgumentParser(description="Get the current version of Memgraph.") parser = argparse.ArgumentParser(description="Get the current version of Memgraph.")
parser.add_argument("--open-source", action="store_true", help="set the current offering to 'open-source'") parser.add_argument(
parser.add_argument("version", help="manual version override, if supplied the version isn't " "determined using git") "--open-source",
action="store_true",
help="set the current offering to 'open-source'",
)
parser.add_argument(
"version",
help="manual version override, if supplied the version isn't " "determined using git",
)
parser.add_argument("suffix", help="custom suffix for the current version being built") parser.add_argument("suffix", help="custom suffix for the current version being built")
parser.add_argument( parser.add_argument(
"--variant", "--variant",
@@ -173,7 +182,9 @@ parser.add_argument(
help="which variant of the version string should be generated", help="which variant of the version string should be generated",
) )
parser.add_argument( parser.add_argument(
"--memgraph-root-dir", help="The root directory of the checked out " "Memgraph repository.", default="." "--memgraph-root-dir",
help="The root directory of the checked out " "Memgraph repository.",
default=".",
) )
args = parser.parse_args() args = parser.parse_args()
@@ -256,14 +267,27 @@ for version in versions:
if current_version is None: if current_version is None:
raise Exception("You are attempting to determine the version for a very " "old version of Memgraph!") raise Exception("You are attempting to determine the version for a very " "old version of Memgraph!")
version, branch, master_branch_merge = current_version version, branch, master_branch_merge = current_version
distance = int(get_output("git", "rev-list", "--count", "--first-parent", master_branch_merge + ".." + current_hash)) distance = int(
get_output(
"git",
"rev-list",
"--count",
"--first-parent",
master_branch_merge + ".." + current_hash,
)
)
version_str = ".".join(map(str, version)) + ".0" version_str = ".".join(map(str, version)) + ".0"
if distance == 0: if distance == 0:
print(format_version(args.variant, version_str, offering, suffix=args.suffix), end="") print(format_version(args.variant, version_str, offering, suffix=args.suffix), end="")
else: else:
print( print(
format_version( format_version(
args.variant, version_str, offering, distance=distance, shorthash=current_hash_short, suffix=args.suffix args.variant,
version_str,
offering,
distance=distance,
shorthash=current_hash_short,
suffix=args.suffix,
), ),
end="", end="",
) )

View File

@@ -84,6 +84,8 @@ std::string PermissionToString(Permission permission) {
return "MODULE_WRITE"; return "MODULE_WRITE";
case Permission::WEBSOCKET: case Permission::WEBSOCKET:
return "WEBSOCKET"; return "WEBSOCKET";
case Permission::LABELS:
return "LABELS";
} }
} }
@@ -183,19 +185,107 @@ bool operator==(const Permissions &first, const Permissions &second) {
bool operator!=(const Permissions &first, const Permissions &second) { return !(first == second); } bool operator!=(const Permissions &first, const Permissions &second) { return !(first == second); }
LabelPermissions::LabelPermissions(const std::unordered_set<std::string> &grants,
const std::unordered_set<std::string> &denies)
: grants_(grants), denies_(denies) {}
PermissionLevel LabelPermissions::Has(const std::string &permission) const {
if (denies_.find(permission) != denies_.end()) {
return PermissionLevel::DENY;
}
if (grants_.find(permission) != denies_.end()) {
return PermissionLevel::GRANT;
}
return PermissionLevel::NEUTRAL;
}
void LabelPermissions::Grant(const std::string &permission) {
auto deniedPermissionIter = denies_.find(permission);
if (deniedPermissionIter != denies_.end()) {
denies_.erase(deniedPermissionIter);
}
if (grants_.find(permission) == grants_.end()) {
grants_.insert(permission);
}
}
void LabelPermissions::Revoke(const std::string &permission) {
auto deniedPermissionIter = denies_.find(permission);
auto grantedPermissionIter = grants_.find(permission);
if (deniedPermissionIter != denies_.end()) {
denies_.erase(deniedPermissionIter);
}
if (grantedPermissionIter != grants_.end()) {
grants_.erase(grantedPermissionIter);
}
}
void LabelPermissions::Deny(const std::string &permission) {
auto grantedPermissionIter = grants_.find(permission);
if (grantedPermissionIter != grants_.end()) {
grants_.erase(grantedPermissionIter);
}
if (denies_.find(permission) == denies_.end()) {
denies_.insert(permission);
}
}
std::unordered_set<std::string> LabelPermissions::GetGrants() const { return grants_; }
std::unordered_set<std::string> LabelPermissions::GetDenies() const { return denies_; }
nlohmann::json LabelPermissions::Serialize() const {
nlohmann::json data = nlohmann::json::object();
data["grants"] = grants_;
data["denies"] = denies_;
return data;
}
LabelPermissions LabelPermissions::Deserialize(const nlohmann::json &data) {
if (!data.is_object()) {
throw AuthException("Couldn't load permissions data!");
}
return {LabelPermissions(data["grants"], data["denies"])};
}
std::unordered_set<std::string> LabelPermissions::grants() const { return grants_; }
std::unordered_set<std::string> LabelPermissions::denies() const { return denies_; }
bool operator==(const LabelPermissions &first, const LabelPermissions &second) {
return first.grants() == second.grants() && first.denies() == second.denies();
}
bool operator!=(const LabelPermissions &first, const LabelPermissions &second) { return !(first == second); }
Role::Role(const std::string &rolename) : rolename_(utils::ToLowerCase(rolename)) {} Role::Role(const std::string &rolename) : rolename_(utils::ToLowerCase(rolename)) {}
Role::Role(const std::string &rolename, const Permissions &permissions) Role::Role(const std::string &rolename, const Permissions &permissions)
: rolename_(utils::ToLowerCase(rolename)), permissions_(permissions) {} : rolename_(utils::ToLowerCase(rolename)), permissions_(permissions) {}
Role::Role(const std::string &rolename, const Permissions &permissions, const LabelPermissions &labelPermissions)
: rolename_(utils::ToLowerCase(rolename)), permissions_(permissions), labelPermissions_(labelPermissions) {}
const std::string &Role::rolename() const { return rolename_; } const std::string &Role::rolename() const { return rolename_; }
const Permissions &Role::permissions() const { return permissions_; } const Permissions &Role::permissions() const { return permissions_; }
Permissions &Role::permissions() { return permissions_; } Permissions &Role::permissions() { return permissions_; }
LabelPermissions &Role::labelPermissions() { return labelPermissions_; }
nlohmann::json Role::Serialize() const { nlohmann::json Role::Serialize() const {
nlohmann::json data = nlohmann::json::object(); nlohmann::json data = nlohmann::json::object();
data["rolename"] = rolename_; data["rolename"] = rolename_;
data["permissions"] = permissions_.Serialize(); data["permissions"] = permissions_.Serialize();
data["labelPermissions"] = labelPermissions_.Serialize();
return data; return data;
} }
@@ -207,7 +297,9 @@ Role Role::Deserialize(const nlohmann::json &data) {
throw AuthException("Couldn't load role data!"); throw AuthException("Couldn't load role data!");
} }
auto permissions = Permissions::Deserialize(data["permissions"]); auto permissions = Permissions::Deserialize(data["permissions"]);
return {data["rolename"], permissions}; auto labelPermissions = LabelPermissions::Deserialize(data["labelPermissions"]);
return {data["rolename"], permissions, labelPermissions};
} }
bool operator==(const Role &first, const Role &second) { bool operator==(const Role &first, const Role &second) {
@@ -219,6 +311,13 @@ User::User(const std::string &username) : username_(utils::ToLowerCase(username)
User::User(const std::string &username, const std::string &password_hash, const Permissions &permissions) User::User(const std::string &username, const std::string &password_hash, const Permissions &permissions)
: username_(utils::ToLowerCase(username)), password_hash_(password_hash), permissions_(permissions) {} : username_(utils::ToLowerCase(username)), password_hash_(password_hash), permissions_(permissions) {}
User::User(const std::string &username, const std::string &password_hash, const Permissions &permissions,
const LabelPermissions &labelPermissions)
: username_(utils::ToLowerCase(username)),
password_hash_(password_hash),
permissions_(permissions),
labelPermissions_(labelPermissions) {}
bool User::CheckPassword(const std::string &password) { bool User::CheckPassword(const std::string &password) {
if (password_hash_.empty()) return true; if (password_hash_.empty()) return true;
return VerifyPassword(password, password_hash_); return VerifyPassword(password, password_hash_);
@@ -271,6 +370,8 @@ const std::string &User::username() const { return username_; }
const Permissions &User::permissions() const { return permissions_; } const Permissions &User::permissions() const { return permissions_; }
Permissions &User::permissions() { return permissions_; } Permissions &User::permissions() { return permissions_; }
LabelPermissions &User::labelPermissions() { return labelPermissions_; }
const Role *User::role() const { const Role *User::role() const {
if (role_.has_value()) { if (role_.has_value()) {
return &role_.value(); return &role_.value();
@@ -283,6 +384,7 @@ nlohmann::json User::Serialize() const {
data["username"] = username_; data["username"] = username_;
data["password_hash"] = password_hash_; data["password_hash"] = password_hash_;
data["permissions"] = permissions_.Serialize(); data["permissions"] = permissions_.Serialize();
data["labelPermissions"] = labelPermissions_.Serialize();
// The role shouldn't be serialized here, it is stored as a foreign key. // The role shouldn't be serialized here, it is stored as a foreign key.
return data; return data;
} }
@@ -295,11 +397,14 @@ User User::Deserialize(const nlohmann::json &data) {
throw AuthException("Couldn't load user data!"); throw AuthException("Couldn't load user data!");
} }
auto permissions = Permissions::Deserialize(data["permissions"]); auto permissions = Permissions::Deserialize(data["permissions"]);
return {data["username"], data["password_hash"], permissions}; auto labelPermissions = LabelPermissions::Deserialize(data["labelPermissions"]);
return {data["username"], data["password_hash"], permissions, labelPermissions};
} }
bool operator==(const User &first, const User &second) { bool operator==(const User &first, const User &second) {
return first.username_ == second.username_ && first.password_hash_ == second.password_hash_ && return first.username_ == second.username_ && first.password_hash_ == second.password_hash_ &&
first.permissions_ == second.permissions_ && first.role_ == second.role_; first.permissions_ == second.permissions_ && first.role_ == second.role_;
} }
} // namespace memgraph::auth } // namespace memgraph::auth

View File

@@ -12,6 +12,7 @@
#include <string> #include <string>
#include <json/json.hpp> #include <json/json.hpp>
#include <unordered_set>
namespace memgraph::auth { namespace memgraph::auth {
// These permissions must have values that are applicable for usage in a // These permissions must have values that are applicable for usage in a
@@ -38,7 +39,8 @@ enum class Permission : uint64_t {
STREAM = 1U << 17U, STREAM = 1U << 17U,
MODULE_READ = 1U << 18U, MODULE_READ = 1U << 18U,
MODULE_WRITE = 1U << 19U, MODULE_WRITE = 1U << 19U,
WEBSOCKET = 1U << 20U WEBSOCKET = 1U << 20U,
LABELS = 1U << 21U
}; };
// clang-format on // clang-format on
@@ -88,16 +90,52 @@ bool operator==(const Permissions &first, const Permissions &second);
bool operator!=(const Permissions &first, const Permissions &second); bool operator!=(const Permissions &first, const Permissions &second);
class LabelPermissions final {
public:
LabelPermissions(const std::unordered_set<std::string> &grants = {},
const std::unordered_set<std::string> &denies = {});
PermissionLevel Has(const std::string &permission) const;
void Grant(const std::string &permission);
void Revoke(const std::string &permission);
void Deny(const std::string &permission);
std::unordered_set<std::string> GetGrants() const;
std::unordered_set<std::string> GetDenies() const;
nlohmann::json Serialize() const;
/// @throw AuthException if unable to deserialize.
static LabelPermissions Deserialize(const nlohmann::json &data);
std::unordered_set<std::string> grants() const;
std::unordered_set<std::string> denies() const;
private:
std::unordered_set<std::string> grants_{};
std::unordered_set<std::string> denies_{};
};
bool operator==(const LabelPermissions &first, const LabelPermissions &second);
bool operator!=(const LabelPermissions &first, const LabelPermissions &second);
class Role final { class Role final {
public: public:
Role(const std::string &rolename); Role(const std::string &rolename);
Role(const std::string &rolename, const Permissions &permissions); Role(const std::string &rolename, const Permissions &permissions);
Role(const std::string &rolename, const Permissions &permissions, const LabelPermissions &labelPermissions);
const std::string &rolename() const; const std::string &rolename() const;
const Permissions &permissions() const; const Permissions &permissions() const;
Permissions &permissions(); Permissions &permissions();
LabelPermissions &labelPermissions();
nlohmann::json Serialize() const; nlohmann::json Serialize() const;
/// @throw AuthException if unable to deserialize. /// @throw AuthException if unable to deserialize.
@@ -108,6 +146,7 @@ class Role final {
private: private:
std::string rolename_; std::string rolename_;
Permissions permissions_; Permissions permissions_;
LabelPermissions labelPermissions_;
}; };
bool operator==(const Role &first, const Role &second); bool operator==(const Role &first, const Role &second);
@@ -119,6 +158,9 @@ class User final {
User(const std::string &username, const std::string &password_hash, const Permissions &permissions); User(const std::string &username, const std::string &password_hash, const Permissions &permissions);
User(const std::string &username, const std::string &password_hash, const Permissions &permissions,
const LabelPermissions &labelPermissions);
/// @throw AuthException if unable to verify the password. /// @throw AuthException if unable to verify the password.
bool CheckPassword(const std::string &password); bool CheckPassword(const std::string &password);
@@ -138,6 +180,8 @@ class User final {
const Role *role() const; const Role *role() const;
LabelPermissions &labelPermissions();
nlohmann::json Serialize() const; nlohmann::json Serialize() const;
/// @throw AuthException if unable to deserialize. /// @throw AuthException if unable to deserialize.
@@ -150,7 +194,9 @@ class User final {
std::string password_hash_; std::string password_hash_;
Permissions permissions_; Permissions permissions_;
std::optional<Role> role_; std::optional<Role> role_;
LabelPermissions labelPermissions_;
}; };
bool operator==(const User &first, const User &second); bool operator==(const User &first, const User &second);
} // namespace memgraph::auth } // namespace memgraph::auth

View File

@@ -18,19 +18,24 @@ roles_config = config["roles"]
# Initialize LDAP server. # Initialize LDAP server.
tls = None tls = None
if server_config["encryption"] != "disabled": if server_config["encryption"] != "disabled":
cert_file = server_config["cert_file"] if server_config["cert_file"] \ cert_file = server_config["cert_file"] if server_config["cert_file"] else None
else None
key_file = server_config["key_file"] if server_config["key_file"] else None key_file = server_config["key_file"] if server_config["key_file"] else None
ca_file = server_config["ca_file"] if server_config["ca_file"] else None ca_file = server_config["ca_file"] if server_config["ca_file"] else None
validate = ssl.CERT_REQUIRED if server_config["validate_cert"] \ validate = ssl.CERT_REQUIRED if server_config["validate_cert"] else ssl.CERT_NONE
else ssl.CERT_NONE tls = ldap3.Tls(
tls = ldap3.Tls(local_private_key_file=key_file, local_private_key_file=key_file,
local_certificate_file=cert_file, local_certificate_file=cert_file,
ca_certs_file=ca_file, ca_certs_file=ca_file,
validate=validate) validate=validate,
)
use_ssl = server_config["encryption"] == "ssl" use_ssl = server_config["encryption"] == "ssl"
server = ldap3.Server(server_config["host"], port=server_config["port"], server = ldap3.Server(
tls=tls, use_ssl=use_ssl, get_info=ldap3.ALL) server_config["host"],
port=server_config["port"],
tls=tls,
use_ssl=use_ssl,
get_info=ldap3.ALL,
)
# Main authentication/authorization function. # Main authentication/authorization function.
@@ -40,14 +45,12 @@ def authenticate(username, password):
return {"authenticated": False, "role": ""} return {"authenticated": False, "role": ""}
# Create the DN of the user # Create the DN of the user
dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + \ dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + users_config["suffix"]
users_config["suffix"]
# Bind to the server # Bind to the server
conn = ldap3.Connection(server, dn, password) conn = ldap3.Connection(server, dn, password)
if server_config["encryption"] == "starttls" and not conn.start_tls(): if server_config["encryption"] == "starttls" and not conn.start_tls():
print("ERROR: Couldn't issue STARTTLS to the LDAP server!", print("ERROR: Couldn't issue STARTTLS to the LDAP server!", file=sys.stderr)
file=sys.stderr)
return {"authenticated": False, "role": ""} return {"authenticated": False, "role": ""}
if not conn.bind(): if not conn.bind():
return {"authenticated": False, "role": ""} return {"authenticated": False, "role": ""}
@@ -56,25 +59,32 @@ def authenticate(username, password):
if roles_config["root_dn"] != "": if roles_config["root_dn"] != "":
# search for role # search for role
search_filter = "(&(objectclass={objclass})({attr}={value}))".format( search_filter = "(&(objectclass={objclass})({attr}={value}))".format(
objclass=roles_config["root_objectclass"], objclass=roles_config["root_objectclass"],
attr=roles_config["user_attribute"], attr=roles_config["user_attribute"],
value=ldap3.utils.conv.escape_filter_chars(dn)) value=ldap3.utils.conv.escape_filter_chars(dn),
succ = conn.search(roles_config["root_dn"], search_filter, )
search_scope=ldap3.LEVEL, succ = conn.search(
attributes=[roles_config["role_attribute"]]) roles_config["root_dn"],
search_filter,
search_scope=ldap3.LEVEL,
attributes=[roles_config["role_attribute"]],
)
if not succ or len(conn.entries) == 0: if not succ or len(conn.entries) == 0:
return {"authenticated": True, "role": ""} return {"authenticated": True, "role": ""}
if len(conn.entries) > 1: if len(conn.entries) > 1:
roles = list(map(lambda x: x[roles_config["role_attribute"]].value, roles = list(map(lambda x: x[roles_config["role_attribute"]].value, conn.entries))
conn.entries))
# Because we don't know exactly which role the user should have # Because we don't know exactly which role the user should have
# we authorize the user with an empty role. # we authorize the user with an empty role.
print("WARNING: Found more than one role for " print(
"user '" + username + "':", ", ".join(roles) + "!", "WARNING: Found more than one role for " "user '" + username + "':",
file=sys.stderr) ", ".join(roles) + "!",
file=sys.stderr,
)
return {"authenticated": True, "role": ""} return {"authenticated": True, "role": ""}
return {"authenticated": True, return {
"role": conn.entries[0][roles_config["role_attribute"]].value} "authenticated": True,
"role": conn.entries[0][roles_config["role_attribute"]].value,
}
else: else:
return {"authenticated": True, "role": ""} return {"authenticated": True, "role": ""}

View File

@@ -57,6 +57,8 @@ auth::Permission PrivilegeToPermission(query::AuthQuery::Privilege privilege) {
return auth::Permission::MODULE_WRITE; return auth::Permission::MODULE_WRITE;
case query::AuthQuery::Privilege::WEBSOCKET: case query::AuthQuery::Privilege::WEBSOCKET:
return auth::Permission::WEBSOCKET; return auth::Permission::WEBSOCKET;
case query::AuthQuery::Privilege::LABELS:
return auth::Permission::LABELS;
} }
} }
} // namespace memgraph::glue } // namespace memgraph::glue

View File

@@ -501,7 +501,7 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
if (first_user) { if (first_user) {
spdlog::info("{} is first created user. Granting all privileges.", username); spdlog::info("{} is first created user. Granting all privileges.", username);
GrantPrivilege(username, memgraph::query::kPrivilegesAll); GrantPrivilege(username, memgraph::query::kPrivilegesAll, {"*"});
} }
return user_added; return user_added;
@@ -747,8 +747,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
} }
void GrantPrivilege(const std::string &user_or_role, void GrantPrivilege(const std::string &user_or_role,
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override { const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) { const std::vector<std::string> &labels) override {
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
// TODO (mferencevic): should we first check that the // TODO (mferencevic): should we first check that the
// privilege is granted/denied/revoked before // privilege is granted/denied/revoked before
// unconditionally granting/denying/revoking it? // unconditionally granting/denying/revoking it?
@@ -757,8 +758,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
} }
void DenyPrivilege(const std::string &user_or_role, void DenyPrivilege(const std::string &user_or_role,
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override { const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) { const std::vector<std::string> &labels) override {
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
// TODO (mferencevic): should we first check that the // TODO (mferencevic): should we first check that the
// privilege is granted/denied/revoked before // privilege is granted/denied/revoked before
// unconditionally granting/denying/revoking it? // unconditionally granting/denying/revoking it?
@@ -767,8 +769,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
} }
void RevokePrivilege(const std::string &user_or_role, void RevokePrivilege(const std::string &user_or_role,
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override { const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) { const std::vector<std::string> &labels) override {
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
// TODO (mferencevic): should we first check that the // TODO (mferencevic): should we first check that the
// privilege is granted/denied/revoked before // privilege is granted/denied/revoked before
// unconditionally granting/denying/revoking it? // unconditionally granting/denying/revoking it?
@@ -779,7 +782,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
private: private:
template <class TEditFun> template <class TEditFun>
void EditPermissions(const std::string &user_or_role, void EditPermissions(const std::string &user_or_role,
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges, const TEditFun &edit_fun) { const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
const std::vector<std::string> &labels, const TEditFun &edit_fun) {
if (!std::regex_match(user_or_role, name_regex_)) { if (!std::regex_match(user_or_role, name_regex_)) {
throw memgraph::query::QueryRuntimeException("Invalid user or role name."); throw memgraph::query::QueryRuntimeException("Invalid user or role name.");
} }
@@ -799,11 +803,17 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
for (const auto &permission : permissions) { for (const auto &permission : permissions) {
edit_fun(&user->permissions(), permission); edit_fun(&user->permissions(), permission);
} }
for (const auto &label : labels) {
edit_fun(&user->labelPermissions(), label);
}
locked_auth->SaveUser(*user); locked_auth->SaveUser(*user);
} else { } else {
for (const auto &permission : permissions) { for (const auto &permission : permissions) {
edit_fun(&role->permissions(), permission); edit_fun(&role->permissions(), permission);
} }
for (const auto &label : labels) {
edit_fun(&role->labelPermissions(), label);
}
locked_auth->SaveRole(*role); locked_auth->SaveRole(*role);
} }
} catch (const memgraph::auth::AuthException &e) { } catch (const memgraph::auth::AuthException &e) {

View File

@@ -2239,9 +2239,11 @@ cpp<#
(user "std::string" :scope :public) (user "std::string" :scope :public)
(role "std::string" :scope :public) (role "std::string" :scope :public)
(user-or-role "std::string" :scope :public) (user-or-role "std::string" :scope :public)
(password "Expression *" :initval "nullptr" :scope :public (password "Expression *" :initval "nullptr" :scope :public
:slk-save #'slk-save-ast-pointer :slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Expression")) :slk-load (slk-load-ast-pointer "Expression"))
(labels "std::vector<std::string>" :scope :public)
(privileges "std::vector<Privilege>" :scope :public)) (privileges "std::vector<Privilege>" :scope :public))
(:public (:public
(lcp:define-enum action (lcp:define-enum action
@@ -2253,7 +2255,7 @@ cpp<#
(lcp:define-enum privilege (lcp:define-enum privilege
(create delete match merge set remove index stats auth constraint (create delete match merge set remove index stats auth constraint
dump replication durability read_file free_memory trigger config stream module_read module_write dump replication durability read_file free_memory trigger config stream module_read module_write
websocket) websocket labels)
(:serialize)) (:serialize))
#>cpp #>cpp
AuthQuery() = default; AuthQuery() = default;
@@ -2264,13 +2266,14 @@ cpp<#
#>cpp #>cpp
AuthQuery(Action action, std::string user, std::string role, AuthQuery(Action action, std::string user, std::string role,
std::string user_or_role, Expression *password, std::string user_or_role, Expression *password,
std::vector<Privilege> privileges) std::vector<std::string> labels ,std::vector<Privilege> privileges)
: action_(action), : action_(action),
user_(user), user_(user),
role_(role), role_(role),
user_or_role_(user_or_role), user_or_role_(user_or_role),
password_(password), password_(password),
privileges_(privileges) {} labels_(labels),
privileges_(privileges){}
cpp<#) cpp<#)
(:private (:private
#>cpp #>cpp
@@ -2295,7 +2298,8 @@ const std::vector<AuthQuery::Privilege> kPrivilegesAll = {
AuthQuery::Privilege::FREE_MEMORY, AuthQuery::Privilege::TRIGGER, AuthQuery::Privilege::FREE_MEMORY, AuthQuery::Privilege::TRIGGER,
AuthQuery::Privilege::CONFIG, AuthQuery::Privilege::STREAM, AuthQuery::Privilege::CONFIG, AuthQuery::Privilege::STREAM,
AuthQuery::Privilege::MODULE_READ, AuthQuery::Privilege::MODULE_WRITE, AuthQuery::Privilege::MODULE_READ, AuthQuery::Privilege::MODULE_WRITE,
AuthQuery::Privilege::WEBSOCKET}; AuthQuery::Privilege::WEBSOCKET,
AuthQuery::Privilege::LABELS};
cpp<# cpp<#
(lcp:define-class info-query (query) (lcp:define-class info-query (query)

View File

@@ -1285,7 +1285,11 @@ antlrcpp::Any CypherMainVisitor::visitGrantPrivilege(MemgraphCypher::GrantPrivil
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>(); auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
if (ctx->privilegeList()) { if (ctx->privilegeList()) {
for (auto *privilege : ctx->privilegeList()->privilege()) { for (auto *privilege : ctx->privilegeList()->privilege()) {
auth->privileges_.push_back(privilege->accept(this)); if (privilege->LABELS()) {
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
} else {
auth->privileges_.push_back(privilege->accept(this));
}
} }
} else { } else {
/* grant all privileges */ /* grant all privileges */
@@ -1303,7 +1307,11 @@ antlrcpp::Any CypherMainVisitor::visitDenyPrivilege(MemgraphCypher::DenyPrivileg
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>(); auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
if (ctx->privilegeList()) { if (ctx->privilegeList()) {
for (auto *privilege : ctx->privilegeList()->privilege()) { for (auto *privilege : ctx->privilegeList()->privilege()) {
auth->privileges_.push_back(privilege->accept(this)); if (privilege->LABELS()) {
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
} else {
auth->privileges_.push_back(privilege->accept(this));
}
} }
} else { } else {
/* deny all privileges */ /* deny all privileges */
@@ -1321,7 +1329,11 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege(MemgraphCypher::RevokePriv
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>(); auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
if (ctx->privilegeList()) { if (ctx->privilegeList()) {
for (auto *privilege : ctx->privilegeList()->privilege()) { for (auto *privilege : ctx->privilegeList()->privilege()) {
auth->privileges_.push_back(privilege->accept(this)); if (privilege->LABELS()) {
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
} else {
auth->privileges_.push_back(privilege->accept(this));
}
} }
} else { } else {
/* revoke all privileges */ /* revoke all privileges */
@@ -1330,6 +1342,22 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege(MemgraphCypher::RevokePriv
return auth; return auth;
} }
/**
* @return AuthQuery*
*/
antlrcpp::Any CypherMainVisitor::visitLabelList(MemgraphCypher::LabelListContext *ctx) {
std::vector<std::string> labels;
for (auto *label : ctx->label()) {
if (label->ASTERISK()) {
labels.push_back("*");
} else {
labels.push_back(label->symbolicName()->accept(this).as<std::string>());
}
}
return labels;
}
/** /**
* @return AuthQuery::Privilege * @return AuthQuery::Privilege
*/ */
@@ -1355,6 +1383,10 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege(MemgraphCypher::PrivilegeContext
if (ctx->MODULE_READ()) return AuthQuery::Privilege::MODULE_READ; if (ctx->MODULE_READ()) return AuthQuery::Privilege::MODULE_READ;
if (ctx->MODULE_WRITE()) return AuthQuery::Privilege::MODULE_WRITE; if (ctx->MODULE_WRITE()) return AuthQuery::Privilege::MODULE_WRITE;
if (ctx->WEBSOCKET()) return AuthQuery::Privilege::WEBSOCKET; if (ctx->WEBSOCKET()) return AuthQuery::Privilege::WEBSOCKET;
if (ctx->LABELS()) {
// fill labels in authquery
return AuthQuery::Privilege::LABELS;
}
LOG_FATAL("Should not get here - unknown privilege!"); LOG_FATAL("Should not get here - unknown privilege!");
} }

View File

@@ -473,6 +473,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
*/ */
antlrcpp::Any visitPrivilege(MemgraphCypher::PrivilegeContext *ctx) override; antlrcpp::Any visitPrivilege(MemgraphCypher::PrivilegeContext *ctx) override;
/**
* @return AuthQuery::LabelList
*/
antlrcpp::Any visitLabelList(MemgraphCypher::LabelListContext *ctx) override;
/** /**
* @return AuthQuery* * @return AuthQuery*
*/ */

View File

@@ -56,6 +56,7 @@ memgraphCypherKeyword : cypherKeyword
| IDENTIFIED | IDENTIFIED
| ISOLATION | ISOLATION
| KAFKA | KAFKA
| LABELS
| LEVEL | LEVEL
| LOAD | LOAD
| LOCK | LOCK
@@ -254,10 +255,15 @@ privilege : CREATE
| MODULE_READ | MODULE_READ
| MODULE_WRITE | MODULE_WRITE
| WEBSOCKET | WEBSOCKET
| LABELS labels=labelList
; ;
privilegeList : privilege ( ',' privilege )* ; privilegeList : privilege ( ',' privilege )* ;
labelList : COLON label ( ',' COLON label )* ;
label : ( '*' | symbolicName ) ;
showPrivileges : SHOW PRIVILEGES FOR userOrRole=userOrRoleName ; showPrivileges : SHOW PRIVILEGES FOR userOrRole=userOrRoleName ;
showRoleForUser : SHOW ROLE FOR user=userOrRoleName ; showRoleForUser : SHOW ROLE FOR user=userOrRoleName ;

View File

@@ -66,6 +66,7 @@ IDENTIFIED : I D E N T I F I E D ;
IGNORE : I G N O R E ; IGNORE : I G N O R E ;
ISOLATION : I S O L A T I O N ; ISOLATION : I S O L A T I O N ;
KAFKA : K A F K A ; KAFKA : K A F K A ;
LABELS : L A B E L S ;
LEVEL : L E V E L ; LEVEL : L E V E L ;
LOAD : L O A D ; LOAD : L O A D ;
LOCK : L O C K ; LOCK : L O C K ;

View File

@@ -204,8 +204,9 @@ const trie::Trie kKeywords = {"union",
"pulsar", "pulsar",
"service_url", "service_url",
"version", "version",
"websocket" "websocket",
"foreach"}; "foreach",
"labels"};
// Unicode codepoints that are allowed at the start of the unescaped name. // Unicode codepoints that are allowed at the start of the unescaped name.
const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts( const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts(

View File

@@ -282,6 +282,8 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
std::string rolename = auth_query->role_; std::string rolename = auth_query->role_;
std::string user_or_role = auth_query->user_or_role_; std::string user_or_role = auth_query->user_or_role_;
std::vector<AuthQuery::Privilege> privileges = auth_query->privileges_; std::vector<AuthQuery::Privilege> privileges = auth_query->privileges_;
std::vector<std::string> labels = auth_query->labels_;
// std::vector<storage::LabelId> labels = NamesToLabels(labels, db_accessor);
auto password = EvaluateOptionalExpression(auth_query->password_, &evaluator); auto password = EvaluateOptionalExpression(auth_query->password_, &evaluator);
Callback callback; Callback callback;
@@ -296,7 +298,8 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
if (license_check_result.HasError() && enterprise_only_methods.contains(auth_query->action_)) { if (license_check_result.HasError() && enterprise_only_methods.contains(auth_query->action_)) {
throw utils::BasicException( throw utils::BasicException(
utils::license::LicenseCheckErrorToString(license_check_result.GetError(), "advanced authentication features")); utils::license::LicenseCheckErrorToString(license_check_result.GetError(), "advanced authentication
features"));
} }
switch (auth_query->action_) { switch (auth_query->action_) {
@@ -311,7 +314,7 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
// If the license is not valid we create users with admin access // If the license is not valid we create users with admin access
if (!valid_enterprise_license) { if (!valid_enterprise_license) {
spdlog::warn("Granting all the privileges to {}.", username); spdlog::warn("Granting all the privileges to {}.", username);
auth->GrantPrivilege(username, kPrivilegesAll); auth->GrantPrivilege(username, kPrivilegesAll, {});
} }
return std::vector<std::vector<TypedValue>>(); return std::vector<std::vector<TypedValue>>();
@@ -386,20 +389,20 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
}; };
return callback; return callback;
case AuthQuery::Action::GRANT_PRIVILEGE: case AuthQuery::Action::GRANT_PRIVILEGE:
callback.fn = [auth, user_or_role, privileges] { callback.fn = [auth, user_or_role, privileges, labels] {
auth->GrantPrivilege(user_or_role, privileges); auth->GrantPrivilege(user_or_role, privileges, labels);
return std::vector<std::vector<TypedValue>>(); return std::vector<std::vector<TypedValue>>();
}; };
return callback; return callback;
case AuthQuery::Action::DENY_PRIVILEGE: case AuthQuery::Action::DENY_PRIVILEGE:
callback.fn = [auth, user_or_role, privileges] { callback.fn = [auth, user_or_role, privileges, labels] {
auth->DenyPrivilege(user_or_role, privileges); auth->DenyPrivilege(user_or_role, privileges, labels);
return std::vector<std::vector<TypedValue>>(); return std::vector<std::vector<TypedValue>>();
}; };
return callback; return callback;
case AuthQuery::Action::REVOKE_PRIVILEGE: { case AuthQuery::Action::REVOKE_PRIVILEGE: {
callback.fn = [auth, user_or_role, privileges] { callback.fn = [auth, user_or_role, privileges, labels] {
auth->RevokePrivilege(user_or_role, privileges); auth->RevokePrivilege(user_or_role, privileges, labels);
return std::vector<std::vector<TypedValue>>(); return std::vector<std::vector<TypedValue>>();
}; };
return callback; return callback;

View File

@@ -99,14 +99,16 @@ class AuthQueryHandler {
virtual std::vector<std::vector<TypedValue>> GetPrivileges(const std::string &user_or_role) = 0; virtual std::vector<std::vector<TypedValue>> GetPrivileges(const std::string &user_or_role) = 0;
/// @throw QueryRuntimeException if an error ocurred. /// @throw QueryRuntimeException if an error ocurred.
virtual void GrantPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges) = 0; virtual void GrantPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
const std::vector<std::string> &labels) = 0;
/// @throw QueryRuntimeException if an error ocurred. /// @throw QueryRuntimeException if an error ocurred.
virtual void DenyPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges) = 0; virtual void DenyPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
const std::vector<std::string> &labels) = 0;
/// @throw QueryRuntimeException if an error ocurred. /// @throw QueryRuntimeException if an error ocurred.
virtual void RevokePrivilege(const std::string &user_or_role, virtual void RevokePrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
const std::vector<AuthQuery::Privilege> &privileges) = 0; const std::vector<std::string> &labels) = 0;
}; };
enum class QueryHandlerResult { COMMIT, ABORT, NOTHING }; enum class QueryHandlerResult { COMMIT, ABORT, NOTHING };

View File

@@ -15,33 +15,31 @@
import sys import sys
from neo4j import GraphDatabase, basic_auth from neo4j import GraphDatabase, basic_auth
driver = GraphDatabase.driver('bolt://localhost:7687', driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False)
auth=basic_auth('', ''),
encrypted=False)
session = driver.session() session = driver.session()
session.run('MATCH (n) DETACH DELETE n').consume() session.run("MATCH (n) DETACH DELETE n").consume()
print('Database cleared.') print("Database cleared.")
session.run('CREATE (alice:Person {name: "Alice", age: 22})').consume() session.run('CREATE (alice:Person {name: "Alice", age: 22})').consume()
print('Record created.') print("Record created.")
node = session.run('MATCH (n) RETURN n').single()['n'] node = session.run("MATCH (n) RETURN n").single()["n"]
print('Record matched.') print("Record matched.")
label = list(node.labels)[0] label = list(node.labels)[0]
name = node['name'] name = node["name"]
age = node['age'] age = node["age"]
if label != 'Person' or name != 'Alice' or age != 22: if label != "Person" or name != "Alice" or age != 22:
print('Data does not match') print("Data does not match")
sys.exit(1) sys.exit(1)
print('Label: %s' % label) print("Label: %s" % label)
print('name: %s' % name) print("name: %s" % name)
print('age: %s' % age) print("age: %s" % age)
session.close() session.close()
driver.close() driver.close()
print('All ok!') print("All ok!")

View File

@@ -14,9 +14,7 @@
from neo4j import GraphDatabase, basic_auth from neo4j import GraphDatabase, basic_auth
driver = GraphDatabase.driver("bolt://localhost:7687", driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False)
auth=basic_auth("", ""),
encrypted=False)
query_template = 'CREATE (n {name:"%s"})' query_template = 'CREATE (n {name:"%s"})'
template_size = len(query_template) - 2 # because of %s template_size = len(query_template) - 2 # because of %s
@@ -26,10 +24,11 @@ max_len = 1000000
# binary search because we have to find the maximum size (in number of chars) # binary search because we have to find the maximum size (in number of chars)
# of a query that can be executed via driver # of a query that can be executed via driver
while True: while True:
assert min_len > 0 and max_len > 0, \ assert min_len > 0 and max_len > 0, (
"The lengths have to be positive values! If this happens something" \ "The lengths have to be positive values! If this happens something"
" is terrible wrong with min & max lengths OR the database" \ " is terrible wrong with min & max lengths OR the database"
" isn't available." " isn't available."
)
property_size = (max_len + min_len) // 2 property_size = (max_len + min_len) // 2
try: try:
driver.session().run(query_template % ("a" * property_size)).consume() driver.session().run(query_template % ("a" * property_size)).consume()
@@ -42,8 +41,7 @@ while True:
assert property_size == max_len, "max_len probably has to be increased!" assert property_size == max_len, "max_len probably has to be increased!"
print("\nThe max length of a query from Python driver is: %s\n" % print("\nThe max length of a query from Python driver is: %s\n" % (template_size + property_size))
(template_size + property_size))
# sessions are not closed bacause all sessions that are # sessions are not closed bacause all sessions that are
# executed with wrong query size might be broken # executed with wrong query size might be broken

View File

@@ -15,6 +15,7 @@
from neo4j import GraphDatabase, basic_auth from neo4j import GraphDatabase, basic_auth
from neo4j.exceptions import ClientError, TransientError from neo4j.exceptions import ClientError, TransientError
def tx_error(tx, name, name2): def tx_error(tx, name, name2):
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value() a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
print(a[0]) print(a[0])
@@ -22,17 +23,19 @@ def tx_error(tx, name, name2):
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value() a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
print(a[0]) print(a[0])
def tx_good(tx, name, name2): def tx_good(tx, name, name2):
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value() a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
print(a[0]) print(a[0])
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value() a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
print(a[0]) print(a[0])
def tx_too_long(tx): def tx_too_long(tx):
tx.run("MATCH (a), (b), (c), (d), (e), (f) RETURN COUNT(*) AS cnt") tx.run("MATCH (a), (b), (c), (d), (e), (f) RETURN COUNT(*) AS cnt")
with GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""),
encrypted=False) as driver: with GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False) as driver:
def add_person(f, name, name2): def add_person(f, name, name2):
with driver.session() as session: with driver.session() as session:

View File

@@ -106,6 +106,7 @@ def test_try_to_write(connection, function_type):
f"MATCH (n) RETURN {function_type}_write.try_to_write(n, 'property', 1);", f"MATCH (n) RETURN {function_type}_write.try_to_write(n, 'property', 1);",
) )
@pytest.mark.parametrize("function_type", ["py", "c"]) @pytest.mark.parametrize("function_type", ["py", "c"])
def test_case_sensitivity(connection, function_type): def test_case_sensitivity(connection, function_type):
cursor = connection.cursor() cursor = connection.cursor()

View File

@@ -141,10 +141,16 @@ def test_add_replica_invalid_timeout(connection):
cursor = connection(7687, "main").cursor() cursor = connection(7687, "main").cursor()
with pytest.raises(mgclient.DatabaseError): with pytest.raises(mgclient.DatabaseError):
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 0 TO '127.0.0.1:10001';") execute_and_fetch_all(
cursor,
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 0 TO '127.0.0.1:10001';",
)
with pytest.raises(mgclient.DatabaseError): with pytest.raises(mgclient.DatabaseError):
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_1 SYNC WITH TIMEOUT -5 TO '127.0.0.1:10001';") execute_and_fetch_all(
cursor,
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT -5 TO '127.0.0.1:10001';",
)
actual_data = execute_and_fetch_all(cursor, "SHOW REPLICAS;") actual_data = execute_and_fetch_all(cursor, "SHOW REPLICAS;")
assert 0 == len(actual_data) assert 0 == len(actual_data)

View File

@@ -115,7 +115,10 @@ def start_stream(cursor, stream_name):
def start_stream_with_limit(cursor, stream_name, batch_limit, timeout=None): def start_stream_with_limit(cursor, stream_name, batch_limit, timeout=None):
if timeout is not None: if timeout is not None:
execute_and_fetch_all(cursor, f"START STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout} ") execute_and_fetch_all(
cursor,
f"START STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout} ",
)
else: else:
execute_and_fetch_all(cursor, f"START STREAM {stream_name} BATCH_LIMIT {batch_limit}") execute_and_fetch_all(cursor, f"START STREAM {stream_name} BATCH_LIMIT {batch_limit}")
@@ -156,7 +159,12 @@ def pulsar_default_namespace_topic(topic):
def test_start_and_stop_during_check( def test_start_and_stop_during_check(
operation, connection, stream_creator, message_sender, already_stopped_error, batchSize operation,
connection,
stream_creator,
message_sender,
already_stopped_error,
batchSize,
): ):
# This test is quite complex. The goal is to call START/STOP queries # This test is quite complex. The goal is to call START/STOP queries
# while a CHECK query is waiting for its result. Because the Global # while a CHECK query is waiting for its result. Because the Global
@@ -317,24 +325,42 @@ def test_check_stream_same_number_of_queries_than_messages(connection, stream_cr
expected_queries_and_raw_messages_1 = ( expected_queries_and_raw_messages_1 = (
[ # queries [ # queries
{PARAMETERS_LITERAL: {"value": "Parameter: 01"}, QUERY_LITERAL: "Message: 01"}, {
{PARAMETERS_LITERAL: {"value": "Parameter: 02"}, QUERY_LITERAL: "Message: 02"}, PARAMETERS_LITERAL: {"value": "Parameter: 01"},
QUERY_LITERAL: "Message: 01",
},
{
PARAMETERS_LITERAL: {"value": "Parameter: 02"},
QUERY_LITERAL: "Message: 02",
},
], ],
["01", "02"], # raw message ["01", "02"], # raw message
) )
expected_queries_and_raw_messages_2 = ( expected_queries_and_raw_messages_2 = (
[ # queries [ # queries
{PARAMETERS_LITERAL: {"value": "Parameter: 03"}, QUERY_LITERAL: "Message: 03"}, {
{PARAMETERS_LITERAL: {"value": "Parameter: 04"}, QUERY_LITERAL: "Message: 04"}, PARAMETERS_LITERAL: {"value": "Parameter: 03"},
QUERY_LITERAL: "Message: 03",
},
{
PARAMETERS_LITERAL: {"value": "Parameter: 04"},
QUERY_LITERAL: "Message: 04",
},
], ],
["03", "04"], # raw message ["03", "04"], # raw message
) )
expected_queries_and_raw_messages_3 = ( expected_queries_and_raw_messages_3 = (
[ # queries [ # queries
{PARAMETERS_LITERAL: {"value": "Parameter: 05"}, QUERY_LITERAL: "Message: 05"}, {
{PARAMETERS_LITERAL: {"value": "Parameter: 06"}, QUERY_LITERAL: "Message: 06"}, PARAMETERS_LITERAL: {"value": "Parameter: 05"},
QUERY_LITERAL: "Message: 05",
},
{
PARAMETERS_LITERAL: {"value": "Parameter: 06"},
QUERY_LITERAL: "Message: 06",
},
], ],
["05", "06"], # raw message ["05", "06"], # raw message
) )
@@ -389,20 +415,32 @@ def test_check_stream_different_number_of_queries_than_messages(connection, stre
expected_queries_and_raw_messages_2 = ( expected_queries_and_raw_messages_2 = (
[ # queries [ # queries
{PARAMETERS_LITERAL: {"value": "Parameter: 03"}, QUERY_LITERAL: "Message: 03"}, {
{PARAMETERS_LITERAL: {"value": "Parameter: 04"}, QUERY_LITERAL: "Message: 04"}, PARAMETERS_LITERAL: {"value": "Parameter: 03"},
QUERY_LITERAL: "Message: 03",
},
{
PARAMETERS_LITERAL: {"value": "Parameter: 04"},
QUERY_LITERAL: "Message: 04",
},
], ],
["03", "04"], # raw message ["03", "04"], # raw message
) )
expected_queries_and_raw_messages_3 = ( expected_queries_and_raw_messages_3 = (
[ # queries [ # queries
{PARAMETERS_LITERAL: {"value": "Parameter: b_05"}, QUERY_LITERAL: "Message: b_05"}, {
PARAMETERS_LITERAL: {"value": "Parameter: b_05"},
QUERY_LITERAL: "Message: b_05",
},
{ {
PARAMETERS_LITERAL: {"value": "Parameter: extra_b_05"}, PARAMETERS_LITERAL: {"value": "Parameter: extra_b_05"},
QUERY_LITERAL: "Message: extra_b_05", QUERY_LITERAL: "Message: extra_b_05",
}, },
{PARAMETERS_LITERAL: {"value": "Parameter: 06"}, QUERY_LITERAL: "Message: 06"}, {
PARAMETERS_LITERAL: {"value": "Parameter: 06"},
QUERY_LITERAL: "Message: 06",
},
], ],
["b_05", "06"], # raw message ["b_05", "06"], # raw message
) )
@@ -467,7 +505,10 @@ def test_start_stream_with_batch_limit_reaching_timeout(connection, stream_creat
start_time = time.time() start_time = time.time()
with pytest.raises(mgclient.DatabaseError): with pytest.raises(mgclient.DatabaseError):
execute_and_fetch_all(cursor, f"START STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}") execute_and_fetch_all(
cursor,
f"START STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}",
)
end_time = time.time() end_time = time.time()
assert ( assert (
@@ -483,7 +524,10 @@ def test_start_stream_with_batch_limit_while_check_running(
def start_check_stream(stream_name, batch_limit, timeout): def start_check_stream(stream_name, batch_limit, timeout):
connection = connect() connection = connect()
cursor = connection.cursor() cursor = connection.cursor()
execute_and_fetch_all(cursor, f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}") execute_and_fetch_all(
cursor,
f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}",
)
def start_new_stream_with_limit(stream_name, batch_limit, timeout): def start_new_stream_with_limit(stream_name, batch_limit, timeout):
connection = connect() connection = connect()
@@ -518,7 +562,9 @@ def test_start_stream_with_batch_limit_while_check_running(
# 2/ # 2/
thread_stream_running = Process( thread_stream_running = Process(
target=start_new_stream_with_limit, daemon=True, args=(STREAM_NAME, BATCH_LIMIT + 1, TIMEOUT) target=start_new_stream_with_limit,
daemon=True,
args=(STREAM_NAME, BATCH_LIMIT + 1, TIMEOUT),
) # Sending BATCH_LIMIT + 1 messages as BATCH_LIMIT messages have already been sent during the CHECK STREAM (and not consumed) ) # Sending BATCH_LIMIT + 1 messages as BATCH_LIMIT messages have already been sent during the CHECK STREAM (and not consumed)
thread_stream_running.start() thread_stream_running.start()
time.sleep(2) time.sleep(2)
@@ -541,7 +587,10 @@ def test_check_while_stream_with_batch_limit_running(connection, stream_creator,
def start_check_stream(stream_name, batch_limit, timeout): def start_check_stream(stream_name, batch_limit, timeout):
connection = connect() connection = connect()
cursor = connection.cursor() cursor = connection.cursor()
execute_and_fetch_all(cursor, f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}") execute_and_fetch_all(
cursor,
f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}",
)
STREAM_NAME = "test_batch_limit_and_check" STREAM_NAME = "test_batch_limit_and_check"
BATCH_LIMIT = 1 BATCH_LIMIT = 1
@@ -553,7 +602,9 @@ def test_check_while_stream_with_batch_limit_running(connection, stream_creator,
# 1/ # 1/
thread_stream_running = Process( thread_stream_running = Process(
target=start_new_stream_with_limit, daemon=True, args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT) target=start_new_stream_with_limit,
daemon=True,
args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT),
) )
start_time = time.time() start_time = time.time()
thread_stream_running.start() thread_stream_running.start()
@@ -561,7 +612,10 @@ def test_check_while_stream_with_batch_limit_running(connection, stream_creator,
assert get_is_running(cursor, STREAM_NAME) assert get_is_running(cursor, STREAM_NAME)
with pytest.raises(mgclient.DatabaseError): with pytest.raises(mgclient.DatabaseError):
execute_and_fetch_all(cursor, f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}") execute_and_fetch_all(
cursor,
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}",
)
end_time = time.time() end_time = time.time()
assert (end_time - start_time) < 0.8 * TIMEOUT, "The CHECK STREAM has probably thrown due to timeout!" assert (end_time - start_time) < 0.8 * TIMEOUT, "The CHECK STREAM has probably thrown due to timeout!"
@@ -632,7 +686,10 @@ def test_check_stream_with_batch_limit_with_invalid_batch_limit(connection, stre
start_time = time.time() start_time = time.time()
with pytest.raises(mgclient.DatabaseError): with pytest.raises(mgclient.DatabaseError):
execute_and_fetch_all(cursor, f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}") execute_and_fetch_all(
cursor,
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}",
)
end_time = time.time() end_time = time.time()
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!" assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
@@ -642,7 +699,10 @@ def test_check_stream_with_batch_limit_with_invalid_batch_limit(connection, stre
start_time = time.time() start_time = time.time()
with pytest.raises(mgclient.DatabaseError): with pytest.raises(mgclient.DatabaseError):
execute_and_fetch_all(cursor, f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}") execute_and_fetch_all(
cursor,
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}",
)
end_time = time.time() end_time = time.time()
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!" assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"

View File

@@ -37,29 +37,22 @@ def connection():
def get_topics(num): def get_topics(num):
return [f'topic_{i}' for i in range(num)] return [f"topic_{i}" for i in range(num)]
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def kafka_topics(): def kafka_topics():
admin_client = KafkaAdminClient( admin_client = KafkaAdminClient(bootstrap_servers="localhost:9092", client_id="test")
bootstrap_servers="localhost:9092",
client_id="test")
# The issue arises if we remove default kafka topics, e.g. # The issue arises if we remove default kafka topics, e.g.
# "__consumer_offsets" # "__consumer_offsets"
previous_topics = [ previous_topics = [topic for topic in admin_client.list_topics() if topic != "__consumer_offsets"]
topic for topic in admin_client.list_topics() if topic != "__consumer_offsets"]
if previous_topics: if previous_topics:
admin_client.delete_topics(topics=previous_topics, timeout_ms=5000) admin_client.delete_topics(topics=previous_topics, timeout_ms=5000)
topics = get_topics(3) topics = get_topics(3)
topics_to_create = [] topics_to_create = []
for topic in topics: for topic in topics:
topics_to_create.append( topics_to_create.append(NewTopic(name=topic, num_partitions=1, replication_factor=1))
NewTopic(
name=topic,
num_partitions=1,
replication_factor=1))
admin_client.create_topics(new_topics=topics_to_create, timeout_ms=5000) admin_client.create_topics(new_topics=topics_to_create, timeout_ms=5000)
yield topics yield topics
@@ -80,6 +73,5 @@ def pulsar_client():
def pulsar_topics(): def pulsar_topics():
topics = get_topics(3) topics = get_topics(3)
for topic in topics: for topic in topics:
requests.delete( requests.delete(f"http://127.0.0.1:6652/admin/v2/persistent/public/default/{topic}?force=true")
f'http://127.0.0.1:6652/admin/v2/persistent/public/default/{topic}?force=true')
yield topics yield topics

View File

@@ -20,7 +20,10 @@ import common
TRANSFORMATIONS_TO_CHECK_C = ["c_transformations.empty_transformation"] TRANSFORMATIONS_TO_CHECK_C = ["c_transformations.empty_transformation"]
TRANSFORMATIONS_TO_CHECK_PY = ["kafka_transform.simple", "kafka_transform.with_parameters"] TRANSFORMATIONS_TO_CHECK_PY = [
"kafka_transform.simple",
"kafka_transform.with_parameters",
]
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK_PY) @pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK_PY)
@@ -463,7 +466,11 @@ def test_start_stream_with_batch_limit_while_check_running(kafka_producer, kafka
kafka_producer.send(kafka_topics[0], message).get(timeout=6000) kafka_producer.send(kafka_topics[0], message).get(timeout=6000)
def setup_function(start_check_stream, cursor, stream_name, batch_limit, timeout): def setup_function(start_check_stream, cursor, stream_name, batch_limit, timeout):
thread_stream_check = Process(target=start_check_stream, daemon=True, args=(stream_name, batch_limit, timeout)) thread_stream_check = Process(
target=start_check_stream,
daemon=True,
args=(stream_name, batch_limit, timeout),
)
thread_stream_check.start() thread_stream_check.start()
time.sleep(2) time.sleep(2)
assert common.get_is_running(cursor, stream_name) assert common.get_is_running(cursor, stream_name)

View File

@@ -18,13 +18,20 @@ import time
from multiprocessing import Process, Value from multiprocessing import Process, Value
import common import common
TRANSFORMATIONS_TO_CHECK = ["pulsar_transform.simple", "pulsar_transform.with_parameters"] TRANSFORMATIONS_TO_CHECK = [
"pulsar_transform.simple",
"pulsar_transform.with_parameters",
]
def check_vertex_exists_with_topic_and_payload(cursor, topic, payload_byte): def check_vertex_exists_with_topic_and_payload(cursor, topic, payload_byte):
decoded_payload = payload_byte.decode("utf-8") decoded_payload = payload_byte.decode("utf-8")
common.check_vertex_exists_with_properties( common.check_vertex_exists_with_properties(
cursor, {"topic": f'"{common.pulsar_default_namespace_topic(topic)}"', "payload": f'"{decoded_payload}"'} cursor,
{
"topic": f'"{common.pulsar_default_namespace_topic(topic)}"',
"payload": f'"{decoded_payload}"',
},
) )
@@ -100,7 +107,8 @@ def test_start_from_latest_messages(pulsar_client, pulsar_topics, connection):
assert len(vertices_with_msg) == 0 assert len(vertices_with_msg) == 0
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
producer.send(common.SIMPLE_MSG) producer.send(common.SIMPLE_MSG)
@@ -157,7 +165,8 @@ def test_check_stream(pulsar_client, pulsar_topics, connection, transformation):
time.sleep(1) time.sleep(1)
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
producer.send(common.SIMPLE_MSG) producer.send(common.SIMPLE_MSG)
check_vertex_exists_with_topic_and_payload(cursor, pulsar_topics[0], common.SIMPLE_MSG) check_vertex_exists_with_topic_and_payload(cursor, pulsar_topics[0], common.SIMPLE_MSG)
@@ -263,7 +272,8 @@ def test_start_and_stop_during_check(pulsar_client, pulsar_topics, connection, o
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE {BATCH_SIZE}" return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE {BATCH_SIZE}"
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
def message_sender(msg): def message_sender(msg):
@@ -311,7 +321,8 @@ def test_restart_after_error(pulsar_client, pulsar_topics, connection):
time.sleep(1) time.sleep(1)
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
producer.send(common.SIMPLE_MSG) producer.send(common.SIMPLE_MSG)
assert common.timed_wait(lambda: not common.get_is_running(cursor, "test_stream")) assert common.timed_wait(lambda: not common.get_is_running(cursor, "test_stream"))
@@ -351,7 +362,8 @@ def test_start_stream_with_batch_limit(pulsar_client, pulsar_topics, connection)
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1" return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1"
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
def messages_sender(nof_messages): def messages_sender(nof_messages):
@@ -386,7 +398,8 @@ def test_start_stream_with_batch_limit_while_check_running(pulsar_client, pulsar
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1" return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1"
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
def message_sender(message): def message_sender(message):
@@ -402,7 +415,8 @@ def test_check_while_stream_with_batch_limit_running(pulsar_client, pulsar_topic
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1" return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1"
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
def message_sender(message): def message_sender(message):
@@ -420,7 +434,8 @@ def test_check_stream_same_number_of_queries_than_messages(pulsar_client, pulsar
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM {TRANSFORMATION} BATCH_INTERVAL 3000 BATCH_SIZE {batch_size} " return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM {TRANSFORMATION} BATCH_INTERVAL 3000 BATCH_SIZE {batch_size} "
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
def message_sender(msg): def message_sender(msg):
@@ -438,7 +453,8 @@ def test_check_stream_different_number_of_queries_than_messages(pulsar_client, p
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM {TRANSFORMATION} BATCH_INTERVAL 3000 BATCH_SIZE {batch_size} " return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM {TRANSFORMATION} BATCH_INTERVAL 3000 BATCH_SIZE {batch_size} "
producer = pulsar_client.create_producer( producer = pulsar_client.create_producer(
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000 common.pulsar_default_namespace_topic(pulsar_topics[0]),
send_timeout_millis=60000,
) )
def message_sender(msg): def message_sender(msg):

View File

@@ -15,6 +15,7 @@ import time
import mgclient import mgclient
import common import common
def get_cursor_with_user(username): def get_cursor_with_user(username):
connection = common.connect(username=username, password="") connection = common.connect(username=username, password="")
return connection.cursor() return connection.cursor()
@@ -22,23 +23,21 @@ def get_cursor_with_user(username):
def create_admin_user(cursor, admin_user): def create_admin_user(cursor, admin_user):
common.execute_and_fetch_all(cursor, f"CREATE USER {admin_user}") common.execute_and_fetch_all(cursor, f"CREATE USER {admin_user}")
common.execute_and_fetch_all( common.execute_and_fetch_all(cursor, f"GRANT ALL PRIVILEGES TO {admin_user}")
cursor, f"GRANT ALL PRIVILEGES TO {admin_user}")
def create_stream_user(cursor, stream_user): def create_stream_user(cursor, stream_user):
common.execute_and_fetch_all(cursor, f"CREATE USER {stream_user}") common.execute_and_fetch_all(cursor, f"CREATE USER {stream_user}")
common.execute_and_fetch_all( common.execute_and_fetch_all(cursor, f"GRANT STREAM TO {stream_user}")
cursor, f"GRANT STREAM TO {stream_user}")
def test_ownerless_stream(kafka_producer, kafka_topics, connection): def test_ownerless_stream(kafka_producer, kafka_topics, connection):
assert len(kafka_topics) > 0 assert len(kafka_topics) > 0
userless_cursor = connection.cursor() userless_cursor = connection.cursor()
common.execute_and_fetch_all(userless_cursor, common.execute_and_fetch_all(
"CREATE KAFKA STREAM ownerless " userless_cursor,
f"TOPICS {kafka_topics[0]} " "CREATE KAFKA STREAM ownerless " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
f"TRANSFORM kafka_transform.simple") )
common.start_stream(userless_cursor, "ownerless") common.start_stream(userless_cursor, "ownerless")
time.sleep(1) time.sleep(1)
@@ -46,11 +45,9 @@ def test_ownerless_stream(kafka_producer, kafka_topics, connection):
create_admin_user(userless_cursor, admin_user) create_admin_user(userless_cursor, admin_user)
kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60) kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60)
assert common.timed_wait( assert common.timed_wait(lambda: not common.get_is_running(userless_cursor, "ownerless"))
lambda: not common.get_is_running(userless_cursor, "ownerless"))
assert len(common.execute_and_fetch_all( assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 0
userless_cursor, "MATCH (n) RETURN n")) == 0
common.execute_and_fetch_all(userless_cursor, f"DROP USER {admin_user}") common.execute_and_fetch_all(userless_cursor, f"DROP USER {admin_user}")
common.start_stream(userless_cursor, "ownerless") common.start_stream(userless_cursor, "ownerless")
@@ -58,11 +55,9 @@ def test_ownerless_stream(kafka_producer, kafka_topics, connection):
second_message = b"second message" second_message = b"second message"
kafka_producer.send(kafka_topics[0], second_message).get(timeout=60) kafka_producer.send(kafka_topics[0], second_message).get(timeout=60)
common.kafka_check_vertex_exists_with_topic_and_payload( common.kafka_check_vertex_exists_with_topic_and_payload(userless_cursor, kafka_topics[0], second_message)
userless_cursor, kafka_topics[0], second_message)
assert len(common.execute_and_fetch_all( assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
userless_cursor, "MATCH (n) RETURN n")) == 1
def test_owner_is_shown(kafka_topics, connection): def test_owner_is_shown(kafka_topics, connection):
@@ -73,12 +68,16 @@ def test_owner_is_shown(kafka_topics, connection):
create_stream_user(userless_cursor, stream_user) create_stream_user(userless_cursor, stream_user)
stream_cursor = get_cursor_with_user(stream_user) stream_cursor = get_cursor_with_user(stream_user)
common.execute_and_fetch_all(stream_cursor, "CREATE KAFKA STREAM test " common.execute_and_fetch_all(
f"TOPICS {kafka_topics[0]} " stream_cursor,
f"TRANSFORM kafka_transform.simple") "CREATE KAFKA STREAM test " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
)
common.check_stream_info(userless_cursor, "test", ("test", "kafka", 100, 1000, common.check_stream_info(
"kafka_transform.simple", stream_user, False)) userless_cursor,
"test",
("test", "kafka", 100, 1000, "kafka_transform.simple", stream_user, False),
)
def test_insufficient_privileges(kafka_producer, kafka_topics, connection): def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
@@ -93,10 +92,10 @@ def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
create_stream_user(userless_cursor, stream_user) create_stream_user(userless_cursor, stream_user)
stream_cursor = get_cursor_with_user(stream_user) stream_cursor = get_cursor_with_user(stream_user)
common.execute_and_fetch_all(stream_cursor, common.execute_and_fetch_all(
"CREATE KAFKA STREAM insufficient_test " stream_cursor,
f"TOPICS {kafka_topics[0]} " "CREATE KAFKA STREAM insufficient_test " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
f"TRANSFORM kafka_transform.simple") )
# the stream is started by admin, but should check against the owner # the stream is started by admin, but should check against the owner
# privileges # privileges
@@ -104,24 +103,19 @@ def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
time.sleep(1) time.sleep(1)
kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60) kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60)
assert common.timed_wait( assert common.timed_wait(lambda: not common.get_is_running(userless_cursor, "insufficient_test"))
lambda: not common.get_is_running(userless_cursor, "insufficient_test"))
assert len(common.execute_and_fetch_all( assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 0
userless_cursor, "MATCH (n) RETURN n")) == 0
common.execute_and_fetch_all( common.execute_and_fetch_all(admin_cursor, f"GRANT CREATE TO {stream_user}")
admin_cursor, f"GRANT CREATE TO {stream_user}")
common.start_stream(userless_cursor, "insufficient_test") common.start_stream(userless_cursor, "insufficient_test")
time.sleep(1) time.sleep(1)
second_message = b"second message" second_message = b"second message"
kafka_producer.send(kafka_topics[0], second_message).get(timeout=60) kafka_producer.send(kafka_topics[0], second_message).get(timeout=60)
common.kafka_check_vertex_exists_with_topic_and_payload( common.kafka_check_vertex_exists_with_topic_and_payload(userless_cursor, kafka_topics[0], second_message)
userless_cursor, kafka_topics[0], second_message)
assert len(common.execute_and_fetch_all( assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
userless_cursor, "MATCH (n) RETURN n")) == 1
def test_happy_case(kafka_producer, kafka_topics, connection): def test_happy_case(kafka_producer, kafka_topics, connection):
@@ -135,13 +129,12 @@ def test_happy_case(kafka_producer, kafka_topics, connection):
stream_user = "stream_user" stream_user = "stream_user"
create_stream_user(userless_cursor, stream_user) create_stream_user(userless_cursor, stream_user)
stream_cursor = get_cursor_with_user(stream_user) stream_cursor = get_cursor_with_user(stream_user)
common.execute_and_fetch_all( common.execute_and_fetch_all(admin_cursor, f"GRANT CREATE TO {stream_user}")
admin_cursor, f"GRANT CREATE TO {stream_user}")
common.execute_and_fetch_all(stream_cursor, common.execute_and_fetch_all(
"CREATE KAFKA STREAM insufficient_test " stream_cursor,
f"TOPICS {kafka_topics[0]} " "CREATE KAFKA STREAM insufficient_test " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
f"TRANSFORM kafka_transform.simple") )
common.start_stream(stream_cursor, "insufficient_test") common.start_stream(stream_cursor, "insufficient_test")
time.sleep(1) time.sleep(1)
@@ -149,11 +142,9 @@ def test_happy_case(kafka_producer, kafka_topics, connection):
first_message = b"first message" first_message = b"first message"
kafka_producer.send(kafka_topics[0], first_message).get(timeout=60) kafka_producer.send(kafka_topics[0], first_message).get(timeout=60)
common.kafka_check_vertex_exists_with_topic_and_payload( common.kafka_check_vertex_exists_with_topic_and_payload(userless_cursor, kafka_topics[0], first_message)
userless_cursor, kafka_topics[0], first_message)
assert len(common.execute_and_fetch_all( assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
userless_cursor, "MATCH (n) RETURN n")) == 1
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -23,7 +23,10 @@ def check_stream_no_filtering(
message = messages.message_at(i) message = messages.message_at(i)
payload_as_str = message.payload().decode("utf-8") payload_as_str = message.payload().decode("utf-8")
result_queries.append( result_queries.append(
mgp.Record(query=f"Message: {payload_as_str}", parameters={"value": f"Parameter: {payload_as_str}"}) mgp.Record(
query=f"Message: {payload_as_str}",
parameters={"value": f"Parameter: {payload_as_str}"},
)
) )
return result_queries return result_queries
@@ -44,13 +47,17 @@ def check_stream_with_filtering(
continue continue
result_queries.append( result_queries.append(
mgp.Record(query=f"Message: {payload_as_str}", parameters={"value": f"Parameter: {payload_as_str}"}) mgp.Record(
query=f"Message: {payload_as_str}",
parameters={"value": f"Parameter: {payload_as_str}"},
)
) )
if "b" in payload_as_str: if "b" in payload_as_str:
result_queries.append( result_queries.append(
mgp.Record( mgp.Record(
query=f"Message: extra_{payload_as_str}", parameters={"value": f"Parameter: extra_{payload_as_str}"} query=f"Message: extra_{payload_as_str}",
parameters={"value": f"Parameter: extra_{payload_as_str}"},
) )
) )

View File

@@ -59,7 +59,9 @@ def with_parameters(context: mgp.TransCtx, messages: mgp.Messages) -> mgp.Record
@mgp.transformation @mgp.transformation
def query(messages: mgp.Messages) -> mgp.Record(query=str, parameters=mgp.Nullable[mgp.Map]): def query(
messages: mgp.Messages,
) -> mgp.Record(query=str, parameters=mgp.Nullable[mgp.Map]):
result_queries = [] result_queries = []
for i in range(0, messages.total_messages()): for i in range(0, messages.total_messages()):

View File

@@ -11,6 +11,7 @@
import mgp import mgp
@mgp.write_proc @mgp.write_proc
def create_vertex(ctx: mgp.ProcCtx, id: mgp.Any) -> mgp.Record(v=mgp.Any): def create_vertex(ctx: mgp.ProcCtx, id: mgp.Any) -> mgp.Record(v=mgp.Any):
v = None v = None
@@ -36,15 +37,14 @@ def detach_delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record():
@mgp.write_proc @mgp.write_proc
def create_edge(ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, def create_edge(
to_vertex: mgp.Vertex, ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, to_vertex: mgp.Vertex, edge_type: str
edge_type: str) -> mgp.Record(e=mgp.Any): ) -> mgp.Record(e=mgp.Any):
e = None e = None
try: try:
e = ctx.graph.create_edge( e = ctx.graph.create_edge(from_vertex, to_vertex, mgp.EdgeType(edge_type))
from_vertex, to_vertex, mgp.EdgeType(edge_type)) e.properties.set("id", 1)
e.properties.set("id", 1); e.properties.set("tbd", 0)
e.properties.set("tbd", 0);
except RuntimeError as ex: except RuntimeError as ex:
return mgp.Record(e=str(ex)) return mgp.Record(e=str(ex))
return mgp.Record(e=e) return mgp.Record(e=e)
@@ -61,19 +61,20 @@ def set_property(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record():
object.properties.set("id", 2) object.properties.set("id", 2)
return mgp.Record() return mgp.Record()
@mgp.write_proc @mgp.write_proc
def remove_property(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(): def remove_property(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record():
object.properties.set("tbd", None) object.properties.set("tbd", None)
return mgp.Record() return mgp.Record()
@mgp.write_proc @mgp.write_proc
def add_label(ctx: mgp.ProcCtx, object: mgp.Any, def add_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
name: str) -> mgp.Record(o=mgp.Any):
object.add_label(name) object.add_label(name)
return mgp.Record(o=object) return mgp.Record(o=object)
@mgp.write_proc @mgp.write_proc
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
name: str) -> mgp.Record(o=mgp.Any):
object.remove_label(name) object.remove_label(name)
return mgp.Record(o=object) return mgp.Record(o=object)

View File

@@ -13,8 +13,7 @@ import mgclient
import typing import typing
def execute_and_fetch_all(cursor: mgclient.Cursor, query: str, def execute_and_fetch_all(cursor: mgclient.Cursor, query: str, params: dict = {}) -> typing.List[tuple]:
params: dict = {}) -> typing.List[tuple]:
cursor.execute(query, params) cursor.execute(query, params)
return cursor.fetchall() return cursor.fetchall()

View File

@@ -13,8 +13,7 @@ import mgp
@mgp.read_proc @mgp.read_proc
def underlying_graph_is_mutable(ctx: mgp.ProcCtx, def underlying_graph_is_mutable(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(mutable=bool):
object: mgp.Any) -> mgp.Record(mutable=bool):
return mgp.Record(mutable=object.underlying_graph_is_mutable()) return mgp.Record(mutable=object.underlying_graph_is_mutable())

View File

@@ -35,13 +35,12 @@ def detach_delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record():
@mgp.write_proc @mgp.write_proc
def create_edge(ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, def create_edge(
to_vertex: mgp.Vertex, ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, to_vertex: mgp.Vertex, edge_type: str
edge_type: str) -> mgp.Record(e=mgp.Any): ) -> mgp.Record(e=mgp.Any):
e = None e = None
try: try:
e = ctx.graph.create_edge( e = ctx.graph.create_edge(from_vertex, to_vertex, mgp.EdgeType(edge_type))
from_vertex, to_vertex, mgp.EdgeType(edge_type))
except RuntimeError as ex: except RuntimeError as ex:
return mgp.Record(e=str(ex)) return mgp.Record(e=str(ex))
return mgp.Record(e=e) return mgp.Record(e=e)
@@ -54,29 +53,25 @@ def delete_edge(ctx: mgp.ProcCtx, edge: mgp.Edge) -> mgp.Record():
@mgp.write_proc @mgp.write_proc
def set_property(ctx: mgp.ProcCtx, object: mgp.Any, def set_property(ctx: mgp.ProcCtx, object: mgp.Any, name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record():
name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record():
object.properties.set(name, value) object.properties.set(name, value)
return mgp.Record() return mgp.Record()
@mgp.write_proc @mgp.write_proc
def add_label(ctx: mgp.ProcCtx, object: mgp.Any, def add_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
name: str) -> mgp.Record(o=mgp.Any):
object.add_label(name) object.add_label(name)
return mgp.Record(o=object) return mgp.Record(o=object)
@mgp.write_proc @mgp.write_proc
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
name: str) -> mgp.Record(o=mgp.Any):
object.remove_label(name) object.remove_label(name)
return mgp.Record(o=object) return mgp.Record(o=object)
@mgp.write_proc @mgp.write_proc
def underlying_graph_is_mutable(ctx: mgp.ProcCtx, def underlying_graph_is_mutable(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(mutable=bool):
object: mgp.Any) -> mgp.Record(mutable=bool):
return mgp.Record(mutable=object.underlying_graph_is_mutable()) return mgp.Record(mutable=object.underlying_graph_is_mutable())

View File

@@ -13,8 +13,7 @@ import typing
import mgclient import mgclient
import sys import sys
import pytest import pytest
from common import (execute_and_fetch_all, from common import execute_and_fetch_all, has_one_result_row, has_n_result_row
has_one_result_row, has_n_result_row)
def test_is_write(connection): def test_is_write(connection):
@@ -22,15 +21,19 @@ def test_is_write(connection):
result_order = "name, signature, is_write" result_order = "name, signature, is_write"
cursor = connection.cursor() cursor = connection.cursor()
for proc in execute_and_fetch_all( for proc in execute_and_fetch_all(
cursor, "CALL mg.procedures() YIELD * WITH name, signature, " cursor,
"is_write WHERE name STARTS WITH 'write' " "CALL mg.procedures() YIELD * WITH name, signature, "
f"RETURN {result_order}"): "is_write WHERE name STARTS WITH 'write' "
f"RETURN {result_order}",
):
assert proc[is_write] is True assert proc[is_write] is True
for proc in execute_and_fetch_all( for proc in execute_and_fetch_all(
cursor, "CALL mg.procedures() YIELD * WITH name, signature, " cursor,
"is_write WHERE NOT name STARTS WITH 'write' " "CALL mg.procedures() YIELD * WITH name, signature, "
f"RETURN {result_order}"): "is_write WHERE NOT name STARTS WITH 'write' "
f"RETURN {result_order}",
):
assert proc[is_write] is False assert proc[is_write] is False
assert cursor.description[0].name == "name" assert cursor.description[0].name == "name"
@@ -41,8 +44,7 @@ def test_is_write(connection):
def test_single_vertex(connection): def test_single_vertex(connection):
cursor = connection.cursor() cursor = connection.cursor()
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
result = execute_and_fetch_all( result = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")
cursor, "CALL write.create_vertex() YIELD v RETURN v")
vertex = result[0][0] vertex = result[0][0]
assert isinstance(vertex, mgclient.Node) assert isinstance(vertex, mgclient.Node)
assert has_one_result_row(cursor, "MATCH (n) RETURN n") assert has_one_result_row(cursor, "MATCH (n) RETURN n")
@@ -50,14 +52,13 @@ def test_single_vertex(connection):
assert vertex.properties == {} assert vertex.properties == {}
def add_label(label: str): def add_label(label: str):
execute_and_fetch_all( execute_and_fetch_all(cursor, f"MATCH (n) CALL write.add_label(n, '{label}') " "YIELD * RETURN *")
cursor, f"MATCH (n) CALL write.add_label(n, '{label}') "
"YIELD * RETURN *")
def remove_label(label: str): def remove_label(label: str):
execute_and_fetch_all( execute_and_fetch_all(
cursor, f"MATCH (n) CALL write.remove_label(n, '{label}') " cursor,
"YIELD * RETURN *") f"MATCH (n) CALL write.remove_label(n, '{label}') " "YIELD * RETURN *",
)
def get_vertex() -> mgclient.Node: def get_vertex() -> mgclient.Node:
return execute_and_fetch_all(cursor, "MATCH (n) RETURN n")[0][0] return execute_and_fetch_all(cursor, "MATCH (n) RETURN n")[0][0]
@@ -65,8 +66,10 @@ def test_single_vertex(connection):
def set_property(property_name: str, property: typing.Any): def set_property(property_name: str, property: typing.Any):
nonlocal cursor nonlocal cursor
execute_and_fetch_all( execute_and_fetch_all(
cursor, f"MATCH (n) CALL write.set_property(n, '{property_name}', " cursor,
"$property) YIELD * RETURN *", {"property": property}) f"MATCH (n) CALL write.set_property(n, '{property_name}', " "$property) YIELD * RETURN *",
{"property": property},
)
label_1 = "LABEL1" label_1 = "LABEL1"
label_2 = "LABEL2" label_2 = "LABEL2"
@@ -89,24 +92,23 @@ def test_single_vertex(connection):
set_property(property_name, None) set_property(property_name, None)
assert get_vertex().properties == {} assert get_vertex().properties == {}
execute_and_fetch_all( execute_and_fetch_all(cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1")
cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1")
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
def test_single_edge(connection): def test_single_edge(connection):
cursor = connection.cursor() cursor = connection.cursor()
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
v1_id = execute_and_fetch_all( v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
edge_type = "EDGE" edge_type = "EDGE"
edge = execute_and_fetch_all( edge = execute_and_fetch_all(
cursor, f"MATCH (n) WHERE id(n) = {v1_id} " cursor,
f"MATCH (m) WHERE id(m) = {v2_id} " f"MATCH (n) WHERE id(n) = {v1_id} "
f"CALL write.create_edge(n, m, '{edge_type}') " f"MATCH (m) WHERE id(m) = {v2_id} "
"YIELD e RETURN e")[0][0] f"CALL write.create_edge(n, m, '{edge_type}') "
"YIELD e RETURN e",
)[0][0]
assert edge.type == edge_type assert edge.type == edge_type
assert edge.properties == {} assert edge.properties == {}
@@ -120,9 +122,10 @@ def test_single_edge(connection):
def set_property(property_name: str, property: typing.Any): def set_property(property_name: str, property: typing.Any):
nonlocal cursor nonlocal cursor
execute_and_fetch_all( execute_and_fetch_all(
cursor, "MATCH ()-[e]->() " cursor,
f"CALL write.set_property(e, '{property_name}', " "MATCH ()-[e]->() " f"CALL write.set_property(e, '{property_name}', " "$property) YIELD * RETURN *",
"$property) YIELD * RETURN *", {"property": property}) {"property": property},
)
set_property(property_name, property_value_1) set_property(property_name, property_value_1)
assert get_edge().properties == {property_name: property_value_1} assert get_edge().properties == {property_name: property_value_1}
@@ -130,60 +133,68 @@ def test_single_edge(connection):
assert get_edge().properties == {property_name: property_value_2} assert get_edge().properties == {property_name: property_value_2}
set_property(property_name, None) set_property(property_name, None)
assert get_edge().properties == {} assert get_edge().properties == {}
execute_and_fetch_all( execute_and_fetch_all(cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1")
cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1")
assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0) assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0)
def test_detach_delete_vertex(connection): def test_detach_delete_vertex(connection):
cursor = connection.cursor() cursor = connection.cursor()
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
v1_id = execute_and_fetch_all( v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
execute_and_fetch_all( execute_and_fetch_all(
cursor, f"MATCH (n) WHERE id(n) = {v1_id} " cursor,
f"MATCH (n) WHERE id(n) = {v1_id} "
f"MATCH (m) WHERE id(m) = {v2_id} " f"MATCH (m) WHERE id(m) = {v2_id} "
f"CALL write.create_edge(n, m, 'EDGE') " f"CALL write.create_edge(n, m, 'EDGE') "
"YIELD e RETURN e") "YIELD e RETURN e",
)
assert has_one_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m") assert has_one_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m")
execute_and_fetch_all( execute_and_fetch_all(
cursor, f"MATCH (n) WHERE id(n) = {v1_id} " cursor,
"CALL write.detach_delete_vertex(n) YIELD * RETURN 1") f"MATCH (n) WHERE id(n) = {v1_id} " "CALL write.detach_delete_vertex(n) YIELD * RETURN 1",
)
assert has_n_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m", 0) assert has_n_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m", 0)
assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0) assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0)
assert has_one_result_row( assert has_one_result_row(cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n")
cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n")
def test_graph_mutability(connection): def test_graph_mutability(connection):
cursor = connection.cursor() cursor = connection.cursor()
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
v1_id = execute_and_fetch_all( v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
execute_and_fetch_all( execute_and_fetch_all(
cursor, f"MATCH (n) WHERE id(n) = {v1_id} " cursor,
f"MATCH (n) WHERE id(n) = {v1_id} "
f"MATCH (m) WHERE id(m) = {v2_id} " f"MATCH (m) WHERE id(m) = {v2_id} "
f"CALL write.create_edge(n, m, 'EDGE') " f"CALL write.create_edge(n, m, 'EDGE') "
"YIELD e RETURN e") "YIELD e RETURN e",
)
def test_mutability(is_write: bool): def test_mutability(is_write: bool):
module = "write" if is_write else "read" module = "write" if is_write else "read"
assert execute_and_fetch_all( assert (
cursor, f"CALL {module}.graph_is_mutable() " execute_and_fetch_all(cursor, f"CALL {module}.graph_is_mutable() " "YIELD mutable RETURN mutable",)[
"YIELD mutable RETURN mutable")[0][0] is is_write 0
assert execute_and_fetch_all( ][0]
cursor, "MATCH (n) " is is_write
f"CALL {module}.underlying_graph_is_mutable(n) " )
"YIELD mutable RETURN mutable")[0][0] is is_write assert (
assert execute_and_fetch_all( execute_and_fetch_all(
cursor, "MATCH (n)-[e]->(m) " cursor,
f"CALL {module}.underlying_graph_is_mutable(e) " "MATCH (n) " f"CALL {module}.underlying_graph_is_mutable(n) " "YIELD mutable RETURN mutable",
"YIELD mutable RETURN mutable")[0][0] is is_write )[0][0]
is is_write
)
assert (
execute_and_fetch_all(
cursor,
"MATCH (n)-[e]->(m) " f"CALL {module}.underlying_graph_is_mutable(e) " "YIELD mutable RETURN mutable",
)[0][0]
is is_write
)
test_mutability(True) test_mutability(True)
test_mutability(False) test_mutability(False)

View File

@@ -20,6 +20,7 @@ from neo4j import GraphDatabase, basic_auth
# Helper class and functions # Helper class and functions
class TestResults: class TestResults:
def __init__(self): def __init__(self):
self.total = 0 self.total = 0
@@ -39,18 +40,16 @@ class TestResults:
# Behave specific functions # Behave specific functions
def before_all(context): def before_all(context):
# logging # logging
logging.basicConfig(level="DEBUG") logging.basicConfig(level="DEBUG")
context.log = logging.getLogger(__name__) context.log = logging.getLogger(__name__)
# driver # driver
uri = "bolt://{}:{}".format(context.config.db_host, uri = "bolt://{}:{}".format(context.config.db_host, context.config.db_port)
context.config.db_port) auth_token = basic_auth(context.config.db_user, context.config.db_pass)
auth_token = basic_auth( context.driver = GraphDatabase.driver(uri, auth=auth_token, encrypted=False)
context.config.db_user, context.config.db_pass)
context.driver = GraphDatabase.driver(uri, auth=auth_token,
encrypted=False)
# test results # test results
context.test_results = TestResults() context.test_results = TestResults()
@@ -63,8 +62,7 @@ def before_scenario(context, scenario):
def after_scenario(context, scenario): def after_scenario(context, scenario):
context.test_results.add_test(scenario.status) context.test_results.add_test(scenario.status)
if context.config.single_scenario or \ if context.config.single_scenario or (context.config.single_fail and scenario.status == "failed"):
(context.config.single_fail and scenario.status == "failed"):
print("Press enter to continue") print("Press enter to continue")
sys.stdin.readline() sys.stdin.readline()
@@ -87,5 +85,5 @@ def after_all(context):
"test_suite": context.config.test_suite, "test_suite": context.config.test_suite,
} }
with open(context.config.stats_file, 'w') as f: with open(context.config.stats_file, "w") as f:
json.dump(js, f) json.dump(js, f)

View File

@@ -55,22 +55,14 @@ def main():
add_config("--test-directory") add_config("--test-directory")
# Arguments that should be passed on to Behave # Arguments that should be passed on to Behave
add_argument("--db-host", default="127.0.0.1", add_argument("--db-host", default="127.0.0.1", help="server host (default is 127.0.0.1)")
help="server host (default is 127.0.0.1)") add_argument("--db-port", default="7687", help="server port (default is 7687)")
add_argument("--db-port", default="7687", add_argument("--db-user", default="memgraph", help="server user (default is memgraph)")
help="server port (default is 7687)") add_argument("--db-pass", default="memgraph", help="server pass (default is memgraph)")
add_argument("--db-user", default="memgraph", add_argument("--stop", action="store_true", help="stop testing after first fail")
help="server user (default is memgraph)") add_argument("--single-fail", action="store_true", help="pause after failed scenario")
add_argument("--db-pass", default="memgraph", add_argument("--single-scenario", action="store_true", help="pause after every scenario")
help="server pass (default is memgraph)") add_argument("--single-feature", action="store_true", help="pause after every feature")
add_argument("--stop", action="store_true",
help="stop testing after first fail")
add_argument("--single-fail", action="store_true",
help="pause after failed scenario")
add_argument("--single-scenario", action="store_true",
help="pause after every scenario")
add_argument("--single-feature", action="store_true",
help="pause after every feature")
add_argument("--stats-file", default="", help="statistics output file") add_argument("--stats-file", default="", help="statistics output file")
# Parse arguments # Parse arguments
@@ -96,5 +88,5 @@ def main():
return behave_main(behave_args) return behave_main(behave_args)
if __name__ == '__main__': if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@@ -15,11 +15,11 @@ from behave import given
import graph import graph
@given(u'the binary-tree-1 graph') @given("the binary-tree-1 graph")
def step_impl(context): def step_impl(context):
graph.create_graph('binary-tree-1', context) graph.create_graph("binary-tree-1", context)
@given(u'the binary-tree-2 graph') @given("the binary-tree-2 graph")
def step_impl(context): def step_impl(context):
graph.create_graph('binary-tree-2', context) graph.create_graph("binary-tree-2", context)

View File

@@ -11,6 +11,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
def query(q, context, params={}): def query(q, context, params={}):
""" """
Function used to execute query on database. Query results are Function used to execute query on database. Query results are
@@ -44,7 +45,7 @@ def query(q, context, params={}):
except Exception as e: except Exception as e:
# exception # exception
context.exception = e context.exception = e
context.log.info('%s', str(e)) context.log.info("%s", str(e))
finally: finally:
session.close() session.close()

View File

@@ -24,234 +24,234 @@ def handle_error(context):
@param context: @param context:
behave.runner.Context, context of behave. behave.runner.Context, context of behave.
""" """
assert(context.exception is not None) assert context.exception is not None
@then('an error should be raised') @then("an error should be raised")
def error(context): def error(context):
handle_error(context) handle_error(context)
@then('a SyntaxError should be raised at compile time: NestedAggregation') @then("a SyntaxError should be raised at compile time: NestedAggregation")
def syntax_error(context): def syntax_error(context):
handle_error(context) handle_error(context)
@then('TypeError should be raised at compile time: IncomparableValues') @then("TypeError should be raised at compile time: IncomparableValues")
def type_error(context): def type_error(context):
handle_error(context) handle_error(context)
@then(u'a TypeError should be raised at compile time: IncomparableValues') @then("a TypeError should be raised at compile time: IncomparableValues")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: RequiresDirectedRelationship') @then("a SyntaxError should be raised at compile time: RequiresDirectedRelationship")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidRelationshipPattern') @then("a SyntaxError should be raised at compile time: InvalidRelationshipPattern")
def syntax_error(context): def syntax_error(context):
handle_error(context) handle_error(context)
@then(u'a TypeError should be raised at runtime: MapElementAccessByNonString') @then("a TypeError should be raised at runtime: MapElementAccessByNonString")
def type_error(context): def type_error(context):
handle_error(context) handle_error(context)
@then(u'a ConstraintVerificationFailed should be raised at runtime: DeleteConnectedNode') @then("a ConstraintVerificationFailed should be raised at runtime: DeleteConnectedNode")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a TypeError should be raised at runtime: ListElementAccessByNonInteger') @then("a TypeError should be raised at runtime: ListElementAccessByNonInteger")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentType') @then("a SyntaxError should be raised at compile time: InvalidArgumentType")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a TypeError should be raised at runtime: InvalidElementAccess') @then("a TypeError should be raised at runtime: InvalidElementAccess")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a ArgumentError should be raised at runtime: NumberOutOfRange') @then("a ArgumentError should be raised at runtime: NumberOutOfRange")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a TypeError should be raised at runtime: InvalidArgumentValue') @then("a TypeError should be raised at runtime: InvalidArgumentValue")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: VariableAlreadyBound') @then("a SyntaxError should be raised at compile time: VariableAlreadyBound")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a TypeError should be raised at runtime: IncomparableValues') @then("a TypeError should be raised at runtime: IncomparableValues")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a TypeError should be raised at runtime: PropertyAccessOnNonMap') @then("a TypeError should be raised at runtime: PropertyAccessOnNonMap")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidUnicodeLiteral') @then("a SyntaxError should be raised at compile time: InvalidUnicodeLiteral")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SemanticError should be raised at compile time: MergeReadOwnWrites') @then("a SemanticError should be raised at compile time: MergeReadOwnWrites")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidAggregation') @then("a SyntaxError should be raised at compile time: InvalidAggregation")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: NoExpressionAlias') @then("a SyntaxError should be raised at compile time: NoExpressionAlias")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: UndefinedVariable') @then("a SyntaxError should be raised at compile time: UndefinedVariable")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: VariableTypeConflict') @then("a SyntaxError should be raised at compile time: VariableTypeConflict")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: DifferentColumnsInUnion') @then("a SyntaxError should be raised at compile time: DifferentColumnsInUnion")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidClauseComposition') @then("a SyntaxError should be raised at compile time: InvalidClauseComposition")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a TypeError should be raised at compile time: InvalidPropertyType') @then("a TypeError should be raised at compile time: InvalidPropertyType")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: ColumnNameConflict') @then("a SyntaxError should be raised at compile time: ColumnNameConflict")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: NoVariablesInScope') @then("a SyntaxError should be raised at compile time: NoVariablesInScope")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidDelete') @then("a SyntaxError should be raised at compile time: InvalidDelete")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: NegativeIntegerArgument') @then("a SyntaxError should be raised at compile time: NegativeIntegerArgument")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a EntityNotFound should be raised at runtime: DeletedEntityAccess') @then("a EntityNotFound should be raised at runtime: DeletedEntityAccess")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: RelationshipUniquenessViolation') @then("a SyntaxError should be raised at compile time: RelationshipUniquenessViolation")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: CreatingVarLength') @then("a SyntaxError should be raised at compile time: CreatingVarLength")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidParameterUse') @then("a SyntaxError should be raised at compile time: InvalidParameterUse")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: FloatingPointOverflow') @then("a SyntaxError should be raised at compile time: FloatingPointOverflow")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time InvalidArgumentExpression') @then("a SyntaxError should be raised at compile time InvalidArgumentExpression")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time InvalidUnicodeCharacter') @then("a SyntaxError should be raised at compile time InvalidUnicodeCharacter")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: NonConstantExpression') @then("a SyntaxError should be raised at compile time: NonConstantExpression")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: NoSingleRelationshipType') @then("a SyntaxError should be raised at compile time: NoSingleRelationshipType")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: UnknownFunction') @then("a SyntaxError should be raised at compile time: UnknownFunction")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidNumberLiteral') @then("a SyntaxError should be raised at compile time: InvalidNumberLiteral")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentExpression') @then("a SyntaxError should be raised at compile time: InvalidArgumentExpression")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidUnicodeCharacter') @then("a SyntaxError should be raised at compile time: InvalidUnicodeCharacter")
def step(context): def step(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentPassingMode') @then("a SyntaxError should be raised at compile time: InvalidArgumentPassingMode")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a SyntaxError should be raised at compile time: InvalidNumberOfArguments') @then("a SyntaxError should be raised at compile time: InvalidNumberOfArguments")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a ParameterMissing should be raised at compile time: MissingParameter') @then("a ParameterMissing should be raised at compile time: MissingParameter")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)
@then(u'a ProcedureError should be raised at compile time: ProcedureNotFound') @then("a ProcedureError should be raised at compile time: ProcedureNotFound")
def step_impl(context): def step_impl(context):
handle_error(context) handle_error(context)

View File

@@ -23,12 +23,12 @@ def clear_graph(context):
database.query("MATCH (n) DETACH DELETE n", context) database.query("MATCH (n) DETACH DELETE n", context)
@given('an empty graph') @given("an empty graph")
def empty_graph_step(context): def empty_graph_step(context):
clear_graph(context) clear_graph(context)
@given('any graph') @given("any graph")
def any_graph_step(context): def any_graph_step(context):
clear_graph(context) clear_graph(context)
@@ -46,20 +46,18 @@ def create_graph(name, context):
and sets graph properties to beginning values. and sets graph properties to beginning values.
""" """
clear_graph(context) clear_graph(context)
path = os.path.join(context.config.test_directory, "graphs", path = os.path.join(context.config.test_directory, "graphs", name + ".cypher")
name + ".cypher")
q_marks = ["'", '"', '`'] q_marks = ["'", '"', "`"]
with open(path, 'r') as f: with open(path, "r") as f:
content = f.read().replace('\n', ' ') content = f.read().replace("\n", " ")
single_query = '' single_query = ""
quote = None quote = None
i = 0 i = 0
while i < len(content): while i < len(content):
ch = content[i] ch = content[i]
if ch == '\\' and i != len(content) - 1 and \ if ch == "\\" and i != len(content) - 1 and content[i + 1] in q_marks:
content[i + 1] in q_marks:
single_query += ch + content[i + 1] single_query += ch + content[i + 1]
i += 2 i += 2
else: else:
@@ -68,9 +66,9 @@ def create_graph(name, context):
quote = None quote = None
elif ch in q_marks and quote is None: elif ch in q_marks and quote is None:
quote = ch quote = ch
if ch == ';' and quote is None: if ch == ";" and quote is None:
database.query(single_query, context) database.query(single_query, context)
single_query = '' single_query = ""
i += 1 i += 1
if single_query.strip() != '': if single_query.strip() != "":
database.query(single_query, context) database.query(single_query, context)

View File

@@ -29,13 +29,13 @@ def parse(el, ignore_order):
@return: @return:
Parsed string of element. Parsed string of element.
""" """
if el.startswith('(') and el.endswith(')'): if el.startswith("(") and el.endswith(")"):
return parse_node(el, ignore_order) return parse_node(el, ignore_order)
if el.startswith('<') and el.endswith('>'): if el.startswith("<") and el.endswith(">"):
return parse_path(el, ignore_order) return parse_path(el, ignore_order)
if el.startswith('{') and el.endswith('}'): if el.startswith("{") and el.endswith("}"):
return parse_map(el, ignore_order) return parse_map(el, ignore_order)
if el.startswith('[') and el.endswith(']'): if el.startswith("[") and el.endswith("]"):
if is_list(el): if is_list(el):
return parse_list(el, ignore_order) return parse_list(el, ignore_order)
else: else:
@@ -51,7 +51,7 @@ def is_list(el):
@return: @return:
true if el is list. true if el is list.
""" """
if el[1] == ':': if el[1] == ":":
return False return False
return True return True
@@ -64,20 +64,20 @@ def parse_path(path, ignore_order):
@return: @return:
parsed path parsed path
""" """
parsed_path = '<' parsed_path = "<"
dif_open_closed_brackets = 0 dif_open_closed_brackets = 0
for i in range(1, len(path) - 1): for i in range(1, len(path) - 1):
if path[i] == '(' or path[i] == '{' or path[i] == '[': if path[i] == "(" or path[i] == "{" or path[i] == "[":
dif_open_closed_brackets += 1 dif_open_closed_brackets += 1
if dif_open_closed_brackets == 1: if dif_open_closed_brackets == 1:
start = i start = i
if path[i] == ')' or path[i] == '}' or path[i] == ']': if path[i] == ")" or path[i] == "}" or path[i] == "]":
dif_open_closed_brackets -= 1 dif_open_closed_brackets -= 1
if dif_open_closed_brackets == 0: if dif_open_closed_brackets == 0:
parsed_path += parse(path[start:(i + 1)], ignore_order) parsed_path += parse(path[start : (i + 1)], ignore_order)
elif dif_open_closed_brackets == 0: elif dif_open_closed_brackets == 0:
parsed_path += path[i] parsed_path += path[i]
parsed_path += '>' parsed_path += ">"
return parsed_path return parsed_path
@@ -89,28 +89,27 @@ def parse_node(node_str, ignore_order):
@return: @return:
parsed node parsed node
""" """
label = '' label = ""
labels = [] labels = []
props_start = None props_start = None
for i in range(1, len(node_str)): for i in range(1, len(node_str)):
if node_str[i] == ':' or node_str[i] == ')' or node_str[i] == '{': if node_str[i] == ":" or node_str[i] == ")" or node_str[i] == "{":
if label.startswith(':'): if label.startswith(":"):
labels.append(label) labels.append(label)
label = '' label = ""
label += node_str[i] label += node_str[i]
if node_str[i] == '{': if node_str[i] == "{":
props_start = i props_start = i
break break
labels.sort() labels.sort()
parsed_node = '(' parsed_node = "("
for label in labels: for label in labels:
parsed_node += label parsed_node += label
if props_start is not None: if props_start is not None:
parsed_node += parse_map( parsed_node += parse_map(node_str[props_start : len(node_str) - 1], ignore_order)
node_str[props_start:len(node_str) - 1], ignore_order) parsed_node += ")"
parsed_node += ')'
return parsed_node return parsed_node
@@ -123,23 +122,23 @@ def parse_map(props, ignore_order):
parsed map parsed map
""" """
dif_open_closed_brackets = 0 dif_open_closed_brackets = 0
prop = '' prop = ""
list_props = [] list_props = []
for i in range(1, len(props) - 1): for i in range(1, len(props) - 1):
if props[i] == ',' and dif_open_closed_brackets == 0: if props[i] == "," and dif_open_closed_brackets == 0:
list_props.append(prop_to_str(prop, ignore_order)) list_props.append(prop_to_str(prop, ignore_order))
prop = '' prop = ""
else: else:
prop += props[i] prop += props[i]
if props[i] == '(' or props[i] == '{' or props[i] == '[': if props[i] == "(" or props[i] == "{" or props[i] == "[":
dif_open_closed_brackets += 1 dif_open_closed_brackets += 1
elif props[i] == ')' or props[i] == '}' or props[i] == ']': elif props[i] == ")" or props[i] == "}" or props[i] == "]":
dif_open_closed_brackets -= 1 dif_open_closed_brackets -= 1
if prop != '': if prop != "":
list_props.append(prop_to_str(prop, ignore_order)) list_props.append(prop_to_str(prop, ignore_order))
list_props.sort() list_props.sort()
return '{' + ','.join(list_props) + '}' return "{" + ",".join(list_props) + "}"
def prop_to_str(prop, ignore_order): def prop_to_str(prop, ignore_order):
@@ -152,8 +151,8 @@ def prop_to_str(prop, ignore_order):
@return: @return:
parsed prop parsed prop
""" """
key = prop.split(':', 1)[0] key = prop.split(":", 1)[0]
val = prop.split(':', 1)[1] val = prop.split(":", 1)[1]
return key + ":" + parse(val, ignore_order) return key + ":" + parse(val, ignore_order)
@@ -166,25 +165,25 @@ def parse_list(l, ignore_order):
parsed list parsed list
""" """
dif_open_closed_brackets = 0 dif_open_closed_brackets = 0
el = '' el = ""
list_el = [] list_el = []
for i in range(1, len(l) - 1): for i in range(1, len(l) - 1):
if l[i] == ',' and dif_open_closed_brackets == 0: if l[i] == "," and dif_open_closed_brackets == 0:
list_el.append(parse(el, ignore_order)) list_el.append(parse(el, ignore_order))
el = '' el = ""
else: else:
el += l[i] el += l[i]
if l[i] == '(' or l[i] == '{' or l[i] == '[': if l[i] == "(" or l[i] == "{" or l[i] == "[":
dif_open_closed_brackets += 1 dif_open_closed_brackets += 1
elif l[i] == ')' or l[i] == '}' or l[i] == ']': elif l[i] == ")" or l[i] == "}" or l[i] == "]":
dif_open_closed_brackets -= 1 dif_open_closed_brackets -= 1
if el != '': if el != "":
list_el.append(parse(el, ignore_order)) list_el.append(parse(el, ignore_order))
if ignore_order: if ignore_order:
list_el.sort() list_el.sort()
return '[' + ','.join(list_el) + ']' return "[" + ",".join(list_el) + "]"
def parse_rel(rel, ignore_order): def parse_rel(rel, ignore_order):
@@ -195,25 +194,25 @@ def parse_rel(rel, ignore_order):
@return: @return:
parsed relationship parsed relationship
""" """
label = '' label = ""
labels = [] labels = []
props_start = None props_start = None
for i in range(1, len(rel)): for i in range(1, len(rel)):
if rel[i] == ':' or rel[i] == ']' or rel[i] == '{': if rel[i] == ":" or rel[i] == "]" or rel[i] == "{":
if label.startswith(':'): if label.startswith(":"):
labels.append(label) labels.append(label)
label = '' label = ""
label += rel[i] label += rel[i]
if rel[i] == '{': if rel[i] == "{":
props_start = i props_start = i
break break
labels.sort() labels.sort()
parsed_rel = '[' parsed_rel = "["
for label in labels: for label in labels:
parsed_rel += label parsed_rel += label
if props_start is not None: if props_start is not None:
parsed_rel += parse_map(rel[props_start:len(rel) - 1], ignore_order) parsed_rel += parse_map(rel[props_start : len(rel) - 1], ignore_order)
parsed_rel += ']' parsed_rel += "]"
return parsed_rel return parsed_rel

View File

@@ -17,32 +17,29 @@ from behave import given, then, step, when
from neo4j.graph import Node, Path, Relationship from neo4j.graph import Node, Path, Relationship
@given('parameters are') @given("parameters are")
def parameters_step(context): def parameters_step(context):
context.test_parameters.set_parameters_from_table(context.table) context.test_parameters.set_parameters_from_table(context.table)
@then('parameters are') @then("parameters are")
def parameters_step(context): def parameters_step(context):
context.test_parameters.set_parameters_from_table(context.table) context.test_parameters.set_parameters_from_table(context.table)
@step('having executed') @step("having executed")
def having_executed_step(context): def having_executed_step(context):
context.results = database.query( context.results = database.query(context.text, context, context.test_parameters.get_parameters())
context.text, context, context.test_parameters.get_parameters())
@when('executing query') @when("executing query")
def executing_query_step(context): def executing_query_step(context):
context.results = database.query( context.results = database.query(context.text, context, context.test_parameters.get_parameters())
context.text, context, context.test_parameters.get_parameters())
@when('executing control query') @when("executing control query")
def executing_query_step(context): def executing_query_step(context):
context.results = database.query( context.results = database.query(context.text, context, context.test_parameters.get_parameters())
context.text, context, context.test_parameters.get_parameters())
def parse_props(props_key_value): def parse_props(props_key_value):
@@ -93,11 +90,11 @@ def to_string(element):
# parsing Node # parsing Node
sol = "(" sol = "("
if element.labels: if element.labels:
sol += ':' + ': '.join(element.labels) sol += ":" + ": ".join(element.labels)
if element.keys(): if element.keys():
if element.labels: if element.labels:
sol += ' ' sol += " "
sol += parse_props(element.items()) sol += parse_props(element.items())
sol += ")" sol += ")"
@@ -109,7 +106,7 @@ def to_string(element):
if element.type: if element.type:
sol += element.type sol += element.type
if element.keys(): if element.keys():
sol += ' ' sol += " "
sol += parse_props(element.items()) sol += parse_props(element.items())
sol += "]" sol += "]"
return sol return sol
@@ -144,12 +141,12 @@ def to_string(element):
elif isinstance(element, list): elif isinstance(element, list):
# parsing list # parsing list
sol = '[' sol = "["
el_str = [] el_str = []
for el in element: for el in element:
el_str.append(to_string(el)) el_str.append(to_string(el))
sol += ', '.join(el_str) sol += ", ".join(el_str)
sol += ']' sol += "]"
return sol return sol
@@ -162,23 +159,22 @@ def to_string(element):
elif isinstance(element, dict): elif isinstance(element, dict):
# parsing map # parsing map
if len(element) == 0: if len(element) == 0:
return '{}' return "{}"
sol = '{' sol = "{"
for key, val in element.items(): for key, val in element.items():
sol += key + ':' + to_string(val) + ',' sol += key + ":" + to_string(val) + ","
sol = sol[:-1] + '}' sol = sol[:-1] + "}"
return sol return sol
elif isinstance(element, float): elif isinstance(element, float):
# parsing float, scientific # parsing float, scientific
if 'e' in str(element): if "e" in str(element):
if str(element)[-3] == '-': if str(element)[-3] == "-":
zeroes = int(str(element)[-2:]) - 1 zeroes = int(str(element)[-2:]) - 1
num_str = '' num_str = ""
if str(element)[0] == '-': if str(element)[0] == "-":
num_str += '-' num_str += "-"
num_str += '.' + zeroes * '0' + \ num_str += "." + zeroes * "0" + str(element)[:-4].replace("-", "").replace(".", "")
str(element)[:-4].replace("-", "").replace(".", "")
return num_str return num_str
return str(element) return str(element)
@@ -201,9 +197,14 @@ def get_result_rows(context, ignore_order):
keys = result.keys() keys = result.keys()
values = result.values() values = result.values()
for i in range(0, len(keys)): for i in range(0, len(keys)):
result_rows.append(keys[i] + ":" + parser.parse( result_rows.append(
to_string(values[i]).replace("\n", "\\n").replace(" ", ""), keys[i]
ignore_order)) + ":"
+ parser.parse(
to_string(values[i]).replace("\n", "\\n").replace(" ", ""),
ignore_order,
)
)
return result_rows return result_rows
@@ -221,9 +222,7 @@ def get_expected_rows(context, ignore_order):
expected_rows = [] expected_rows = []
for row in context.table: for row in context.table:
for col in context.table.headings: for col in context.table.headings:
expected_rows.append( expected_rows.append(col + ":" + parser.parse(row[col].replace(" ", ""), ignore_order))
col + ":" + parser.parse(row[col].replace(" ", ""),
ignore_order))
return expected_rows return expected_rows
@@ -242,13 +241,13 @@ def validate(context, ignore_order):
context.log.info("Expected: %s", str(expected_rows)) context.log.info("Expected: %s", str(expected_rows))
context.log.info("Results: %s", str(result_rows)) context.log.info("Results: %s", str(result_rows))
assert(len(expected_rows) == len(result_rows)) assert len(expected_rows) == len(result_rows)
for i in range(0, len(expected_rows)): for i in range(0, len(expected_rows)):
if expected_rows[i] in result_rows: if expected_rows[i] in result_rows:
result_rows.remove(expected_rows[i]) result_rows.remove(expected_rows[i])
else: else:
assert(False) assert False
def validate_in_order(context, ignore_order): def validate_in_order(context, ignore_order):
@@ -267,26 +266,26 @@ def validate_in_order(context, ignore_order):
context.log.info("Expected: %s", str(expected_rows)) context.log.info("Expected: %s", str(expected_rows))
context.log.info("Results: %s", str(result_rows)) context.log.info("Results: %s", str(result_rows))
assert(len(expected_rows) == len(result_rows)) assert len(expected_rows) == len(result_rows)
for i in range(0, len(expected_rows)): for i in range(0, len(expected_rows)):
if expected_rows[i] != result_rows[i]: if expected_rows[i] != result_rows[i]:
assert(False) assert False
@then('the result should be') @then("the result should be")
def expected_result_step(context): def expected_result_step(context):
validate(context, False) validate(context, False)
check_exception(context) check_exception(context)
@then('the result should be, in order') @then("the result should be, in order")
def expected_result_step(context): def expected_result_step(context):
validate_in_order(context, False) validate_in_order(context, False)
check_exception(context) check_exception(context)
@then('the result should be (ignoring element order for lists)') @then("the result should be (ignoring element order for lists)")
def expected_result_step(context): def expected_result_step(context):
validate(context, True) validate(context, True)
check_exception(context) check_exception(context)
@@ -295,20 +294,20 @@ def expected_result_step(context):
def check_exception(context): def check_exception(context):
if context.exception is not None: if context.exception is not None:
context.log.info("Exception when executing query!") context.log.info("Exception when executing query!")
assert(False) assert False
@then('the result should be empty') @then("the result should be empty")
def empty_result_step(context): def empty_result_step(context):
assert(len(context.results) == 0) assert len(context.results) == 0
check_exception(context) check_exception(context)
@then('the side effects should be') @then("the side effects should be")
def side_effects_step(context): def side_effects_step(context):
return return
@then('no side effects') @then("no side effects")
def side_effects_step(context): def side_effects_step(context):
return return

View File

@@ -40,15 +40,15 @@ class TestParameters:
par = dict() par = dict()
for row in table: for row in table:
par[row[0]] = self.parse_parameters(row[1]) par[row[0]] = self.parse_parameters(row[1])
if isinstance(par[row[0]], str) and par[row[0]].startswith("'") \ if isinstance(par[row[0]], str) and par[row[0]].startswith("'") and par[row[0]].endswith("'"):
and par[row[0]].endswith("'"): par[row[0]] = par[row[0]][1 : len(par[row[0]]) - 1]
par[row[0]] = par[row[0]][1:len(par[row[0]]) - 1]
par[table.headings[0]] = self.parse_parameters(table.headings[1]) par[table.headings[0]] = self.parse_parameters(table.headings[1])
if isinstance(par[table.headings[0]], str) and \ if (
par[table.headings[0]].startswith("'") and \ isinstance(par[table.headings[0]], str)
par[table.headings[0]].endswith("'"): and par[table.headings[0]].startswith("'")
par[table.headings[0]] = \ and par[table.headings[0]].endswith("'")
par[table.headings[0]][1:len(par[table.headings[0]]) - 1] ):
par[table.headings[0]] = par[table.headings[0]][1 : len(par[table.headings[0]]) - 1]
self.parameters = par self.parameters = par

View File

@@ -37,20 +37,14 @@ QUERIES = [
("CREATE (n {name: $name})", {"name": 5, "leftover": 42}), ("CREATE (n {name: $name})", {"name": 5, "leftover": 42}),
("MATCH (n), (m) CREATE (n)-[:e {when: $when}]->(m)", {"when": 42}), ("MATCH (n), (m) CREATE (n)-[:e {when: $when}]->(m)", {"when": 42}),
("MATCH (n) RETURN n", {}), ("MATCH (n) RETURN n", {}),
( ("MATCH (n), (m {type: $type}) RETURN count(n), count(m)", {"type": "dadada"}),
"MATCH (n), (m {type: $type}) RETURN count(n), count(m)",
{"type": "dadada"}
),
( (
"MERGE (n) ON CREATE SET n.created = timestamp() " "MERGE (n) ON CREATE SET n.created = timestamp() "
"ON MATCH SET n.lastSeen = timestamp() " "ON MATCH SET n.lastSeen = timestamp() "
"RETURN n.name, n.created, n.lastSeen", "RETURN n.name, n.created, n.lastSeen",
{} {},
),
(
"MATCH (n {value: $value}) SET n.value = 0 RETURN n",
{"value": "nandare!"}
), ),
("MATCH (n {value: $value}) SET n.value = 0 RETURN n", {"value": "nandare!"}),
("MATCH (n), (m) SET n.value = m.value", {}), ("MATCH (n), (m) SET n.value = m.value", {}),
("MATCH (n {test: $test}) REMOVE n.value", {"test": 48}), ("MATCH (n {test: $test}) REMOVE n.value", {"test": 48}),
("MATCH (n), (m) REMOVE n.value, m.value", {}), ("MATCH (n), (m) REMOVE n.value, m.value", {}),
@@ -74,7 +68,8 @@ def execute_test(memgraph_binary, tester_binary):
storage_directory.name, storage_directory.name,
"--audit-enabled", "--audit-enabled",
"--log-file=memgraph.log", "--log-file=memgraph.log",
"--log-level=TRACE"] "--log-level=TRACE",
]
# Start the memgraph binary # Start the memgraph binary
memgraph = subprocess.Popen(list(map(str, memgraph_args))) memgraph = subprocess.Popen(list(map(str, memgraph_args)))
@@ -92,8 +87,13 @@ def execute_test(memgraph_binary, tester_binary):
def execute_queries(queries): def execute_queries(queries):
for query, params in queries: for query, params in queries:
print(query, params) print(query, params)
args = [tester_binary, "--query", query, args = [
"--params-json", json.dumps(params)] tester_binary,
"--query",
query,
"--params-json",
json.dumps(params),
]
subprocess.run(args).check_returncode() subprocess.run(args).check_returncode()
# Execute all queries # Execute all queries
@@ -109,10 +109,17 @@ def execute_test(memgraph_binary, tester_binary):
# Verify the written log # Verify the written log
print("\033[1;36m~~ Starting log verification ~~\033[0m") print("\033[1;36m~~ Starting log verification ~~\033[0m")
with open(os.path.join(storage_directory.name, "audit", "audit.log")) as f: with open(os.path.join(storage_directory.name, "audit", "audit.log")) as f:
reader = csv.reader(f, delimiter=',', doublequote=False, reader = csv.reader(
escapechar='\\', lineterminator='\n', f,
quotechar='"', quoting=csv.QUOTE_MINIMAL, delimiter=",",
skipinitialspace=False, strict=True) doublequote=False,
escapechar="\\",
lineterminator="\n",
quotechar='"',
quoting=csv.QUOTE_MINIMAL,
skipinitialspace=False,
strict=True,
)
queries = [] queries = []
for line in reader: for line in reader:
timestamp, address, username, query, params = line timestamp, address, username, query, params = line
@@ -120,15 +127,13 @@ def execute_test(memgraph_binary, tester_binary):
queries.append((query, params)) queries.append((query, params))
print(query, params) print(query, params)
assert queries == QUERIES, "Logged queries don't match " \ assert queries == QUERIES, "Logged queries don't match " "executed queries!"
"executed queries!"
print("\033[1;36m~~ Finished log verification ~~\033[0m\n") print("\033[1;36m~~ Finished log verification ~~\033[0m\n")
if __name__ == "__main__": if __name__ == "__main__":
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph") memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "audit", "tester")
"integration", "audit", "tester")
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--memgraph", default=memgraph_binary) parser.add_argument("--memgraph", default=memgraph_binary)

View File

@@ -29,15 +29,8 @@ PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
QUERIES = [ QUERIES = [
# CREATE # CREATE
( ("CREATE (n)", ("CREATE",)),
"CREATE (n)", ("MATCH (n), (m) CREATE (n)-[:e]->(m)", ("CREATE", "MATCH")),
("CREATE",)
),
(
"MATCH (n), (m) CREATE (n)-[:e]->(m)",
("CREATE", "MATCH")
),
# DELETE # DELETE
( (
"MATCH (n) DELETE n", "MATCH (n) DELETE n",
@@ -47,116 +40,43 @@ QUERIES = [
"MATCH (n) DETACH DELETE n", "MATCH (n) DETACH DELETE n",
("DELETE", "MATCH"), ("DELETE", "MATCH"),
), ),
# MATCH # MATCH
( ("MATCH (n) RETURN n", ("MATCH",)),
"MATCH (n) RETURN n", ("MATCH (n), (m) RETURN count(n), count(m)", ("MATCH",)),
("MATCH",)
),
(
"MATCH (n), (m) RETURN count(n), count(m)",
("MATCH",)
),
# MERGE # MERGE
( (
"MERGE (n) ON CREATE SET n.created = timestamp() " "MERGE (n) ON CREATE SET n.created = timestamp() "
"ON MATCH SET n.lastSeen = timestamp() " "ON MATCH SET n.lastSeen = timestamp() "
"RETURN n.name, n.created, n.lastSeen", "RETURN n.name, n.created, n.lastSeen",
("MERGE",) ("MERGE",),
), ),
# SET # SET
( ("MATCH (n) SET n.value = 0 RETURN n", ("SET", "MATCH")),
"MATCH (n) SET n.value = 0 RETURN n", ("MATCH (n), (m) SET n.value = m.value", ("SET", "MATCH")),
("SET", "MATCH")
),
(
"MATCH (n), (m) SET n.value = m.value",
("SET", "MATCH")
),
# REMOVE # REMOVE
( ("MATCH (n) REMOVE n.value", ("REMOVE", "MATCH")),
"MATCH (n) REMOVE n.value", ("MATCH (n), (m) REMOVE n.value, m.value", ("REMOVE", "MATCH")),
("REMOVE", "MATCH")
),
(
"MATCH (n), (m) REMOVE n.value, m.value",
("REMOVE", "MATCH")
),
# INDEX # INDEX
( ("CREATE INDEX ON :User (id)", ("INDEX",)),
"CREATE INDEX ON :User (id)",
("INDEX",)
),
# AUTH # AUTH
( ("CREATE ROLE test_role", ("AUTH",)),
"CREATE ROLE test_role", ("DROP ROLE test_role", ("AUTH",)),
("AUTH",) ("SHOW ROLES", ("AUTH",)),
), ("CREATE USER test_user", ("AUTH",)),
( ("SET PASSWORD FOR test_user TO '1234'", ("AUTH",)),
"DROP ROLE test_role", ("DROP USER test_user", ("AUTH",)),
("AUTH",) ("SHOW USERS", ("AUTH",)),
), ("SET ROLE FOR test_user TO test_role", ("AUTH",)),
( ("CLEAR ROLE FOR test_user", ("AUTH",)),
"SHOW ROLES", ("GRANT ALL PRIVILEGES TO test_user", ("AUTH",)),
("AUTH",) ("DENY ALL PRIVILEGES TO test_user", ("AUTH",)),
), ("REVOKE ALL PRIVILEGES FROM test_user", ("AUTH",)),
( ("SHOW PRIVILEGES FOR test_user", ("AUTH",)),
"CREATE USER test_user", ("SHOW ROLE FOR test_user", ("AUTH",)),
("AUTH",) ("SHOW USERS FOR test_role", ("AUTH",)),
),
(
"SET PASSWORD FOR test_user TO '1234'",
("AUTH",)
),
(
"DROP USER test_user",
("AUTH",)
),
(
"SHOW USERS",
("AUTH",)
),
(
"SET ROLE FOR test_user TO test_role",
("AUTH",)
),
(
"CLEAR ROLE FOR test_user",
("AUTH",)
),
(
"GRANT ALL PRIVILEGES TO test_user",
("AUTH",)
),
(
"DENY ALL PRIVILEGES TO test_user",
("AUTH",)
),
(
"REVOKE ALL PRIVILEGES FROM test_user",
("AUTH",)
),
(
"SHOW PRIVILEGES FOR test_user",
("AUTH",)
),
(
"SHOW ROLE FOR test_user",
("AUTH",)
),
(
"SHOW USERS FOR test_role",
("AUTH",)
),
] ]
UNAUTHORIZED_ERROR = "You are not authorized to execute this query! Please " \ UNAUTHORIZED_ERROR = "You are not authorized to execute this query! Please " "contact your database administrator."
"contact your database administrator."
def wait_for_server(port, delay=0.1): def wait_for_server(port, delay=0.1):
@@ -166,8 +86,15 @@ def wait_for_server(port, delay=0.1):
time.sleep(delay) time.sleep(delay)
def execute_tester(binary, queries, should_fail=False, failure_message="", def execute_tester(
username="", password="", check_failure=True): binary,
queries,
should_fail=False,
failure_message="",
username="",
password="",
check_failure=True,
):
args = [binary, "--username", username, "--password", password] args = [binary, "--username", username, "--password", password]
if should_fail: if should_fail:
args.append("--should-fail") args.append("--should-fail")
@@ -200,18 +127,28 @@ def check_permissions(query_perms, user_perms):
def execute_test(memgraph_binary, tester_binary, checker_binary): def execute_test(memgraph_binary, tester_binary, checker_binary):
storage_directory = tempfile.TemporaryDirectory() storage_directory = tempfile.TemporaryDirectory()
memgraph_args = [memgraph_binary, memgraph_args = [memgraph_binary, "--data-directory", storage_directory.name]
"--data-directory", storage_directory.name]
def execute_admin_queries(queries): def execute_admin_queries(queries):
return execute_tester(tester_binary, queries, should_fail=False, return execute_tester(
check_failure=True, username="admin", tester_binary,
password="admin") queries,
should_fail=False,
check_failure=True,
username="admin",
password="admin",
)
def execute_user_queries(queries, should_fail=False, failure_message="", def execute_user_queries(queries, should_fail=False, failure_message="", check_failure=True):
check_failure=True): return execute_tester(
return execute_tester(tester_binary, queries, should_fail, tester_binary,
failure_message, "user", "user", check_failure) queries,
should_fail,
failure_message,
"user",
"user",
check_failure,
)
# Start the memgraph binary # Start the memgraph binary
memgraph = subprocess.Popen(list(map(str, memgraph_args))) memgraph = subprocess.Popen(list(map(str, memgraph_args)))
@@ -227,11 +164,13 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!" assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
# Prepare all users # Prepare all users
execute_admin_queries([ execute_admin_queries(
"CREATE USER ADmin IDENTIFIED BY 'admin'", [
"GRANT ALL PRIVILEGES TO admIN", "CREATE USER ADmin IDENTIFIED BY 'admin'",
"CREATE USER usEr IDENTIFIED BY 'user'", "GRANT ALL PRIVILEGES TO admIN",
]) "CREATE USER usEr IDENTIFIED BY 'user'",
]
)
# Find all existing permissions # Find all existing permissions
permissions = set() permissions = set()
@@ -243,12 +182,14 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
print("\033[1;36m~~ Starting query test ~~\033[0m") print("\033[1;36m~~ Starting query test ~~\033[0m")
for mask in range(0, 2 ** len(permissions)): for mask in range(0, 2 ** len(permissions)):
user_perms = get_permissions(permissions, mask) user_perms = get_permissions(permissions, mask)
print("\033[1;34m~~ Checking queries with privileges: ", print(
", ".join(user_perms), " ~~\033[0m") "\033[1;34m~~ Checking queries with privileges: ",
", ".join(user_perms),
" ~~\033[0m",
)
admin_queries = ["REVOKE ALL PRIVILEGES FROM uSer"] admin_queries = ["REVOKE ALL PRIVILEGES FROM uSer"]
if len(user_perms) > 0: if len(user_perms) > 0:
admin_queries.append( admin_queries.append("GRANT {} TO User".format(", ".join(user_perms)))
"GRANT {} TO User".format(", ".join(user_perms)))
execute_admin_queries(admin_queries) execute_admin_queries(admin_queries)
authorized, unauthorized = [], [] authorized, unauthorized = [], []
for query, query_perms in QUERIES: for query, query_perms in QUERIES:
@@ -256,35 +197,43 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
authorized.append(query) authorized.append(query)
else: else:
unauthorized.append(query) unauthorized.append(query)
execute_user_queries(authorized, check_failure=False, execute_user_queries(authorized, check_failure=False, failure_message=UNAUTHORIZED_ERROR)
failure_message=UNAUTHORIZED_ERROR) execute_user_queries(unauthorized, should_fail=True, failure_message=UNAUTHORIZED_ERROR)
execute_user_queries(unauthorized, should_fail=True,
failure_message=UNAUTHORIZED_ERROR)
print("\033[1;36m~~ Finished query test ~~\033[0m\n") print("\033[1;36m~~ Finished query test ~~\033[0m\n")
# Run the user/role permissions test # Run the user/role permissions test
print("\033[1;36m~~ Starting permissions test ~~\033[0m") print("\033[1;36m~~ Starting permissions test ~~\033[0m")
execute_admin_queries([ execute_admin_queries(
"CREATE ROLE roLe", [
"REVOKE ALL PRIVILEGES FROM uSeR", "CREATE ROLE roLe",
]) "REVOKE ALL PRIVILEGES FROM uSeR",
]
)
execute_checker(checker_binary, []) execute_checker(checker_binary, [])
for user_perm in ["GRANT", "DENY", "REVOKE"]: for user_perm in ["GRANT", "DENY", "REVOKE"]:
for role_perm in ["GRANT", "DENY", "REVOKE"]: for role_perm in ["GRANT", "DENY", "REVOKE"]:
for mapped in [True, False]: for mapped in [True, False]:
print("\033[1;34m~~ Checking permissions with user ", print(
user_perm, ", role ", role_perm, "\033[1;34m~~ Checking permissions with user ",
"user mapped to role:", mapped, " ~~\033[0m") user_perm,
", role ",
role_perm,
"user mapped to role:",
mapped,
" ~~\033[0m",
)
if mapped: if mapped:
execute_admin_queries(["SET ROLE FOR USER TO roLE"]) execute_admin_queries(["SET ROLE FOR USER TO roLE"])
else: else:
execute_admin_queries(["CLEAR ROLE FOR user"]) execute_admin_queries(["CLEAR ROLE FOR user"])
user_prep = "FROM" if user_perm == "REVOKE" else "TO" user_prep = "FROM" if user_perm == "REVOKE" else "TO"
role_prep = "FROM" if role_perm == "REVOKE" else "TO" role_prep = "FROM" if role_perm == "REVOKE" else "TO"
execute_admin_queries([ execute_admin_queries(
"{} MATCH {} user".format(user_perm, user_prep), [
"{} MATCH {} rOLe".format(role_perm, role_prep) "{} MATCH {} user".format(user_perm, user_prep),
]) "{} MATCH {} rOLe".format(role_perm, role_prep),
]
)
expected = [] expected = []
perms = [user_perm, role_perm] if mapped else [user_perm] perms = [user_perm, role_perm] if mapped else [user_perm]
if "DENY" in perms: if "DENY" in perms:
@@ -313,10 +262,8 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
if __name__ == "__main__": if __name__ == "__main__":
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph") memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "auth", "tester")
"integration", "auth", "tester") checker_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "auth", "checker")
checker_binary = os.path.join(PROJECT_DIR, "build", "tests",
"integration", "auth", "checker")
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--memgraph", default=memgraph_binary) parser.add_argument("--memgraph", default=memgraph_binary)

View File

@@ -40,7 +40,7 @@ def wait_for_server(port, delay=0.1):
def sorted_content(file_path): def sorted_content(file_path):
with open(file_path, 'r') as fin: with open(file_path, "r") as fin:
return sorted(list(map(lambda x: x.strip(), fin.readlines()))) return sorted(list(map(lambda x: x.strip(), fin.readlines())))
@@ -52,32 +52,30 @@ def list_to_string(data):
return ret return ret
def execute_test( def execute_test(memgraph_binary, dump_binary, test_directory, test_type, write_expected):
memgraph_binary, assert test_type in [
dump_binary, "SNAPSHOT",
test_directory, "WAL",
test_type, ], "Test type should be either 'SNAPSHOT' or 'WAL'."
write_expected): print("\033[1;36m~~ Executing test {} ({}) ~~\033[0m".format(os.path.relpath(test_directory, TESTS_DIR), test_type))
assert test_type in ["SNAPSHOT", "WAL"], \
"Test type should be either 'SNAPSHOT' or 'WAL'."
print("\033[1;36m~~ Executing test {} ({}) ~~\033[0m"
.format(os.path.relpath(test_directory, TESTS_DIR), test_type))
working_data_directory = tempfile.TemporaryDirectory() working_data_directory = tempfile.TemporaryDirectory()
if test_type == "SNAPSHOT": if test_type == "SNAPSHOT":
snapshots_dir = os.path.join(working_data_directory.name, "snapshots") snapshots_dir = os.path.join(working_data_directory.name, "snapshots")
os.makedirs(snapshots_dir) os.makedirs(snapshots_dir)
shutil.copy(os.path.join(test_directory, SNAPSHOT_FILE_NAME), shutil.copy(os.path.join(test_directory, SNAPSHOT_FILE_NAME), snapshots_dir)
snapshots_dir)
else: else:
wal_dir = os.path.join(working_data_directory.name, "wal") wal_dir = os.path.join(working_data_directory.name, "wal")
os.makedirs(wal_dir) os.makedirs(wal_dir)
shutil.copy(os.path.join(test_directory, WAL_FILE_NAME), wal_dir) shutil.copy(os.path.join(test_directory, WAL_FILE_NAME), wal_dir)
memgraph_args = [memgraph_binary, memgraph_args = [
"--storage-recover-on-startup", memgraph_binary,
"--storage-properties-on-edges", "--storage-recover-on-startup",
"--data-directory", working_data_directory.name] "--storage-properties-on-edges",
"--data-directory",
working_data_directory.name,
]
# Start the memgraph binary # Start the memgraph binary
memgraph = subprocess.Popen(memgraph_args) memgraph = subprocess.Popen(memgraph_args)
@@ -104,22 +102,21 @@ def execute_test(
dump_file_name = DUMP_SNAPSHOT_FILE_NAME if test_type == "SNAPSHOT" else DUMP_WAL_FILE_NAME dump_file_name = DUMP_SNAPSHOT_FILE_NAME if test_type == "SNAPSHOT" else DUMP_WAL_FILE_NAME
if write_expected: if write_expected:
with open(dump_output_file.name, 'r') as dump: with open(dump_output_file.name, "r") as dump:
queries_got = dump.readlines() queries_got = dump.readlines()
# Write dump files # Write dump files
expected_dump_file = os.path.join(test_directory, dump_file_name) expected_dump_file = os.path.join(test_directory, dump_file_name)
with open(expected_dump_file, 'w') as expected: with open(expected_dump_file, "w") as expected:
expected.writelines(queries_got) expected.writelines(queries_got)
else: else:
# Compare dump files # Compare dump files
expected_dump_file = os.path.join(test_directory, dump_file_name) expected_dump_file = os.path.join(test_directory, dump_file_name)
assert os.path.exists(expected_dump_file), \ assert os.path.exists(expected_dump_file), "Could not find expected dump path {}".format(expected_dump_file)
"Could not find expected dump path {}".format(expected_dump_file)
queries_got = sorted_content(dump_output_file.name) queries_got = sorted_content(dump_output_file.name)
queries_expected = sorted_content(expected_dump_file) queries_expected = sorted_content(expected_dump_file)
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" \ assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" "{}".format(
"{}".format(list_to_string(queries_got), list_to_string(queries_got), list_to_string(queries_expected)
list_to_string(queries_expected)) )
print("\033[1;32m~~ Test successful ~~\033[0m\n") print("\033[1;32m~~ Test successful ~~\033[0m\n")
@@ -141,15 +138,17 @@ def find_test_directories(directory):
continue continue
snapshot_file = os.path.join(test_dir_path, SNAPSHOT_FILE_NAME) snapshot_file = os.path.join(test_dir_path, SNAPSHOT_FILE_NAME)
wal_file = os.path.join(test_dir_path, WAL_FILE_NAME) wal_file = os.path.join(test_dir_path, WAL_FILE_NAME)
dump_snapshot_file = os.path.join( dump_snapshot_file = os.path.join(test_dir_path, DUMP_SNAPSHOT_FILE_NAME)
test_dir_path, DUMP_SNAPSHOT_FILE_NAME)
dump_wal_file = os.path.join(test_dir_path, DUMP_WAL_FILE_NAME) dump_wal_file = os.path.join(test_dir_path, DUMP_WAL_FILE_NAME)
if (os.path.isfile(snapshot_file) and os.path.isfile(dump_snapshot_file) if (
and os.path.isfile(wal_file) and os.path.isfile(dump_wal_file)): os.path.isfile(snapshot_file)
and os.path.isfile(dump_snapshot_file)
and os.path.isfile(wal_file)
and os.path.isfile(dump_wal_file)
):
test_dirs.append(test_dir_path) test_dirs.append(test_dir_path)
else: else:
raise Exception("Missing data in test directory '{}'" raise Exception("Missing data in test directory '{}'".format(test_dir_path))
.format(test_dir_path))
return test_dirs return test_dirs
@@ -161,26 +160,17 @@ if __name__ == "__main__":
parser.add_argument("--memgraph", default=memgraph_binary) parser.add_argument("--memgraph", default=memgraph_binary)
parser.add_argument("--dump", default=dump_binary) parser.add_argument("--dump", default=dump_binary)
parser.add_argument( parser.add_argument(
'--write-expected', "--write-expected",
action='store_true', action="store_true",
help='Overwrite the expected cypher with results from current run') help="Overwrite the expected cypher with results from current run",
)
args = parser.parse_args() args = parser.parse_args()
test_directories = find_test_directories(TESTS_DIR) test_directories = find_test_directories(TESTS_DIR)
assert len(test_directories) > 0, "No tests have been found!" assert len(test_directories) > 0, "No tests have been found!"
for test_directory in test_directories: for test_directory in test_directories:
execute_test( execute_test(args.memgraph, args.dump, test_directory, "SNAPSHOT", args.write_expected)
args.memgraph, execute_test(args.memgraph, args.dump, test_directory, "WAL", args.write_expected)
args.dump,
test_directory,
"SNAPSHOT",
args.write_expected)
execute_test(
args.memgraph,
args.dump,
test_directory,
"WAL",
args.write_expected)
sys.exit(0) sys.exit(0)

View File

@@ -52,8 +52,14 @@ def wait_for_server(port, delay=0.1):
time.sleep(delay) time.sleep(delay)
def execute_tester(binary, queries, username="", password="", def execute_tester(
auth_should_fail=False, query_should_fail=False): binary,
queries,
username="",
password="",
auth_should_fail=False,
query_should_fail=False,
):
if password == "": if password == "":
password = username password = username
args = [binary, "--username", username, "--password", password] args = [binary, "--username", username, "--password", password]
@@ -76,18 +82,14 @@ class Memgraph:
def start(self, **kwargs): def start(self, **kwargs):
self.stop() self.stop()
self._storage_directory = tempfile.TemporaryDirectory() self._storage_directory = tempfile.TemporaryDirectory()
self._auth_module = os.path.join(self._storage_directory.name, self._auth_module = os.path.join(self._storage_directory.name, "ldap.py")
"ldap.py") self._auth_config = os.path.join(self._storage_directory.name, "ldap.yaml")
self._auth_config = os.path.join(self._storage_directory.name, script_file = os.path.join(PROJECT_DIR, "src", "auth", "reference_modules", "ldap.py")
"ldap.yaml")
script_file = os.path.join(PROJECT_DIR, "src", "auth",
"reference_modules", "ldap.py")
virtualenv_bin = os.path.join(SCRIPT_DIR, "ve3", "bin", "python3") virtualenv_bin = os.path.join(SCRIPT_DIR, "ve3", "bin", "python3")
with open(script_file) as fin: with open(script_file) as fin:
data = fin.read() data = fin.read()
data = data.replace("/usr/bin/python3", virtualenv_bin) data = data.replace("/usr/bin/python3", virtualenv_bin)
data = data.replace("/etc/memgraph/auth/ldap.yaml", data = data.replace("/etc/memgraph/auth/ldap.yaml", self._auth_config)
self._auth_config)
with open(self._auth_module, "w") as fout: with open(self._auth_module, "w") as fout:
fout.write(data) fout.write(data)
os.chmod(self._auth_module, stat.S_IRWXU | stat.S_IRWXG) os.chmod(self._auth_module, stat.S_IRWXU | stat.S_IRWXG)
@@ -106,10 +108,13 @@ class Memgraph:
} }
with open(self._auth_config, "w") as f: with open(self._auth_config, "w") as f:
f.write(CONFIG_TEMPLATE.format(**config)) f.write(CONFIG_TEMPLATE.format(**config))
args = [self._binary, args = [
"--data-directory", self._storage_directory.name, self._binary,
"--auth-module-executable", "--data-directory",
kwargs.pop("module_executable", self._auth_module)] self._storage_directory.name,
"--auth-module-executable",
kwargs.pop("module_executable", self._auth_module),
]
for key, value in kwargs.items(): for key, value in kwargs.items():
ldap_key = "--auth-module-" + key.replace("_", "-") ldap_key = "--auth-module-" + key.replace("_", "-")
if isinstance(value, bool): if isinstance(value, bool):
@@ -119,8 +124,7 @@ class Memgraph:
args.append(value) args.append(value)
self._process = subprocess.Popen(args) self._process = subprocess.Popen(args)
time.sleep(0.1) time.sleep(0.1)
assert self._process.poll() is None, "Memgraph process died " \ assert self._process.poll() is None, "Memgraph process died " "prematurely!"
"prematurely!"
wait_for_server(7687) wait_for_server(7687)
def stop(self, check=True): def stop(self, check=True):
@@ -137,8 +141,7 @@ class Memgraph:
def initialize_test(memgraph, tester_binary, **kwargs): def initialize_test(memgraph, tester_binary, **kwargs):
memgraph.start(module_executable="") memgraph.start(module_executable="")
execute_tester(tester_binary, execute_tester(tester_binary, ["CREATE USER root", "GRANT ALL PRIVILEGES TO root"])
["CREATE USER root", "GRANT ALL PRIVILEGES TO root"])
check_login = kwargs.pop("check_login", True) check_login = kwargs.pop("check_login", True)
memgraph.restart(**kwargs) memgraph.restart(**kwargs)
if check_login: if check_login:
@@ -170,18 +173,15 @@ def test_role_mapping(memgraph, tester_binary):
initialize_test(memgraph, tester_binary) initialize_test(memgraph, tester_binary)
execute_tester(tester_binary, [], "alice") execute_tester(tester_binary, [], "alice")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
query_should_fail=True)
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root") execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
execute_tester(tester_binary, [], "bob") execute_tester(tester_binary, [], "bob")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "bob", execute_tester(tester_binary, ["MATCH (n) RETURN n"], "bob", query_should_fail=True)
query_should_fail=True)
execute_tester(tester_binary, [], "carol") execute_tester(tester_binary, [], "carol")
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol", execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol", query_should_fail=True)
query_should_fail=True)
execute_tester(tester_binary, ["GRANT CREATE TO admin"], "root") execute_tester(tester_binary, ["GRANT CREATE TO admin"], "root")
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol") execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol")
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "dave") execute_tester(tester_binary, ["CREATE (n) RETURN n"], "dave")
@@ -192,15 +192,13 @@ def test_role_mapping(memgraph, tester_binary):
def test_role_removal(memgraph, tester_binary): def test_role_removal(memgraph, tester_binary):
initialize_test(memgraph, tester_binary) initialize_test(memgraph, tester_binary)
execute_tester(tester_binary, [], "alice") execute_tester(tester_binary, [], "alice")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
query_should_fail=True)
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root") execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
memgraph.restart(manage_roles=False) memgraph.restart(manage_roles=False)
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
execute_tester(tester_binary, ["CLEAR ROLE FOR alice"], "root") execute_tester(tester_binary, ["CLEAR ROLE FOR alice"], "root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
query_should_fail=True)
memgraph.stop() memgraph.stop()
@@ -229,28 +227,22 @@ def test_user_is_role(memgraph, tester_binary):
def test_user_permissions_persistancy(memgraph, tester_binary): def test_user_permissions_persistancy(memgraph, tester_binary):
initialize_test(memgraph, tester_binary) initialize_test(memgraph, tester_binary)
execute_tester(tester_binary, execute_tester(tester_binary, ["CREATE USER alice", "GRANT MATCH TO alice"], "root")
["CREATE USER alice", "GRANT MATCH TO alice"], "root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
memgraph.stop() memgraph.stop()
def test_role_permissions_persistancy(memgraph, tester_binary): def test_role_permissions_persistancy(memgraph, tester_binary):
initialize_test(memgraph, tester_binary) initialize_test(memgraph, tester_binary)
execute_tester(tester_binary, execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
"root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
memgraph.stop() memgraph.stop()
def test_only_authentication(memgraph, tester_binary): def test_only_authentication(memgraph, tester_binary):
initialize_test(memgraph, tester_binary, manage_roles=False) initialize_test(memgraph, tester_binary, manage_roles=False)
execute_tester(tester_binary, execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
["CREATE ROLE moderator", "GRANT MATCH TO moderator"], execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
"root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
query_should_fail=True)
memgraph.stop() memgraph.stop()
@@ -267,22 +259,16 @@ def test_wrong_suffix(memgraph, tester_binary):
def test_suffix_with_spaces(memgraph, tester_binary): def test_suffix_with_spaces(memgraph, tester_binary):
initialize_test(memgraph, tester_binary, initialize_test(memgraph, tester_binary, suffix=", ou= people, dc = memgraph, dc = com")
suffix=", ou= people, dc = memgraph, dc = com") execute_tester(tester_binary, ["CREATE USER alice", "GRANT MATCH TO alice"], "root")
execute_tester(tester_binary,
["CREATE USER alice", "GRANT MATCH TO alice"], "root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
memgraph.stop() memgraph.stop()
def test_role_mapping_wrong_root_dn(memgraph, tester_binary): def test_role_mapping_wrong_root_dn(memgraph, tester_binary):
initialize_test(memgraph, tester_binary, initialize_test(memgraph, tester_binary, root_dn="ou=invalid,dc=memgraph,dc=com")
root_dn="ou=invalid,dc=memgraph,dc=com") execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
execute_tester(tester_binary, execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
"root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
query_should_fail=True)
memgraph.restart() memgraph.restart()
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
memgraph.stop() memgraph.stop()
@@ -290,11 +276,8 @@ def test_role_mapping_wrong_root_dn(memgraph, tester_binary):
def test_role_mapping_wrong_root_objectclass(memgraph, tester_binary): def test_role_mapping_wrong_root_objectclass(memgraph, tester_binary):
initialize_test(memgraph, tester_binary, root_objectclass="person") initialize_test(memgraph, tester_binary, root_objectclass="person")
execute_tester(tester_binary, execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
["CREATE ROLE moderator", "GRANT MATCH TO moderator"], execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
"root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
query_should_fail=True)
memgraph.restart() memgraph.restart()
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
memgraph.stop() memgraph.stop()
@@ -302,11 +285,8 @@ def test_role_mapping_wrong_root_objectclass(memgraph, tester_binary):
def test_role_mapping_wrong_user_attribute(memgraph, tester_binary): def test_role_mapping_wrong_user_attribute(memgraph, tester_binary):
initialize_test(memgraph, tester_binary, user_attribute="cn") initialize_test(memgraph, tester_binary, user_attribute="cn")
execute_tester(tester_binary, execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
["CREATE ROLE moderator", "GRANT MATCH TO moderator"], execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
"root")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
query_should_fail=True)
memgraph.restart() memgraph.restart()
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
memgraph.stop() memgraph.stop()
@@ -314,8 +294,7 @@ def test_role_mapping_wrong_user_attribute(memgraph, tester_binary):
def test_wrong_password(memgraph, tester_binary): def test_wrong_password(memgraph, tester_binary):
initialize_test(memgraph, tester_binary) initialize_test(memgraph, tester_binary)
execute_tester(tester_binary, [], "root", password="sudo", execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
auth_should_fail=True)
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root") execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
memgraph.stop() memgraph.stop()
@@ -326,12 +305,10 @@ def test_password_persistancy(memgraph, tester_binary):
execute_tester(tester_binary, ["SHOW USERS"], "root", password="sudo") execute_tester(tester_binary, ["SHOW USERS"], "root", password="sudo")
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root") execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
memgraph.restart() memgraph.restart()
execute_tester(tester_binary, [], "root", password="sudo", execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
auth_should_fail=True)
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root") execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
memgraph.restart(module_executable="") memgraph.restart(module_executable="")
execute_tester(tester_binary, [], "root", password="sudo", execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
auth_should_fail=True)
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root") execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
memgraph.stop() memgraph.stop()
@@ -339,33 +316,25 @@ def test_password_persistancy(memgraph, tester_binary):
def test_user_multiple_roles(memgraph, tester_binary): def test_user_multiple_roles(memgraph, tester_binary):
initialize_test(memgraph, tester_binary, check_login=False) initialize_test(memgraph, tester_binary, check_login=False)
memgraph.restart() memgraph.restart()
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
query_should_fail=True) execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
query_should_fail=True)
memgraph.restart(manage_roles=False) memgraph.restart(manage_roles=False)
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
query_should_fail=True) execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
query_should_fail=True)
memgraph.restart(manage_roles=False, root_dn="") memgraph.restart(manage_roles=False, root_dn="")
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
query_should_fail=True) execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
query_should_fail=True)
memgraph.stop() memgraph.stop()
def test_starttls_failure(memgraph, tester_binary): def test_starttls_failure(memgraph, tester_binary):
initialize_test(memgraph, tester_binary, encryption="starttls", initialize_test(memgraph, tester_binary, encryption="starttls", check_login=False)
check_login=False)
execute_tester(tester_binary, [], "root", auth_should_fail=True) execute_tester(tester_binary, [], "root", auth_should_fail=True)
memgraph.stop() memgraph.stop()
def test_ssl_failure(memgraph, tester_binary): def test_ssl_failure(memgraph, tester_binary):
initialize_test(memgraph, tester_binary, encryption="ssl", initialize_test(memgraph, tester_binary, encryption="ssl", check_login=False)
check_login=False)
execute_tester(tester_binary, [], "root", auth_should_fail=True) execute_tester(tester_binary, [], "root", auth_should_fail=True)
memgraph.stop() memgraph.stop()
@@ -375,22 +344,25 @@ def test_ssl_failure(memgraph, tester_binary):
if __name__ == "__main__": if __name__ == "__main__":
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph") memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "ldap", "tester")
"integration", "ldap", "tester")
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--memgraph", default=memgraph_binary) parser.add_argument("--memgraph", default=memgraph_binary)
parser.add_argument("--tester", default=tester_binary) parser.add_argument("--tester", default=tester_binary)
parser.add_argument("--openldap-dir", parser.add_argument("--openldap-dir", default=os.path.join(SCRIPT_DIR, "openldap-2.4.47"))
default=os.path.join(SCRIPT_DIR, "openldap-2.4.47"))
args = parser.parse_args() args = parser.parse_args()
# Setup Memgraph handler # Setup Memgraph handler
memgraph = Memgraph(args.memgraph) memgraph = Memgraph(args.memgraph)
# Start the slapd binary # Start the slapd binary
slapd_args = [os.path.join(args.openldap_dir, "exe", "libexec", "slapd"), slapd_args = [
"-h", "ldap://127.0.0.1:1389/", "-d", "0"] os.path.join(args.openldap_dir, "exe", "libexec", "slapd"),
"-h",
"ldap://127.0.0.1:1389/",
"-d",
"0",
]
slapd = subprocess.Popen(slapd_args) slapd = subprocess.Popen(slapd_args)
time.sleep(0.1) time.sleep(0.1)
assert slapd.poll() is None, "slapd process died prematurely!" assert slapd.poll() is None, "slapd process died prematurely!"
@@ -409,8 +381,7 @@ if __name__ == "__main__":
if slapd_stat != 0: if slapd_stat != 0:
print("slapd process didn't exit cleanly!") print("slapd process didn't exit cleanly!")
assert mg_stat == 0 and slapd_stat == 0, "Some of the processes " \ assert mg_stat == 0 and slapd_stat == 0, "Some of the processes " "(memgraph, slapd) crashed!"
"(memgraph, slapd) crashed!"
# Execute tests # Execute tests
names = sorted(globals().keys()) names = sorted(globals().keys())

View File

@@ -46,17 +46,18 @@ def list_to_string(data):
def verify_lifetime(memgraph_binary, mg_import_csv_binary): def verify_lifetime(memgraph_binary, mg_import_csv_binary):
print("\033[1;36m~~ Verifying that mg_import_csv can't be started while " print("\033[1;36m~~ Verifying that mg_import_csv can't be started while " "memgraph is running ~~\033[0m")
"memgraph is running ~~\033[0m")
storage_directory = tempfile.TemporaryDirectory() storage_directory = tempfile.TemporaryDirectory()
# Generate common args # Generate common args
common_args = ["--data-directory", storage_directory.name, common_args = [
"--storage-properties-on-edges=false"] "--data-directory",
storage_directory.name,
"--storage-properties-on-edges=false",
]
# Start the memgraph binary # Start the memgraph binary
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + \ memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + common_args
common_args
memgraph = subprocess.Popen(list(map(str, memgraph_args))) memgraph = subprocess.Popen(list(map(str, memgraph_args)))
time.sleep(0.1) time.sleep(0.1)
assert memgraph.poll() is None, "Memgraph process died prematurely!" assert memgraph.poll() is None, "Memgraph process died prematurely!"
@@ -70,14 +71,12 @@ def verify_lifetime(memgraph_binary, mg_import_csv_binary):
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!" assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
# Execute mg_import_csv. # Execute mg_import_csv.
mg_import_csv_args = [mg_import_csv_binary, "--nodes", "/dev/null"] + \ mg_import_csv_args = [mg_import_csv_binary, "--nodes", "/dev/null"] + common_args
common_args
ret = subprocess.run(mg_import_csv_args) ret = subprocess.run(mg_import_csv_args)
# Check the return code # Check the return code
if ret.returncode == 0: if ret.returncode == 0:
raise Exception( raise Exception("The importer was able to run while memgraph was running!")
"The importer was able to run while memgraph was running!")
# Shutdown the memgraph binary # Shutdown the memgraph binary
memgraph.terminate() memgraph.terminate()
@@ -86,27 +85,34 @@ def verify_lifetime(memgraph_binary, mg_import_csv_binary):
print("\033[1;32m~~ Test successful ~~\033[0m\n") print("\033[1;32m~~ Test successful ~~\033[0m\n")
def execute_test(name, test_path, test_config, memgraph_binary, def execute_test(
mg_import_csv_binary, tester_binary, write_expected): name,
test_path,
test_config,
memgraph_binary,
mg_import_csv_binary,
tester_binary,
write_expected,
):
print("\033[1;36m~~ Executing test", name, "~~\033[0m") print("\033[1;36m~~ Executing test", name, "~~\033[0m")
storage_directory = tempfile.TemporaryDirectory() storage_directory = tempfile.TemporaryDirectory()
# Verify test configuration # Verify test configuration
if ("import_should_fail" not in test_config and if ("import_should_fail" not in test_config and "expected" not in test_config) or (
"expected" not in test_config) or \ "import_should_fail" in test_config and "expected" in test_config
("import_should_fail" in test_config and ):
"expected" in test_config): raise Exception("The test should specify either 'import_should_fail' " "or 'expected'!")
raise Exception("The test should specify either 'import_should_fail' "
"or 'expected'!")
expected_path = test_config.pop("expected", "") expected_path = test_config.pop("expected", "")
import_should_fail = test_config.pop("import_should_fail", False) import_should_fail = test_config.pop("import_should_fail", False)
# Generate common args # Generate common args
properties_on_edges = bool(test_config.pop("properties_on_edges", False)) properties_on_edges = bool(test_config.pop("properties_on_edges", False))
common_args = ["--data-directory", storage_directory.name, common_args = [
"--storage-properties-on-edges=" + "--data-directory",
str(properties_on_edges).lower()] storage_directory.name,
"--storage-properties-on-edges=" + str(properties_on_edges).lower(),
]
# Generate mg_import_csv args using flags specified in the test # Generate mg_import_csv args using flags specified in the test
mg_import_csv_args = [mg_import_csv_binary] + common_args mg_import_csv_args = [mg_import_csv_binary] + common_args
@@ -125,19 +131,16 @@ def execute_test(name, test_path, test_config, memgraph_binary,
if import_should_fail: if import_should_fail:
if ret.returncode == 0: if ret.returncode == 0:
raise Exception("The import should have failed, but it " raise Exception("The import should have failed, but it " "succeeded instead!")
"succeeded instead!")
else: else:
print("\033[1;32m~~ Test successful ~~\033[0m\n") print("\033[1;32m~~ Test successful ~~\033[0m\n")
return return
else: else:
if ret.returncode != 0: if ret.returncode != 0:
raise Exception("The import should have succeeded, but it " raise Exception("The import should have succeeded, but it " "failed instead!")
"failed instead!")
# Start the memgraph binary # Start the memgraph binary
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + \ memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + common_args
common_args
memgraph = subprocess.Popen(list(map(str, memgraph_args))) memgraph = subprocess.Popen(list(map(str, memgraph_args)))
time.sleep(0.1) time.sleep(0.1)
assert memgraph.poll() is None, "Memgraph process died prematurely!" assert memgraph.poll() is None, "Memgraph process died prematurely!"
@@ -151,17 +154,17 @@ def execute_test(name, test_path, test_config, memgraph_binary,
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!" assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
# Get the contents of the database # Get the contents of the database
queries_got = extract_rows(subprocess.run( queries_got = extract_rows(
[tester_binary], stdout=subprocess.PIPE, subprocess.run([tester_binary], stdout=subprocess.PIPE, check=True).stdout.decode("utf-8")
check=True).stdout.decode("utf-8")) )
# Shutdown the memgraph binary # Shutdown the memgraph binary
memgraph.terminate() memgraph.terminate()
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!" assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
if write_expected: if write_expected:
with open(os.path.join(test_path, expected_path), 'w') as expected: with open(os.path.join(test_path, expected_path), "w") as expected:
expected.write('\n'.join(queries_got)) expected.write("\n".join(queries_got))
else: else:
if expected_path: if expected_path:
@@ -173,18 +176,16 @@ def execute_test(name, test_path, test_config, memgraph_binary,
# Verify the queries # Verify the queries
queries_expected.sort() queries_expected.sort()
queries_got.sort() queries_got.sort()
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" \ assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" "{}".format(
"{}".format(list_to_string(queries_got), list_to_string(queries_got), list_to_string(queries_expected)
list_to_string(queries_expected)) )
print("\033[1;32m~~ Test successful ~~\033[0m\n") print("\033[1;32m~~ Test successful ~~\033[0m\n")
if __name__ == "__main__": if __name__ == "__main__":
memgraph_binary = os.path.join(BUILD_DIR, "memgraph") memgraph_binary = os.path.join(BUILD_DIR, "memgraph")
mg_import_csv_binary = os.path.join( mg_import_csv_binary = os.path.join(BUILD_DIR, "src", "mg_import_csv")
BUILD_DIR, "src", "mg_import_csv") tester_binary = os.path.join(BUILD_DIR, "tests", "integration", "mg_import_csv", "tester")
tester_binary = os.path.join(
BUILD_DIR, "tests", "integration", "mg_import_csv", "tester")
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--memgraph", default=memgraph_binary) parser.add_argument("--memgraph", default=memgraph_binary)
@@ -193,7 +194,8 @@ if __name__ == "__main__":
parser.add_argument( parser.add_argument(
"--write-expected", "--write-expected",
action="store_true", action="store_true",
help="Overwrite the expected values with the results of the current run") help="Overwrite the expected values with the results of the current run",
)
args = parser.parse_args() args = parser.parse_args()
# First test whether the CSV importer can be started while the main # First test whether the CSV importer can be started while the main
@@ -211,7 +213,14 @@ if __name__ == "__main__":
testcases = yaml.safe_load(f) testcases = yaml.safe_load(f)
for test_config in testcases: for test_config in testcases:
test_name = name + "/" + test_config.pop("name") test_name = name + "/" + test_config.pop("name")
execute_test(test_name, test_path, test_config, args.memgraph, execute_test(
args.mg_import_csv, args.tester, args.write_expected) test_name,
test_path,
test_config,
args.memgraph,
args.mg_import_csv,
args.tester,
args.write_expected,
)
sys.exit(0) sys.exit(0)

View File

@@ -36,8 +36,7 @@ def execute_test(**kwargs):
timeout = duration * 2 if "hang" not in kwargs else duration * 2 + 60 timeout = duration * 2 if "hang" not in kwargs else duration * 2 + 60
success = False success = False
server_args = [server_binary, "--interval", interval, server_args = [server_binary, "--interval", interval, "--duration", duration]
"--duration", duration]
for flag, value in kwargs.items(): for flag, value in kwargs.items():
flag = "--" + flag.replace("_", "-") flag = "--" + flag.replace("_", "-")
# We handle boolean flags here. The type of value must be `bool`, and # We handle boolean flags here. The type of value must be `bool`, and
@@ -48,9 +47,15 @@ def execute_test(**kwargs):
else: else:
server_args.extend([flag, value]) server_args.extend([flag, value])
client_args = [client_binary, "--interval", interval, client_args = [
"--duration", duration, client_binary,
"--storage-directory", storage_directory] "--interval",
interval,
"--duration",
duration,
"--storage-directory",
storage_directory,
]
if endpoint: if endpoint:
client_args.extend(["--endpoint", endpoint]) client_args.extend(["--endpoint", endpoint])
@@ -61,8 +66,7 @@ def execute_test(**kwargs):
assert server.poll() is None, "Server process died prematurely!" assert server.poll() is None, "Server process died prematurely!"
try: try:
subprocess.run(list(map(str, client_args)), timeout=timeout, subprocess.run(list(map(str, client_args)), timeout=timeout, check=True)
check=True)
finally: finally:
if server is None: if server is None:
success = True success = True
@@ -88,16 +92,14 @@ TESTS = [
{"endpoint": "http://127.0.0.1:9000/nonexistant/", "no_check": True}, {"endpoint": "http://127.0.0.1:9000/nonexistant/", "no_check": True},
{"start_server": False}, {"start_server": False},
{"startups": 4, "no_check_duration": True}, # the last 3 tests failed {"startups": 4, "no_check_duration": True}, # the last 3 tests failed
# to send any data + this test # to send any data + this test
{"add_garbage": True} {"add_garbage": True},
] ]
if __name__ == "__main__": if __name__ == "__main__":
server_binary = os.path.join(SCRIPT_DIR, "server.py") server_binary = os.path.join(SCRIPT_DIR, "server.py")
client_binary = os.path.join(PROJECT_DIR, "build", "tests", client_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "telemetry", "client")
"integration", "telemetry", "client") kvstore_console_binary = os.path.join(PROJECT_DIR, "build", "tests", "manual", "kvstore_console")
kvstore_console_binary = os.path.join(PROJECT_DIR, "build", "tests",
"manual", "kvstore_console")
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--client", default=client_binary) parser.add_argument("--client", default=client_binary)
@@ -108,19 +110,23 @@ if __name__ == "__main__":
storage = tempfile.TemporaryDirectory() storage = tempfile.TemporaryDirectory()
for test in TESTS: for test in TESTS:
print("\033[1;36m~~ Executing test with arguments:", print(
json.dumps(test, sort_keys=True), "~~\033[0m") "\033[1;36m~~ Executing test with arguments:",
json.dumps(test, sort_keys=True),
"~~\033[0m",
)
if test.pop("add_garbage", False): if test.pop("add_garbage", False):
proc = subprocess.Popen([args.kvstore_console, "--path", proc = subprocess.Popen(
storage.name], stdin=subprocess.PIPE, [args.kvstore_console, "--path", storage.name],
stdout=subprocess.DEVNULL) stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
)
proc.communicate("put garbage garbage".encode("utf-8")) proc.communicate("put garbage garbage".encode("utf-8"))
assert proc.wait() == 0 assert proc.wait() == 0
try: try:
success = execute_test(client=args.client, server=args.server, success = execute_test(client=args.client, server=args.server, storage=storage.name, **test)
storage=storage.name, **test)
except Exception as e: except Exception as e:
print("\033[1;33m", e, "\033[0m", sep="") print("\033[1;33m", e, "\033[0m", sep="")
success = False success = False

View File

@@ -46,7 +46,7 @@ def build_handler(storage, args):
assert self.headers["accept"] == "application/json" assert self.headers["accept"] == "application/json"
assert self.headers["content-type"] == "application/json" assert self.headers["content-type"] == "application/json"
content_len = int(self.headers.get('content-length', 0)) content_len = int(self.headers.get("content-length", 0))
data = json.loads(self.rfile.read(content_len).decode("utf-8")) data = json.loads(self.rfile.read(content_len).decode("utf-8"))
if self.path not in [args.path, args.redirect_path]: if self.path not in [args.path, args.redirect_path]:
@@ -195,4 +195,4 @@ if __name__ == "__main__":
verify_storage(startup, args) verify_storage(startup, args)
# machine id has to be same for every run on the same machine # machine id has to be same for every run on the same machine
assert len(set(map(lambda x: x['machine_id'], itertools.chain(*startups)))) == 1 assert len(set(map(lambda x: x["machine_id"], itertools.chain(*startups)))) == 1

View File

@@ -34,7 +34,8 @@ class QueryClient:
self.default_num_workers = default_num_workers self.default_num_workers = default_num_workers
def __call__(self, queries, database, num_workers=None): def __call__(self, queries, database, num_workers=None):
if num_workers is None: num_workers = self.default_num_workers if num_workers is None:
num_workers = self.default_num_workers
self.log.debug("execute('%s')", str(queries)) self.log.debug("execute('%s')", str(queries))
client_path = "tests/macro_benchmark/query_client" client_path = "tests/macro_benchmark/query_client"
@@ -53,29 +54,36 @@ class QueryClient:
output_fd, output = tempfile.mkstemp() output_fd, output = tempfile.mkstemp()
os.close(output_fd) os.close(output_fd)
client_args = ["--port", database.args.port, client_args = [
"--num-workers", str(num_workers), "--port",
"--output", output] database.args.port,
"--num-workers",
str(num_workers),
"--output",
output,
]
cpu_time_start = database.database_bin.get_usage()["cpu"] cpu_time_start = database.database_bin.get_usage()["cpu"]
# TODO make the timeout configurable per query or something # TODO make the timeout configurable per query or something
return_code = self.client.run_and_wait( return_code = self.client.run_and_wait(client, client_args, timeout=600, stdin=queries_path)
client, client_args, timeout=600, stdin=queries_path)
usage = database.database_bin.get_usage() usage = database.database_bin.get_usage()
cpu_time_end = usage["cpu"] cpu_time_end = usage["cpu"]
os.remove(queries_path) os.remove(queries_path)
if return_code != 0: if return_code != 0:
with open(self.client.get_stderr()) as f: with open(self.client.get_stderr()) as f:
stderr = f.read() stderr = f.read()
self.log.error("Error while executing queries '%s'. " self.log.error(
"Failed with return_code %d and stderr:\n%s", "Error while executing queries '%s'. " "Failed with return_code %d and stderr:\n%s",
str(queries), return_code, stderr) str(queries),
return_code,
stderr,
)
raise Exception("BoltClient execution failed") raise Exception("BoltClient execution failed")
data = {"groups" : []} data = {"groups": []}
with open(output) as f: with open(output) as f:
for line in f: for line in f:
data["groups"].append(json.loads(line)) data["groups"].append(json.loads(line))
data[CPU_TIME] = cpu_time_end - cpu_time_start data[CPU_TIME] = cpu_time_end - cpu_time_start
data[MAX_MEMORY] = usage["max_memory"] data[MAX_MEMORY] = usage["max_memory"]
@@ -94,7 +102,8 @@ class LongRunningClient:
# TODO: This is quite similar to __call__ method of QueryClient. Remove # TODO: This is quite similar to __call__ method of QueryClient. Remove
# duplication. # duplication.
def __call__(self, config, database, duration, client, num_workers=None): def __call__(self, config, database, duration, client, num_workers=None):
if num_workers is None: num_workers = self.default_num_workers if num_workers is None:
num_workers = self.default_num_workers
self.log.debug("execute('%s')", config) self.log.debug("execute('%s')", config)
client_path = "tests/macro_benchmark/{}".format(client) client_path = "tests/macro_benchmark/{}".format(client)
@@ -113,32 +122,41 @@ class LongRunningClient:
output_fd, output = tempfile.mkstemp() output_fd, output = tempfile.mkstemp()
os.close(output_fd) os.close(output_fd)
client_args = ["--port", database.args.port, client_args = [
"--num-workers", str(num_workers), "--port",
"--output", output, database.args.port,
"--duration", str(duration), "--num-workers",
"--db", database.name, str(num_workers),
"--scenario", self.workload] "--output",
output,
"--duration",
str(duration),
"--db",
database.name,
"--scenario",
self.workload,
]
return_code = self.client.run_and_wait( return_code = self.client.run_and_wait(client, client_args, timeout=600, stdin=config_path)
client, client_args, timeout=600, stdin=config_path)
os.remove(config_path) os.remove(config_path)
if return_code != 0: if return_code != 0:
with open(self.client.get_stderr()) as f: with open(self.client.get_stderr()) as f:
stderr = f.read() stderr = f.read()
self.log.error("Error while executing config '%s'. " self.log.error(
"Failed with return_code %d and stderr:\n%s", "Error while executing config '%s'. " "Failed with return_code %d and stderr:\n%s",
str(config), return_code, stderr) str(config),
return_code,
stderr,
)
raise Exception("BoltClient execution failed") raise Exception("BoltClient execution failed")
# TODO: We shouldn't wait for process to finish to start reading output. # TODO: We shouldn't wait for process to finish to start reading output.
# We should implement periodic reading of data and stream data when it # We should implement periodic reading of data and stream data when it
# becomes available. # becomes available.
data = [] data = []
with open(output) as f: with open(output) as f:
for line in f: for line in f:
data.append(json.loads(line)) data.append(json.loads(line))
os.remove(output) os.remove(output)
return data return data

View File

@@ -14,9 +14,11 @@ from argparse import ArgumentParser
try: try:
import jail import jail
APOLLO = True APOLLO = True
except: except:
import jail_faker as jail import jail_faker as jail
APOLLO = False APOLLO = False
@@ -45,13 +47,15 @@ def get_absolute_path(path, base=""):
def set_cpus(flag_name, process, args): def set_cpus(flag_name, process, args):
argp = ArgumentParser() argp = ArgumentParser()
# named, optional arguments # named, optional arguments
argp.add_argument("--" + flag_name, nargs="+", type=int, help="cpus that " argp.add_argument(
"will be used by process. Obligatory on Apollo, ignored " "--" + flag_name,
"otherwise.") nargs="+",
type=int,
help="cpus that " "will be used by process. Obligatory on Apollo, ignored " "otherwise.",
)
args, _ = argp.parse_known_args(args) args, _ = argp.parse_known_args(args)
attr_flag_name = flag_name.replace("-", "_") attr_flag_name = flag_name.replace("-", "_")
cpus = getattr(args, attr_flag_name) cpus = getattr(args, attr_flag_name)
assert not APOLLO or cpus, \ assert not APOLLO or cpus, "flag --{} is obligatory on Apollo".format(flag_name)
"flag --{} is obligatory on Apollo".format(flag_name)
if cpus: if cpus:
process.set_cpus(cpus, hyper = False) process.set_cpus(cpus, hyper=False)

View File

@@ -36,13 +36,12 @@ class Memgraph:
""" """
Knows how to start and stop memgraph. Knows how to start and stop memgraph.
""" """
def __init__(self, args, num_workers): def __init__(self, args, num_workers):
self.log = logging.getLogger("MemgraphRunner") self.log = logging.getLogger("MemgraphRunner")
argp = ArgumentParser("MemgraphArgumentParser") argp = ArgumentParser("MemgraphArgumentParser")
argp.add_argument("--runner-bin", argp.add_argument("--runner-bin", default=get_absolute_path("memgraph", "build"))
default=get_absolute_path("memgraph", "build")) argp.add_argument("--port", default="7687", help="Database and client port")
argp.add_argument("--port", default="7687",
help="Database and client port")
argp.add_argument("--data-directory", default=None) argp.add_argument("--data-directory", default=None)
argp.add_argument("--storage-snapshot-on-exit", action="store_true") argp.add_argument("--storage-snapshot-on-exit", action="store_true")
argp.add_argument("--storage-recover-on-startup", action="store_true") argp.add_argument("--storage-recover-on-startup", action="store_true")
@@ -55,8 +54,12 @@ class Memgraph:
def start(self): def start(self):
self.log.info("start") self.log.info("start")
database_args = ["--bolt-port", self.args.port, database_args = [
"--query-execution-timeout-sec", "0"] "--bolt-port",
self.args.port,
"--query-execution-timeout-sec",
"0",
]
if self.num_workers: if self.num_workers:
database_args += ["--bolt-num-workers", str(self.num_workers)] database_args += ["--bolt-num-workers", str(self.num_workers)]
if self.args.data_directory: if self.args.data_directory:
@@ -82,15 +85,13 @@ class Neo:
""" """
Knows how to start and stop neo4j. Knows how to start and stop neo4j.
""" """
def __init__(self, args, config): def __init__(self, args, config):
self.log = logging.getLogger("NeoRunner") self.log = logging.getLogger("NeoRunner")
argp = ArgumentParser("NeoArgumentParser") argp = ArgumentParser("NeoArgumentParser")
argp.add_argument("--runner-bin", default=get_absolute_path( argp.add_argument("--runner-bin", default=get_absolute_path("neo4j/bin/neo4j", "libs"))
"neo4j/bin/neo4j", "libs")) argp.add_argument("--port", default="7687", help="Database and client port")
argp.add_argument("--port", default="7687", argp.add_argument("--http-port", default="7474", help="Database and client port")
help="Database and client port")
argp.add_argument("--http-port", default="7474",
help="Database and client port")
self.log.info("Initializing Runner with arguments %r", args) self.log.info("Initializing Runner with arguments %r", args)
self.args, _ = argp.parse_known_args(args) self.args, _ = argp.parse_known_args(args)
self.config = config self.config = config
@@ -105,24 +106,23 @@ class Neo:
self.neo4j_home_path = tempfile.mkdtemp(dir="/dev/shm") self.neo4j_home_path = tempfile.mkdtemp(dir="/dev/shm")
try: try:
os.symlink(os.path.join(get_absolute_path("neo4j", "libs"), "lib"), os.symlink(
os.path.join(self.neo4j_home_path, "lib")) os.path.join(get_absolute_path("neo4j", "libs"), "lib"),
os.path.join(self.neo4j_home_path, "lib"),
)
neo4j_conf_dir = os.path.join(self.neo4j_home_path, "conf") neo4j_conf_dir = os.path.join(self.neo4j_home_path, "conf")
neo4j_conf_file = os.path.join(neo4j_conf_dir, "neo4j.conf") neo4j_conf_file = os.path.join(neo4j_conf_dir, "neo4j.conf")
os.mkdir(neo4j_conf_dir) os.mkdir(neo4j_conf_dir)
shutil.copyfile(self.config, neo4j_conf_file) shutil.copyfile(self.config, neo4j_conf_file)
with open(neo4j_conf_file, "a") as f: with open(neo4j_conf_file, "a") as f:
f.write("\ndbms.connector.bolt.listen_address=:" + f.write("\ndbms.connector.bolt.listen_address=:" + self.args.port + "\n")
self.args.port + "\n") f.write("\ndbms.connector.http.listen_address=:" + self.args.http_port + "\n")
f.write("\ndbms.connector.http.listen_address=:" +
self.args.http_port + "\n")
# environment # environment
cwd = os.path.dirname(self.args.runner_bin) cwd = os.path.dirname(self.args.runner_bin)
env = {"NEO4J_HOME": self.neo4j_home_path} env = {"NEO4J_HOME": self.neo4j_home_path}
self.database_bin.run(self.args.runner_bin, args=["console"], self.database_bin.run(self.args.runner_bin, args=["console"], env=env, timeout=600, cwd=cwd)
env=env, timeout=600, cwd=cwd)
except: except:
shutil.rmtree(self.neo4j_home_path) shutil.rmtree(self.neo4j_home_path)
raise Exception("Couldn't run Neo4j!") raise Exception("Couldn't run Neo4j!")

View File

@@ -9,4 +9,7 @@
# by the Apache License, Version 2.0, included in the file # by the Apache License, Version 2.0, included in the file
# licenses/APL.txt. # licenses/APL.txt.
print("""CREATE (:L1:L2:L3:L4:L5:L6:L7 {p1: true, p2: 42, p3: "Here is some text that is not extremely short", p4:"Short text", p5: 234.434, p6: 11.11, p7: false});""" * 1000) print(
"""CREATE (:L1:L2:L3:L4:L5:L6:L7 {p1: true, p2: 42, p3: "Here is some text that is not extremely short", p4:"Short text", p5: 234.434, p6: 11.11, p7: false});"""
* 1000
)

View File

@@ -15,6 +15,5 @@ VERTEX_COUNT = 100000
for i in range(VERTEX_COUNT): for i in range(VERTEX_COUNT):
print("CREATE (n%d {x: %d})" % (i, i)) print("CREATE (n%d {x: %d})" % (i, i))
# batch CREATEs because we can't execute all at once # batch CREATEs because we can't execute all at once
if (i != 0 and i % BATCH_SIZE == 0) or \ if (i != 0 and i % BATCH_SIZE == 0) or (i + 1 == VERTEX_COUNT):
(i + 1 == VERTEX_COUNT):
print(";") print(";")

View File

@@ -15,6 +15,5 @@ VERTEX_COUNT = 1000000
for i in range(VERTEX_COUNT): for i in range(VERTEX_COUNT):
print("CREATE (n%d {x: %d})" % (i, i)) print("CREATE (n%d {x: %d})" % (i, i))
# batch CREATEs because we can't execute all at once # batch CREATEs because we can't execute all at once
if (i != 0 and i % BATCH_SIZE == 0) or \ if (i != 0 and i % BATCH_SIZE == 0) or (i + 1 == VERTEX_COUNT):
(i + 1 == VERTEX_COUNT):
print(";") print(";")

View File

@@ -19,8 +19,9 @@ random.seed(1)
for i in range(common.BFS_ITERS): for i in range(common.BFS_ITERS):
a = int(random.random() * common.VERTEX_COUNT) a = int(random.random() * common.VERTEX_COUNT)
b = int(random.random() * common.VERTEX_COUNT) b = int(random.random() * common.VERTEX_COUNT)
print("MATCH (from: Node {id: %d}) WITH from " print(
"MATCH (to: Node {id: %d}) WITH to " "MATCH (from: Node {id: %d}) WITH from "
"MATCH path = (from)-[*bfs..%d (e, n | true)]->(to) WITH path " "MATCH (to: Node {id: %d}) WITH to "
"LIMIT 10 RETURN 0;" "MATCH path = (from)-[*bfs..%d (e, n | true)]->(to) WITH path "
% (a, b, common.PATH_LENGTH)) "LIMIT 10 RETURN 0;" % (a, b, common.PATH_LENGTH)
)

View File

@@ -13,4 +13,3 @@ VERTEX_COUNT = 1000
SPARSE_FACTOR = 10 SPARSE_FACTOR = 10
BFS_ITERS = 50 BFS_ITERS = 50
PATH_LENGTH = 5000 PATH_LENGTH = 5000

View File

@@ -32,4 +32,3 @@ for i in range(common.VERTEX_COUNT * common.VERTEX_COUNT // common.SPARSE_FACTOR
a = int(random.random() * common.VERTEX_COUNT) a = int(random.random() * common.VERTEX_COUNT)
b = int(random.random() * common.VERTEX_COUNT) b = int(random.random() * common.VERTEX_COUNT)
print("MATCH (a: Node {id: %d}), (b: Node {id: %d}) CREATE (a)-[:Friend]->(b);" % (a, b)) print("MATCH (a: Node {id: %d}), (b: Node {id: %d}) CREATE (a)-[:Friend]->(b);" % (a, b))

View File

@@ -11,13 +11,10 @@
import random import random
def init_data(card_count, pos_count): def init_data(card_count, pos_count):
print("UNWIND range(0, {} - 1) AS id " print("UNWIND range(0, {} - 1) AS id " "CREATE (:Card {{id: id, compromised: false}});".format(card_count))
"CREATE (:Card {{id: id, compromised: false}});".format( print("UNWIND range(0, {} - 1) AS id " "CREATE (:Pos {{id: id, compromised: false}});".format(pos_count))
card_count))
print("UNWIND range(0, {} - 1) AS id "
"CREATE (:Pos {{id: id, compromised: false}});".format(
pos_count))
def compromise_pos_device(pos_id): def compromise_pos_device(pos_id):
@@ -34,20 +31,24 @@ def pump_transactions(card_count, pos_count, tx_count, report_pct):
# Card of the transaction gets compromised too. If the card # Card of the transaction gets compromised too. If the card
# is compromised, there is a 0.1 chance the transaction is # is compromised, there is a 0.1 chance the transaction is
# fraudulent and detected (regardless of POS). # fraudulent and detected (regardless of POS).
q = ("MATCH (c:Card {{id: {}}}), (p:Pos {{id: {}}}) " q = (
"CREATE (t:Transaction " "MATCH (c:Card {{id: {}}}), (p:Pos {{id: {}}}) "
"{{id: {}, fraud_reported: c.compromised AND (rand() < %f)}}) " "CREATE (t:Transaction "
"CREATE (c)<-[:Using]-(t)-[:At]->(p) " "{{id: {}, fraud_reported: c.compromised AND (rand() < %f)}}) "
"SET c.compromised = p.compromised;" % report_pct) "CREATE (c)<-[:Using]-(t)-[:At]->(p) "
"SET c.compromised = p.compromised;" % report_pct
)
def rint(max):
return random.randint(0, max - 1) # NOQA
def rint(max): return random.randint(0, max - 1) # NOQA
for i in range(tx_count): for i in range(tx_count):
print(q.format(rint(card_count), rint(pos_count), i)) print(q.format(rint(card_count), rint(pos_count), i))
POS_COUNT = 1000 POS_COUNT = 1000
CARD_COUNT = 10000 CARD_COUNT = 10000
FRAUD_POS_COUNT = 20 FRAUD_POS_COUNT = 20
TX_COUNT = 50000 TX_COUNT = 50000
REPORT_PCT = 0.1 REPORT_PCT = 0.1

View File

@@ -21,22 +21,21 @@ seed(0)
def create_vertices(vertex_count): def create_vertices(vertex_count):
for vertex in range(vertex_count): for vertex in range(vertex_count):
print("CREATE (:Label {id: %d})" % vertex) print("CREATE (:Label {id: %d})" % vertex)
if (vertex != 0 and vertex % BATCH_SIZE == 0) or \ if (vertex != 0 and vertex % BATCH_SIZE == 0) or (vertex + 1 == vertex_count):
(vertex + 1 == vertex_count):
print(";") print(";")
def create_edges(edge_count, vertex_count): def create_edges(edge_count, vertex_count):
""" vertex_count is the number of already existing vertices in graph """ """vertex_count is the number of already existing vertices in graph"""
matches = [] matches = []
merges = [] merges = []
for edge in range(edge_count): for edge in range(edge_count):
matches.append("MATCH (a%d :Label {id: %d}), (b%d :Label {id: %d})" % matches.append(
(edge, randint(0, vertex_count - 1), "MATCH (a%d :Label {id: %d}), (b%d :Label {id: %d})"
edge, randint(0, vertex_count - 1))) % (edge, randint(0, vertex_count - 1), edge, randint(0, vertex_count - 1))
)
merges.append("CREATE (a%d)-[:Type]->(b%d)" % (edge, edge)) merges.append("CREATE (a%d)-[:Type]->(b%d)" % (edge, edge))
if (edge != 0 and edge % BATCH_SIZE == 0) or \ if (edge != 0 and edge % BATCH_SIZE == 0) or ((edge + 1) == edge_count):
((edge + 1) == edge_count):
print(" ".join(matches + merges)) print(" ".join(matches + merges))
print(";") print(";")
matches = [] matches = []

View File

@@ -9,8 +9,10 @@
# by the Apache License, Version 2.0, included in the file # by the Apache License, Version 2.0, included in the file
# licenses/APL.txt. # licenses/APL.txt.
def generate(expressions, repetitions): def generate(expressions, repetitions):
idx = 0 idx = 0
def get_alias(): def get_alias():
nonlocal idx nonlocal idx
idx += 1 idx += 1

View File

@@ -11,11 +11,31 @@
import common import common
expressions = ['1 + 3', '2 - 1', '2 * 5', '5 / 2', '5 % 5', '-5' + '1.4 + 3.3', expressions = [
'6.2 - 5.4', '6.5 * 1.2', '6.6 / 1.2', '8.7 % 3.2', '-6.6', "1 + 3",
'"Flo" + "Lasta"', 'true AND false', 'true OR false', "2 - 1",
'true XOR false', 'NOT true', '1 < 2', '2 = 3', '6.66 < 10.2', "2 * 5",
'3.14 = 3.2', '"Ana" < "Ivana"', '"Ana" = "Mmmmm"', "5 / 2",
'Null < Null', 'Null = Null'] "5 % 5",
"-5" + "1.4 + 3.3",
"6.2 - 5.4",
"6.5 * 1.2",
"6.6 / 1.2",
"8.7 % 3.2",
"-6.6",
'"Flo" + "Lasta"',
"true AND false",
"true OR false",
"true XOR false",
"NOT true",
"1 < 2",
"2 = 3",
"6.66 < 10.2",
"3.14 = 3.2",
'"Ana" < "Ivana"',
'"Ana" = "Mmmmm"',
"Null < Null",
"Null = Null",
]
print(common.generate(expressions, 30)) print(common.generate(expressions, 30))

View File

@@ -18,9 +18,11 @@ from random import randint, seed
seed(0) seed(0)
def rint(upper_bound_exclusive): def rint(upper_bound_exclusive):
return randint(0, upper_bound_exclusive - 1) return randint(0, upper_bound_exclusive - 1)
VERTEX_COUNT = 1500 VERTEX_COUNT = 1500
EDGE_COUNT = VERTEX_COUNT * 15 EDGE_COUNT = VERTEX_COUNT * 15
@@ -28,7 +30,7 @@ EDGE_COUNT = VERTEX_COUNT * 15
LABEL_COUNT = 10 LABEL_COUNT = 10
MAX_LABELS = 5 # maximum number of labels in a vertex MAX_LABELS = 5 # maximum number of labels in a vertex
MAX_PROPS = 4 # maximum number of properties in a vertex/edge MAX_PROPS = 4 # maximum number of properties in a vertex/edge
MAX_PROP_VALUE = 1000 MAX_PROP_VALUE = 1000
# some consts used in mutiple files # some consts used in mutiple files
@@ -38,7 +40,6 @@ PROP_PREFIX = "Prop"
ID = "id" ID = "id"
def labels(): def labels():
labels = ":" + LABEL_INDEX labels = ":" + LABEL_INDEX
for _ in range(rint(MAX_LABELS)): for _ in range(rint(MAX_LABELS)):
@@ -47,12 +48,11 @@ def labels():
def properties(id): def properties(id):
""" Generates a properties string with [0, MAX_PROPS) properties. """Generates a properties string with [0, MAX_PROPS) properties.
Note that if PropX is generated, then all the PropY where Y < X Note that if PropX is generated, then all the PropY where Y < X
are generated. Thus most labels have Prop0, and least have PropMAX_PROPS. are generated. Thus most labels have Prop0, and least have PropMAX_PROPS.
""" """
props = {"%s%d" % (PROP_PREFIX, i): rint(MAX_PROP_VALUE) props = {"%s%d" % (PROP_PREFIX, i): rint(MAX_PROP_VALUE) for i in range(rint(MAX_PROPS))}
for i in range(rint(MAX_PROPS))}
props[ID] = id props[ID] = id
return "{" + ", ".join("%s: %s" % kv for kv in props.items()) + "}" return "{" + ", ".join("%s: %s" % kv for kv in props.items()) + "}"
@@ -74,21 +74,20 @@ def main():
# create vertices # create vertices
for vertex_index in range(VERTEX_COUNT): for vertex_index in range(VERTEX_COUNT):
print("CREATE %s" % vertex(vertex_index)) print("CREATE %s" % vertex(vertex_index))
if (vertex_index != 0 and vertex_index % BATCH_SIZE == 0) or \ if (vertex_index != 0 and vertex_index % BATCH_SIZE == 0) or vertex_index + 1 == VERTEX_COUNT:
vertex_index + 1 == VERTEX_COUNT:
print(";") print(";")
print("MATCH (n) RETURN assert(count(n) = %d);" % VERTEX_COUNT) print("MATCH (n) RETURN assert(count(n) = %d);" % VERTEX_COUNT)
# create edges stohastically # create edges stohastically
attempts = VERTEX_COUNT ** 2 attempts = VERTEX_COUNT**2
p = EDGE_COUNT / VERTEX_COUNT ** 2 p = EDGE_COUNT / VERTEX_COUNT**2
print("MATCH (a) WITH a MATCH (b) WITH a, b WHERE rand() < %f " print("MATCH (a) WITH a MATCH (b) WITH a, b WHERE rand() < %f " " CREATE (a)-[:EdgeType]->(b);" % p)
" CREATE (a)-[:EdgeType]->(b);" % p)
sigma = (attempts * p * (1 - p)) ** 0.5 sigma = (attempts * p * (1 - p)) ** 0.5
delta = 5 * sigma delta = 5 * sigma
print("MATCH (n)-[r]->() WITH count(r) AS c " print(
"RETURN assert(c >= %d AND c <= %d);" % ( "MATCH (n)-[r]->() WITH count(r) AS c "
EDGE_COUNT - delta, EDGE_COUNT + delta)) "RETURN assert(c >= %d AND c <= %d);" % (EDGE_COUNT - delta, EDGE_COUNT + delta)
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -11,6 +11,6 @@
from setup import LABEL_INDEX, ID, VERTEX_COUNT, rint from setup import LABEL_INDEX, ID, VERTEX_COUNT, rint
print("UNWIND range(0, 10000) AS i " print(
"MATCH (n:%s {%s: %d}) RETURN n SKIP 1000000" % ( "UNWIND range(0, 10000) AS i " "MATCH (n:%s {%s: %d}) RETURN n SKIP 1000000" % (LABEL_INDEX, ID, rint(VERTEX_COUNT))
LABEL_INDEX, ID, rint(VERTEX_COUNT))) )

View File

@@ -12,5 +12,4 @@
from setup import LABEL_COUNT, LABEL_PREFIX from setup import LABEL_COUNT, LABEL_PREFIX
for i in range(LABEL_COUNT): for i in range(LABEL_COUNT):
print("UNWIND range(0, 30) AS i MATCH (n:%s%d) " print("UNWIND range(0, 30) AS i MATCH (n:%s%d) " "RETURN n SKIP 1000000;" % (LABEL_PREFIX, i))
"RETURN n SKIP 1000000;" % (LABEL_PREFIX, i))

View File

@@ -9,8 +9,17 @@
# by the Apache License, Version 2.0, included in the file # by the Apache License, Version 2.0, included in the file
# licenses/APL.txt. # licenses/APL.txt.
from setup import LABEL_PREFIX, PROP_PREFIX, MAX_PROPS, MAX_PROP_VALUE, LABEL_COUNT, rint from setup import (
LABEL_PREFIX,
PROP_PREFIX,
MAX_PROPS,
MAX_PROP_VALUE,
LABEL_COUNT,
rint,
)
for i in range(LABEL_COUNT): for i in range(LABEL_COUNT):
print("UNWIND range(0, 50) AS i MATCH (n:%s%d {%s%d: %d}) RETURN n SKIP 10000;" % ( print(
LABEL_PREFIX, i, PROP_PREFIX, rint(MAX_PROPS), rint(MAX_PROP_VALUE))) "UNWIND range(0, 50) AS i MATCH (n:%s%d {%s%d: %d}) RETURN n SKIP 10000;"
% (LABEL_PREFIX, i, PROP_PREFIX, rint(MAX_PROPS), rint(MAX_PROP_VALUE))
)

View File

@@ -11,5 +11,7 @@
from setup import PROP_PREFIX, MAX_PROPS, rint, MAX_PROP_VALUE from setup import PROP_PREFIX, MAX_PROPS, rint, MAX_PROP_VALUE
print("UNWIND range(0, 50) AS i MATCH (n {%s%d: %d}) RETURN n SKIP 10000" % ( print(
PROP_PREFIX, rint(MAX_PROPS), rint(MAX_PROP_VALUE))) "UNWIND range(0, 50) AS i MATCH (n {%s%d: %d}) RETURN n SKIP 10000"
% (PROP_PREFIX, rint(MAX_PROPS), rint(MAX_PROP_VALUE))
)

View File

@@ -21,5 +21,6 @@ def main():
if i != 0 and i % BATCH_SIZE == 0: if i != 0 and i % BATCH_SIZE == 0:
print(";") print(";")
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

View File

@@ -28,6 +28,7 @@ from signal import *
class ProcessException(Exception): class ProcessException(Exception):
pass pass
class StorageException(Exception): class StorageException(Exception):
pass pass
@@ -41,10 +42,11 @@ class Process:
self._usage = {} self._usage = {}
self._files = [] self._files = []
def run(self, binary, args=None, env=None, timeout=120, def run(self, binary, args=None, env=None, timeout=120, stdin="/dev/null", cwd="."):
stdin="/dev/null", cwd="."): if args is None:
if args is None: args = [] args = []
if env is None: env = {} if env is None:
env = {}
# don't start a new process if one is already running # don't start a new process if one is already running
if self._proc != None and self._proc.returncode == None: if self._proc != None and self._proc.returncode == None:
raise ProcessException raise ProcessException
@@ -65,15 +67,14 @@ class Process:
self._timeout = timeout self._timeout = timeout
# start process # start process
self._proc = subprocess.Popen(exe, env=env, cwd=cwd, self._proc = subprocess.Popen(exe, env=env, cwd=cwd, stdin=open(stdin, "r"))
stdin=open(stdin, "r"))
def run_and_wait(self, *args, **kwargs): def run_and_wait(self, *args, **kwargs):
check = kwargs.pop("check", True) check = kwargs.pop("check", True)
self.run(*args, **kwargs) self.run(*args, **kwargs)
return self.wait(check) return self.wait(check)
def wait(self, check = True): def wait(self, check=True):
if self._proc == None: if self._proc == None:
raise ProcessException raise ProcessException
self._proc.wait() self._proc.wait()
@@ -100,18 +101,17 @@ class Process:
# this is implemented only in the real API # this is implemented only in the real API
def set_cpus(self, cpus, hyper=True): def set_cpus(self, cpus, hyper=True):
s = "out" if not hyper else "" s = "out" if not hyper else ""
sys.stderr.write("WARNING: Trying to set cpus for {} to " sys.stderr.write(
"{} with{} hyperthreading!\n".format(str(self), cpus, s)) "WARNING: Trying to set cpus for {} to " "{} with{} hyperthreading!\n".format(str(self), cpus, s)
)
# this is implemented only in the real API # this is implemented only in the real API
def set_nproc(self, nproc): def set_nproc(self, nproc):
sys.stderr.write("WARNING: Trying to set nproc for {} to " sys.stderr.write("WARNING: Trying to set nproc for {} to " "{}!\n".format(str(self), nproc))
"{}!\n".format(str(self), nproc))
# this is implemented only in the real API # this is implemented only in the real API
def set_memory(self, memory): def set_memory(self, memory):
sys.stderr.write("WARNING: Trying to set memory for {} to " sys.stderr.write("WARNING: Trying to set memory for {} to " "{}\n".format(str(self), memory))
"{}\n".format(str(self), memory))
# WARNING: this won't be implemented in the real API # WARNING: this won't be implemented in the real API
def get_pid(self): def get_pid(self):
@@ -121,7 +121,8 @@ class Process:
def _set_usage(self, val, name, only_value=False): def _set_usage(self, val, name, only_value=False):
self._usage[name] = val self._usage[name] = val
if only_value: return if only_value:
return
maxname = "max_" + name maxname = "max_" + name
maxval = val maxval = val
if maxname in self._usage: if maxname in self._usage:
@@ -133,7 +134,8 @@ class Process:
self._watchdog() self._watchdog()
def _update_usage(self): def _update_usage(self):
if self._proc == None: return if self._proc == None:
return
try: try:
f = open("/proc/{}/stat".format(self._proc.pid), "r") f = open("/proc/{}/stat".format(self._proc.pid), "r")
data_stat = f.read().split() data_stat = f.read().split()
@@ -144,21 +146,20 @@ class Process:
except: except:
return return
# for a description of these fields see: man proc; man times # for a description of these fields see: man proc; man times
utime, stime, cutime, cstime = map( utime, stime, cutime, cstime = map(lambda x: int(x) / self._ticks_per_sec, data_stat[13:17])
lambda x: int(x) / self._ticks_per_sec, data_stat[13:17])
self._set_usage(utime + stime + cutime + cstime, "cpu", only_value=True) self._set_usage(utime + stime + cutime + cstime, "cpu", only_value=True)
self._set_usage(utime + cutime, "cpu_user", only_value=True) self._set_usage(utime + cutime, "cpu_user", only_value=True)
self._set_usage(stime + cstime, "cpu_sys", only_value=True) self._set_usage(stime + cstime, "cpu_sys", only_value=True)
self._set_usage(int(data_stat[19]), "threads") self._set_usage(int(data_stat[19]), "threads")
mem_vm, mem_res, mem_shr = map( mem_vm, mem_res, mem_shr = map(lambda x: int(x) * self._page_size // 1024, data_statm[:3])
lambda x: int(x) * self._page_size // 1024, data_statm[:3])
self._set_usage(mem_res, "memory") self._set_usage(mem_res, "memory")
def _watchdog(self): def _watchdog(self):
if self._proc == None or self._proc.returncode != None: return if self._proc == None or self._proc.returncode != None:
if time.time() - self._start_time < self._timeout: return return
sys.stderr.write("Timeout of {}s reached, sending " if time.time() - self._start_time < self._timeout:
"SIGKILL to {}!\n".format(self._timeout, self)) return
sys.stderr.write("Timeout of {}s reached, sending " "SIGKILL to {}!\n".format(self._timeout, self))
self.send_signal(SIGKILL) self.send_signal(SIGKILL)
self.get_status() self.get_status()
@@ -172,22 +173,27 @@ PROCESSES_NUM = 8
_processes = [Process(i) for i in range(1, PROCESSES_NUM + 1)] _processes = [Process(i) for i in range(1, PROCESSES_NUM + 1)]
_last_process = 0 _last_process = 0
def _usage_updater(): def _usage_updater():
while True: while True:
for proc in _processes: for proc in _processes:
proc._do_background_tasks() proc._do_background_tasks()
time.sleep(0.1) time.sleep(0.1)
_thread = threading.Thread(target=_usage_updater, daemon=True) _thread = threading.Thread(target=_usage_updater, daemon=True)
_thread.start() _thread.start()
@atexit.register @atexit.register
def cleanup(): def cleanup():
for proc in _processes: for proc in _processes:
if proc._proc == None: continue if proc._proc == None:
continue
proc.send_signal(SIGKILL) proc.send_signal(SIGKILL)
proc.get_status() proc.get_status()
# end of private methods ------------------------------------------------------ # end of private methods ------------------------------------------------------
@@ -199,6 +205,7 @@ def get_process():
return proc return proc
return None return None
def get_host_info(): def get_host_info():
with open("/proc/meminfo") as f: with open("/proc/meminfo") as f:
memdata = f.read() memdata = f.read()
@@ -215,21 +222,24 @@ def get_host_info():
threads, cpus = 0, set() threads, cpus = 0, set()
for row in cpudata.split("\n\n"): for row in cpudata.split("\n\n"):
if not row: continue if not row:
continue
data = row.split("\n") data = row.split("\n")
core_id, physical_id = -1, -1 core_id, physical_id = -1, -1
for line in data: for line in data:
name, val = map(lambda x: x.strip(), line.split(":")) name, val = map(lambda x: x.strip(), line.split(":"))
if name == "physical id": physical_id = int(val) if name == "physical id":
elif name == "core id": core_id = int(val) physical_id = int(val)
elif name == "core id":
core_id = int(val)
threads += 1 threads += 1
cpus.add((core_id, physical_id)) cpus.add((core_id, physical_id))
cpus = len(cpus) cpus = len(cpus)
hyper = True if cpus != threads else False hyper = True if cpus != threads else False
return {"cpus": cpus, "memory": memory, "hyperthreading": hyper, return {"cpus": cpus, "memory": memory, "hyperthreading": hyper, "threads": threads}
"threads": threads}
# placeholder function that stores a label in the real API # placeholder function that stores a label in the real API
def store_label(label): def store_label(label):
@@ -253,6 +263,8 @@ If chain is None, this function performs the following commands:
If chain is either "INPUT" or "OUTPUT" then only that chain is cleared using If chain is either "INPUT" or "OUTPUT" then only that chain is cleared using
the appropriate subset of the above mentioned commands. the appropriate subset of the above mentioned commands.
""" """
def network_flush_rules(chain=None): def network_flush_rules(chain=None):
print("Network flush rules: chain={}".format(chain)) print("Network flush rules: chain={}".format(chain))
@@ -300,12 +312,13 @@ in the following diagram:
Other combinations of `chain`, `src`/`dst` and `sport`/`dport` can be used, Other combinations of `chain`, `src`/`dst` and `sport`/`dport` can be used,
but are advised to be used only when you exactly know what you are doing :) but are advised to be used only when you exactly know what you are doing :)
""" """
def network_block_tcp(chain=None,
src=None, dst=None,
sport=None, dport=None, def network_block_tcp(chain=None, src=None, dst=None, sport=None, dport=None, action=None):
action=None): print(
print("Network block TCP: chain={}, src={}, dst={}, sport={}, dport={}, " "Network block TCP: chain={}, src={}, dst={}, sport={}, dport={}, "
"action={}".format(chain, src, dst, sport, dport, action)) "action={}".format(chain, src, dst, sport, dport, action)
)
""" """
@@ -319,28 +332,24 @@ same* parameters that were used to define the rule in the first place.
All other documentation for this function is the same as for All other documentation for this function is the same as for
`network_block_tcp`, so take a look there. `network_block_tcp`, so take a look there.
""" """
def network_unblock_tcp(chain=None,
src=None, dst=None,
sport=None, dport=None, def network_unblock_tcp(chain=None, src=None, dst=None, sport=None, dport=None, action=None):
action=None): print(
print("Network unblock TCP: chain={}, src={}, dst={}, sport={}, dport={}, " "Network unblock TCP: chain={}, src={}, dst={}, sport={}, dport={}, "
"action={}".format(chain, src, dst, sport, dport, action)) "action={}".format(chain, src, dst, sport, dport, action)
)
# this function is deprecated # this function is deprecated
def store_data(data): def store_data(data):
pass pass
# placeholder function that returns real data in the real API # placeholder function that returns real data in the real API
def get_network_usage(): def get_network_usage():
usage = { usage = {
"lo": { "lo": {"bytes": {"rx": 0, "tx": 0}, "packets": {"rx": 0, "tx": 0}},
"bytes": {"rx": 0, "tx": 0}, "eth0": {"bytes": {"rx": 0, "tx": 0}, "packets": {"rx": 0, "tx": 0}},
"packets": {"rx": 0, "tx": 0}
},
"eth0": {
"bytes": {"rx": 0, "tx": 0},
"packets": {"rx": 0, "tx": 0}
}
} }
return usage return usage

View File

@@ -38,27 +38,30 @@ class LongRunningSuite:
duration = config["duration"] duration = config["duration"]
if self.args.duration: if self.args.duration:
duration = self.args.duration duration = self.args.duration
log.info("Executing run for {} seconds".format( log.info("Executing run for {} seconds".format(duration))
duration))
results = runner.run(next(scenario.get("run")()), duration, config["client"]) results = runner.run(next(scenario.get("run")()), duration, config["client"])
runner.stop() runner.stop()
measurements = [] measurements = []
summary_format = "{:>15} {:>22} {:>22}\n" summary_format = "{:>15} {:>22} {:>22}\n"
self.summary = summary_format.format( self.summary = summary_format.format("elapsed_time", "num_executed_queries", "num_executed_steps")
"elapsed_time", "num_executed_queries", "num_executed_steps")
for result in results: for result in results:
self.summary += summary_format.format( self.summary += summary_format.format(
result["elapsed_time"], result["num_executed_queries"], result["elapsed_time"],
result["num_executed_steps"]) result["num_executed_queries"],
measurements.append({ result["num_executed_steps"],
"target": "throughput", )
"time": result["elapsed_time"], measurements.append(
"value": result["num_executed_queries"], {
"steps": result["num_executed_steps"], "target": "throughput",
"unit": "number of executed queries", "time": result["elapsed_time"],
"type": "throughput"}) "value": result["num_executed_queries"],
"steps": result["num_executed_steps"],
"unit": "number of executed queries",
"type": "throughput",
}
)
self.summary += "\n\nThroughput: " + str(measurements[-1]["value"]) self.summary += "\n\nThroughput: " + str(measurements[-1]["value"])
self.summary += "\nExecuted steps: " + str(measurements[-1]["steps"]) self.summary += "\nExecuted steps: " + str(measurements[-1]["steps"])
return measurements return measurements
@@ -75,8 +78,7 @@ class _LongRunningRunner:
self.log = logging.getLogger("_LongRunningRunner") self.log = logging.getLogger("_LongRunningRunner")
self.database = database self.database = database
self.query_client = QueryClient(args, num_client_workers) self.query_client = QueryClient(args, num_client_workers)
self.long_running_client = LongRunningClient(args, num_client_workers, self.long_running_client = LongRunningClient(args, num_client_workers, workload)
workload)
def start(self): def start(self):
self.database.start() self.database.start()
@@ -85,8 +87,7 @@ class _LongRunningRunner:
return self.query_client(queries, self.database, num_client_workers) return self.query_client(queries, self.database, num_client_workers)
def run(self, config, duration, client, num_client_workers=None): def run(self, config, duration, client, num_client_workers=None):
return self.long_running_client( return self.long_running_client(config, self.database, duration, client, num_client_workers)
config, self.database, duration, client, num_client_workers)
def stop(self): def stop(self):
self.log.info("stop") self.log.info("stop")
@@ -99,44 +100,46 @@ class MemgraphRunner(_LongRunningRunner):
""" """
Configures memgraph database for LongRunningSuite execution. Configures memgraph database for LongRunningSuite execution.
""" """
def __init__(self, args): def __init__(self, args):
argp = ArgumentParser("MemgraphRunnerArgumentParser") argp = ArgumentParser("MemgraphRunnerArgumentParser")
argp.add_argument("--num-database-workers", type=int, default=8, argp.add_argument("--num-database-workers", type=int, default=8, help="Number of workers")
help="Number of workers") argp.add_argument("--num-client-workers", type=int, default=24, help="Number of clients")
argp.add_argument("--num-client-workers", type=int, default=24, argp.add_argument(
help="Number of clients") "--workload",
argp.add_argument("--workload", type=str, default="", type=str,
help="Type of client workload. Sets \ default="",
scenario flag for 'TestClient'") help="Type of client workload. Sets \
scenario flag for 'TestClient'",
)
self.args, remaining_args = argp.parse_known_args(args) self.args, remaining_args = argp.parse_known_args(args)
assert not APOLLO or self.args.num_database_workers, \ assert not APOLLO or self.args.num_database_workers, "--num-database-workers is obligatory flag on apollo"
"--num-database-workers is obligatory flag on apollo" assert not APOLLO or self.args.num_client_workers, "--num-client-workers is obligatory flag on apollo"
assert not APOLLO or self.args.num_client_workers, \
"--num-client-workers is obligatory flag on apollo"
database = Memgraph(remaining_args, self.args.num_database_workers) database = Memgraph(remaining_args, self.args.num_database_workers)
super(MemgraphRunner, self).__init__( super(MemgraphRunner, self).__init__(remaining_args, database, self.args.num_client_workers, self.args.workload)
remaining_args, database, self.args.num_client_workers,
self.args.workload)
class NeoRunner(_LongRunningRunner): class NeoRunner(_LongRunningRunner):
""" """
Configures neo4j database for QuerySuite execution. Configures neo4j database for QuerySuite execution.
""" """
def __init__(self, args): def __init__(self, args):
argp = ArgumentParser("NeoRunnerArgumentParser") argp = ArgumentParser("NeoRunnerArgumentParser")
argp.add_argument("--runner-config", argp.add_argument(
default=get_absolute_path("config/neo4j.conf"), "--runner-config",
help="Path to neo config file") default=get_absolute_path("config/neo4j.conf"),
argp.add_argument("--num-client-workers", type=int, default=24, help="Path to neo config file",
help="Number of clients") )
argp.add_argument("--workload", type=str, default="", argp.add_argument("--num-client-workers", type=int, default=24, help="Number of clients")
help="Type of client workload. Sets \ argp.add_argument(
scenario flag for 'TestClient'") "--workload",
type=str,
default="",
help="Type of client workload. Sets \
scenario flag for 'TestClient'",
)
self.args, remaining_args = argp.parse_known_args(args) self.args, remaining_args = argp.parse_known_args(args)
assert not APOLLO or self.args.num_client_workers, \ assert not APOLLO or self.args.num_client_workers, "--client-num-clients is obligatory flag on apollo"
"--client-num-clients is obligatory flag on apollo"
database = Neo(remaining_args, self.args.runner_config) database = Neo(remaining_args, self.args.runner_config)
super(NeoRunner, self).__init__( super(NeoRunner, self).__init__(remaining_args, database, self.args.num_client_workers, self.args.workload)
remaining_args, database, self.args.num_client_workers,
self.args.workload)

View File

@@ -33,21 +33,48 @@ class _QuerySuite:
a single Cypher query that is benchmarked, and teardown steps a single Cypher query that is benchmarked, and teardown steps
(Cypher queries) executed after the benchmark. (Cypher queries) executed after the benchmark.
""" """
# what the QuerySuite can work with # what the QuerySuite can work with
KNOWN_KEYS = {"config", "setup", "itersetup", "run", "iterteardown", KNOWN_KEYS = {
"teardown", "common"} "config",
FORMAT = ["{:>24}", "{:>28}", "{:>16}", "{:>18}", "{:>22}", "setup",
"{:>16}", "{:>16}", "{:>16}"] "itersetup",
"run",
"iterteardown",
"teardown",
"common",
}
FORMAT = [
"{:>24}",
"{:>28}",
"{:>16}",
"{:>18}",
"{:>22}",
"{:>16}",
"{:>16}",
"{:>16}",
]
FULL_FORMAT = "".join(FORMAT) + "\n" FULL_FORMAT = "".join(FORMAT) + "\n"
headers = ["group_name", "scenario_name", "parsing_time", headers = [
"planning_time", "plan_execution_time", "group_name",
WALL_TIME, CPU_TIME, MAX_MEMORY] "scenario_name",
"parsing_time",
"planning_time",
"plan_execution_time",
WALL_TIME,
CPU_TIME,
MAX_MEMORY,
]
summary = summary_raw = FULL_FORMAT.format(*headers) summary = summary_raw = FULL_FORMAT.format(*headers)
def __init__(self, args): def __init__(self, args):
argp = ArgumentParser("MemgraphRunnerArgumentParser") argp = ArgumentParser("MemgraphRunnerArgumentParser")
argp.add_argument("--perf", default=False, action="store_true", argp.add_argument(
help="Run perf on running tests and store data") "--perf",
default=False,
action="store_true",
help="Run perf on running tests and store data",
)
self.args, remaining_args = argp.parse_known_args(args) self.args, remaining_args = argp.parse_known_args(args)
def run(self, scenario, group_name, scenario_name, runner): def run(self, scenario, group_name, scenario_name, runner):
@@ -62,8 +89,7 @@ class _QuerySuite:
r_val = runner.execute(queries(), num_client_workers) r_val = runner.execute(queries(), num_client_workers)
else: else:
r_val = None r_val = None
log.info("\t%s done in %.2f seconds" % (config_name, log.info("\t%s done in %.2f seconds" % (config_name, time.time() - start_time))
time.time() - start_time))
return r_val return r_val
measurements = defaultdict(list) measurements = defaultdict(list)
@@ -75,8 +101,12 @@ class _QuerySuite:
execute("setup") execute("setup")
# warmup phase # warmup phase
for _ in range(min(scenario_config.get("iterations", 1), for _ in range(
scenario_config.get("warmup", 2))): min(
scenario_config.get("iterations", 1),
scenario_config.get("warmup", 2),
)
):
execute("itersetup") execute("itersetup")
execute("run") execute("run")
execute("iterteardown") execute("iterteardown")
@@ -91,15 +121,28 @@ class _QuerySuite:
execute("itersetup") execute("itersetup")
if self.args.perf: if self.args.perf:
file_directory = './perf_results/run_%d/%s/%s/' \ file_directory = "./perf_results/run_%d/%s/%s/" % (
% (rerun_cnt, group_name, scenario_name) rerun_cnt,
group_name,
scenario_name,
)
os.makedirs(file_directory, exist_ok=True) os.makedirs(file_directory, exist_ok=True)
file_name = '%d.perf.data' % iteration file_name = "%d.perf.data" % iteration
path = file_directory + file_name path = file_directory + file_name
database_pid = str(runner.database.database_bin._proc.pid) database_pid = str(runner.database.database_bin._proc.pid)
self.perf_proc = subprocess.Popen( self.perf_proc = subprocess.Popen(
["perf", "record", "-F", "999", "-g", "-o", path, "-p", [
database_pid]) "perf",
"record",
"-F",
"999",
"-g",
"-o",
path,
"-p",
database_pid,
]
)
run_result = execute("run") run_result = execute("run")
@@ -110,16 +153,15 @@ class _QuerySuite:
measurements["cpu_time"].append(run_result["cpu_time"]) measurements["cpu_time"].append(run_result["cpu_time"])
measurements["max_memory"].append(run_result["max_memory"]) measurements["max_memory"].append(run_result["max_memory"])
assert len(run_result["groups"]) == 1, \ assert len(run_result["groups"]) == 1, "Multiple groups in run step not yet supported"
"Multiple groups in run step not yet supported"
group = run_result["groups"][0] group = run_result["groups"][0]
measurements["wall_time"].append(group["wall_time"]) measurements["wall_time"].append(group["wall_time"])
for key in ["parsing_time", "plan_execution_time", for key in ["parsing_time", "plan_execution_time", "planning_time"]:
"planning_time"]:
for i in range(len(group.get("metadatas", []))): for i in range(len(group.get("metadatas", []))):
if not key in group["metadatas"][i]: continue if not key in group["metadatas"][i]:
continue
measurements[key].append(group["metadatas"][i][key]) measurements[key].append(group["metadatas"][i][key])
execute("iterteardown") execute("iterteardown")
@@ -127,27 +169,35 @@ class _QuerySuite:
execute("teardown") execute("teardown")
runner.stop() runner.stop()
self.append_scenario_summary(group_name, scenario_name, self.append_scenario_summary(group_name, scenario_name, measurements, num_iterations)
measurements, num_iterations)
# calculate mean, median and stdev of measurements # calculate mean, median and stdev of measurements
for key in measurements: for key in measurements:
samples = measurements[key] samples = measurements[key]
measurements[key] = {"mean": mean(samples), measurements[key] = {
"median": median(samples), "mean": mean(samples),
"stdev": stdev(samples), "median": median(samples),
"count": len(samples)} "stdev": stdev(samples),
"count": len(samples),
}
measurements["group_name"] = group_name measurements["group_name"] = group_name
measurements["scenario_name"] = scenario_name measurements["scenario_name"] = scenario_name
return measurements return measurements
def append_scenario_summary(self, group_name, scenario_name, def append_scenario_summary(self, group_name, scenario_name, measurement_lists, num_iterations):
measurement_lists, num_iterations):
self.summary += self.FORMAT[0].format(group_name) self.summary += self.FORMAT[0].format(group_name)
self.summary += self.FORMAT[1].format(scenario_name) self.summary += self.FORMAT[1].format(scenario_name)
for i, key in enumerate(("parsing_time", "planning_time", for i, key in enumerate(
"plan_execution_time", WALL_TIME, CPU_TIME, MAX_MEMORY)): (
"parsing_time",
"planning_time",
"plan_execution_time",
WALL_TIME,
CPU_TIME,
MAX_MEMORY,
)
):
if key not in measurement_lists: if key not in measurement_lists:
time = "-" time = "-"
else: else:
@@ -162,11 +212,11 @@ class _QuerySuite:
self.summary += "\n" self.summary += "\n"
def runners(self): def runners(self):
""" Which runners can execute a QuerySuite scenario """ """Which runners can execute a QuerySuite scenario"""
assert False, "This is a base class, use one of derived suites" assert False, "This is a base class, use one of derived suites"
def groups(self): def groups(self):
""" Which groups can be executed by a QuerySuite scenario """ """Which groups can be executed by a QuerySuite scenario"""
assert False, "This is a base class, use one of derived suites" assert False, "This is a base class, use one of derived suites"
@@ -175,11 +225,20 @@ class QuerySuite(_QuerySuite):
_QuerySuite.__init__(self, args) _QuerySuite.__init__(self, args)
def runners(self): def runners(self):
return {"MemgraphRunner" : MemgraphRunner, "NeoRunner" : NeoRunner} return {"MemgraphRunner": MemgraphRunner, "NeoRunner": NeoRunner}
def groups(self): def groups(self):
return ["1000_create", "unwind_create", "match", "dense_expand", return [
"expression", "aggregation", "return", "update", "delete"] "1000_create",
"unwind_create",
"match",
"dense_expand",
"expression",
"aggregation",
"return",
"update",
"delete",
]
class QueryParallelSuite(_QuerySuite): class QueryParallelSuite(_QuerySuite):
@@ -187,8 +246,10 @@ class QueryParallelSuite(_QuerySuite):
_QuerySuite.__init__(self, args) _QuerySuite.__init__(self, args)
def runners(self): def runners(self):
return {"MemgraphRunner" : MemgraphParallelRunner, "NeoRunner" : return {
NeoParallelRunner} "MemgraphRunner": MemgraphParallelRunner,
"NeoRunner": NeoParallelRunner,
}
def groups(self): def groups(self):
return ["aggregation_parallel", "create_parallel", "bfs_parallel"] return ["aggregation_parallel", "create_parallel", "bfs_parallel"]
@@ -201,6 +262,7 @@ class _QueryRunner:
Execution returns benchmarking data (execution times, memory Execution returns benchmarking data (execution times, memory
usage etc). usage etc).
""" """
def __init__(self, args, database, num_client_workers): def __init__(self, args, database, num_client_workers):
self.log = logging.getLogger("_HarnessClientRunner") self.log = logging.getLogger("_HarnessClientRunner")
self.database = database self.database = database
@@ -221,6 +283,7 @@ class MemgraphRunner(_QueryRunner):
""" """
Configures memgraph database for QuerySuite execution. Configures memgraph database for QuerySuite execution.
""" """
def __init__(self, args): def __init__(self, args):
database = Memgraph(args, 1) database = Memgraph(args, 1)
super(MemgraphRunner, self).__init__(args, database, 1) super(MemgraphRunner, self).__init__(args, database, 1)
@@ -230,11 +293,14 @@ class NeoRunner(_QueryRunner):
""" """
Configures neo4j database for QuerySuite execution. Configures neo4j database for QuerySuite execution.
""" """
def __init__(self, args): def __init__(self, args):
argp = ArgumentParser("NeoRunnerArgumentParser") argp = ArgumentParser("NeoRunnerArgumentParser")
argp.add_argument("--runner-config", argp.add_argument(
default=get_absolute_path("config/neo4j.conf"), "--runner-config",
help="Path to neo config file") default=get_absolute_path("config/neo4j.conf"),
help="Path to neo config file",
)
self.args, remaining_args = argp.parse_known_args(args) self.args, remaining_args = argp.parse_known_args(args)
database = Neo(remaining_args, self.args.runner_config) database = Neo(remaining_args, self.args.runner_config)
super(NeoRunner, self).__init__(remaining_args, database) super(NeoRunner, self).__init__(remaining_args, database)
@@ -244,36 +310,32 @@ class NeoParallelRunner(_QueryRunner):
""" """
Configures neo4j database for QuerySuite execution. Configures neo4j database for QuerySuite execution.
""" """
def __init__(self, args): def __init__(self, args):
argp = ArgumentParser("NeoRunnerArgumentParser") argp = ArgumentParser("NeoRunnerArgumentParser")
argp.add_argument("--runner-config", argp.add_argument(
default=get_absolute_path("config/neo4j.conf"), "--runner-config",
help="Path to neo config file") default=get_absolute_path("config/neo4j.conf"),
argp.add_argument("--num-client-workers", type=int, default=24, help="Path to neo config file",
help="Number of clients") )
argp.add_argument("--num-client-workers", type=int, default=24, help="Number of clients")
self.args, remaining_args = argp.parse_known_args(args) self.args, remaining_args = argp.parse_known_args(args)
assert not APOLLO or self.args.num_client_workers, \ assert not APOLLO or self.args.num_client_workers, "--client-num-clients is obligatory flag on apollo"
"--client-num-clients is obligatory flag on apollo"
database = Neo(remaining_args, self.args.runner_config) database = Neo(remaining_args, self.args.runner_config)
super(NeoRunner, self).__init__( super(NeoRunner, self).__init__(remaining_args, database, self.args.num_client_workers)
remaining_args, database, self.args.num_client_workers)
class MemgraphParallelRunner(_QueryRunner): class MemgraphParallelRunner(_QueryRunner):
""" """
Configures memgraph database for QuerySuite execution. Configures memgraph database for QuerySuite execution.
""" """
def __init__(self, args): def __init__(self, args):
argp = ArgumentParser("MemgraphRunnerArgumentParser") argp = ArgumentParser("MemgraphRunnerArgumentParser")
argp.add_argument("--num-database-workers", type=int, default=8, argp.add_argument("--num-database-workers", type=int, default=8, help="Number of workers")
help="Number of workers") argp.add_argument("--num-client-workers", type=int, default=24, help="Number of clients")
argp.add_argument("--num-client-workers", type=int, default=24,
help="Number of clients")
self.args, remaining_args = argp.parse_known_args(args) self.args, remaining_args = argp.parse_known_args(args)
assert not APOLLO or self.args.num_database_workers, \ assert not APOLLO or self.args.num_database_workers, "--num-database-workers is obligatory flag on apollo"
"--num-database-workers is obligatory flag on apollo" assert not APOLLO or self.args.num_client_workers, "--num-client-workers is obligatory flag on apollo"
assert not APOLLO or self.args.num_client_workers, \
"--num-client-workers is obligatory flag on apollo"
database = Memgraph(remaining_args, self.args.num_database_workers) database = Memgraph(remaining_args, self.args.num_database_workers)
super(MemgraphParallelRunner, self).__init__( super(MemgraphParallelRunner, self).__init__(remaining_args, database, self.args.num_client_workers)
remaining_args, database, self.args.num_client_workers)

View File

@@ -37,8 +37,7 @@ def get_queries(gen, count):
return ret return ret
def match_patterns(dataset, variant, group, test, is_default_variant, def match_patterns(dataset, variant, group, test, is_default_variant, patterns):
patterns):
for pattern in patterns: for pattern in patterns:
verdict = [fnmatch.fnmatchcase(dataset, pattern[0])] verdict = [fnmatch.fnmatchcase(dataset, pattern[0])]
if pattern[1] != "": if pattern[1] != "":
@@ -58,7 +57,7 @@ def filter_benchmarks(generators, patterns):
pattern = patterns[i].split("/") pattern = patterns[i].split("/")
if len(pattern) > 4 or len(pattern) == 0: if len(pattern) > 4 or len(pattern) == 0:
raise Exception("Invalid benchmark description '" + pattern + "'!") raise Exception("Invalid benchmark description '" + pattern + "'!")
pattern.extend(["", "*", "*"][len(pattern) - 1:]) pattern.extend(["", "*", "*"][len(pattern) - 1 :])
patterns[i] = pattern patterns[i] = pattern
filtered = [] filtered = []
for dataset in sorted(generators.keys()): for dataset in sorted(generators.keys()):
@@ -68,8 +67,7 @@ def filter_benchmarks(generators, patterns):
current = collections.defaultdict(list) current = collections.defaultdict(list)
for group in tests: for group in tests:
for test_name, test_func in tests[group]: for test_name, test_func in tests[group]:
if match_patterns(dataset, variant, group, test_name, if match_patterns(dataset, variant, group, test_name, is_default_variant, patterns):
is_default_variant, patterns):
current[group].append((test_name, test_func)) current[group].append((test_name, test_func))
if len(current) > 0: if len(current) > 0:
filtered.append((generator(variant), dict(current))) filtered.append((generator(variant), dict(current)))
@@ -79,43 +77,71 @@ def filter_benchmarks(generators, patterns):
# Parse options. # Parse options.
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Memgraph benchmark executor.", description="Memgraph benchmark executor.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter,
parser.add_argument("benchmarks", nargs="*", default="", )
help="descriptions of benchmarks that should be run; " parser.add_argument(
"multiple descriptions can be specified to run multiple " "benchmarks",
"benchmarks; the description is specified as " nargs="*",
"dataset/variant/group/test; Unix shell-style wildcards " default="",
"can be used in the descriptions; variant, group and test " help="descriptions of benchmarks that should be run; "
"are optional and they can be left out; the default " "multiple descriptions can be specified to run multiple "
"variant is '' which selects the default dataset variant; " "benchmarks; the description is specified as "
"the default group is '*' which selects all groups; the " "dataset/variant/group/test; Unix shell-style wildcards "
"default test is '*' which selects all tests") "can be used in the descriptions; variant, group and test "
parser.add_argument("--memgraph-binary", "are optional and they can be left out; the default "
default=helpers.get_binary_path("memgraph"), "variant is '' which selects the default dataset variant; "
help="Memgraph binary used for benchmarking") "the default group is '*' which selects all groups; the "
parser.add_argument("--client-binary", "default test is '*' which selects all tests",
default=helpers.get_binary_path("tests/mgbench/client"), )
help="client binary used for benchmarking") parser.add_argument(
parser.add_argument("--num-workers-for-import", type=int, "--memgraph-binary",
default=multiprocessing.cpu_count() // 2, default=helpers.get_binary_path("memgraph"),
help="number of workers used to import the dataset") help="Memgraph binary used for benchmarking",
parser.add_argument("--num-workers-for-benchmark", type=int, )
default=1, parser.add_argument(
help="number of workers used to execute the benchmark") "--client-binary",
parser.add_argument("--single-threaded-runtime-sec", type=int, default=helpers.get_binary_path("tests/mgbench/client"),
default=10, help="client binary used for benchmarking",
help="single threaded duration of each test") )
parser.add_argument("--no-load-query-counts", action="store_true", parser.add_argument(
help="disable loading of cached query counts") "--num-workers-for-import",
parser.add_argument("--no-save-query-counts", action="store_true", type=int,
help="disable storing of cached query counts") default=multiprocessing.cpu_count() // 2,
parser.add_argument("--export-results", default="", help="number of workers used to import the dataset",
help="file path into which results should be exported") )
parser.add_argument("--temporary-directory", default="/tmp", parser.add_argument(
help="directory path where temporary data should " "--num-workers-for-benchmark",
"be stored") type=int,
parser.add_argument("--no-properties-on-edges", action="store_true", default=1,
help="disable properties on edges") help="number of workers used to execute the benchmark",
)
parser.add_argument(
"--single-threaded-runtime-sec",
type=int,
default=10,
help="single threaded duration of each test",
)
parser.add_argument(
"--no-load-query-counts",
action="store_true",
help="disable loading of cached query counts",
)
parser.add_argument(
"--no-save-query-counts",
action="store_true",
help="disable storing of cached query counts",
)
parser.add_argument(
"--export-results",
default="",
help="file path into which results should be exported",
)
parser.add_argument(
"--temporary-directory",
default="/tmp",
help="directory path where temporary data should " "be stored",
)
parser.add_argument("--no-properties-on-edges", action="store_true", help="disable properties on edges")
args = parser.parse_args() args = parser.parse_args()
# Detect available datasets. # Detect available datasets.
@@ -124,8 +150,7 @@ for key in dir(datasets):
if key.startswith("_"): if key.startswith("_"):
continue continue
dataset = getattr(datasets, key) dataset = getattr(datasets, key)
if not inspect.isclass(dataset) or dataset == datasets.Dataset or \ if not inspect.isclass(dataset) or dataset == datasets.Dataset or not issubclass(dataset, datasets.Dataset):
not issubclass(dataset, datasets.Dataset):
continue continue
tests = collections.defaultdict(list) tests = collections.defaultdict(list)
for funcname in dir(dataset): for funcname in dir(dataset):
@@ -135,8 +160,9 @@ for key in dir(datasets):
tests[group].append((test, funcname)) tests[group].append((test, funcname))
generators[dataset.NAME] = (dataset, dict(tests)) generators[dataset.NAME] = (dataset, dict(tests))
if dataset.PROPERTIES_ON_EDGES and args.no_properties_on_edges: if dataset.PROPERTIES_ON_EDGES and args.no_properties_on_edges:
raise Exception("The \"{}\" dataset requires properties on edges, " raise Exception(
"but you have disabled them!".format(dataset.NAME)) 'The "{}" dataset requires properties on edges, ' "but you have disabled them!".format(dataset.NAME)
)
# List datasets if there is no specified dataset. # List datasets if there is no specified dataset.
if len(args.benchmarks) == 0: if len(args.benchmarks) == 0:
@@ -144,8 +170,11 @@ if len(args.benchmarks) == 0:
for name in sorted(generators.keys()): for name in sorted(generators.keys()):
print("Dataset:", name) print("Dataset:", name)
dataset, tests = generators[name] dataset, tests = generators[name]
print(" Variants:", ", ".join(dataset.VARIANTS), print(
"(default: " + dataset.DEFAULT_VARIANT + ")") " Variants:",
", ".join(dataset.VARIANTS),
"(default: " + dataset.DEFAULT_VARIANT + ")",
)
for group in sorted(tests.keys()): for group in sorted(tests.keys()):
print(" Group:", group) print(" Group:", group)
for test_name, test_func in tests[group]: for test_name, test_func in tests[group]:
@@ -165,31 +194,38 @@ benchmarks = filter_benchmarks(generators, args.benchmarks)
# Run all specified benchmarks. # Run all specified benchmarks.
for dataset, tests in benchmarks: for dataset, tests in benchmarks:
log.init("Preparing", dataset.NAME + "/" + dataset.get_variant(), log.init("Preparing", dataset.NAME + "/" + dataset.get_variant(), "dataset")
"dataset") dataset.prepare(cache.cache_directory("datasets", dataset.NAME, dataset.get_variant()))
dataset.prepare(cache.cache_directory("datasets", dataset.NAME,
dataset.get_variant()))
# Prepare runners and import the dataset. # Prepare runners and import the dataset.
memgraph = runners.Memgraph(args.memgraph_binary, args.temporary_directory, memgraph = runners.Memgraph(args.memgraph_binary, args.temporary_directory, not args.no_properties_on_edges)
not args.no_properties_on_edges)
client = runners.Client(args.client_binary, args.temporary_directory) client = runners.Client(args.client_binary, args.temporary_directory)
memgraph.start_preparation() memgraph.start_preparation()
ret = client.execute(file_path=dataset.get_file(), ret = client.execute(file_path=dataset.get_file(), num_workers=args.num_workers_for_import)
num_workers=args.num_workers_for_import)
usage = memgraph.stop() usage = memgraph.stop()
# Display import statistics. # Display import statistics.
print() print()
for row in ret: for row in ret:
print("Executed", row["count"], "queries in", row["duration"], print(
"seconds using", row["num_workers"], "Executed",
"workers with a total throughput of", row["throughput"], row["count"],
"queries/second.") "queries in",
row["duration"],
"seconds using",
row["num_workers"],
"workers with a total throughput of",
row["throughput"],
"queries/second.",
)
print() print()
print("The database used", usage["cpu"], print(
"seconds of CPU time and peaked at", "The database used",
usage["memory"] / 1024 / 1024, "MiB of RAM.") usage["cpu"],
"seconds of CPU time and peaked at",
usage["memory"] / 1024 / 1024,
"MiB of RAM.",
)
# Save import results. # Save import results.
import_key = [dataset.NAME, dataset.get_variant(), "__import__"] import_key = [dataset.NAME, dataset.get_variant(), "__import__"]
@@ -208,24 +244,26 @@ for dataset, tests in benchmarks:
config_key = [dataset.NAME, dataset.get_variant(), group, test] config_key = [dataset.NAME, dataset.get_variant(), group, test]
cached_count = config.get_value(*config_key) cached_count = config.get_value(*config_key)
if cached_count is None: if cached_count is None:
print("Determining the number of queries necessary for", print(
args.single_threaded_runtime_sec, "Determining the number of queries necessary for",
"seconds of single-threaded runtime...") args.single_threaded_runtime_sec,
"seconds of single-threaded runtime...",
)
# First run to prime the query caches. # First run to prime the query caches.
memgraph.start_benchmark() memgraph.start_benchmark()
client.execute(queries=get_queries(func, 1), num_workers=1) client.execute(queries=get_queries(func, 1), num_workers=1)
# Get a sense of the runtime. # Get a sense of the runtime.
count = 1 count = 1
while True: while True:
ret = client.execute(queries=get_queries(func, count), ret = client.execute(queries=get_queries(func, count), num_workers=1)
num_workers=1)
duration = ret[0]["duration"] duration = ret[0]["duration"]
should_execute = int(args.single_threaded_runtime_sec / should_execute = int(args.single_threaded_runtime_sec / (duration / count))
(duration / count)) print(
print("executed_queries={}, total_duration={}, " "executed_queries={}, total_duration={}, "
"query_duration={}, estimated_count={}".format( "query_duration={}, estimated_count={}".format(
count, duration, duration / count, count, duration, duration / count, should_execute
should_execute)) )
)
# We don't have to execute the next iteration when # We don't have to execute the next iteration when
# `should_execute` becomes the same order of magnitude as # `should_execute` becomes the same order of magnitude as
# `count * 10`. # `count * 10`.
@@ -235,45 +273,52 @@ for dataset, tests in benchmarks:
else: else:
count = count * 10 count = count * 10
memgraph.stop() memgraph.stop()
config.set_value(*config_key, value={ config.set_value(*config_key, value={"count": count, "duration": args.single_threaded_runtime_sec})
"count": count,
"duration": args.single_threaded_runtime_sec})
else: else:
print("Using cached query count of", cached_count["count"], print(
"queries for", cached_count["duration"], "Using cached query count of",
"seconds of single-threaded runtime.") cached_count["count"],
count = int(cached_count["count"] * "queries for",
args.single_threaded_runtime_sec / cached_count["duration"],
cached_count["duration"]) "seconds of single-threaded runtime.",
)
count = int(cached_count["count"] * args.single_threaded_runtime_sec / cached_count["duration"])
# Benchmark run. # Benchmark run.
print("Sample query:", get_queries(func, 1)[0][0]) print("Sample query:", get_queries(func, 1)[0][0])
print("Executing benchmark with", count, "queries that should " print(
"yield a single-threaded runtime of", "Executing benchmark with",
args.single_threaded_runtime_sec, "seconds.") count,
print("Queries are executed using", args.num_workers_for_benchmark, "queries that should " "yield a single-threaded runtime of",
"concurrent clients.") args.single_threaded_runtime_sec,
"seconds.",
)
print(
"Queries are executed using",
args.num_workers_for_benchmark,
"concurrent clients.",
)
memgraph.start_benchmark() memgraph.start_benchmark()
ret = client.execute(queries=get_queries(func, count), ret = client.execute(
num_workers=args.num_workers_for_benchmark)[0] queries=get_queries(func, count),
num_workers=args.num_workers_for_benchmark,
)[0]
usage = memgraph.stop() usage = memgraph.stop()
ret["database"] = usage ret["database"] = usage
# Output summary. # Output summary.
print() print()
print("Executed", ret["count"], "queries in", print("Executed", ret["count"], "queries in", ret["duration"], "seconds.")
ret["duration"], "seconds.")
print("Queries have been retried", ret["retries"], "times.") print("Queries have been retried", ret["retries"], "times.")
print("Database used {:.3f} seconds of CPU time.".format( print("Database used {:.3f} seconds of CPU time.".format(usage["cpu"]))
usage["cpu"])) print("Database peaked at {:.3f} MiB of memory.".format(usage["memory"] / 1024.0 / 1024.0))
print("Database peaked at {:.3f} MiB of memory.".format( print("{:<31} {:>20} {:>20} {:>20}".format("Metadata:", "min", "avg", "max"))
usage["memory"] / 1024.0 / 1024.0))
print("{:<31} {:>20} {:>20} {:>20}".format("Metadata:", "min",
"avg", "max"))
metadata = ret["metadata"] metadata = ret["metadata"]
for key in sorted(metadata.keys()): for key in sorted(metadata.keys()):
print("{name:>30}: {minimum:>20.06f} {average:>20.06f} " print(
"{maximum:>20.06f}".format(name=key, **metadata[key])) "{name:>30}: {minimum:>20.06f} {average:>20.06f} "
"{maximum:>20.06f}".format(name=key, **metadata[key])
)
log.success("Throughput: {:02f} QPS".format(ret["throughput"])) log.success("Throughput: {:02f} QPS".format(ret["throughput"]))
# Save results. # Save results.

View File

@@ -85,39 +85,41 @@ def compare_results(results_from, results_to, fields):
if group == "__import__": if group == "__import__":
continue continue
for scenario, summary_to in scenarios.items(): for scenario, summary_to in scenarios.items():
summary_from = recursive_get( summary_from = recursive_get(results_from, dataset, variant, group, scenario, value={})
results_from, dataset, variant, group, scenario, if (
value={}) len(summary_from) > 0
if len(summary_from) > 0 and \ and summary_to["count"] != summary_from["count"]
summary_to["count"] != summary_from["count"] or \ or summary_to["num_workers"] != summary_from["num_workers"]
summary_to["num_workers"] != \ ):
summary_from["num_workers"]:
raise Exception("Incompatible results!") raise Exception("Incompatible results!")
testcode = "/".join([dataset, variant, group, scenario, testcode = "/".join(
"{:02d}".format( [
summary_to["num_workers"])]) dataset,
variant,
group,
scenario,
"{:02d}".format(summary_to["num_workers"]),
]
)
row = {} row = {}
performance_changed = False performance_changed = False
for field in fields: for field in fields:
key = field["name"] key = field["name"]
if key in summary_to: if key in summary_to:
row[key] = compute_diff( row[key] = compute_diff(summary_from.get(key, None), summary_to[key])
summary_from.get(key, None),
summary_to[key])
elif key in summary_to["database"]: elif key in summary_to["database"]:
row[key] = compute_diff( row[key] = compute_diff(
recursive_get(summary_from, "database", key, recursive_get(summary_from, "database", key, value=None),
value=None), summary_to["database"][key],
summary_to["database"][key]) )
else: else:
row[key] = compute_diff( row[key] = compute_diff(
recursive_get(summary_from, "metadata", key, recursive_get(summary_from, "metadata", key, "average", value=None),
"average", value=None), summary_to["metadata"][key]["average"],
summary_to["metadata"][key]["average"]) )
if "diff" not in row[key] or \ if "diff" not in row[key] or (
("diff_treshold" in field and "diff_treshold" in field and abs(row[key]["diff"]) >= field["diff_treshold"]
abs(row[key]["diff"]) >= ):
field["diff_treshold"]):
performance_changed = True performance_changed = True
if performance_changed: if performance_changed:
ret[testcode] = row ret[testcode] = row
@@ -130,8 +132,15 @@ def generate_remarkup(fields, data):
ret += "<table>\n" ret += "<table>\n"
ret += " <tr>\n" ret += " <tr>\n"
ret += " <th>Testcode</th>\n" ret += " <th>Testcode</th>\n"
ret += "\n".join(map(lambda x: " <th>{}</th>".format( ret += (
x["name"].replace("_", " ").capitalize()), fields)) + "\n" "\n".join(
map(
lambda x: " <th>{}</th>".format(x["name"].replace("_", " ").capitalize()),
fields,
)
)
+ "\n"
)
ret += " </tr>\n" ret += " </tr>\n"
for testcode in sorted(data.keys()): for testcode in sorted(data.keys()):
ret += " <tr>\n" ret += " <tr>\n"
@@ -147,12 +156,9 @@ def generate_remarkup(fields, data):
else: else:
color = "red" color = "red"
sign = "{{icon {} color={}}}".format(arrow, color) sign = "{{icon {} color={}}}".format(arrow, color)
ret += " <td>{:.3f}{} //({:+.2%})// {}</td>\n".format( ret += " <td>{:.3f}{} //({:+.2%})// {}</td>\n".format(value, field["unit"], diff, sign)
value, field["unit"], diff, sign)
else: else:
ret += " <td>{:.3f}{} //(new)// " \ ret += " <td>{:.3f}{} //(new)// " "{{icon plus color=blue}}</td>\n".format(value, field["unit"])
"{{icon plus color=blue}}</td>\n".format(
value, field["unit"])
ret += " </tr>\n" ret += " </tr>\n"
ret += "</table>\n" ret += "</table>\n"
else: else:
@@ -161,11 +167,14 @@ def generate_remarkup(fields, data):
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description="Compare results of multiple benchmark runs.")
description="Compare results of multiple benchmark runs.") parser.add_argument(
parser.add_argument("--compare", action="append", nargs=2, "--compare",
metavar=("from", "to"), action="append",
help="compare results between `from` and `to` files") nargs=2,
metavar=("from", "to"),
help="compare results between `from` and `to` files",
)
parser.add_argument("--output", default="", help="output file name") parser.add_argument("--output", default="", help="output file name")
args = parser.parse_args() args = parser.parse_args()

View File

@@ -45,13 +45,10 @@ class Dataset:
variant = self.DEFAULT_VARIANT variant = self.DEFAULT_VARIANT
if variant not in self.VARIANTS: if variant not in self.VARIANTS:
raise ValueError("Invalid test variant!") raise ValueError("Invalid test variant!")
if (self.FILES and variant not in self.FILES) and \ if (self.FILES and variant not in self.FILES) and (self.URLS and variant not in self.URLS):
(self.URLS and variant not in self.URLS): raise ValueError("The variant doesn't have a defined URL or " "file path!")
raise ValueError("The variant doesn't have a defined URL or "
"file path!")
if variant not in self.SIZES: if variant not in self.SIZES:
raise ValueError("The variant doesn't have a defined dataset " raise ValueError("The variant doesn't have a defined dataset " "size!")
"size!")
self._variant = variant self._variant = variant
if self.FILES is not None: if self.FILES is not None:
self._file = self.FILES.get(variant, None) self._file = self.FILES.get(variant, None)
@@ -63,8 +60,7 @@ class Dataset:
self._url = None self._url = None
self._size = self.SIZES[variant] self._size = self.SIZES[variant]
if "vertices" not in self._size or "edges" not in self._size: if "vertices" not in self._size or "edges" not in self._size:
raise ValueError("The size defined for this variant doesn't " raise ValueError("The size defined for this variant doesn't " "have the number of vertices and/or edges!")
"have the number of vertices and/or edges!")
self._num_vertices = self._size["vertices"] self._num_vertices = self._size["vertices"]
self._num_edges = self._size["edges"] self._num_edges = self._size["edges"]
@@ -76,8 +72,7 @@ class Dataset:
cached_input, exists = directory.get_file("dataset.cypher") cached_input, exists = directory.get_file("dataset.cypher")
if not exists: if not exists:
print("Downloading dataset file:", self._url) print("Downloading dataset file:", self._url)
downloaded_file = helpers.download_file( downloaded_file = helpers.download_file(self._url, directory.get_path())
self._url, directory.get_path())
print("Unpacking and caching file:", downloaded_file) print("Unpacking and caching file:", downloaded_file)
helpers.unpack_and_move_file(downloaded_file, cached_input) helpers.unpack_and_move_file(downloaded_file, cached_input)
print("Using cached dataset file:", cached_input) print("Using cached dataset file:", cached_input)
@@ -137,18 +132,20 @@ class Pokec(Dataset):
# Arango benchmarks # Arango benchmarks
def benchmark__arango__single_vertex_read(self): def benchmark__arango__single_vertex_read(self):
return ("MATCH (n:User {id : $id}) RETURN n", return ("MATCH (n:User {id : $id}) RETURN n", {"id": self._get_random_vertex()})
{"id": self._get_random_vertex()})
def benchmark__arango__single_vertex_write(self): def benchmark__arango__single_vertex_write(self):
return ("CREATE (n:UserTemp {id : $id}) RETURN n", return (
{"id": random.randint(1, self._num_vertices * 10)}) "CREATE (n:UserTemp {id : $id}) RETURN n",
{"id": random.randint(1, self._num_vertices * 10)},
)
def benchmark__arango__single_edge_write(self): def benchmark__arango__single_edge_write(self):
vertex_from, vertex_to = self._get_random_from_to() vertex_from, vertex_to = self._get_random_from_to()
return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " return (
"CREATE (n)-[e:Temp]->(m) RETURN e", "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " "CREATE (n)-[e:Temp]->(m) RETURN e",
{"from": vertex_from, "to": vertex_to}) {"from": vertex_from, "to": vertex_to},
)
def benchmark__arango__aggregate(self): def benchmark__arango__aggregate(self):
return ("MATCH (n:User) RETURN n.age, COUNT(*)", {}) return ("MATCH (n:User) RETURN n.age, COUNT(*)", {})
@@ -157,92 +154,103 @@ class Pokec(Dataset):
return ("MATCH (n:User) WHERE n.age >= 18 RETURN n.age, COUNT(*)", {}) return ("MATCH (n:User) WHERE n.age >= 18 RETURN n.age, COUNT(*)", {})
def benchmark__arango__expansion_1(self): def benchmark__arango__expansion_1(self):
return ("MATCH (s:User {id: $id})-->(n:User) " return (
"RETURN n.id", "MATCH (s:User {id: $id})-->(n:User) " "RETURN n.id",
{"id": self._get_random_vertex()}) {"id": self._get_random_vertex()},
)
def benchmark__arango__expansion_1_with_filter(self): def benchmark__arango__expansion_1_with_filter(self):
return ("MATCH (s:User {id: $id})-->(n:User) " return (
"WHERE n.age >= 18 " "MATCH (s:User {id: $id})-->(n:User) " "WHERE n.age >= 18 " "RETURN n.id",
"RETURN n.id", {"id": self._get_random_vertex()},
{"id": self._get_random_vertex()}) )
def benchmark__arango__expansion_2(self): def benchmark__arango__expansion_2(self):
return ("MATCH (s:User {id: $id})-->()-->(n:User) " return (
"RETURN DISTINCT n.id", "MATCH (s:User {id: $id})-->()-->(n:User) " "RETURN DISTINCT n.id",
{"id": self._get_random_vertex()}) {"id": self._get_random_vertex()},
)
def benchmark__arango__expansion_2_with_filter(self): def benchmark__arango__expansion_2_with_filter(self):
return ("MATCH (s:User {id: $id})-->()-->(n:User) " return (
"WHERE n.age >= 18 " "MATCH (s:User {id: $id})-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id",
"RETURN DISTINCT n.id", {"id": self._get_random_vertex()},
{"id": self._get_random_vertex()}) )
def benchmark__arango__expansion_3(self): def benchmark__arango__expansion_3(self):
return ("MATCH (s:User {id: $id})-->()-->()-->(n:User) " return (
"RETURN DISTINCT n.id", "MATCH (s:User {id: $id})-->()-->()-->(n:User) " "RETURN DISTINCT n.id",
{"id": self._get_random_vertex()}) {"id": self._get_random_vertex()},
)
def benchmark__arango__expansion_3_with_filter(self): def benchmark__arango__expansion_3_with_filter(self):
return ("MATCH (s:User {id: $id})-->()-->()-->(n:User) " return (
"WHERE n.age >= 18 " "MATCH (s:User {id: $id})-->()-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id",
"RETURN DISTINCT n.id", {"id": self._get_random_vertex()},
{"id": self._get_random_vertex()}) )
def benchmark__arango__expansion_4(self): def benchmark__arango__expansion_4(self):
return ("MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " return (
"RETURN DISTINCT n.id", "MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " "RETURN DISTINCT n.id",
{"id": self._get_random_vertex()}) {"id": self._get_random_vertex()},
)
def benchmark__arango__expansion_4_with_filter(self): def benchmark__arango__expansion_4_with_filter(self):
return ("MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " return (
"WHERE n.age >= 18 " "MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id",
"RETURN DISTINCT n.id", {"id": self._get_random_vertex()},
{"id": self._get_random_vertex()}) )
def benchmark__arango__neighbours_2(self): def benchmark__arango__neighbours_2(self):
return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " return (
"RETURN DISTINCT n.id", "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "RETURN DISTINCT n.id",
{"id": self._get_random_vertex()}) {"id": self._get_random_vertex()},
)
def benchmark__arango__neighbours_2_with_filter(self): def benchmark__arango__neighbours_2_with_filter(self):
return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " return (
"WHERE n.age >= 18 " "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id",
"RETURN DISTINCT n.id", {"id": self._get_random_vertex()},
{"id": self._get_random_vertex()}) )
def benchmark__arango__neighbours_2_with_data(self): def benchmark__arango__neighbours_2_with_data(self):
return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " return (
"RETURN DISTINCT n.id, n", "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "RETURN DISTINCT n.id, n",
{"id": self._get_random_vertex()}) {"id": self._get_random_vertex()},
)
def benchmark__arango__neighbours_2_with_data_and_filter(self): def benchmark__arango__neighbours_2_with_data_and_filter(self):
return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " return (
"WHERE n.age >= 18 " "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id, n",
"RETURN DISTINCT n.id, n", {"id": self._get_random_vertex()},
{"id": self._get_random_vertex()}) )
def benchmark__arango__shortest_path(self): def benchmark__arango__shortest_path(self):
vertex_from, vertex_to = self._get_random_from_to() vertex_from, vertex_to = self._get_random_from_to()
return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " return (
"MATCH p=(n)-[*bfs..15]->(m) " "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m "
"RETURN extract(n in nodes(p) | n.id) AS path", "MATCH p=(n)-[*bfs..15]->(m) "
{"from": vertex_from, "to": vertex_to}) "RETURN extract(n in nodes(p) | n.id) AS path",
{"from": vertex_from, "to": vertex_to},
)
def benchmark__arango__shortest_path_with_filter(self): def benchmark__arango__shortest_path_with_filter(self):
vertex_from, vertex_to = self._get_random_from_to() vertex_from, vertex_to = self._get_random_from_to()
return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " return (
"MATCH p=(n)-[*bfs..15 (e, n | n.age >= 18)]->(m) " "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m "
"RETURN extract(n in nodes(p) | n.id) AS path", "MATCH p=(n)-[*bfs..15 (e, n | n.age >= 18)]->(m) "
{"from": vertex_from, "to": vertex_to}) "RETURN extract(n in nodes(p) | n.id) AS path",
{"from": vertex_from, "to": vertex_to},
)
# Our benchmark queries # Our benchmark queries
def benchmark__create__edge(self): def benchmark__create__edge(self):
vertex_from, vertex_to = self._get_random_from_to() vertex_from, vertex_to = self._get_random_from_to()
return ("MATCH (a:User {id: $from}), (b:User {id: $to}) " return (
"CREATE (a)-[:TempEdge]->(b)", "MATCH (a:User {id: $from}), (b:User {id: $to}) " "CREATE (a)-[:TempEdge]->(b)",
{"from": vertex_from, "to": vertex_to}) {"from": vertex_from, "to": vertex_to},
)
def benchmark__create__pattern(self): def benchmark__create__pattern(self):
return ("CREATE ()-[:TempEdge]->()", {}) return ("CREATE ()-[:TempEdge]->()", {})
@@ -251,9 +259,12 @@ class Pokec(Dataset):
return ("CREATE ()", {}) return ("CREATE ()", {})
def benchmark__create__vertex_big(self): def benchmark__create__vertex_big(self):
return ("CREATE (:L1:L2:L3:L4:L5:L6:L7 {p1: true, p2: 42, " return (
"p3: \"Here is some text that is not extremely short\", " "CREATE (:L1:L2:L3:L4:L5:L6:L7 {p1: true, p2: 42, "
"p4:\"Short text\", p5: 234.434, p6: 11.11, p7: false})", {}) 'p3: "Here is some text that is not extremely short", '
'p4:"Short text", p5: 234.434, p6: 11.11, p7: false})',
{},
)
def benchmark__aggregation__count(self): def benchmark__aggregation__count(self):
return ("MATCH (n) RETURN count(n), count(n.age)", {}) return ("MATCH (n) RETURN count(n), count(n.age)", {})
@@ -262,29 +273,31 @@ class Pokec(Dataset):
return ("MATCH (n) RETURN min(n.age), max(n.age), avg(n.age)", {}) return ("MATCH (n) RETURN min(n.age), max(n.age), avg(n.age)", {})
def benchmark__match__pattern_cycle(self): def benchmark__match__pattern_cycle(self):
return ("MATCH (n:User {id: $id})-[e1]->(m)-[e2]->(n) " return (
"RETURN e1, m, e2", "MATCH (n:User {id: $id})-[e1]->(m)-[e2]->(n) " "RETURN e1, m, e2",
{"id": self._get_random_vertex()}) {"id": self._get_random_vertex()},
)
def benchmark__match__pattern_long(self): def benchmark__match__pattern_long(self):
return ("MATCH (n1:User {id: $id})-[e1]->(n2)-[e2]->" return (
"(n3)-[e3]->(n4)<-[e4]-(n5) " "MATCH (n1:User {id: $id})-[e1]->(n2)-[e2]->" "(n3)-[e3]->(n4)<-[e4]-(n5) " "RETURN n5 LIMIT 1",
"RETURN n5 LIMIT 1", {"id": self._get_random_vertex()},
{"id": self._get_random_vertex()}) )
def benchmark__match__pattern_short(self): def benchmark__match__pattern_short(self):
return ("MATCH (n:User {id: $id})-[e]->(m) " return (
"RETURN m LIMIT 1", "MATCH (n:User {id: $id})-[e]->(m) " "RETURN m LIMIT 1",
{"id": self._get_random_vertex()}) {"id": self._get_random_vertex()},
)
def benchmark__match__vertex_on_label_property(self): def benchmark__match__vertex_on_label_property(self):
return ("MATCH (n:User) WITH n WHERE n.id = $id RETURN n", return (
{"id": self._get_random_vertex()}) "MATCH (n:User) WITH n WHERE n.id = $id RETURN n",
{"id": self._get_random_vertex()},
)
def benchmark__match__vertex_on_label_property_index(self): def benchmark__match__vertex_on_label_property_index(self):
return ("MATCH (n:User {id: $id}) RETURN n", return ("MATCH (n:User {id: $id}) RETURN n", {"id": self._get_random_vertex()})
{"id": self._get_random_vertex()})
def benchmark__match__vertex_on_property(self): def benchmark__match__vertex_on_property(self):
return ("MATCH (n {id: $id}) RETURN n", return ("MATCH (n {id: $id}) RETURN n", {"id": self._get_random_vertex()})
{"id": self._get_random_vertex()})

View File

@@ -28,18 +28,21 @@ def get_binary_path(path, base=""):
def download_file(url, path): def download_file(url, path):
ret = subprocess.run(["wget", "-nv", "--content-disposition", url], ret = subprocess.run(
stderr=subprocess.PIPE, cwd=path, check=True) ["wget", "-nv", "--content-disposition", url],
stderr=subprocess.PIPE,
cwd=path,
check=True,
)
data = ret.stderr.decode("utf-8") data = ret.stderr.decode("utf-8")
tmp = data.split("->")[1] tmp = data.split("->")[1]
name = tmp[tmp.index('"') + 1:tmp.rindex('"')] name = tmp[tmp.index('"') + 1 : tmp.rindex('"')]
return os.path.join(path, name) return os.path.join(path, name)
def unpack_and_move_file(input_path, output_path): def unpack_and_move_file(input_path, output_path):
if input_path.endswith(".gz"): if input_path.endswith(".gz"):
subprocess.run(["gunzip", input_path], subprocess.run(["gunzip", input_path], stdout=subprocess.DEVNULL, check=True)
stdout=subprocess.DEVNULL, check=True)
input_path = input_path[:-3] input_path = input_path[:-3]
os.rename(input_path, output_path) os.rename(input_path, output_path)

View File

@@ -40,8 +40,7 @@ def _convert_args_to_flags(*args, **kwargs):
def _get_usage(pid): def _get_usage(pid):
total_cpu = 0 total_cpu = 0
with open("/proc/{}/stat".format(pid)) as f: with open("/proc/{}/stat".format(pid)) as f:
total_cpu = (sum(map(int, f.read().split(")")[1].split()[11:15])) / total_cpu = sum(map(int, f.read().split(")")[1].split()[11:15])) / os.sysconf(os.sysconf_names["SC_CLK_TCK"])
os.sysconf(os.sysconf_names["SC_CLK_TCK"]))
peak_rss = 0 peak_rss = 0
with open("/proc/{}/status".format(pid)) as f: with open("/proc/{}/status".format(pid)) as f:
for row in f: for row in f:
@@ -60,10 +59,8 @@ class Memgraph:
atexit.register(self._cleanup) atexit.register(self._cleanup)
# Determine Memgraph version # Determine Memgraph version
ret = subprocess.run([memgraph_binary, "--version"], ret = subprocess.run([memgraph_binary, "--version"], stdout=subprocess.PIPE, check=True)
stdout=subprocess.PIPE, check=True) version = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", ret.stdout.decode("utf-8")).group(0)
version = re.search(r"[0-9]+\.[0-9]+\.[0-9]+",
ret.stdout.decode("utf-8")).group(0)
self._memgraph_version = tuple(map(int, version.split("."))) self._memgraph_version = tuple(map(int, version.split(".")))
def __del__(self): def __del__(self):
@@ -79,8 +76,7 @@ class Memgraph:
if self._memgraph_version >= (0, 50, 0): if self._memgraph_version >= (0, 50, 0):
kwargs["storage_properties_on_edges"] = self._properties_on_edges kwargs["storage_properties_on_edges"] = self._properties_on_edges
else: else:
assert self._properties_on_edges, \ assert self._properties_on_edges, "Older versions of Memgraph can't disable properties on edges!"
"Older versions of Memgraph can't disable properties on edges!"
return _convert_args_to_flags(self._memgraph_binary, **kwargs) return _convert_args_to_flags(self._memgraph_binary, **kwargs)
def _start(self, **kwargs): def _start(self, **kwargs):
@@ -94,8 +90,7 @@ class Memgraph:
raise Exception("The database process died prematurely!") raise Exception("The database process died prematurely!")
wait_for_server(7687) wait_for_server(7687)
ret = self._proc_mg.poll() ret = self._proc_mg.poll()
assert ret is None, "The database process died prematurely " \ assert ret is None, "The database process died prematurely " "({})!".format(ret)
"({})!".format(ret)
def _cleanup(self): def _cleanup(self):
if self._proc_mg is None: if self._proc_mg is None:
@@ -121,8 +116,7 @@ class Memgraph:
def stop(self): def stop(self):
ret, usage = self._cleanup() ret, usage = self._cleanup()
assert ret == 0, "The database process exited with a non-zero " \ assert ret == 0, "The database process exited with a non-zero " "status ({})!".format(ret)
"status ({})!".format(ret)
return usage return usage
@@ -135,8 +129,7 @@ class Client:
return _convert_args_to_flags(self._client_binary, **kwargs) return _convert_args_to_flags(self._client_binary, **kwargs)
def execute(self, queries=None, file_path=None, num_workers=1): def execute(self, queries=None, file_path=None, num_workers=1):
if (queries is None and file_path is None) or \ if (queries is None and file_path is None) or (queries is not None and file_path is not None):
(queries is not None and file_path is not None):
raise ValueError("Either queries or input_path must be specified!") raise ValueError("Either queries or input_path must be specified!")
# TODO: check `file_path.endswith(".json")` to support advanced # TODO: check `file_path.endswith(".json")` to support advanced
@@ -151,8 +144,7 @@ class Client:
json.dump(query, f) json.dump(query, f)
f.write("\n") f.write("\n")
args = self._get_args(input=file_path, num_workers=num_workers, args = self._get_args(input=file_path, num_workers=num_workers, queries_json=queries_json)
queries_json=queries_json)
ret = subprocess.run(args, stdout=subprocess.PIPE, check=True) ret = subprocess.run(args, stdout=subprocess.PIPE, check=True)
data = ret.stdout.decode("utf-8").strip().split("\n") data = ret.stdout.decode("utf-8").strip().split("\n")
return list(map(json.loads, data)) return list(map(json.loads, data))

View File

@@ -12,44 +12,60 @@
# by the Apache License, Version 2.0, included in the file # by the Apache License, Version 2.0, included in the file
# licenses/APL.txt. # licenses/APL.txt.
''' """
Large bipartite graph stress test. Large bipartite graph stress test.
''' """
import logging import logging
import multiprocessing import multiprocessing
import time import time
import atexit import atexit
from common import connection_argument_parser, assert_equal, \ from common import (
OutputData, execute_till_success, \ connection_argument_parser,
batch, render, SessionCache assert_equal,
OutputData,
execute_till_success,
batch,
render,
SessionCache,
)
def parse_args(): def parse_args():
''' """
Parses user arguments Parses user arguments
:return: parsed arguments :return: parsed arguments
''' """
parser = connection_argument_parser() parser = connection_argument_parser()
parser.add_argument('--worker-count', type=int, parser.add_argument(
default=multiprocessing.cpu_count(), "--worker-count",
help='Number of concurrent workers.') type=int,
parser.add_argument("--logging", default="INFO", default=multiprocessing.cpu_count(),
choices=["INFO", "DEBUG", "WARNING", "ERROR"], help="Number of concurrent workers.",
help="Logging level") )
parser.add_argument('--u-count', type=int, default=100, parser.add_argument(
help='Size of U set in the bipartite graph.') "--logging",
parser.add_argument('--v-count', type=int, default=100, default="INFO",
help='Size of V set in the bipartite graph.') choices=["INFO", "DEBUG", "WARNING", "ERROR"],
parser.add_argument('--vertex-batch-size', type=int, default=100, help="Logging level",
help="Create vertices in batches of this size.") )
parser.add_argument('--edge-batching', action='store_true', parser.add_argument("--u-count", type=int, default=100, help="Size of U set in the bipartite graph.")
help='Create edges in batches.') parser.add_argument("--v-count", type=int, default=100, help="Size of V set in the bipartite graph.")
parser.add_argument('--edge-batch-size', type=int, default=100, parser.add_argument(
help='Number of edges in a batch when edges ' "--vertex-batch-size",
'are created in batches.') type=int,
default=100,
help="Create vertices in batches of this size.",
)
parser.add_argument("--edge-batching", action="store_true", help="Create edges in batches.")
parser.add_argument(
"--edge-batch-size",
type=int,
default=100,
help="Number of edges in a batch when edges " "are created in batches.",
)
return parser.parse_args() return parser.parse_args()
@@ -62,18 +78,18 @@ atexit.register(SessionCache.cleanup)
def create_u_v_edges(u): def create_u_v_edges(u):
''' """
Creates nodes and checks that all nodes were created. Creates nodes and checks that all nodes were created.
create edges from one vertex in U set to all vertex of V set create edges from one vertex in U set to all vertex of V set
:param worker_id: worker id :param worker_id: worker id
:return: tuple (worker_id, create execution time, time unit) :return: tuple (worker_id, create execution time, time unit)
''' """
start_time = time.time() start_time = time.time()
session = SessionCache.argument_session(args) session = SessionCache.argument_session(args)
no_failures = 0 no_failures = 0
match_u = 'MATCH (u:U {id: %d})' % u match_u = "MATCH (u:U {id: %d})" % u
if args.edge_batching: if args.edge_batching:
# TODO: try to randomize execution, the execution time should # TODO: try to randomize execution, the execution time should
# be smaller, add randomize flag # be smaller, add randomize flag
@@ -83,143 +99,126 @@ def create_u_v_edges(u):
query = match_u + "".join(match_v) + "".join(create_u) query = match_u + "".join(match_v) + "".join(create_u)
no_failures += execute_till_success(session, query)[1] no_failures += execute_till_success(session, query)[1]
else: else:
no_failures += execute_till_success( no_failures += execute_till_success(session, match_u + " MATCH (v:V) CREATE (u)-[:R]->(v)")[1]
session, match_u + ' MATCH (v:V) CREATE (u)-[:R]->(v)')[1]
end_time = time.time() end_time = time.time()
return u, end_time - start_time, "s", no_failures return u, end_time - start_time, "s", no_failures
def traverse_from_u_worker(u): def traverse_from_u_worker(u):
''' """
Traverses edges starting from an element of U set. Traverses edges starting from an element of U set.
Traversed labels are: :U -> :V -> :U. Traversed labels are: :U -> :V -> :U.
''' """
session = SessionCache.argument_session(args) session = SessionCache.argument_session(args)
start_time = time.time() start_time = time.time()
assert_equal( assert_equal(
args.u_count * args.v_count - args.v_count, # cypher morphism args.u_count * args.v_count - args.v_count, # cypher morphism
session.run("MATCH (u1:U {id: %s})-[e1]->(v:V)<-[e2]-(u2:U) " session.run("MATCH (u1:U {id: %s})-[e1]->(v:V)<-[e2]-(u2:U) " "RETURN count(v) AS cnt" % u).data()[0]["cnt"],
"RETURN count(v) AS cnt" % u).data()[0]['cnt'], "Number of traversed edges started " "from U(id:%s) is wrong!. " % u + "Expected: %s Actual: %s",
"Number of traversed edges started " )
"from U(id:%s) is wrong!. " % u +
"Expected: %s Actual: %s")
end_time = time.time() end_time = time.time()
return u, end_time - start_time, 's' return u, end_time - start_time, "s"
def traverse_from_v_worker(v): def traverse_from_v_worker(v):
''' """
Traverses edges starting from an element of V set. Traverses edges starting from an element of V set.
Traversed labels are: :V -> :U -> :V. Traversed labels are: :V -> :U -> :V.
''' """
session = SessionCache.argument_session(args) session = SessionCache.argument_session(args)
start_time = time.time() start_time = time.time()
assert_equal( assert_equal(
args.u_count * args.v_count - args.u_count, # cypher morphism args.u_count * args.v_count - args.u_count, # cypher morphism
session.run("MATCH (v1:V {id: %s})<-[e1]-(u:U)-[e2]->(v2:V) " session.run("MATCH (v1:V {id: %s})<-[e1]-(u:U)-[e2]->(v2:V) " "RETURN count(u) AS cnt" % v).data()[0]["cnt"],
"RETURN count(u) AS cnt" % v).data()[0]['cnt'], "Number of traversed edges started " "from V(id:%s) is wrong!. " % v + "Expected: %s Actual: %s",
"Number of traversed edges started " )
"from V(id:%s) is wrong!. " % v +
"Expected: %s Actual: %s")
end_time = time.time() end_time = time.time()
return v, end_time - start_time, 's' return v, end_time - start_time, "s"
def execution_handler(): def execution_handler():
''' """
Initializes client processes, database and starts the execution. Initializes client processes, database and starts the execution.
''' """
# instance cleanup # instance cleanup
session = SessionCache.argument_session(args) session = SessionCache.argument_session(args)
start_time = time.time() start_time = time.time()
# clean existing database # clean existing database
session.run('MATCH (n) DETACH DELETE n').consume() session.run("MATCH (n) DETACH DELETE n").consume()
cleanup_end_time = time.time() cleanup_end_time = time.time()
output_data.add_measurement("cleanup_time", output_data.add_measurement("cleanup_time", cleanup_end_time - start_time)
cleanup_end_time - start_time)
log.info("Database is clean.") log.info("Database is clean.")
# create indices # create indices
session.run('CREATE INDEX ON :U').consume() session.run("CREATE INDEX ON :U").consume()
session.run('CREATE INDEX ON :V').consume() session.run("CREATE INDEX ON :V").consume()
# create U vertices # create U vertices
for b in batch(render('CREATE (:U {{id: {}}})', range(args.u_count)), for b in batch(render("CREATE (:U {{id: {}}})", range(args.u_count)), args.vertex_batch_size):
args.vertex_batch_size):
session.run(" ".join(b)).consume() session.run(" ".join(b)).consume()
# create V vertices # create V vertices
for b in batch(render('CREATE (:V {{id: {}}})', range(args.v_count)), for b in batch(render("CREATE (:V {{id: {}}})", range(args.v_count)), args.vertex_batch_size):
args.vertex_batch_size):
session.run(" ".join(b)).consume() session.run(" ".join(b)).consume()
vertices_create_end_time = time.time() vertices_create_end_time = time.time()
output_data.add_measurement( output_data.add_measurement("vertices_create_time", vertices_create_end_time - cleanup_end_time)
'vertices_create_time',
vertices_create_end_time - cleanup_end_time)
log.info("All nodes created.") log.info("All nodes created.")
# concurrent create execution & tests # concurrent create execution & tests
with multiprocessing.Pool(args.worker_count) as p: with multiprocessing.Pool(args.worker_count) as p:
create_edges_start_time = time.time() create_edges_start_time = time.time()
for worker_id, create_time, time_unit, no_failures in \ for worker_id, create_time, time_unit, no_failures in p.map(create_u_v_edges, [i for i in range(args.u_count)]):
p.map(create_u_v_edges, [i for i in range(args.u_count)]): log.info("Worker ID: %s; Create time: %s%s Failures: %s" % (worker_id, create_time, time_unit, no_failures))
log.info('Worker ID: %s; Create time: %s%s Failures: %s' %
(worker_id, create_time, time_unit, no_failures))
create_edges_end_time = time.time() create_edges_end_time = time.time()
output_data.add_measurement( output_data.add_measurement("edges_create_time", create_edges_end_time - create_edges_start_time)
'edges_create_time',
create_edges_end_time - create_edges_start_time)
# check total number of edges # check total number of edges
assert_equal( assert_equal(
args.v_count * args.u_count, args.v_count * args.u_count,
session.run( session.run("MATCH ()-[r]->() " "RETURN count(r) AS cnt").data()[0]["cnt"],
'MATCH ()-[r]->() ' "Total number of edges isn't correct! Expected: %s Actual: %s",
'RETURN count(r) AS cnt').data()[0]['cnt'], )
"Total number of edges isn't correct! Expected: %s Actual: %s")
# check traversals starting from all elements of U # check traversals starting from all elements of U
traverse_from_u_start_time = time.time() traverse_from_u_start_time = time.time()
for u, traverse_u_time, time_unit in \ for u, traverse_u_time, time_unit in p.map(traverse_from_u_worker, [i for i in range(args.u_count)]):
p.map(traverse_from_u_worker,
[i for i in range(args.u_count)]):
log.info("U {id: %s} %s%s" % (u, traverse_u_time, time_unit)) log.info("U {id: %s} %s%s" % (u, traverse_u_time, time_unit))
traverse_from_u_end_time = time.time() traverse_from_u_end_time = time.time()
output_data.add_measurement( output_data.add_measurement(
'traverse_from_u_time', "traverse_from_u_time",
traverse_from_u_end_time - traverse_from_u_start_time) traverse_from_u_end_time - traverse_from_u_start_time,
)
# check traversals starting from all elements of V # check traversals starting from all elements of V
traverse_from_v_start_time = time.time() traverse_from_v_start_time = time.time()
for v, traverse_v_time, time_unit in \ for v, traverse_v_time, time_unit in p.map(traverse_from_v_worker, [i for i in range(args.v_count)]):
p.map(traverse_from_v_worker,
[i for i in range(args.v_count)]):
log.info("V {id: %s} %s%s" % (v, traverse_v_time, time_unit)) log.info("V {id: %s} %s%s" % (v, traverse_v_time, time_unit))
traverse_from_v_end_time = time.time() traverse_from_v_end_time = time.time()
output_data.add_measurement( output_data.add_measurement(
'traverse_from_v_time', "traverse_from_v_time",
traverse_from_v_end_time - traverse_from_v_start_time) traverse_from_v_end_time - traverse_from_v_start_time,
)
# check total number of vertices # check total number of vertices
assert_equal( assert_equal(
args.v_count + args.u_count, args.v_count + args.u_count,
session.run('MATCH (n) RETURN count(n) AS cnt').data()[0]['cnt'], session.run("MATCH (n) RETURN count(n) AS cnt").data()[0]["cnt"],
"Total number of vertices isn't correct! Expected: %s Actual: %s") "Total number of vertices isn't correct! Expected: %s Actual: %s",
)
# check total number of edges # check total number of edges
assert_equal( assert_equal(
args.v_count * args.u_count, args.v_count * args.u_count,
session.run( session.run("MATCH ()-[r]->() RETURN count(r) AS cnt").data()[0]["cnt"],
'MATCH ()-[r]->() RETURN count(r) AS cnt').data()[0]['cnt'], "Total number of edges isn't correct! Expected: %s Actual: %s",
"Total number of edges isn't correct! Expected: %s Actual: %s") )
end_time = time.time() end_time = time.time()
output_data.add_measurement("total_execution_time", output_data.add_measurement("total_execution_time", end_time - start_time)
end_time - start_time)
if __name__ == '__main__': if __name__ == "__main__":
logging.basicConfig(level=args.logging) logging.basicConfig(level=args.logging)
if args.logging != "DEBUG": if args.logging != "DEBUG":
logging.getLogger("neo4j").setLevel(logging.WARNING) logging.getLogger("neo4j").setLevel(logging.WARNING)

View File

@@ -11,12 +11,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' """
Common methods for writing graph database Common methods for writing graph database
integration tests in python. integration tests in python.
Only Bolt communication protocol is supported. Only Bolt communication protocol is supported.
''' """
import contextlib import contextlib
import os import os
@@ -28,9 +28,9 @@ from neo4j import GraphDatabase, TRUST_ALL_CERTIFICATES
class OutputData: class OutputData:
''' """
Encapsulates results and info about the tests. Encapsulates results and info about the tests.
''' """
def __init__(self): def __init__(self):
# data in time format (name, time, unit) # data in time format (name, time, unit)
@@ -39,32 +39,32 @@ class OutputData:
self._statuses = [] self._statuses = []
def add_measurement(self, name, time, unit="s"): def add_measurement(self, name, time, unit="s"):
''' """
Stores measurement. Stores measurement.
:param name: str, name of measurement :param name: str, name of measurement
:param time: float, time value :param time: float, time value
:param unit: str, time unit :param unit: str, time unit
''' """
self._measurements.append((name, time, unit)) self._measurements.append((name, time, unit))
def add_status(self, name, status): def add_status(self, name, status):
''' """
Stores status data point. Stores status data point.
:param name: str, name of data point :param name: str, name of data point
:param status: printable value :param status: printable value
''' """
self._statuses.append((name, status)) self._statuses.append((name, status))
def dump(self, print_f=print): def dump(self, print_f=print):
''' """
Dumps output using the given ouput function. Dumps output using the given ouput function.
Args: Args:
print_f - the function that consumes ouptput. Defaults to print_f - the function that consumes ouptput. Defaults to
the 'print' function. the 'print' function.
''' """
print_f("Output data:") print_f("Output data:")
for name, status in self._statuses: for name, status in self._statuses:
print_f(" %s: %s" % (name, status)) print_f(" %s: %s" % (name, status))
@@ -73,7 +73,7 @@ class OutputData:
def execute_till_success(session, query, max_retries=1000): def execute_till_success(session, query, max_retries=1000):
''' """
Executes a query within Bolt session until the query is Executes a query within Bolt session until the query is
successfully executed against the database. successfully executed against the database.
@@ -86,7 +86,7 @@ def execute_till_success(session, query, max_retries=1000):
:param query: query to execute :param query: query to execute
:return: tuple (results_data_list, number_of_failures, result_summary) :return: tuple (results_data_list, number_of_failures, result_summary)
''' """
no_failures = 0 no_failures = 0
while True: while True:
try: try:
@@ -97,12 +97,11 @@ def execute_till_success(session, query, max_retries=1000):
except Exception: except Exception:
no_failures += 1 no_failures += 1
if no_failures >= max_retries: if no_failures >= max_retries:
raise Exception("Query '%s' failed %d times, aborting" % raise Exception("Query '%s' failed %d times, aborting" % (query, max_retries))
(query, max_retries))
def batch(input, batch_size): def batch(input, batch_size):
""" Batches the given input (must be iterable). """Batches the given input (must be iterable).
Supports input generators. Returns a generator. Supports input generators. Returns a generator.
All is lazy. The last batch can contain less elements All is lazy. The last batch can contain less elements
then `batch_size`, but is for sure more then zero. then `batch_size`, but is for sure more then zero.
@@ -134,7 +133,7 @@ def render(template, iterable_arguments):
def assert_equal(expected, actual, message): def assert_equal(expected, actual, message):
''' """
Compares expected and actual values. If values are not the same terminate Compares expected and actual values. If values are not the same terminate
the execution. the execution.
@@ -142,45 +141,41 @@ def assert_equal(expected, actual, message):
:param actual: actual value :param actual: actual value
:param message: str, message in case that the values are not equal, must :param message: str, message in case that the values are not equal, must
contain two placeholders (%s) to print the values. contain two placeholders (%s) to print the values.
''' """
assert expected == actual, message % (expected, actual) assert expected == actual, message % (expected, actual)
def connection_argument_parser(): def connection_argument_parser():
''' """
Parses arguments related to establishing database connection like Parses arguments related to establishing database connection like
host, port, username, etc. host, port, username, etc.
:return: An instance of ArgumentParser :return: An instance of ArgumentParser
''' """
parser = ArgumentParser(description=__doc__) parser = ArgumentParser(description=__doc__)
parser.add_argument('--endpoint', type=str, default='127.0.0.1:7687', parser.add_argument(
help='DBMS instance endpoint. ' "--endpoint",
'Bolt protocol is the only option.') type=str,
parser.add_argument('--username', type=str, default='neo4j', default="127.0.0.1:7687",
help='DBMS instance username.') help="DBMS instance endpoint. " "Bolt protocol is the only option.",
parser.add_argument('--password', type=int, default='1234', )
help='DBMS instance password.') parser.add_argument("--username", type=str, default="neo4j", help="DBMS instance username.")
parser.add_argument('--use-ssl', action='store_true', parser.add_argument("--password", type=int, default="1234", help="DBMS instance password.")
help="Is SSL enabled?") parser.add_argument("--use-ssl", action="store_true", help="Is SSL enabled?")
return parser return parser
@contextlib.contextmanager @contextlib.contextmanager
def bolt_session(url, auth, ssl=False): def bolt_session(url, auth, ssl=False):
''' """
with wrapper around Bolt session. with wrapper around Bolt session.
:param url: str, e.g. "bolt://127.0.0.1:7687" :param url: str, e.g. "bolt://127.0.0.1:7687"
:param auth: auth method, goes directly to the Bolt driver constructor :param auth: auth method, goes directly to the Bolt driver constructor
:param ssl: bool, is ssl enabled :param ssl: bool, is ssl enabled
''' """
driver = GraphDatabase.driver( driver = GraphDatabase.driver(url, auth=auth, encrypted=ssl, trust=TRUST_ALL_CERTIFICATES)
url,
auth=auth,
encrypted=ssl,
trust=TRUST_ALL_CERTIFICATES)
session = driver.session() session = driver.session()
try: try:
yield session yield session
@@ -192,19 +187,20 @@ def bolt_session(url, auth, ssl=False):
# If you are using session with multiprocessing take a look at SesssionCache # If you are using session with multiprocessing take a look at SesssionCache
# in bipartite for an idea how to reuse sessions. # in bipartite for an idea how to reuse sessions.
def argument_session(args): def argument_session(args):
''' """
:return: Bolt session context manager based on program arguments :return: Bolt session context manager based on program arguments
''' """
return bolt_session('bolt://' + args.endpoint, return bolt_session("bolt://" + args.endpoint, (args.username, str(args.password)), args.use_ssl)
(args.username, str(args.password)),
args.use_ssl)
def argument_driver(args): def argument_driver(args):
return GraphDatabase.driver( return GraphDatabase.driver(
'bolt://' + args.endpoint, "bolt://" + args.endpoint,
auth=(args.username, str(args.password)), auth=(args.username, str(args.password)),
encrypted=args.use_ssl, trust=TRUST_ALL_CERTIFICATES) encrypted=args.use_ssl,
trust=TRUST_ALL_CERTIFICATES,
)
# This class is used to create and cache sessions. Session is cached by args # This class is used to create and cache sessions. Session is cached by args
# used to create it and process' pid in which it was created. This makes it # used to create it and process' pid in which it was created. This makes it
@@ -219,8 +215,8 @@ class SessionCache:
key = tuple(vars(args).items()) + (os.getpid(),) key = tuple(vars(args).items()) + (os.getpid(),)
if key in SessionCache.cache: if key in SessionCache.cache:
return SessionCache.cache[key][1] return SessionCache.cache[key][1]
driver = argument_driver(args) # | driver = argument_driver(args) # |
session = driver.session() # V session = driver.session() # V
SessionCache.cache[key] = (driver, session) SessionCache.cache[key] = (driver, session)
return session return session
@@ -241,6 +237,7 @@ def periodically_execute(callable, args, interval, daemon=True):
interval - time (in seconds) between two calls interval - time (in seconds) between two calls
deamon - if the execution thread should be a daemon deamon - if the execution thread should be a daemon
""" """
def periodic_call(): def periodic_call():
while True: while True:
sleep(interval) sleep(interval)

View File

@@ -12,11 +12,11 @@
# by the Apache License, Version 2.0, included in the file # by the Apache License, Version 2.0, included in the file
# licenses/APL.txt. # licenses/APL.txt.
''' """
Large scale stress test. Tests only node creation. Large scale stress test. Tests only node creation.
The idea is to run this test on machines with huge amount of memory e.g. 2TB. The idea is to run this test on machines with huge amount of memory e.g. 2TB.
''' """
import logging import logging
import multiprocessing import multiprocessing
@@ -28,28 +28,41 @@ from common import connection_argument_parser, argument_session
def parse_args(): def parse_args():
''' """
Parses user arguments Parses user arguments
:return: parsed arguments :return: parsed arguments
''' """
parser = connection_argument_parser() parser = connection_argument_parser()
# specific # specific
parser.add_argument('--worker-count', type=int, parser.add_argument(
default=multiprocessing.cpu_count(), "--worker-count",
help='Number of concurrent workers.') type=int,
parser.add_argument("--logging", default="INFO", default=multiprocessing.cpu_count(),
choices=["INFO", "DEBUG", "WARNING", "ERROR"], help="Number of concurrent workers.",
help="Logging level") )
parser.add_argument('--vertex-count', type=int, default=100, parser.add_argument(
help='Number of created vertices.') "--logging",
parser.add_argument('--max-property-value', type=int, default=1000, default="INFO",
help='Maximum value of property - 1. A created node ' choices=["INFO", "DEBUG", "WARNING", "ERROR"],
'will have a property with random value from 0 to ' help="Logging level",
'max_property_value - 1.') )
parser.add_argument('--create-pack-size', type=int, default=1, parser.add_argument("--vertex-count", type=int, default=100, help="Number of created vertices.")
help='Number of CREATE clauses in a query') parser.add_argument(
"--max-property-value",
type=int,
default=1000,
help="Maximum value of property - 1. A created node "
"will have a property with random value from 0 to "
"max_property_value - 1.",
)
parser.add_argument(
"--create-pack-size",
type=int,
default=1,
help="Number of CREATE clauses in a query",
)
return parser.parse_args() return parser.parse_args()
@@ -58,51 +71,49 @@ args = parse_args()
def create_worker(worker_id): def create_worker(worker_id):
''' """
Creates nodes and checks that all nodes were created. Creates nodes and checks that all nodes were created.
:param worker_id: worker id :param worker_id: worker id
:return: tuple (worker_id, create execution time, time unit) :return: tuple (worker_id, create execution time, time unit)
''' """
assert args.vertex_count > 0, 'Number of vertices has to be positive int' assert args.vertex_count > 0, "Number of vertices has to be positive int"
generated_xs = defaultdict(int) generated_xs = defaultdict(int)
create_query = '' create_query = ""
with argument_session(args) as session: with argument_session(args) as session:
# create vertices # create vertices
start_time = time.time() start_time = time.time()
for i in range(0, args.vertex_count): for i in range(0, args.vertex_count):
random_number = random.randint(0, args.max_property_value - 1) random_number = random.randint(0, args.max_property_value - 1)
generated_xs[random_number] += 1 generated_xs[random_number] += 1
create_query += 'CREATE (:Label_T%s {x: %s}) ' % \ create_query += "CREATE (:Label_T%s {x: %s}) " % (worker_id, random_number)
(worker_id, random_number)
# if full back or last item -> execute query # if full back or last item -> execute query
if (i + 1) % args.create_pack_size == 0 or \ if (i + 1) % args.create_pack_size == 0 or i == args.vertex_count - 1:
i == args.vertex_count - 1:
session.run(create_query).consume() session.run(create_query).consume()
create_query = '' create_query = ""
create_time = time.time() create_time = time.time()
# check total count # check total count
result_set = session.run('MATCH (n:Label_T%s) RETURN count(n) AS cnt' % result_set = session.run("MATCH (n:Label_T%s) RETURN count(n) AS cnt" % worker_id).data()[0]
worker_id).data()[0] assert result_set["cnt"] == args.vertex_count, "Create vertices Expected: %s Created: %s" % (
assert result_set['cnt'] == args.vertex_count, \ args.vertex_count,
'Create vertices Expected: %s Created: %s' % \ result_set["cnt"],
(args.vertex_count, result_set['cnt']) )
# check count per property value # check count per property value
for i, size in generated_xs.items(): for i, size in generated_xs.items():
result_set = session.run('MATCH (n:Label_T%s {x: %s}) ' result_set = session.run("MATCH (n:Label_T%s {x: %s}) " "RETURN count(n) AS cnt" % (worker_id, i)).data()[0]
'RETURN count(n) AS cnt' assert result_set["cnt"] == size, "Per x count isn't good " "(Label: Label_T%s, prop x: %s" % (
% (worker_id, i)).data()[0] worker_id,
assert result_set['cnt'] == size, "Per x count isn't good " \ i,
"(Label: Label_T%s, prop x: %s" % (worker_id, i) )
return (worker_id, create_time - start_time, "s") return (worker_id, create_time - start_time, "s")
def create_handler(): def create_handler():
''' """
Initializes processes and starts the execution. Initializes processes and starts the execution.
''' """
# instance cleanup # instance cleanup
with argument_session(args) as session: with argument_session(args) as session:
session.run("MATCH (n) DETACH DELETE n").consume() session.run("MATCH (n) DETACH DELETE n").consume()
@@ -113,21 +124,19 @@ def create_handler():
# concurrent create execution & tests # concurrent create execution & tests
with multiprocessing.Pool(args.worker_count) as p: with multiprocessing.Pool(args.worker_count) as p:
for worker_id, create_time, time_unit in \ for worker_id, create_time, time_unit in p.map(create_worker, [i for i in range(args.worker_count)]):
p.map(create_worker, [i for i in range(args.worker_count)]): log.info("Worker ID: %s; Create time: %s%s" % (worker_id, create_time, time_unit))
log.info('Worker ID: %s; Create time: %s%s' %
(worker_id, create_time, time_unit))
# check total count # check total count
expected_total_count = args.worker_count * args.vertex_count expected_total_count = args.worker_count * args.vertex_count
total_count = session.run( total_count = session.run("MATCH (n) RETURN count(n) AS cnt").data()[0]["cnt"]
'MATCH (n) RETURN count(n) AS cnt').data()[0]['cnt'] assert total_count == expected_total_count, "Total vertex number: %s Expected: %s" % (
assert total_count == expected_total_count, \ total_count,
'Total vertex number: %s Expected: %s' % \ expected_total_count,
(total_count, expected_total_count) )
if __name__ == '__main__': if __name__ == "__main__":
logging.basicConfig(level=args.logging) logging.basicConfig(level=args.logging)
if args.logging != "DEBUG": if args.logging != "DEBUG":
logging.getLogger("neo4j").setLevel(logging.WARNING) logging.getLogger("neo4j").setLevel(logging.WARNING)

View File

@@ -20,9 +20,7 @@ GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY", "")
GITHUB_SHA = os.getenv("GITHUB_SHA", "") GITHUB_SHA = os.getenv("GITHUB_SHA", "")
GITHUB_REF = os.getenv("GITHUB_REF", "") GITHUB_REF = os.getenv("GITHUB_REF", "")
BENCH_GRAPH_SERVER_ENDPOINT = os.getenv( BENCH_GRAPH_SERVER_ENDPOINT = os.getenv("BENCH_GRAPH_SERVER_ENDPOINT", "http://bench-graph-api:9001")
"BENCH_GRAPH_SERVER_ENDPOINT",
"http://bench-graph-api:9001")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -52,12 +50,12 @@ def post_measurement(args):
"github_run_id": args.github_run_id, "github_run_id": args.github_run_id,
"github_run_number": args.github_run_number, "github_run_number": args.github_run_number,
"results": data, "results": data,
"git_branch": args.head_branch_name}, "git_branch": args.head_branch_name,
timeout=1) },
assert req.status_code == 200, \ timeout=1,
f"Uploading {args.benchmark_name} data failed." )
log.info(f"{args.benchmark_name} data sent to " assert req.status_code == 200, f"Uploading {args.benchmark_name} data failed."
f"{BENCH_GRAPH_SERVER_ENDPOINT}") log.info(f"{args.benchmark_name} data sent to " f"{BENCH_GRAPH_SERVER_ENDPOINT}")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -4,14 +4,14 @@ import gdb
def _logical_operator_type(): def _logical_operator_type():
'''Returns the LogicalOperator gdb.Type''' """Returns the LogicalOperator gdb.Type"""
# This is a function, because the type may appear during gdb runtime. # This is a function, because the type may appear during gdb runtime.
# Therefore, we cannot assign it on import. # Therefore, we cannot assign it on import.
return gdb.lookup_type('memgraph::query::plan::LogicalOperator') return gdb.lookup_type("memgraph::query::plan::LogicalOperator")
def _iter_fields_and_base_classes(value): def _iter_fields_and_base_classes(value):
'''Iterate all fields of value.type''' """Iterate all fields of value.type"""
types_to_process = [value.type] types_to_process = [value.type]
while types_to_process: while types_to_process:
for field in types_to_process.pop().fields(): for field in types_to_process.pop().fields():
@@ -21,31 +21,27 @@ def _iter_fields_and_base_classes(value):
def _fields(value): def _fields(value):
'''Return a list of value.type fields.''' """Return a list of value.type fields."""
return [f for f in _iter_fields_and_base_classes(value) return [f for f in _iter_fields_and_base_classes(value) if not f.is_base_class]
if not f.is_base_class]
def _has_field(value, field_name): def _has_field(value, field_name):
'''Return True if value.type has a field named field_name.''' """Return True if value.type has a field named field_name."""
return field_name in [f.name for f in _fields(value)] return field_name in [f.name for f in _fields(value)]
def _base_classes(value): def _base_classes(value):
'''Return a list of base classes for value.type.''' """Return a list of base classes for value.type."""
return [f for f in _iter_fields_and_base_classes(value) return [f for f in _iter_fields_and_base_classes(value) if f.is_base_class]
if f.is_base_class]
def _is_instance(value, type_): def _is_instance(value, type_):
'''Return True if value is an instance of type.''' """Return True if value is an instance of type."""
return value.type.unqualified() == type_ or \ return value.type.unqualified() == type_ or type_ in [base.type for base in _base_classes(value)]
type_ in [base.type for base in _base_classes(value)]
# Pattern for matching std::unique_ptr<T, Deleter> and std::shared_ptr<T> # Pattern for matching std::unique_ptr<T, Deleter> and std::shared_ptr<T>
_SMART_PTR_TYPE_PATTERN = \ _SMART_PTR_TYPE_PATTERN = re.compile("^std::(unique|shared)_ptr<(?P<pointee_type>[\w:]*)")
re.compile('^std::(unique|shared)_ptr<(?P<pointee_type>[\w:]*)')
def _is_smart_ptr(maybe_smart_ptr, type_name=None): def _is_smart_ptr(maybe_smart_ptr, type_name=None):
@@ -55,40 +51,39 @@ def _is_smart_ptr(maybe_smart_ptr, type_name=None):
match = _SMART_PTR_TYPE_PATTERN.match(type_.name) match = _SMART_PTR_TYPE_PATTERN.match(type_.name)
if match is None or type_name is None: if match is None or type_name is None:
return bool(match) return bool(match)
return type_name == match.group('pointee_type') return type_name == match.group("pointee_type")
def _smart_ptr_pointee(smart_ptr): def _smart_ptr_pointee(smart_ptr):
'''Returns the pointer to object in shared_ptr/unique_ptr.''' """Returns the pointer to object in shared_ptr/unique_ptr."""
# This function may not be needed when gdb adds dereferencing # This function may not be needed when gdb adds dereferencing
# shared_ptr/unique_ptr via Python API. # shared_ptr/unique_ptr via Python API.
if _has_field(smart_ptr, '_M_ptr'): if _has_field(smart_ptr, "_M_ptr"):
# shared_ptr # shared_ptr
return smart_ptr['_M_ptr'] return smart_ptr["_M_ptr"]
if _has_field(smart_ptr, '_M_t'): if _has_field(smart_ptr, "_M_t"):
# unique_ptr # unique_ptr
smart_ptr = smart_ptr['_M_t'] smart_ptr = smart_ptr["_M_t"]
if _has_field(smart_ptr, '_M_t'): if _has_field(smart_ptr, "_M_t"):
# Check for one more level of _M_t # Check for one more level of _M_t
smart_ptr = smart_ptr['_M_t'] smart_ptr = smart_ptr["_M_t"]
if _has_field(smart_ptr, '_M_head_impl'): if _has_field(smart_ptr, "_M_head_impl"):
return smart_ptr['_M_head_impl'] return smart_ptr["_M_head_impl"]
def _get_operator_input(operator): def _get_operator_input(operator):
'''Returns the input operator of given operator, if it has any.''' """Returns the input operator of given operator, if it has any."""
if not _has_field(operator, 'input_'): if not _has_field(operator, "input_"):
return None return None
input_op = _smart_ptr_pointee(operator['input_']).dereference() input_op = _smart_ptr_pointee(operator["input_"]).dereference()
return input_op.cast(input_op.dynamic_type) return input_op.cast(input_op.dynamic_type)
class PrintOperatorTree(gdb.Command): class PrintOperatorTree(gdb.Command):
'''Print the tree of logical operators from the expression.''' """Print the tree of logical operators from the expression."""
def __init__(self): def __init__(self):
super(PrintOperatorTree, self).__init__("print-operator-tree", super(PrintOperatorTree, self).__init__("print-operator-tree", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION)
gdb.COMMAND_USER,
gdb.COMPLETE_EXPRESSION)
def invoke(self, argument, from_tty): def invoke(self, argument, from_tty):
try: try:
@@ -98,17 +93,16 @@ class PrintOperatorTree(gdb.Command):
logical_operator_type = _logical_operator_type() logical_operator_type = _logical_operator_type()
if operator.type.code in (gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_REF): if operator.type.code in (gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_REF):
operator = operator.referenced_value() operator = operator.referenced_value()
if _is_smart_ptr(operator, 'memgraph::query::plan::LogicalOperator'): if _is_smart_ptr(operator, "memgraph::query::plan::LogicalOperator"):
operator = _smart_ptr_pointee(operator).dereference() operator = _smart_ptr_pointee(operator).dereference()
if not _is_instance(operator, logical_operator_type): if not _is_instance(operator, logical_operator_type):
raise gdb.GdbError("Expected a '%s', but got '%s'" % raise gdb.GdbError("Expected a '%s', but got '%s'" % (logical_operator_type, operator.type))
(logical_operator_type, operator.type))
next_op = operator.cast(operator.dynamic_type) next_op = operator.cast(operator.dynamic_type)
tree = [] tree = []
while next_op is not None: while next_op is not None:
tree.append('* %s <%s>' % (next_op.type.name, next_op.address)) tree.append("* %s <%s>" % (next_op.type.name, next_op.address))
next_op = _get_operator_input(next_op) next_op = _get_operator_input(next_op)
print('\n'.join(tree)) print("\n".join(tree))
PrintOperatorTree() PrintOperatorTree()

View File

@@ -3,43 +3,49 @@ import gdb.printing
def build_memgraph_pretty_printers(): def build_memgraph_pretty_printers():
'''Instantiate and return all memgraph pretty printer classes.''' """Instantiate and return all memgraph pretty printer classes."""
pp = gdb.printing.RegexpCollectionPrettyPrinter('memgraph') pp = gdb.printing.RegexpCollectionPrettyPrinter("memgraph")
pp.add_printer('memgraph::query::TypedValue', '^memgraph::query::TypedValue$', TypedValuePrinter) pp.add_printer(
"memgraph::query::TypedValue",
"^memgraph::query::TypedValue$",
TypedValuePrinter,
)
return pp return pp
class TypedValuePrinter(gdb.printing.PrettyPrinter): class TypedValuePrinter(gdb.printing.PrettyPrinter):
'''Pretty printer for memgraph::query::TypedValue''' """Pretty printer for memgraph::query::TypedValue"""
def __init__(self, val): def __init__(self, val):
super(TypedValuePrinter, self).__init__('TypedValue') super(TypedValuePrinter, self).__init__("TypedValue")
self.val = val self.val = val
def to_string(self): def to_string(self):
def _to_str(val): def _to_str(val):
return '{%s %s}' % (value_type, self.val[val]) return "{%s %s}" % (value_type, self.val[val])
value_type = str(self.val['type_'])
if value_type == 'memgraph::query::TypedValue::Type::Null':
return '{%s}' % value_type
elif value_type == 'memgraph::query::TypedValue::Type::Bool':
return _to_str('bool_v')
elif value_type == 'memgraph::query::TypedValue::Type::Int':
return _to_str('int_v')
elif value_type == 'memgraph::query::TypedValue::Type::Double':
return _to_str('double_v')
elif value_type == 'memgraph::query::TypedValue::Type::String':
return _to_str('string_v')
elif value_type == 'memgraph::query::TypedValue::Type::List':
return _to_str('list_v')
elif value_type == 'memgraph::query::TypedValue::Type::Map':
return _to_str('map_v')
elif value_type == 'memgraph::query::TypedValue::Type::Vertex':
return _to_str('vertex_v')
elif value_type == 'memgraph::query::TypedValue::Type::Edge':
return _to_str('edge_v')
elif value_type == 'memgraph::query::TypedValue::Type::Path':
return _to_str('path_v')
return '{%s}' % value_type
gdb.printing.register_pretty_printer(None, build_memgraph_pretty_printers(), value_type = str(self.val["type_"])
replace=True) if value_type == "memgraph::query::TypedValue::Type::Null":
return "{%s}" % value_type
elif value_type == "memgraph::query::TypedValue::Type::Bool":
return _to_str("bool_v")
elif value_type == "memgraph::query::TypedValue::Type::Int":
return _to_str("int_v")
elif value_type == "memgraph::query::TypedValue::Type::Double":
return _to_str("double_v")
elif value_type == "memgraph::query::TypedValue::Type::String":
return _to_str("string_v")
elif value_type == "memgraph::query::TypedValue::Type::List":
return _to_str("list_v")
elif value_type == "memgraph::query::TypedValue::Type::Map":
return _to_str("map_v")
elif value_type == "memgraph::query::TypedValue::Type::Vertex":
return _to_str("vertex_v")
elif value_type == "memgraph::query::TypedValue::Type::Edge":
return _to_str("edge_v")
elif value_type == "memgraph::query::TypedValue::Type::Path":
return _to_str("path_v")
return "{%s}" % value_type
gdb.printing.register_pretty_printer(None, build_memgraph_pretty_printers(), replace=True)

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
#===- clang-tidy-diff.py - ClangTidy Diff Checker -----------*- python -*--===# # ===- clang-tidy-diff.py - ClangTidy Diff Checker -----------*- python -*--===#
# #
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information. # See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
# #
#===-----------------------------------------------------------------------===# # ===-----------------------------------------------------------------------===#
r""" r"""
ClangTidy Diff Checker ClangTidy Diff Checker
@@ -37,11 +37,11 @@ import threading
import traceback import traceback
try: try:
import yaml import yaml
except ImportError: except ImportError:
yaml = None yaml = None
is_py2 = sys.version[0] == '2' is_py2 = sys.version[0] == "2"
if is_py2: if is_py2:
import Queue as queue import Queue as queue
@@ -50,220 +50,242 @@ else:
def run_tidy(task_queue, lock, timeout): def run_tidy(task_queue, lock, timeout):
watchdog = None watchdog = None
while True: while True:
command = task_queue.get() command = task_queue.get()
try: try:
proc = subprocess.Popen(command, proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if timeout is not None: if timeout is not None:
watchdog = threading.Timer(timeout, proc.kill) watchdog = threading.Timer(timeout, proc.kill)
watchdog.start() watchdog.start()
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
with lock: with lock:
sys.stdout.write(stdout.decode('utf-8') + '\n') sys.stdout.write(stdout.decode("utf-8") + "\n")
sys.stdout.flush() sys.stdout.flush()
if stderr: if stderr:
sys.stderr.write(stderr.decode('utf-8') + '\n') sys.stderr.write(stderr.decode("utf-8") + "\n")
sys.stderr.flush() sys.stderr.flush()
except Exception as e: except Exception as e:
with lock: with lock:
sys.stderr.write('Failed: ' + str(e) + ': '.join(command) + '\n') sys.stderr.write("Failed: " + str(e) + ": ".join(command) + "\n")
finally: finally:
with lock: with lock:
if not (timeout is None or watchdog is None): if not (timeout is None or watchdog is None):
if not watchdog.is_alive(): if not watchdog.is_alive():
sys.stderr.write('Terminated by timeout: ' + sys.stderr.write("Terminated by timeout: " + " ".join(command) + "\n")
' '.join(command) + '\n') watchdog.cancel()
watchdog.cancel() task_queue.task_done()
task_queue.task_done()
def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout): def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout):
for _ in range(max_tasks): for _ in range(max_tasks):
t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout)) t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout))
t.daemon = True t.daemon = True
t.start() t.start()
def merge_replacement_files(tmpdir, mergefile): def merge_replacement_files(tmpdir, mergefile):
"""Merge all replacement files in a directory into a single file""" """Merge all replacement files in a directory into a single file"""
# The fixes suggested by clang-tidy >= 4.0.0 are given under # The fixes suggested by clang-tidy >= 4.0.0 are given under
# the top level key 'Diagnostics' in the output yaml files # the top level key 'Diagnostics' in the output yaml files
mergekey = "Diagnostics" mergekey = "Diagnostics"
merged = [] merged = []
for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
content = yaml.safe_load(open(replacefile, 'r')) content = yaml.safe_load(open(replacefile, "r"))
if not content: if not content:
continue # Skip empty files. continue # Skip empty files.
merged.extend(content.get(mergekey, [])) merged.extend(content.get(mergekey, []))
if merged: if merged:
# MainSourceFile: The key is required by the definition inside # MainSourceFile: The key is required by the definition inside
# include/clang/Tooling/ReplacementsYaml.h, but the value # include/clang/Tooling/ReplacementsYaml.h, but the value
# is actually never used inside clang-apply-replacements, # is actually never used inside clang-apply-replacements,
# so we set it to '' here. # so we set it to '' here.
output = {'MainSourceFile': '', mergekey: merged} output = {"MainSourceFile": "", mergekey: merged}
with open(mergefile, 'w') as out: with open(mergefile, "w") as out:
yaml.safe_dump(output, out) yaml.safe_dump(output, out)
else: else:
# Empty the file: # Empty the file:
open(mergefile, 'w').close() open(mergefile, "w").close()
def main(): def main():
parser = argparse.ArgumentParser(description= parser = argparse.ArgumentParser(
'Run clang-tidy against changed files, and ' description="Run clang-tidy against changed files, and " "output diagnostics only for modified " "lines."
'output diagnostics only for modified ' )
'lines.') parser.add_argument(
parser.add_argument('-clang-tidy-binary', metavar='PATH', "-clang-tidy-binary",
default='clang-tidy', metavar="PATH",
help='path to clang-tidy binary') default="clang-tidy",
parser.add_argument('-p', metavar='NUM', default=0, help="path to clang-tidy binary",
help='strip the smallest prefix containing P slashes') )
parser.add_argument('-regex', metavar='PATTERN', default=None, parser.add_argument(
help='custom pattern selecting file paths to check ' "-p",
'(case sensitive, overrides -iregex)') metavar="NUM",
parser.add_argument('-iregex', metavar='PATTERN', default= default=0,
r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)', help="strip the smallest prefix containing P slashes",
help='custom pattern selecting file paths to check ' )
'(case insensitive, overridden by -regex)') parser.add_argument(
parser.add_argument('-j', type=int, default=1, "-regex",
help='number of tidy instances to be run in parallel.') metavar="PATTERN",
parser.add_argument('-timeout', type=int, default=None, default=None,
help='timeout per each file in seconds.') help="custom pattern selecting file paths to check " "(case sensitive, overrides -iregex)",
parser.add_argument('-fix', action='store_true', default=False, )
help='apply suggested fixes') parser.add_argument(
parser.add_argument('-checks', "-iregex",
help='checks filter, when not specified, use clang-tidy ' metavar="PATTERN",
'default', default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)",
default='') help="custom pattern selecting file paths to check " "(case insensitive, overridden by -regex)",
parser.add_argument('-path', dest='build_path', )
help='Path used to read a compile command database.') parser.add_argument(
if yaml: "-j",
parser.add_argument('-export-fixes', metavar='FILE', dest='export_fixes', type=int,
help='Create a yaml file to store suggested fixes in, ' default=1,
'which can be applied with clang-apply-replacements.') help="number of tidy instances to be run in parallel.",
parser.add_argument('-extra-arg', dest='extra_arg', )
action='append', default=[], parser.add_argument("-timeout", type=int, default=None, help="timeout per each file in seconds.")
help='Additional argument to append to the compiler ' parser.add_argument("-fix", action="store_true", default=False, help="apply suggested fixes")
'command line.') parser.add_argument(
parser.add_argument('-extra-arg-before', dest='extra_arg_before', "-checks",
action='append', default=[], help="checks filter, when not specified, use clang-tidy " "default",
help='Additional argument to prepend to the compiler ' default="",
'command line.') )
parser.add_argument('-quiet', action='store_true', default=False, parser.add_argument("-path", dest="build_path", help="Path used to read a compile command database.")
help='Run clang-tidy in quiet mode') if yaml:
clang_tidy_args = [] parser.add_argument(
argv = sys.argv[1:] "-export-fixes",
if '--' in argv: metavar="FILE",
clang_tidy_args.extend(argv[argv.index('--'):]) dest="export_fixes",
argv = argv[:argv.index('--')] help="Create a yaml file to store suggested fixes in, "
"which can be applied with clang-apply-replacements.",
)
parser.add_argument(
"-extra-arg",
dest="extra_arg",
action="append",
default=[],
help="Additional argument to append to the compiler " "command line.",
)
parser.add_argument(
"-extra-arg-before",
dest="extra_arg_before",
action="append",
default=[],
help="Additional argument to prepend to the compiler " "command line.",
)
parser.add_argument(
"-quiet",
action="store_true",
default=False,
help="Run clang-tidy in quiet mode",
)
clang_tidy_args = []
argv = sys.argv[1:]
if "--" in argv:
clang_tidy_args.extend(argv[argv.index("--") :])
argv = argv[: argv.index("--")]
args = parser.parse_args(argv) args = parser.parse_args(argv)
# Extract changed lines for each file. # Extract changed lines for each file.
filename = None filename = None
lines_by_file = {} lines_by_file = {}
for line in sys.stdin: for line in sys.stdin:
match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line) match = re.search('^\+\+\+\ "?(.*?/){%s}([^ \t\n"]*)' % args.p, line)
if match: if match:
filename = match.group(2) filename = match.group(2)
if filename is None: if filename is None:
continue continue
if args.regex is not None: if args.regex is not None:
if not re.match('^%s$' % args.regex, filename): if not re.match("^%s$" % args.regex, filename):
continue continue
else: else:
if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE):
continue continue
match = re.search('^@@.*\+(\d+)(,(\d+))?', line) match = re.search("^@@.*\+(\d+)(,(\d+))?", line)
if match: if match:
start_line = int(match.group(1)) start_line = int(match.group(1))
line_count = 1 line_count = 1
if match.group(3): if match.group(3):
line_count = int(match.group(3)) line_count = int(match.group(3))
if line_count == 0: if line_count == 0:
continue continue
end_line = start_line + line_count - 1 end_line = start_line + line_count - 1
lines_by_file.setdefault(filename, []).append([start_line, end_line]) lines_by_file.setdefault(filename, []).append([start_line, end_line])
if not any(lines_by_file): if not any(lines_by_file):
print("No relevant changes found.") print("No relevant changes found.")
sys.exit(0) sys.exit(0)
max_task_count = args.j max_task_count = args.j
if max_task_count == 0: if max_task_count == 0:
max_task_count = multiprocessing.cpu_count() max_task_count = multiprocessing.cpu_count()
max_task_count = min(len(lines_by_file), max_task_count) max_task_count = min(len(lines_by_file), max_task_count)
tmpdir = None tmpdir = None
if yaml and args.export_fixes:
tmpdir = tempfile.mkdtemp()
# Tasks for clang-tidy.
task_queue = queue.Queue(max_task_count)
# A lock for console output.
lock = threading.Lock()
# Run a pool of clang-tidy workers.
start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout)
# Form the common args list.
common_clang_tidy_args = []
if args.fix:
common_clang_tidy_args.append('-fix')
if args.checks != '':
common_clang_tidy_args.append('-checks=' + args.checks)
if args.quiet:
common_clang_tidy_args.append('-quiet')
if args.build_path is not None:
common_clang_tidy_args.append('-p=%s' % args.build_path)
for arg in args.extra_arg:
common_clang_tidy_args.append('-extra-arg=%s' % arg)
for arg in args.extra_arg_before:
common_clang_tidy_args.append('-extra-arg-before=%s' % arg)
for name in lines_by_file:
line_filter_json = json.dumps(
[{"name": name, "lines": lines_by_file[name]}],
separators=(',', ':'))
# Run clang-tidy on files containing changes.
command = [args.clang_tidy_binary]
command.append('-line-filter=' + line_filter_json)
if yaml and args.export_fixes: if yaml and args.export_fixes:
# Get a temporary file. We immediately close the handle so clang-tidy can tmpdir = tempfile.mkdtemp()
# overwrite it.
(handle, tmp_name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
os.close(handle)
command.append('-export-fixes=' + tmp_name)
command.extend(common_clang_tidy_args)
command.append(name)
command.extend(clang_tidy_args)
task_queue.put(command) # Tasks for clang-tidy.
task_queue = queue.Queue(max_task_count)
# A lock for console output.
lock = threading.Lock()
# Wait for all threads to be done. # Run a pool of clang-tidy workers.
task_queue.join() start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout)
if yaml and args.export_fixes: # Form the common args list.
print('Writing fixes to ' + args.export_fixes + ' ...') common_clang_tidy_args = []
try: if args.fix:
merge_replacement_files(tmpdir, args.export_fixes) common_clang_tidy_args.append("-fix")
except: if args.checks != "":
sys.stderr.write('Error exporting fixes.\n') common_clang_tidy_args.append("-checks=" + args.checks)
traceback.print_exc() if args.quiet:
common_clang_tidy_args.append("-quiet")
if args.build_path is not None:
common_clang_tidy_args.append("-p=%s" % args.build_path)
for arg in args.extra_arg:
common_clang_tidy_args.append("-extra-arg=%s" % arg)
for arg in args.extra_arg_before:
common_clang_tidy_args.append("-extra-arg-before=%s" % arg)
if tmpdir: for name in lines_by_file:
shutil.rmtree(tmpdir) line_filter_json = json.dumps([{"name": name, "lines": lines_by_file[name]}], separators=(",", ":"))
# Run clang-tidy on files containing changes.
command = [args.clang_tidy_binary]
command.append("-line-filter=" + line_filter_json)
if yaml and args.export_fixes:
# Get a temporary file. We immediately close the handle so clang-tidy can
# overwrite it.
(handle, tmp_name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir)
os.close(handle)
command.append("-export-fixes=" + tmp_name)
command.extend(common_clang_tidy_args)
command.append(name)
command.extend(clang_tidy_args)
task_queue.put(command)
# Wait for all threads to be done.
task_queue.join()
if yaml and args.export_fixes:
print("Writing fixes to " + args.export_fixes + " ...")
try:
merge_replacement_files(tmpdir, args.export_fixes)
except:
sys.stderr.write("Error exporting fixes.\n")
traceback.print_exc()
if tmpdir:
shutil.rmtree(tmpdir)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
#===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===# # ===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===#
# #
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information. # See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
# #
#===-----------------------------------------------------------------------===# # ===-----------------------------------------------------------------------===#
# FIXME: Integrate with clang-tidy-diff.py # FIXME: Integrate with clang-tidy-diff.py
@@ -50,11 +50,11 @@ import threading
import traceback import traceback
try: try:
import yaml import yaml
except ImportError: except ImportError:
yaml = None yaml = None
is_py2 = sys.version[0] == '2' is_py2 = sys.version[0] == "2"
if is_py2: if is_py2:
import Queue as queue import Queue as queue
@@ -63,275 +63,327 @@ else:
def find_compilation_database(path): def find_compilation_database(path):
"""Adjusts the directory until a compilation database is found.""" """Adjusts the directory until a compilation database is found."""
result = './' result = "./"
while not os.path.isfile(os.path.join(result, path)): while not os.path.isfile(os.path.join(result, path)):
if os.path.realpath(result) == '/': if os.path.realpath(result) == "/":
print('Error: could not find compilation database.') print("Error: could not find compilation database.")
sys.exit(1) sys.exit(1)
result += '../' result += "../"
return os.path.realpath(result) return os.path.realpath(result)
def make_absolute(f, directory): def make_absolute(f, directory):
if os.path.isabs(f): if os.path.isabs(f):
return f return f
return os.path.normpath(os.path.join(directory, f)) return os.path.normpath(os.path.join(directory, f))
def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, def get_tidy_invocation(
header_filter, allow_enabling_alpha_checkers, f,
extra_arg, extra_arg_before, quiet, config): clang_tidy_binary,
"""Gets a command line for clang-tidy.""" checks,
start = [clang_tidy_binary] tmpdir,
if allow_enabling_alpha_checkers: build_path,
start.append('-allow-enabling-analyzer-alpha-checkers') header_filter,
if header_filter is not None: allow_enabling_alpha_checkers,
start.append('-header-filter=' + header_filter) extra_arg,
if checks: extra_arg_before,
start.append('-checks=' + checks) quiet,
if tmpdir is not None: config,
start.append('-export-fixes') ):
# Get a temporary file. We immediately close the handle so clang-tidy can """Gets a command line for clang-tidy."""
# overwrite it. start = [clang_tidy_binary]
(handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) if allow_enabling_alpha_checkers:
os.close(handle) start.append("-allow-enabling-analyzer-alpha-checkers")
start.append(name) if header_filter is not None:
for arg in extra_arg: start.append("-header-filter=" + header_filter)
start.append('-extra-arg=%s' % arg) if checks:
for arg in extra_arg_before: start.append("-checks=" + checks)
start.append('-extra-arg-before=%s' % arg) if tmpdir is not None:
start.append('-p=' + build_path) start.append("-export-fixes")
if quiet: # Get a temporary file. We immediately close the handle so clang-tidy can
start.append('-quiet') # overwrite it.
if config: (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir)
start.append('-config=' + config) os.close(handle)
start.append(f) start.append(name)
return start for arg in extra_arg:
start.append("-extra-arg=%s" % arg)
for arg in extra_arg_before:
start.append("-extra-arg-before=%s" % arg)
start.append("-p=" + build_path)
if quiet:
start.append("-quiet")
if config:
start.append("-config=" + config)
start.append(f)
return start
def merge_replacement_files(tmpdir, mergefile): def merge_replacement_files(tmpdir, mergefile):
"""Merge all replacement files in a directory into a single file""" """Merge all replacement files in a directory into a single file"""
# The fixes suggested by clang-tidy >= 4.0.0 are given under # The fixes suggested by clang-tidy >= 4.0.0 are given under
# the top level key 'Diagnostics' in the output yaml files # the top level key 'Diagnostics' in the output yaml files
mergekey = "Diagnostics" mergekey = "Diagnostics"
merged=[] merged = []
for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
content = yaml.safe_load(open(replacefile, 'r')) content = yaml.safe_load(open(replacefile, "r"))
if not content: if not content:
continue # Skip empty files. continue # Skip empty files.
merged.extend(content.get(mergekey, [])) merged.extend(content.get(mergekey, []))
if merged: if merged:
# MainSourceFile: The key is required by the definition inside # MainSourceFile: The key is required by the definition inside
# include/clang/Tooling/ReplacementsYaml.h, but the value # include/clang/Tooling/ReplacementsYaml.h, but the value
# is actually never used inside clang-apply-replacements, # is actually never used inside clang-apply-replacements,
# so we set it to '' here. # so we set it to '' here.
output = {'MainSourceFile': '', mergekey: merged} output = {"MainSourceFile": "", mergekey: merged}
with open(mergefile, 'w') as out: with open(mergefile, "w") as out:
yaml.safe_dump(output, out) yaml.safe_dump(output, out)
else: else:
# Empty the file: # Empty the file:
open(mergefile, 'w').close() open(mergefile, "w").close()
def check_clang_apply_replacements_binary(args): def check_clang_apply_replacements_binary(args):
"""Checks if invoking supplied clang-apply-replacements binary works.""" """Checks if invoking supplied clang-apply-replacements binary works."""
try: try:
subprocess.check_call([args.clang_apply_replacements_binary, '--version']) subprocess.check_call([args.clang_apply_replacements_binary, "--version"])
except: except:
print('Unable to run clang-apply-replacements. Is clang-apply-replacements ' print(
'binary correctly specified?', file=sys.stderr) "Unable to run clang-apply-replacements. Is clang-apply-replacements " "binary correctly specified?",
traceback.print_exc() file=sys.stderr,
sys.exit(1) )
traceback.print_exc()
sys.exit(1)
def apply_fixes(args, tmpdir): def apply_fixes(args, tmpdir):
"""Calls clang-apply-fixes on a given directory.""" """Calls clang-apply-fixes on a given directory."""
invocation = [args.clang_apply_replacements_binary] invocation = [args.clang_apply_replacements_binary]
if args.format: if args.format:
invocation.append('-format') invocation.append("-format")
if args.style: if args.style:
invocation.append('-style=' + args.style) invocation.append("-style=" + args.style)
invocation.append(tmpdir) invocation.append(tmpdir)
subprocess.call(invocation) subprocess.call(invocation)
def run_tidy(args, tmpdir, build_path, queue, lock, failed_files): def run_tidy(args, tmpdir, build_path, queue, lock, failed_files):
"""Takes filenames out of queue and runs clang-tidy on them.""" """Takes filenames out of queue and runs clang-tidy on them."""
while True: while True:
name = queue.get() name = queue.get()
invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks, invocation = get_tidy_invocation(
tmpdir, build_path, args.header_filter, name,
args.allow_enabling_alpha_checkers, args.clang_tidy_binary,
args.extra_arg, args.extra_arg_before, args.checks,
args.quiet, args.config) tmpdir,
build_path,
args.header_filter,
args.allow_enabling_alpha_checkers,
args.extra_arg,
args.extra_arg_before,
args.quiet,
args.config,
)
proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE) proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate() output, err = proc.communicate()
if proc.returncode != 0: if proc.returncode != 0:
failed_files.append(name) failed_files.append(name)
with lock: with lock:
sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8')) sys.stdout.write(" ".join(invocation) + "\n" + output.decode("utf-8"))
if len(err) > 0: if len(err) > 0:
sys.stdout.flush() sys.stdout.flush()
sys.stderr.write(err.decode('utf-8')) sys.stderr.write(err.decode("utf-8"))
queue.task_done() queue.task_done()
def main(): def main():
parser = argparse.ArgumentParser(description='Runs clang-tidy over all files ' parser = argparse.ArgumentParser(
'in a compilation database. Requires ' description="Runs clang-tidy over all files "
'clang-tidy and clang-apply-replacements in ' "in a compilation database. Requires "
'$PATH.') "clang-tidy and clang-apply-replacements in "
parser.add_argument('-allow-enabling-alpha-checkers', "$PATH."
action='store_true', help='allow alpha checkers from ' )
'clang-analyzer.') parser.add_argument(
parser.add_argument('-clang-tidy-binary', metavar='PATH', "-allow-enabling-alpha-checkers",
default='clang-tidy-11', action="store_true",
help='path to clang-tidy binary') help="allow alpha checkers from " "clang-analyzer.",
parser.add_argument('-clang-apply-replacements-binary', metavar='PATH', )
default='clang-apply-replacements-11', parser.add_argument(
help='path to clang-apply-replacements binary') "-clang-tidy-binary",
parser.add_argument('-checks', default=None, metavar="PATH",
help='checks filter, when not specified, use clang-tidy ' default="clang-tidy-11",
'default') help="path to clang-tidy binary",
parser.add_argument('-config', default=None, )
help='Specifies a configuration in YAML/JSON format: ' parser.add_argument(
' -config="{Checks: \'*\', ' "-clang-apply-replacements-binary",
' CheckOptions: [{key: x, ' metavar="PATH",
' value: y}]}" ' default="clang-apply-replacements-11",
'When the value is empty, clang-tidy will ' help="path to clang-apply-replacements binary",
'attempt to find a file named .clang-tidy for ' )
'each source file in its parent directories.') parser.add_argument(
parser.add_argument('-header-filter', default=None, "-checks",
help='regular expression matching the names of the ' default=None,
'headers to output diagnostics from. Diagnostics from ' help="checks filter, when not specified, use clang-tidy " "default",
'the main file of each translation unit are always ' )
'displayed.') parser.add_argument(
if yaml: "-config",
parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes', default=None,
help='Create a yaml file to store suggested fixes in, ' help="Specifies a configuration in YAML/JSON format: "
'which can be applied with clang-apply-replacements.') " -config=\"{Checks: '*', "
parser.add_argument('-j', type=int, default=0, " CheckOptions: [{key: x, "
help='number of tidy instances to be run in parallel.') ' value: y}]}" '
parser.add_argument('files', nargs='*', default=['.*'], "When the value is empty, clang-tidy will "
help='files to be processed (regex on path)') "attempt to find a file named .clang-tidy for "
parser.add_argument('-fix', action='store_true', help='apply fix-its') "each source file in its parent directories.",
parser.add_argument('-format', action='store_true', help='Reformat code ' )
'after applying fixes') parser.add_argument(
parser.add_argument('-style', default='file', help='The style of reformat ' "-header-filter",
'code after applying fixes') default=None,
parser.add_argument('-p', dest='build_path', help="regular expression matching the names of the "
help='Path used to read a compile command database.') "headers to output diagnostics from. Diagnostics from "
parser.add_argument('-extra-arg', dest='extra_arg', "the main file of each translation unit are always "
action='append', default=[], "displayed.",
help='Additional argument to append to the compiler ' )
'command line.') if yaml:
parser.add_argument('-extra-arg-before', dest='extra_arg_before', parser.add_argument(
action='append', default=[], "-export-fixes",
help='Additional argument to prepend to the compiler ' metavar="filename",
'command line.') dest="export_fixes",
parser.add_argument('-quiet', action='store_true', help="Create a yaml file to store suggested fixes in, "
help='Run clang-tidy in quiet mode') "which can be applied with clang-apply-replacements.",
args = parser.parse_args() )
parser.add_argument(
"-j",
type=int,
default=0,
help="number of tidy instances to be run in parallel.",
)
parser.add_argument("files", nargs="*", default=[".*"], help="files to be processed (regex on path)")
parser.add_argument("-fix", action="store_true", help="apply fix-its")
parser.add_argument("-format", action="store_true", help="Reformat code " "after applying fixes")
parser.add_argument(
"-style",
default="file",
help="The style of reformat " "code after applying fixes",
)
parser.add_argument("-p", dest="build_path", help="Path used to read a compile command database.")
parser.add_argument(
"-extra-arg",
dest="extra_arg",
action="append",
default=[],
help="Additional argument to append to the compiler " "command line.",
)
parser.add_argument(
"-extra-arg-before",
dest="extra_arg_before",
action="append",
default=[],
help="Additional argument to prepend to the compiler " "command line.",
)
parser.add_argument("-quiet", action="store_true", help="Run clang-tidy in quiet mode")
args = parser.parse_args()
db_path = 'compile_commands.json' db_path = "compile_commands.json"
if args.build_path is not None: if args.build_path is not None:
build_path = args.build_path build_path = args.build_path
else:
# Find our database
build_path = find_compilation_database(db_path)
try:
invocation = [args.clang_tidy_binary, '-list-checks']
if args.allow_enabling_alpha_checkers:
invocation.append('-allow-enabling-analyzer-alpha-checkers')
invocation.append('-p=' + build_path)
if args.checks:
invocation.append('-checks=' + args.checks)
invocation.append('-')
if args.quiet:
# Even with -quiet we still want to check if we can call clang-tidy.
with open(os.devnull, 'w') as dev_null:
subprocess.check_call(invocation, stdout=dev_null)
else: else:
subprocess.check_call(invocation) # Find our database
except: build_path = find_compilation_database(db_path)
print("Unable to run clang-tidy.", file=sys.stderr)
sys.exit(1)
# Load the database and extract all files. try:
database = json.load(open(os.path.join(build_path, db_path))) invocation = [args.clang_tidy_binary, "-list-checks"]
files = [make_absolute(entry['file'], entry['directory']) if args.allow_enabling_alpha_checkers:
for entry in database] invocation.append("-allow-enabling-analyzer-alpha-checkers")
invocation.append("-p=" + build_path)
if args.checks:
invocation.append("-checks=" + args.checks)
invocation.append("-")
if args.quiet:
# Even with -quiet we still want to check if we can call clang-tidy.
with open(os.devnull, "w") as dev_null:
subprocess.check_call(invocation, stdout=dev_null)
else:
subprocess.check_call(invocation)
except:
print("Unable to run clang-tidy.", file=sys.stderr)
sys.exit(1)
max_task = args.j # Load the database and extract all files.
if max_task == 0: database = json.load(open(os.path.join(build_path, db_path)))
max_task = multiprocessing.cpu_count() files = [make_absolute(entry["file"], entry["directory"]) for entry in database]
tmpdir = None max_task = args.j
if args.fix or (yaml and args.export_fixes): if max_task == 0:
check_clang_apply_replacements_binary(args) max_task = multiprocessing.cpu_count()
tmpdir = tempfile.mkdtemp()
# Build up a big regexy filter from all command line arguments. tmpdir = None
file_name_re = re.compile('|'.join(args.files)) if args.fix or (yaml and args.export_fixes):
check_clang_apply_replacements_binary(args)
tmpdir = tempfile.mkdtemp()
return_code = 0 # Build up a big regexy filter from all command line arguments.
try: file_name_re = re.compile("|".join(args.files))
# Spin up a bunch of tidy-launching threads.
task_queue = queue.Queue(max_task)
# List of files with a non-zero return code.
failed_files = []
lock = threading.Lock()
for _ in range(max_task):
t = threading.Thread(target=run_tidy,
args=(args, tmpdir, build_path, task_queue, lock, failed_files))
t.daemon = True
t.start()
# Fill the queue with files. return_code = 0
for name in files: try:
if file_name_re.search(name): # Spin up a bunch of tidy-launching threads.
task_queue.put(name) task_queue = queue.Queue(max_task)
# List of files with a non-zero return code.
failed_files = []
lock = threading.Lock()
for _ in range(max_task):
t = threading.Thread(
target=run_tidy,
args=(args, tmpdir, build_path, task_queue, lock, failed_files),
)
t.daemon = True
t.start()
# Wait for all threads to be done. # Fill the queue with files.
task_queue.join() for name in files:
if len(failed_files): if file_name_re.search(name):
return_code = 1 task_queue.put(name)
# Wait for all threads to be done.
task_queue.join()
if len(failed_files):
return_code = 1
except KeyboardInterrupt:
# This is a sad hack. Unfortunately subprocess goes
# bonkers with ctrl-c and we start forking merrily.
print("\nCtrl-C detected, goodbye.")
if tmpdir:
shutil.rmtree(tmpdir)
os.kill(0, 9)
if yaml and args.export_fixes:
print("Writing fixes to " + args.export_fixes + " ...")
try:
merge_replacement_files(tmpdir, args.export_fixes)
except:
print("Error exporting fixes.\n", file=sys.stderr)
traceback.print_exc()
return_code = 1
if args.fix:
print("Applying fixes ...")
try:
apply_fixes(args, tmpdir)
except:
print("Error applying fixes.\n", file=sys.stderr)
traceback.print_exc()
return_code = 1
except KeyboardInterrupt:
# This is a sad hack. Unfortunately subprocess goes
# bonkers with ctrl-c and we start forking merrily.
print('\nCtrl-C detected, goodbye.')
if tmpdir: if tmpdir:
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
os.kill(0, 9) sys.exit(return_code)
if yaml and args.export_fixes:
print('Writing fixes to ' + args.export_fixes + ' ...')
try:
merge_replacement_files(tmpdir, args.export_fixes)
except:
print('Error exporting fixes.\n', file=sys.stderr)
traceback.print_exc()
return_code=1
if args.fix:
print('Applying fixes ...')
try:
apply_fixes(args, tmpdir)
except:
print('Error applying fixes.\n', file=sys.stderr)
traceback.print_exc()
return_code = 1
if tmpdir:
shutil.rmtree(tmpdir)
sys.exit(return_code)
if __name__ == '__main__': if __name__ == "__main__":
main() main()