Compare commits
13 Commits
E150-MG-st
...
T610-FL-Ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83aa71a29f | ||
|
|
c2a1328dcc | ||
|
|
b63db202d6 | ||
|
|
1abe8f8bfc | ||
|
|
4366085d89 | ||
|
|
9369ae9085 | ||
|
|
86a15331d1 | ||
|
|
0c8b35b151 | ||
|
|
11d60c203e | ||
|
|
38c0a08342 | ||
|
|
7e1d39bf86 | ||
|
|
dd85b428bf | ||
|
|
2f9ed0146e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
*.swn
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
*~
|
||||
.DS_Store
|
||||
.gdb_history
|
||||
@@ -26,6 +27,7 @@ src/query/frontend/opencypher/generated/
|
||||
tags
|
||||
ve/
|
||||
ve3/
|
||||
.cache/
|
||||
perf.data*
|
||||
TAGS
|
||||
*.apollo_measurements
|
||||
|
||||
@@ -18,14 +18,16 @@ WIDTH = 80
|
||||
|
||||
def wrap_text(s, initial_indent="# "):
|
||||
return "\n#\n".join(
|
||||
map(lambda x: textwrap.fill(x, WIDTH, initial_indent=initial_indent,
|
||||
subsequent_indent="# "), s.split("\n")))
|
||||
map(
|
||||
lambda x: textwrap.fill(x, WIDTH, initial_indent=initial_indent, subsequent_indent="# "),
|
||||
s.split("\n"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def extract_flags(binary_path):
|
||||
ret = {}
|
||||
data = subprocess.run([binary_path, "--help-xml"],
|
||||
stdout=subprocess.PIPE).stdout.decode("utf-8")
|
||||
data = subprocess.run([binary_path, "--help-xml"], stdout=subprocess.PIPE).stdout.decode("utf-8")
|
||||
root = ET.fromstring(data)
|
||||
for child in root:
|
||||
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"]:
|
||||
name = modification["name"]
|
||||
if name not in flags:
|
||||
print("WARNING: Flag '" + name + "' missing from binary!",
|
||||
file=sys.stderr)
|
||||
print("WARNING: Flag '" + name + "' missing from binary!", file=sys.stderr)
|
||||
continue
|
||||
flags[name]["default"] = modification["value"]
|
||||
flags[name]["override"] = modification["override"]
|
||||
@@ -75,8 +76,9 @@ def extract_sections(flags):
|
||||
else:
|
||||
sections.append((current_section, current_flags))
|
||||
sections.append(("other", other))
|
||||
assert set(sum(map(lambda x: x[1], sections), [])) == set(flags.keys()), \
|
||||
"The section extraction algorithm lost some flags!"
|
||||
assert set(sum(map(lambda x: x[1], sections), [])) == set(
|
||||
flags.keys()
|
||||
), "The section extraction algorithm lost some flags!"
|
||||
return sections
|
||||
|
||||
|
||||
@@ -89,8 +91,7 @@ def generate_config_file(sections, flags):
|
||||
helpstr = flag["meaning"] + " [" + flag["type"] + "]"
|
||||
ret += wrap_text(helpstr) + "\n"
|
||||
prefix = "# " if not flag["override"] else ""
|
||||
ret += prefix + "--" + flag["name"].replace("_", "-") + \
|
||||
"=" + flag["default"] + "\n\n"
|
||||
ret += prefix + "--" + flag["name"].replace("_", "-") + "=" + flag["default"] + "\n\n"
|
||||
ret += "\n"
|
||||
ret += wrap_text(config["footer"])
|
||||
return ret.strip() + "\n"
|
||||
@@ -98,13 +99,16 @@ def generate_config_file(sections, flags):
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("memgraph_binary",
|
||||
help="path to Memgraph binary")
|
||||
parser.add_argument("output_file",
|
||||
help="path where to store the generated Memgraph "
|
||||
"configuration file")
|
||||
parser.add_argument("--config-file", default=CONFIG_FILE,
|
||||
help="path to generator configuration file")
|
||||
parser.add_argument("memgraph_binary", help="path to Memgraph binary")
|
||||
parser.add_argument(
|
||||
"output_file",
|
||||
help="path where to store the generated Memgraph " "configuration file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config-file",
|
||||
default=CONFIG_FILE,
|
||||
help="path to generator configuration file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
flags = extract_flags(args.memgraph_binary)
|
||||
|
||||
@@ -7,13 +7,11 @@ import copy
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def procedure(context: mgp.ProcCtx,
|
||||
required_arg: mgp.Nullable[mgp.Any],
|
||||
optional_arg: mgp.Nullable[mgp.Any] = None
|
||||
) -> mgp.Record(args=list,
|
||||
vertex_count=int,
|
||||
avg_degree=mgp.Number,
|
||||
props=mgp.Nullable[mgp.Map]):
|
||||
def procedure(
|
||||
context: mgp.ProcCtx,
|
||||
required_arg: mgp.Nullable[mgp.Any],
|
||||
optional_arg: mgp.Nullable[mgp.Any] = None,
|
||||
) -> mgp.Record(args=list, vertex_count=int, avg_degree=mgp.Number, props=mgp.Nullable[mgp.Map]):
|
||||
"""
|
||||
This example procedure returns 4 fields.
|
||||
|
||||
@@ -37,7 +35,7 @@ def procedure(context: mgp.ProcCtx,
|
||||
if isinstance(required_arg, (mgp.Edge, mgp.Vertex)):
|
||||
props = dict(required_arg.properties.items())
|
||||
elif isinstance(required_arg, mgp.Path):
|
||||
start_vertex, = required_arg.vertices
|
||||
(start_vertex,) = required_arg.vertices
|
||||
props = dict(start_vertex.properties.items())
|
||||
# Count the vertices and edges in the database; this may take a while.
|
||||
vertex_count = 0
|
||||
@@ -51,15 +49,13 @@ def procedure(context: mgp.ProcCtx,
|
||||
# Copy the received arguments to make it equivalent to the C example.
|
||||
args_copy = [copy.deepcopy(required_arg), copy.deepcopy(optional_arg)]
|
||||
# Multiple rows can be produced by returning an iterable of mgp.Record.
|
||||
return mgp.Record(args=args_copy, vertex_count=vertex_count,
|
||||
avg_degree=avg_degree, props=props)
|
||||
return mgp.Record(args=args_copy, vertex_count=vertex_count, avg_degree=avg_degree, props=props)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def write_procedure(context: mgp.ProcCtx,
|
||||
property_name: str,
|
||||
property_value: mgp.Nullable[mgp.Any]
|
||||
) -> mgp.Record(created_vertex=mgp.Vertex):
|
||||
def write_procedure(
|
||||
context: mgp.ProcCtx, property_name: str, property_value: mgp.Nullable[mgp.Any]
|
||||
) -> mgp.Record(created_vertex=mgp.Vertex):
|
||||
"""
|
||||
This example procedure creates a new vertex with the specified property
|
||||
and connects it to all existing vertex which has the same property with
|
||||
|
||||
@@ -4,15 +4,17 @@ from collections import OrderedDict
|
||||
from itertools import chain, repeat
|
||||
from inspect import cleandoc
|
||||
from typing import List, Tuple
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write((
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use graph_analyzer '
|
||||
'module. Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n'))
|
||||
sys.stderr.write(
|
||||
(
|
||||
"\n"
|
||||
"NOTE: Please install networkx to be able to use graph_analyzer "
|
||||
"module. Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
)
|
||||
raise import_error
|
||||
# Imported last because it also depends on networkx.
|
||||
from mgp_networkx import MemgraphMultiDiGraph # noqa E402
|
||||
@@ -23,16 +25,14 @@ _MAX_LIST_SIZE = 10
|
||||
|
||||
@mgp.read_proc
|
||||
def help() -> mgp.Record(name=str, value=str):
|
||||
'''Shows manual page for graph_analyzer.'''
|
||||
"""Shows manual page for graph_analyzer."""
|
||||
records = []
|
||||
|
||||
def make_records(name, doc):
|
||||
return (mgp.Record(name=n, value=v) for n, v in
|
||||
zip(chain([name], repeat('')), cleandoc(doc).splitlines()))
|
||||
return (mgp.Record(name=n, value=v) for n, v in zip(chain([name], repeat("")), cleandoc(doc).splitlines()))
|
||||
|
||||
for func in (help, analyze, analyze_subgraph):
|
||||
records.extend(make_records("Procedure '{}'".format(func.__name__),
|
||||
func.__doc__))
|
||||
records.extend(make_records("Procedure '{}'".format(func.__name__), func.__doc__))
|
||||
|
||||
for m, v in _get_analysis_mapping().items():
|
||||
records.extend(make_records("Analysis '{}'".format(m), v.__doc__))
|
||||
@@ -41,10 +41,8 @@ def help() -> mgp.Record(name=str, value=str):
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def analyze(context: mgp.ProcCtx,
|
||||
analyses: mgp.Nullable[List[str]] = None
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
'''
|
||||
def analyze(context: mgp.ProcCtx, analyses: mgp.Nullable[List[str]] = None) -> mgp.Record(name=str, value=str):
|
||||
"""
|
||||
Shows graph information.
|
||||
|
||||
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):
|
||||
CALL graph_analyzer.analyze(['nodes', 'edges']) YIELD *;
|
||||
'''
|
||||
"""
|
||||
g = MemgraphMultiDiGraph(ctx=context)
|
||||
recs = _analyze_graph(context, g, analyses)
|
||||
return [mgp.Record(name=name, value=value) for name, value in recs]
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def analyze_subgraph(context: mgp.ProcCtx,
|
||||
vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge],
|
||||
analyses: mgp.Nullable[List[str]] = None
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
'''
|
||||
def analyze_subgraph(
|
||||
context: mgp.ProcCtx,
|
||||
vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge],
|
||||
analyses: mgp.Nullable[List[str]] = None,
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
"""
|
||||
Shows subgraph information.
|
||||
|
||||
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'])
|
||||
YIELD *
|
||||
RETURN name, value;
|
||||
'''
|
||||
"""
|
||||
vertices, edges = map(set, [vertices, edges])
|
||||
g = nx.subgraph_view(
|
||||
MemgraphMultiDiGraph(ctx=context),
|
||||
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)
|
||||
return [mgp.Record(name=name, value=value) for name, value in recs]
|
||||
|
||||
|
||||
def _get_analysis_mapping():
|
||||
return OrderedDict([
|
||||
('nodes', _number_of_nodes),
|
||||
('edges', _number_of_edges),
|
||||
('bridges', _bridges),
|
||||
('articulation_points', _articulation_points),
|
||||
('avg_degree', _avg_degree),
|
||||
('sorted_nodes_degree', _sorted_nodes_degree),
|
||||
('self_loops', _self_loops),
|
||||
('is_bipartite', _is_bipartite),
|
||||
('is_planar', _is_planar),
|
||||
('is_biconnected: ', _is_biconnected),
|
||||
('is_weakly_connected', _is_weakly_connected),
|
||||
('number_of_weakly_components', _weakly_components),
|
||||
('is_strongly_connected', _is_strongly_connected),
|
||||
('strongly_components', _strongly_components),
|
||||
('is_dag', _is_dag),
|
||||
('is_eulerian', _is_eulerian),
|
||||
('is_forest', _is_forest),
|
||||
('is_tree', _is_tree)])
|
||||
return OrderedDict(
|
||||
[
|
||||
("nodes", _number_of_nodes),
|
||||
("edges", _number_of_edges),
|
||||
("bridges", _bridges),
|
||||
("articulation_points", _articulation_points),
|
||||
("avg_degree", _avg_degree),
|
||||
("sorted_nodes_degree", _sorted_nodes_degree),
|
||||
("self_loops", _self_loops),
|
||||
("is_bipartite", _is_bipartite),
|
||||
("is_planar", _is_planar),
|
||||
("is_biconnected: ", _is_biconnected),
|
||||
("is_weakly_connected", _is_weakly_connected),
|
||||
("number_of_weakly_components", _weakly_components),
|
||||
("is_strongly_connected", _is_strongly_connected),
|
||||
("strongly_components", _strongly_components),
|
||||
("is_dag", _is_dag),
|
||||
("is_eulerian", _is_eulerian),
|
||||
("is_forest", _is_forest),
|
||||
("is_tree", _is_tree),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _get_analysis_func(name: str):
|
||||
@@ -132,20 +135,15 @@ def _get_analysis_funcs():
|
||||
return _get_analysis_mapping().values()
|
||||
|
||||
|
||||
def _analyze_graph(context: mgp.ProcCtx,
|
||||
g: nx.MultiDiGraph,
|
||||
analyses: List[str]
|
||||
) -> List[Tuple[str, str]]:
|
||||
def _analyze_graph(context: mgp.ProcCtx, g: nx.MultiDiGraph, analyses: List[str]) -> List[Tuple[str, str]]:
|
||||
|
||||
functions = (_get_analysis_funcs() if analyses is None
|
||||
else [_get_analysis_func(name) for name in analyses])
|
||||
functions = _get_analysis_funcs() if analyses is None else [_get_analysis_func(name) for name in analyses]
|
||||
|
||||
records = []
|
||||
for index, f in enumerate(functions):
|
||||
context.check_must_abort()
|
||||
if f is None:
|
||||
raise KeyError('Graph analysis is not supported: ' +
|
||||
analyses[index])
|
||||
raise KeyError("Graph analysis is not supported: " + analyses[index])
|
||||
name, value = f(g)
|
||||
if isinstance(value, (list, set, tuple)):
|
||||
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]:
|
||||
'''Returns number of nodes.'''
|
||||
return 'Number of nodes', nx.number_of_nodes(g)
|
||||
"""Returns number of nodes."""
|
||||
return "Number of nodes", nx.number_of_nodes(g)
|
||||
|
||||
|
||||
def _number_of_edges(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
'''Returns number of edges.'''
|
||||
return 'Number of edges', nx.number_of_edges(g)
|
||||
"""Returns number of edges."""
|
||||
return "Number of edges", nx.number_of_edges(g)
|
||||
|
||||
|
||||
def _avg_degree(g: nx.MultiDiGraph) -> Tuple[str, float]:
|
||||
'''Returns average degree.'''
|
||||
"""Returns average degree."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
_, number_of_edges = _number_of_edges(g)
|
||||
avg_degree = (0 if number_of_nodes == 0
|
||||
else number_of_edges / number_of_nodes)
|
||||
return 'Average degree', avg_degree
|
||||
avg_degree = 0 if number_of_nodes == 0 else number_of_edges / number_of_nodes
|
||||
return "Average degree", avg_degree
|
||||
|
||||
|
||||
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.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]:
|
||||
'''Returns number of self loops.'''
|
||||
return 'Self loops', sum((1 if e[0] == e[1] else 0 for e in g.edges()))
|
||||
"""Returns number of self loops."""
|
||||
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]:
|
||||
'''Checks if graph is bipartite.'''
|
||||
"""Checks if graph is bipartite."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.bipartite.basic.is_bipartite(g))
|
||||
return 'Is bipartite', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.bipartite.basic.is_bipartite(g)
|
||||
return "Is bipartite", ret
|
||||
|
||||
|
||||
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)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.planarity.check_planarity(g)[0])
|
||||
return 'Is planar', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.planarity.check_planarity(g)[0]
|
||||
return "Is planar", ret
|
||||
|
||||
|
||||
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)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.is_biconnected(nx.MultiDiGraph.to_undirected(g)))
|
||||
return 'Is biconnected', ret
|
||||
ret = False if number_of_nodes == 0 else nx.is_biconnected(nx.MultiDiGraph.to_undirected(g))
|
||||
return "Is biconnected", ret
|
||||
|
||||
|
||||
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)
|
||||
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]:
|
||||
'''Checks if graph is strongly connected.'''
|
||||
"""Checks if graph is strongly connected."""
|
||||
_, number_of_nodes = _number_of_nodes(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]:
|
||||
'''Check if graph is directed acyclic graph (DAG)'''
|
||||
"""Check if graph is directed acyclic graph (DAG)"""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.dag.is_directed_acyclic_graph(g))
|
||||
return 'Is DAG', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.dag.is_directed_acyclic_graph(g)
|
||||
return "Is DAG", ret
|
||||
|
||||
|
||||
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)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.euler.is_eulerian(g))
|
||||
return 'Is eulerian', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.euler.is_eulerian(g)
|
||||
return "Is eulerian", ret
|
||||
|
||||
|
||||
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)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.tree.recognition.is_forest(g))
|
||||
return 'Is forest', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.tree.recognition.is_forest(g)
|
||||
return "Is forest", ret
|
||||
|
||||
|
||||
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)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.tree.recognition.is_tree(g))
|
||||
return 'Is tree', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.tree.recognition.is_tree(g)
|
||||
return "Is tree", ret
|
||||
|
||||
|
||||
def _bridges(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
'''Returns number of bridges, multiple edges between same nodes are
|
||||
mapped to one edge.'''
|
||||
return 'Number of bridges', sum(1 for _ in nx.bridges(nx.Graph(g)))
|
||||
"""Returns number of bridges, multiple edges between same nodes are
|
||||
mapped to one edge."""
|
||||
return "Number of bridges", sum(1 for _ in nx.bridges(nx.Graph(g)))
|
||||
|
||||
|
||||
def _articulation_points(g: nx.MultiDiGraph):
|
||||
'''Returns number of articulation points.'''
|
||||
"""Returns number of articulation points."""
|
||||
undirected = nx.MultiDiGraph.to_undirected(g)
|
||||
return ('Number of articulation points',
|
||||
sum(1 for _ in nx.articulation_points(undirected)))
|
||||
return (
|
||||
"Number of articulation points",
|
||||
sum(1 for _ in nx.articulation_points(undirected)),
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
return 'Number of weakly connected components', comps
|
||||
return "Number of weakly connected components", comps
|
||||
|
||||
|
||||
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)
|
||||
return 'Number of strongly connected components', comps
|
||||
return "Number of strongly connected components", comps
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import sys
|
||||
import mgp
|
||||
import collections
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write((
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use Memgraph NetworkX '
|
||||
'wrappers. Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n'))
|
||||
sys.stderr.write(
|
||||
(
|
||||
"\n"
|
||||
"NOTE: Please install networkx to be able to use Memgraph NetworkX "
|
||||
"wrappers. Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
)
|
||||
raise import_error
|
||||
|
||||
|
||||
class MemgraphAdjlistOuterDict(collections.abc.Mapping):
|
||||
__slots__ = ('_ctx', '_succ', '_multi')
|
||||
__slots__ = ("_ctx", "_succ", "_multi")
|
||||
|
||||
def __init__(self, ctx, succ=True, multi=True):
|
||||
self._ctx = ctx
|
||||
@@ -24,8 +26,7 @@ class MemgraphAdjlistOuterDict(collections.abc.Mapping):
|
||||
def __getitem__(self, key):
|
||||
if key not in self:
|
||||
raise KeyError
|
||||
return MemgraphAdjlistInnerDict(key, succ=self._succ,
|
||||
multi=self._multi)
|
||||
return MemgraphAdjlistInnerDict(key, succ=self._succ, multi=self._multi)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._ctx.graph.vertices)
|
||||
@@ -40,7 +41,7 @@ class MemgraphAdjlistOuterDict(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):
|
||||
self._node = node
|
||||
@@ -71,31 +72,26 @@ class MemgraphAdjlistInnerDict(collections.abc.Mapping):
|
||||
def _get_neighbors(self):
|
||||
if not self._neighbors:
|
||||
if self._succ:
|
||||
self._neighbors = set(
|
||||
e.to_vertex for e in self._node.out_edges)
|
||||
self._neighbors = set(e.to_vertex for e in self._node.out_edges)
|
||||
else:
|
||||
self._neighbors = set(
|
||||
e.from_vertex for e in self._node.in_edges)
|
||||
self._neighbors = set(e.from_vertex for e in self._node.in_edges)
|
||||
return self._neighbors
|
||||
|
||||
def _get_edge(self, neighbor):
|
||||
if self._succ:
|
||||
edge = list(filter(lambda e: e.to_vertex == neighbor,
|
||||
self._node.out_edges))
|
||||
edge = list(filter(lambda e: e.to_vertex == neighbor, self._node.out_edges))
|
||||
else:
|
||||
edge = list(filter(lambda e: e.from_vertex == neighbor,
|
||||
self._node.in_edges))
|
||||
edge = list(filter(lambda e: e.from_vertex == neighbor, self._node.in_edges))
|
||||
|
||||
assert len(edge) >= 1
|
||||
if len(edge) > 1:
|
||||
raise RuntimeError('Graph contains multiedges but '
|
||||
'is of non-multigraph type: {}'.format(edge))
|
||||
raise RuntimeError("Graph contains multiedges but " "is of non-multigraph type: {}".format(edge))
|
||||
|
||||
return edge[0]
|
||||
|
||||
|
||||
class MemgraphEdgeKeyDict(collections.abc.Mapping):
|
||||
__slots__ = ('_node', '_neighbor', '_succ', '_edges')
|
||||
__slots__ = ("_node", "_neighbor", "_succ", "_edges")
|
||||
|
||||
def __init__(self, node, neighbor, succ=True):
|
||||
self._node = node
|
||||
@@ -122,18 +118,14 @@ class MemgraphEdgeKeyDict(collections.abc.Mapping):
|
||||
def _get_edges(self):
|
||||
if not self._edges:
|
||||
if self._succ:
|
||||
self._edges = list(filter(
|
||||
lambda e: e.to_vertex == self._neighbor,
|
||||
self._node.out_edges))
|
||||
self._edges = list(filter(lambda e: e.to_vertex == self._neighbor, self._node.out_edges))
|
||||
else:
|
||||
self._edges = list(filter(
|
||||
lambda e: e.from_vertex == self._neighbor,
|
||||
self._node.in_edges))
|
||||
self._edges = list(filter(lambda e: e.from_vertex == self._neighbor, self._node.in_edges))
|
||||
return self._edges
|
||||
|
||||
|
||||
class UnhashableProperties(collections.abc.Mapping):
|
||||
__slots__ = ('_properties')
|
||||
__slots__ = "_properties"
|
||||
|
||||
def __init__(self, properties):
|
||||
self._properties = properties
|
||||
@@ -155,7 +147,7 @@ class UnhashableProperties(collections.abc.Mapping):
|
||||
|
||||
|
||||
class MemgraphNodeDict(collections.abc.Mapping):
|
||||
__slots__ = ('_ctx',)
|
||||
__slots__ = ("_ctx",)
|
||||
|
||||
def __init__(self, ctx):
|
||||
self._ctx = ctx
|
||||
@@ -187,8 +179,7 @@ class MemgraphNodeDict(collections.abc.Mapping):
|
||||
|
||||
|
||||
class MemgraphDiGraphBase:
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, multi=True,
|
||||
**kwargs):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, multi=True, **kwargs):
|
||||
# NOTE: We assume that our graph will never be given any initial data
|
||||
# because we already pull our data from the Memgraph database. This
|
||||
# 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
|
||||
# with initial data or modify it.
|
||||
|
||||
self.node_dict_factory = lambda: MemgraphNodeDict(ctx) \
|
||||
if ctx else self._error
|
||||
self.node_dict_factory = lambda: MemgraphNodeDict(ctx) if ctx else self._error
|
||||
self.node_attr_dict_factory = self._error
|
||||
|
||||
self.adjlist_outer_dict_factory = \
|
||||
lambda: MemgraphAdjlistOuterDict(ctx, multi=multi) \
|
||||
if ctx else self._error
|
||||
self.adjlist_outer_dict_factory = lambda: MemgraphAdjlistOuterDict(ctx, multi=multi) if ctx else self._error
|
||||
self.adjlist_inner_dict_factory = self._error
|
||||
self.edge_key_dict_factory = self._error
|
||||
self.edge_attr_dict_factory = self._error
|
||||
|
||||
# NOTE: We forbid any mutating operations because our graph is
|
||||
# immutable and pulls its data from the Memgraph database.
|
||||
for f in ['add_node', 'add_nodes_from', 'remove_node',
|
||||
'remove_nodes_from', 'add_edge', 'add_edges_from',
|
||||
'add_weighted_edges_from', 'new_edge_key', 'remove_edge',
|
||||
'remove_edges_from', 'update', 'clear']:
|
||||
for f in [
|
||||
"add_node",
|
||||
"add_nodes_from",
|
||||
"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())
|
||||
|
||||
super().__init__(None, **kwargs)
|
||||
@@ -231,33 +229,29 @@ class MemgraphDiGraphBase:
|
||||
self._pred = MemgraphAdjlistOuterDict(ctx, succ=False, multi=multi)
|
||||
|
||||
def _error(self):
|
||||
raise RuntimeError('Modification operations are not supported')
|
||||
raise RuntimeError("Modification operations are not supported")
|
||||
|
||||
|
||||
class MemgraphMultiDiGraph(MemgraphDiGraphBase, nx.MultiDiGraph):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, **kwargs):
|
||||
super().__init__(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, multi=True, **kwargs)
|
||||
super().__init__(incoming_graph_data=incoming_graph_data, ctx=ctx, multi=True, **kwargs)
|
||||
|
||||
|
||||
def MemgraphMultiGraph(incoming_graph_data=None, ctx=None, **kwargs):
|
||||
return MemgraphMultiDiGraph(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
return MemgraphMultiDiGraph(incoming_graph_data=incoming_graph_data, ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
|
||||
|
||||
class MemgraphDiGraph(MemgraphDiGraphBase, nx.DiGraph):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, **kwargs):
|
||||
super().__init__(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, multi=False, **kwargs)
|
||||
super().__init__(incoming_graph_data=incoming_graph_data, ctx=ctx, multi=False, **kwargs)
|
||||
|
||||
|
||||
def MemgraphGraph(incoming_graph_data=None, ctx=None, **kwargs):
|
||||
return MemgraphDiGraph(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
return MemgraphDiGraph(incoming_graph_data=incoming_graph_data, ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
|
||||
|
||||
class PropertiesDictionary(collections.abc.Mapping):
|
||||
__slots__ = ('_ctx', '_prop', '_len')
|
||||
__slots__ = ("_ctx", "_prop", "_len")
|
||||
|
||||
def __init__(self, ctx, prop):
|
||||
self._ctx = ctx
|
||||
@@ -270,8 +264,7 @@ class PropertiesDictionary(collections.abc.Mapping):
|
||||
try:
|
||||
return vertex.properties[self._prop]
|
||||
except KeyError:
|
||||
raise KeyError(("{} doesn\t have the required " +
|
||||
"property '{}'").format(vertex, self._prop))
|
||||
raise KeyError(("{} doesn\t have the required " + "property '{}'").format(vertex, self._prop))
|
||||
|
||||
def __iter__(self):
|
||||
for v in self._ctx.graph.vertices:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,20 @@
|
||||
import sys
|
||||
import mgp
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write(
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use wcc module.\n'
|
||||
'Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n')
|
||||
"\n" "NOTE: Please install networkx to be able to use wcc module.\n" "Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
raise import_error
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def get_components(vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge]
|
||||
) -> mgp.Record(n_components=int,
|
||||
components=mgp.List[mgp.List[mgp.Vertex]]):
|
||||
'''
|
||||
def get_components(
|
||||
vertices: mgp.List[mgp.Vertex], edges: mgp.List[mgp.Edge]
|
||||
) -> mgp.Record(n_components=int, components=mgp.List[mgp.List[mgp.Vertex]]):
|
||||
"""
|
||||
This procedure finds weakly connected components of a given subgraph of a
|
||||
directed graph.
|
||||
|
||||
@@ -41,7 +38,7 @@ def get_components(vertices: mgp.List[mgp.Vertex],
|
||||
WITH collect(n) AS nodes, collect(e) AS edges
|
||||
CALL wcc.get_components(nodes, edges) YIELD *
|
||||
RETURN n_components, components;
|
||||
'''
|
||||
"""
|
||||
g = nx.DiGraph()
|
||||
g.add_nodes_from(vertices)
|
||||
g.add_edges_from([(edge.from_vertex, edge.to_vertex) for edge in edges])
|
||||
|
||||
@@ -104,7 +104,9 @@ def retry(retry_limit, timeout=100):
|
||||
except Exception:
|
||||
time.sleep(timeout)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner_func
|
||||
|
||||
|
||||
@@ -163,8 +165,15 @@ def format_version(variant, version, offering, distance=None, shorthash=None, su
|
||||
|
||||
# Parse arguments.
|
||||
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("version", help="manual version override, if supplied the version isn't " "determined using git")
|
||||
parser.add_argument(
|
||||
"--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(
|
||||
"--variant",
|
||||
@@ -173,7 +182,9 @@ parser.add_argument(
|
||||
help="which variant of the version string should be generated",
|
||||
)
|
||||
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()
|
||||
|
||||
@@ -256,14 +267,27 @@ for version in versions:
|
||||
if current_version is None:
|
||||
raise Exception("You are attempting to determine the version for a very " "old version of Memgraph!")
|
||||
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"
|
||||
if distance == 0:
|
||||
print(format_version(args.variant, version_str, offering, suffix=args.suffix), end="")
|
||||
else:
|
||||
print(
|
||||
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="",
|
||||
)
|
||||
|
||||
@@ -84,6 +84,8 @@ std::string PermissionToString(Permission permission) {
|
||||
return "MODULE_WRITE";
|
||||
case Permission::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); }
|
||||
|
||||
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, const 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 Permissions &Role::permissions() const { return permissions_; }
|
||||
Permissions &Role::permissions() { return permissions_; }
|
||||
|
||||
LabelPermissions &Role::labelPermissions() { return labelPermissions_; }
|
||||
|
||||
nlohmann::json Role::Serialize() const {
|
||||
nlohmann::json data = nlohmann::json::object();
|
||||
data["rolename"] = rolename_;
|
||||
data["permissions"] = permissions_.Serialize();
|
||||
data["labelPermissions"] = labelPermissions_.Serialize();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -207,7 +297,9 @@ Role Role::Deserialize(const nlohmann::json &data) {
|
||||
throw AuthException("Couldn't load role data!");
|
||||
}
|
||||
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) {
|
||||
@@ -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)
|
||||
: 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) {
|
||||
if (password_hash_.empty()) return true;
|
||||
return VerifyPassword(password, password_hash_);
|
||||
@@ -271,6 +370,8 @@ const std::string &User::username() const { return username_; }
|
||||
const Permissions &User::permissions() const { return permissions_; }
|
||||
Permissions &User::permissions() { return permissions_; }
|
||||
|
||||
LabelPermissions &User::labelPermissions() { return labelPermissions_; }
|
||||
|
||||
const Role *User::role() const {
|
||||
if (role_.has_value()) {
|
||||
return &role_.value();
|
||||
@@ -283,6 +384,7 @@ nlohmann::json User::Serialize() const {
|
||||
data["username"] = username_;
|
||||
data["password_hash"] = password_hash_;
|
||||
data["permissions"] = permissions_.Serialize();
|
||||
data["labelPermissions"] = labelPermissions_.Serialize();
|
||||
// The role shouldn't be serialized here, it is stored as a foreign key.
|
||||
return data;
|
||||
}
|
||||
@@ -295,11 +397,14 @@ User User::Deserialize(const nlohmann::json &data) {
|
||||
throw AuthException("Couldn't load user data!");
|
||||
}
|
||||
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) {
|
||||
return first.username_ == second.username_ && first.password_hash_ == second.password_hash_ &&
|
||||
first.permissions_ == second.permissions_ && first.role_ == second.role_;
|
||||
}
|
||||
|
||||
} // namespace memgraph::auth
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <string>
|
||||
|
||||
#include <json/json.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace memgraph::auth {
|
||||
// These permissions must have values that are applicable for usage in a
|
||||
@@ -38,7 +39,8 @@ enum class Permission : uint64_t {
|
||||
STREAM = 1U << 17U,
|
||||
MODULE_READ = 1U << 18U,
|
||||
MODULE_WRITE = 1U << 19U,
|
||||
WEBSOCKET = 1U << 20U
|
||||
WEBSOCKET = 1U << 20U,
|
||||
LABELS = 1U << 21U
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
@@ -88,16 +90,52 @@ 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 {
|
||||
public:
|
||||
Role(const std::string &rolename);
|
||||
|
||||
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 Permissions &permissions() const;
|
||||
Permissions &permissions();
|
||||
|
||||
LabelPermissions &labelPermissions();
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
/// @throw AuthException if unable to deserialize.
|
||||
@@ -108,6 +146,7 @@ class Role final {
|
||||
private:
|
||||
std::string rolename_;
|
||||
Permissions permissions_;
|
||||
LabelPermissions labelPermissions_;
|
||||
};
|
||||
|
||||
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,
|
||||
const LabelPermissions &labelPermissions);
|
||||
|
||||
/// @throw AuthException if unable to verify the password.
|
||||
bool CheckPassword(const std::string &password);
|
||||
|
||||
@@ -138,6 +180,8 @@ class User final {
|
||||
|
||||
const Role *role() const;
|
||||
|
||||
LabelPermissions &labelPermissions();
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
/// @throw AuthException if unable to deserialize.
|
||||
@@ -150,7 +194,9 @@ class User final {
|
||||
std::string password_hash_;
|
||||
Permissions permissions_;
|
||||
std::optional<Role> role_;
|
||||
LabelPermissions labelPermissions_;
|
||||
};
|
||||
|
||||
bool operator==(const User &first, const User &second);
|
||||
|
||||
} // namespace memgraph::auth
|
||||
|
||||
@@ -18,19 +18,24 @@ roles_config = config["roles"]
|
||||
# Initialize LDAP server.
|
||||
tls = None
|
||||
if server_config["encryption"] != "disabled":
|
||||
cert_file = server_config["cert_file"] if server_config["cert_file"] \
|
||||
else None
|
||||
cert_file = server_config["cert_file"] if server_config["cert_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
|
||||
validate = ssl.CERT_REQUIRED if server_config["validate_cert"] \
|
||||
else ssl.CERT_NONE
|
||||
tls = ldap3.Tls(local_private_key_file=key_file,
|
||||
local_certificate_file=cert_file,
|
||||
ca_certs_file=ca_file,
|
||||
validate=validate)
|
||||
validate = ssl.CERT_REQUIRED if server_config["validate_cert"] else ssl.CERT_NONE
|
||||
tls = ldap3.Tls(
|
||||
local_private_key_file=key_file,
|
||||
local_certificate_file=cert_file,
|
||||
ca_certs_file=ca_file,
|
||||
validate=validate,
|
||||
)
|
||||
use_ssl = server_config["encryption"] == "ssl"
|
||||
server = ldap3.Server(server_config["host"], port=server_config["port"],
|
||||
tls=tls, use_ssl=use_ssl, get_info=ldap3.ALL)
|
||||
server = ldap3.Server(
|
||||
server_config["host"],
|
||||
port=server_config["port"],
|
||||
tls=tls,
|
||||
use_ssl=use_ssl,
|
||||
get_info=ldap3.ALL,
|
||||
)
|
||||
|
||||
|
||||
# Main authentication/authorization function.
|
||||
@@ -40,14 +45,12 @@ def authenticate(username, password):
|
||||
return {"authenticated": False, "role": ""}
|
||||
|
||||
# Create the DN of the user
|
||||
dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + \
|
||||
users_config["suffix"]
|
||||
dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + users_config["suffix"]
|
||||
|
||||
# Bind to the server
|
||||
conn = ldap3.Connection(server, dn, password)
|
||||
if server_config["encryption"] == "starttls" and not conn.start_tls():
|
||||
print("ERROR: Couldn't issue STARTTLS to the LDAP server!",
|
||||
file=sys.stderr)
|
||||
print("ERROR: Couldn't issue STARTTLS to the LDAP server!", file=sys.stderr)
|
||||
return {"authenticated": False, "role": ""}
|
||||
if not conn.bind():
|
||||
return {"authenticated": False, "role": ""}
|
||||
@@ -56,25 +59,32 @@ def authenticate(username, password):
|
||||
if roles_config["root_dn"] != "":
|
||||
# search for role
|
||||
search_filter = "(&(objectclass={objclass})({attr}={value}))".format(
|
||||
objclass=roles_config["root_objectclass"],
|
||||
attr=roles_config["user_attribute"],
|
||||
value=ldap3.utils.conv.escape_filter_chars(dn))
|
||||
succ = conn.search(roles_config["root_dn"], search_filter,
|
||||
search_scope=ldap3.LEVEL,
|
||||
attributes=[roles_config["role_attribute"]])
|
||||
objclass=roles_config["root_objectclass"],
|
||||
attr=roles_config["user_attribute"],
|
||||
value=ldap3.utils.conv.escape_filter_chars(dn),
|
||||
)
|
||||
succ = conn.search(
|
||||
roles_config["root_dn"],
|
||||
search_filter,
|
||||
search_scope=ldap3.LEVEL,
|
||||
attributes=[roles_config["role_attribute"]],
|
||||
)
|
||||
if not succ or len(conn.entries) == 0:
|
||||
return {"authenticated": True, "role": ""}
|
||||
if len(conn.entries) > 1:
|
||||
roles = list(map(lambda x: x[roles_config["role_attribute"]].value,
|
||||
conn.entries))
|
||||
roles = list(map(lambda x: x[roles_config["role_attribute"]].value, conn.entries))
|
||||
# Because we don't know exactly which role the user should have
|
||||
# we authorize the user with an empty role.
|
||||
print("WARNING: Found more than one role for "
|
||||
"user '" + username + "':", ", ".join(roles) + "!",
|
||||
file=sys.stderr)
|
||||
print(
|
||||
"WARNING: Found more than one role for " "user '" + username + "':",
|
||||
", ".join(roles) + "!",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return {"authenticated": True, "role": ""}
|
||||
return {"authenticated": True,
|
||||
"role": conn.entries[0][roles_config["role_attribute"]].value}
|
||||
return {
|
||||
"authenticated": True,
|
||||
"role": conn.entries[0][roles_config["role_attribute"]].value,
|
||||
}
|
||||
else:
|
||||
return {"authenticated": True, "role": ""}
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ auth::Permission PrivilegeToPermission(query::AuthQuery::Privilege privilege) {
|
||||
return auth::Permission::MODULE_WRITE;
|
||||
case query::AuthQuery::Privilege::WEBSOCKET:
|
||||
return auth::Permission::WEBSOCKET;
|
||||
case query::AuthQuery::Privilege::LABELS:
|
||||
return auth::Permission::LABELS;
|
||||
}
|
||||
}
|
||||
} // namespace memgraph::glue
|
||||
|
||||
@@ -501,7 +501,7 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
|
||||
if (first_user) {
|
||||
spdlog::info("{} is first created user. Granting all privileges.", username);
|
||||
GrantPrivilege(username, memgraph::query::kPrivilegesAll);
|
||||
GrantPrivilege(username, memgraph::query::kPrivilegesAll, {"*"});
|
||||
}
|
||||
|
||||
return user_added;
|
||||
@@ -747,8 +747,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void GrantPrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
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
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -757,8 +758,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void DenyPrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
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
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -767,8 +769,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void RevokePrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
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
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -779,7 +782,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
private:
|
||||
template <class TEditFun>
|
||||
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_)) {
|
||||
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) {
|
||||
edit_fun(&user->permissions(), permission);
|
||||
}
|
||||
for (const auto &label : labels) {
|
||||
edit_fun(&user->labelPermissions(), label);
|
||||
}
|
||||
locked_auth->SaveUser(*user);
|
||||
} else {
|
||||
for (const auto &permission : permissions) {
|
||||
edit_fun(&role->permissions(), permission);
|
||||
}
|
||||
for (const auto &label : labels) {
|
||||
edit_fun(&role->labelPermissions(), label);
|
||||
}
|
||||
locked_auth->SaveRole(*role);
|
||||
}
|
||||
} catch (const memgraph::auth::AuthException &e) {
|
||||
|
||||
@@ -2239,9 +2239,11 @@ cpp<#
|
||||
(user "std::string" :scope :public)
|
||||
(role "std::string" :scope :public)
|
||||
(user-or-role "std::string" :scope :public)
|
||||
|
||||
(password "Expression *" :initval "nullptr" :scope :public
|
||||
:slk-save #'slk-save-ast-pointer
|
||||
:slk-load (slk-load-ast-pointer "Expression"))
|
||||
(labels "std::vector<std::string>" :scope :public)
|
||||
(privileges "std::vector<Privilege>" :scope :public))
|
||||
(:public
|
||||
(lcp:define-enum action
|
||||
@@ -2253,7 +2255,7 @@ cpp<#
|
||||
(lcp:define-enum privilege
|
||||
(create delete match merge set remove index stats auth constraint
|
||||
dump replication durability read_file free_memory trigger config stream module_read module_write
|
||||
websocket)
|
||||
websocket labels)
|
||||
(:serialize))
|
||||
#>cpp
|
||||
AuthQuery() = default;
|
||||
@@ -2264,13 +2266,14 @@ cpp<#
|
||||
#>cpp
|
||||
AuthQuery(Action action, std::string user, std::string role,
|
||||
std::string user_or_role, Expression *password,
|
||||
std::vector<Privilege> privileges)
|
||||
std::vector<std::string> labels ,std::vector<Privilege> privileges)
|
||||
: action_(action),
|
||||
user_(user),
|
||||
role_(role),
|
||||
user_or_role_(user_or_role),
|
||||
password_(password),
|
||||
privileges_(privileges) {}
|
||||
labels_(labels),
|
||||
privileges_(privileges){}
|
||||
cpp<#)
|
||||
(:private
|
||||
#>cpp
|
||||
@@ -2295,7 +2298,8 @@ const std::vector<AuthQuery::Privilege> kPrivilegesAll = {
|
||||
AuthQuery::Privilege::FREE_MEMORY, AuthQuery::Privilege::TRIGGER,
|
||||
AuthQuery::Privilege::CONFIG, AuthQuery::Privilege::STREAM,
|
||||
AuthQuery::Privilege::MODULE_READ, AuthQuery::Privilege::MODULE_WRITE,
|
||||
AuthQuery::Privilege::WEBSOCKET};
|
||||
AuthQuery::Privilege::WEBSOCKET,
|
||||
AuthQuery::Privilege::LABELS};
|
||||
cpp<#
|
||||
|
||||
(lcp:define-class info-query (query)
|
||||
|
||||
@@ -1285,7 +1285,11 @@ antlrcpp::Any CypherMainVisitor::visitGrantPrivilege(MemgraphCypher::GrantPrivil
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
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 {
|
||||
/* grant all privileges */
|
||||
@@ -1303,7 +1307,11 @@ antlrcpp::Any CypherMainVisitor::visitDenyPrivilege(MemgraphCypher::DenyPrivileg
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
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 {
|
||||
/* deny all privileges */
|
||||
@@ -1321,7 +1329,11 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege(MemgraphCypher::RevokePriv
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
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 {
|
||||
/* revoke all privileges */
|
||||
@@ -1330,6 +1342,22 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege(MemgraphCypher::RevokePriv
|
||||
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
|
||||
*/
|
||||
@@ -1355,6 +1383,10 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege(MemgraphCypher::PrivilegeContext
|
||||
if (ctx->MODULE_READ()) return AuthQuery::Privilege::MODULE_READ;
|
||||
if (ctx->MODULE_WRITE()) return AuthQuery::Privilege::MODULE_WRITE;
|
||||
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!");
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +473,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
|
||||
*/
|
||||
antlrcpp::Any visitPrivilege(MemgraphCypher::PrivilegeContext *ctx) override;
|
||||
|
||||
/**
|
||||
* @return AuthQuery::LabelList
|
||||
*/
|
||||
antlrcpp::Any visitLabelList(MemgraphCypher::LabelListContext *ctx) override;
|
||||
|
||||
/**
|
||||
* @return AuthQuery*
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,7 @@ memgraphCypherKeyword : cypherKeyword
|
||||
| IDENTIFIED
|
||||
| ISOLATION
|
||||
| KAFKA
|
||||
| LABELS
|
||||
| LEVEL
|
||||
| LOAD
|
||||
| LOCK
|
||||
@@ -254,10 +255,15 @@ privilege : CREATE
|
||||
| MODULE_READ
|
||||
| MODULE_WRITE
|
||||
| WEBSOCKET
|
||||
| LABELS labels=labelList
|
||||
;
|
||||
|
||||
privilegeList : privilege ( ',' privilege )* ;
|
||||
|
||||
labelList : COLON label ( ',' COLON label )* ;
|
||||
|
||||
label : ( '*' | symbolicName ) ;
|
||||
|
||||
showPrivileges : SHOW PRIVILEGES FOR userOrRole=userOrRoleName ;
|
||||
|
||||
showRoleForUser : SHOW ROLE FOR user=userOrRoleName ;
|
||||
|
||||
@@ -66,6 +66,7 @@ IDENTIFIED : I D E N T I F I E D ;
|
||||
IGNORE : I G N O R E ;
|
||||
ISOLATION : I S O L A T I O N ;
|
||||
KAFKA : K A F K A ;
|
||||
LABELS : L A B E L S ;
|
||||
LEVEL : L E V E L ;
|
||||
LOAD : L O A D ;
|
||||
LOCK : L O C K ;
|
||||
|
||||
@@ -204,8 +204,9 @@ const trie::Trie kKeywords = {"union",
|
||||
"pulsar",
|
||||
"service_url",
|
||||
"version",
|
||||
"websocket"
|
||||
"foreach"};
|
||||
"websocket",
|
||||
"foreach",
|
||||
"labels"};
|
||||
|
||||
// Unicode codepoints that are allowed at the start of the unescaped name.
|
||||
const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts(
|
||||
|
||||
@@ -282,6 +282,8 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
std::string rolename = auth_query->role_;
|
||||
std::string user_or_role = auth_query->user_or_role_;
|
||||
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);
|
||||
|
||||
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_)) {
|
||||
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_) {
|
||||
@@ -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 (!valid_enterprise_license) {
|
||||
spdlog::warn("Granting all the privileges to {}.", username);
|
||||
auth->GrantPrivilege(username, kPrivilegesAll);
|
||||
auth->GrantPrivilege(username, kPrivilegesAll, {});
|
||||
}
|
||||
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
@@ -386,20 +389,20 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::GRANT_PRIVILEGE:
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->GrantPrivilege(user_or_role, privileges);
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->GrantPrivilege(user_or_role, privileges, labels);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::DENY_PRIVILEGE:
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->DenyPrivilege(user_or_role, privileges);
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->DenyPrivilege(user_or_role, privileges, labels);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::REVOKE_PRIVILEGE: {
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->RevokePrivilege(user_or_role, privileges);
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->RevokePrivilege(user_or_role, privileges, labels);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
|
||||
@@ -99,14 +99,16 @@ class AuthQueryHandler {
|
||||
virtual std::vector<std::vector<TypedValue>> GetPrivileges(const std::string &user_or_role) = 0;
|
||||
|
||||
/// @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.
|
||||
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.
|
||||
virtual void RevokePrivilege(const std::string &user_or_role,
|
||||
const std::vector<AuthQuery::Privilege> &privileges) = 0;
|
||||
virtual void RevokePrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) = 0;
|
||||
};
|
||||
|
||||
enum class QueryHandlerResult { COMMIT, ABORT, NOTHING };
|
||||
|
||||
@@ -15,33 +15,31 @@
|
||||
import sys
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
driver = GraphDatabase.driver('bolt://localhost:7687',
|
||||
auth=basic_auth('', ''),
|
||||
encrypted=False)
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False)
|
||||
session = driver.session()
|
||||
|
||||
session.run('MATCH (n) DETACH DELETE n').consume()
|
||||
print('Database cleared.')
|
||||
session.run("MATCH (n) DETACH DELETE n").consume()
|
||||
print("Database cleared.")
|
||||
|
||||
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']
|
||||
print('Record matched.')
|
||||
node = session.run("MATCH (n) RETURN n").single()["n"]
|
||||
print("Record matched.")
|
||||
|
||||
label = list(node.labels)[0]
|
||||
name = node['name']
|
||||
age = node['age']
|
||||
name = node["name"]
|
||||
age = node["age"]
|
||||
|
||||
if label != 'Person' or name != 'Alice' or age != 22:
|
||||
print('Data does not match')
|
||||
if label != "Person" or name != "Alice" or age != 22:
|
||||
print("Data does not match")
|
||||
sys.exit(1)
|
||||
|
||||
print('Label: %s' % label)
|
||||
print('name: %s' % name)
|
||||
print('age: %s' % age)
|
||||
print("Label: %s" % label)
|
||||
print("name: %s" % name)
|
||||
print("age: %s" % age)
|
||||
|
||||
session.close()
|
||||
driver.close()
|
||||
|
||||
print('All ok!')
|
||||
print("All ok!")
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687",
|
||||
auth=basic_auth("", ""),
|
||||
encrypted=False)
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False)
|
||||
|
||||
query_template = 'CREATE (n {name:"%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)
|
||||
# of a query that can be executed via driver
|
||||
while True:
|
||||
assert min_len > 0 and max_len > 0, \
|
||||
"The lengths have to be positive values! If this happens something" \
|
||||
" is terrible wrong with min & max lengths OR the database" \
|
||||
assert min_len > 0 and max_len > 0, (
|
||||
"The lengths have to be positive values! If this happens something"
|
||||
" is terrible wrong with min & max lengths OR the database"
|
||||
" isn't available."
|
||||
)
|
||||
property_size = (max_len + min_len) // 2
|
||||
try:
|
||||
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!"
|
||||
|
||||
print("\nThe max length of a query from Python driver is: %s\n" %
|
||||
(template_size + property_size))
|
||||
print("\nThe max length of a query from Python driver is: %s\n" % (template_size + property_size))
|
||||
|
||||
# sessions are not closed bacause all sessions that are
|
||||
# executed with wrong query size might be broken
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
from neo4j.exceptions import ClientError, TransientError
|
||||
|
||||
|
||||
def tx_error(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
|
||||
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()
|
||||
print(a[0])
|
||||
|
||||
|
||||
def tx_good(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
|
||||
print(a[0])
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
|
||||
print(a[0])
|
||||
|
||||
|
||||
def tx_too_long(tx):
|
||||
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):
|
||||
with driver.session() as session:
|
||||
|
||||
@@ -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);",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("function_type", ["py", "c"])
|
||||
def test_case_sensitivity(connection, function_type):
|
||||
cursor = connection.cursor()
|
||||
|
||||
@@ -141,10 +141,16 @@ def test_add_replica_invalid_timeout(connection):
|
||||
cursor = connection(7687, "main").cursor()
|
||||
|
||||
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):
|
||||
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;")
|
||||
assert 0 == len(actual_data)
|
||||
|
||||
@@ -115,7 +115,10 @@ def start_stream(cursor, stream_name):
|
||||
|
||||
def start_stream_with_limit(cursor, stream_name, batch_limit, timeout=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:
|
||||
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(
|
||||
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
|
||||
# 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 = (
|
||||
[ # 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
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_2 = (
|
||||
[ # 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
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_3 = (
|
||||
[ # 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
|
||||
)
|
||||
@@ -389,20 +415,32 @@ def test_check_stream_different_number_of_queries_than_messages(connection, stre
|
||||
|
||||
expected_queries_and_raw_messages_2 = (
|
||||
[ # 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
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_3 = (
|
||||
[ # 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"},
|
||||
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
|
||||
)
|
||||
@@ -467,7 +505,10 @@ def test_start_stream_with_batch_limit_reaching_timeout(connection, stream_creat
|
||||
start_time = time.time()
|
||||
|
||||
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()
|
||||
assert (
|
||||
@@ -483,7 +524,10 @@ def test_start_stream_with_batch_limit_while_check_running(
|
||||
def start_check_stream(stream_name, batch_limit, timeout):
|
||||
connection = connect()
|
||||
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):
|
||||
connection = connect()
|
||||
@@ -518,7 +562,9 @@ def test_start_stream_with_batch_limit_while_check_running(
|
||||
|
||||
# 2/
|
||||
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)
|
||||
thread_stream_running.start()
|
||||
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):
|
||||
connection = connect()
|
||||
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"
|
||||
BATCH_LIMIT = 1
|
||||
@@ -553,7 +602,9 @@ def test_check_while_stream_with_batch_limit_running(connection, stream_creator,
|
||||
|
||||
# 1/
|
||||
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()
|
||||
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)
|
||||
|
||||
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()
|
||||
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()
|
||||
|
||||
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()
|
||||
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()
|
||||
|
||||
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()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
|
||||
@@ -37,29 +37,22 @@ def connection():
|
||||
|
||||
|
||||
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")
|
||||
def kafka_topics():
|
||||
admin_client = KafkaAdminClient(
|
||||
bootstrap_servers="localhost:9092",
|
||||
client_id="test")
|
||||
admin_client = KafkaAdminClient(bootstrap_servers="localhost:9092", client_id="test")
|
||||
# The issue arises if we remove default kafka topics, e.g.
|
||||
# "__consumer_offsets"
|
||||
previous_topics = [
|
||||
topic for topic in admin_client.list_topics() if topic != "__consumer_offsets"]
|
||||
previous_topics = [topic for topic in admin_client.list_topics() if topic != "__consumer_offsets"]
|
||||
if previous_topics:
|
||||
admin_client.delete_topics(topics=previous_topics, timeout_ms=5000)
|
||||
|
||||
topics = get_topics(3)
|
||||
topics_to_create = []
|
||||
for topic in topics:
|
||||
topics_to_create.append(
|
||||
NewTopic(
|
||||
name=topic,
|
||||
num_partitions=1,
|
||||
replication_factor=1))
|
||||
topics_to_create.append(NewTopic(name=topic, num_partitions=1, replication_factor=1))
|
||||
|
||||
admin_client.create_topics(new_topics=topics_to_create, timeout_ms=5000)
|
||||
yield topics
|
||||
@@ -80,6 +73,5 @@ def pulsar_client():
|
||||
def pulsar_topics():
|
||||
topics = get_topics(3)
|
||||
for topic in topics:
|
||||
requests.delete(
|
||||
f'http://127.0.0.1:6652/admin/v2/persistent/public/default/{topic}?force=true')
|
||||
requests.delete(f"http://127.0.0.1:6652/admin/v2/persistent/public/default/{topic}?force=true")
|
||||
yield topics
|
||||
|
||||
@@ -20,7 +20,10 @@ import common
|
||||
|
||||
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)
|
||||
@@ -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)
|
||||
|
||||
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()
|
||||
time.sleep(2)
|
||||
assert common.get_is_running(cursor, stream_name)
|
||||
|
||||
@@ -18,13 +18,20 @@ import time
|
||||
from multiprocessing import Process, Value
|
||||
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):
|
||||
decoded_payload = payload_byte.decode("utf-8")
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
@@ -157,7 +165,8 @@ def test_check_stream(pulsar_client, pulsar_topics, connection, transformation):
|
||||
time.sleep(1)
|
||||
|
||||
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)
|
||||
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}"
|
||||
|
||||
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):
|
||||
@@ -311,7 +321,8 @@ def test_restart_after_error(pulsar_client, pulsar_topics, connection):
|
||||
time.sleep(1)
|
||||
|
||||
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)
|
||||
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"
|
||||
|
||||
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):
|
||||
@@ -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"
|
||||
|
||||
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):
|
||||
@@ -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"
|
||||
|
||||
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):
|
||||
@@ -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} "
|
||||
|
||||
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):
|
||||
@@ -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} "
|
||||
|
||||
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):
|
||||
|
||||
@@ -15,6 +15,7 @@ import time
|
||||
import mgclient
|
||||
import common
|
||||
|
||||
|
||||
def get_cursor_with_user(username):
|
||||
connection = common.connect(username=username, password="")
|
||||
return connection.cursor()
|
||||
@@ -22,23 +23,21 @@ def get_cursor_with_user(username):
|
||||
|
||||
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"GRANT ALL PRIVILEGES TO {admin_user}")
|
||||
common.execute_and_fetch_all(cursor, f"GRANT ALL PRIVILEGES TO {admin_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"GRANT STREAM TO {stream_user}")
|
||||
common.execute_and_fetch_all(cursor, f"GRANT STREAM TO {stream_user}")
|
||||
|
||||
|
||||
def test_ownerless_stream(kafka_producer, kafka_topics, connection):
|
||||
assert len(kafka_topics) > 0
|
||||
userless_cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(userless_cursor,
|
||||
"CREATE KAFKA STREAM ownerless "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
common.execute_and_fetch_all(
|
||||
userless_cursor,
|
||||
"CREATE KAFKA STREAM ownerless " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
|
||||
)
|
||||
common.start_stream(userless_cursor, "ownerless")
|
||||
time.sleep(1)
|
||||
|
||||
@@ -46,11 +45,9 @@ def test_ownerless_stream(kafka_producer, kafka_topics, connection):
|
||||
create_admin_user(userless_cursor, admin_user)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60)
|
||||
assert common.timed_wait(
|
||||
lambda: not common.get_is_running(userless_cursor, "ownerless"))
|
||||
assert common.timed_wait(lambda: not common.get_is_running(userless_cursor, "ownerless"))
|
||||
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 0
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 0
|
||||
|
||||
common.execute_and_fetch_all(userless_cursor, f"DROP USER {admin_user}")
|
||||
common.start_stream(userless_cursor, "ownerless")
|
||||
@@ -58,11 +55,9 @@ def test_ownerless_stream(kafka_producer, kafka_topics, connection):
|
||||
|
||||
second_message = b"second message"
|
||||
kafka_producer.send(kafka_topics[0], second_message).get(timeout=60)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], second_message)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(userless_cursor, kafka_topics[0], second_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
|
||||
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)
|
||||
stream_cursor = get_cursor_with_user(stream_user)
|
||||
|
||||
common.execute_and_fetch_all(stream_cursor, "CREATE KAFKA STREAM test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
common.execute_and_fetch_all(
|
||||
stream_cursor,
|
||||
"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,
|
||||
"kafka_transform.simple", stream_user, False))
|
||||
common.check_stream_info(
|
||||
userless_cursor,
|
||||
"test",
|
||||
("test", "kafka", 100, 1000, "kafka_transform.simple", stream_user, False),
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
stream_cursor = get_cursor_with_user(stream_user)
|
||||
|
||||
common.execute_and_fetch_all(stream_cursor,
|
||||
"CREATE KAFKA STREAM insufficient_test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
common.execute_and_fetch_all(
|
||||
stream_cursor,
|
||||
"CREATE KAFKA STREAM insufficient_test " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
|
||||
)
|
||||
|
||||
# the stream is started by admin, but should check against the owner
|
||||
# privileges
|
||||
@@ -104,24 +103,19 @@ def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
|
||||
time.sleep(1)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60)
|
||||
assert common.timed_wait(
|
||||
lambda: not common.get_is_running(userless_cursor, "insufficient_test"))
|
||||
assert common.timed_wait(lambda: not common.get_is_running(userless_cursor, "insufficient_test"))
|
||||
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 0
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 0
|
||||
|
||||
common.execute_and_fetch_all(
|
||||
admin_cursor, f"GRANT CREATE TO {stream_user}")
|
||||
common.execute_and_fetch_all(admin_cursor, f"GRANT CREATE TO {stream_user}")
|
||||
common.start_stream(userless_cursor, "insufficient_test")
|
||||
time.sleep(1)
|
||||
|
||||
second_message = b"second message"
|
||||
kafka_producer.send(kafka_topics[0], second_message).get(timeout=60)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], second_message)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(userless_cursor, kafka_topics[0], second_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
|
||||
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"
|
||||
create_stream_user(userless_cursor, stream_user)
|
||||
stream_cursor = get_cursor_with_user(stream_user)
|
||||
common.execute_and_fetch_all(
|
||||
admin_cursor, f"GRANT CREATE TO {stream_user}")
|
||||
common.execute_and_fetch_all(admin_cursor, f"GRANT CREATE TO {stream_user}")
|
||||
|
||||
common.execute_and_fetch_all(stream_cursor,
|
||||
"CREATE KAFKA STREAM insufficient_test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
common.execute_and_fetch_all(
|
||||
stream_cursor,
|
||||
"CREATE KAFKA STREAM insufficient_test " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
|
||||
)
|
||||
|
||||
common.start_stream(stream_cursor, "insufficient_test")
|
||||
time.sleep(1)
|
||||
@@ -149,11 +142,9 @@ def test_happy_case(kafka_producer, kafka_topics, connection):
|
||||
first_message = b"first message"
|
||||
kafka_producer.send(kafka_topics[0], first_message).get(timeout=60)
|
||||
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], first_message)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(userless_cursor, kafka_topics[0], first_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -23,7 +23,10 @@ def check_stream_no_filtering(
|
||||
message = messages.message_at(i)
|
||||
payload_as_str = message.payload().decode("utf-8")
|
||||
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
|
||||
@@ -44,13 +47,17 @@ def check_stream_with_filtering(
|
||||
continue
|
||||
|
||||
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:
|
||||
result_queries.append(
|
||||
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}"},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -59,7 +59,9 @@ def with_parameters(context: mgp.TransCtx, messages: mgp.Messages) -> mgp.Record
|
||||
|
||||
|
||||
@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 = []
|
||||
|
||||
for i in range(0, messages.total_messages()):
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import mgp
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def create_vertex(ctx: mgp.ProcCtx, id: mgp.Any) -> mgp.Record(v=mgp.Any):
|
||||
v = None
|
||||
@@ -36,15 +37,14 @@ def detach_delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record():
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def create_edge(ctx: mgp.ProcCtx, from_vertex: mgp.Vertex,
|
||||
to_vertex: mgp.Vertex,
|
||||
edge_type: str) -> mgp.Record(e=mgp.Any):
|
||||
def create_edge(
|
||||
ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, to_vertex: mgp.Vertex, edge_type: str
|
||||
) -> mgp.Record(e=mgp.Any):
|
||||
e = None
|
||||
try:
|
||||
e = ctx.graph.create_edge(
|
||||
from_vertex, to_vertex, mgp.EdgeType(edge_type))
|
||||
e.properties.set("id", 1);
|
||||
e.properties.set("tbd", 0);
|
||||
e = ctx.graph.create_edge(from_vertex, to_vertex, mgp.EdgeType(edge_type))
|
||||
e.properties.set("id", 1)
|
||||
e.properties.set("tbd", 0)
|
||||
except RuntimeError as ex:
|
||||
return mgp.Record(e=str(ex))
|
||||
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)
|
||||
return mgp.Record()
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def remove_property(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record():
|
||||
object.properties.set("tbd", None)
|
||||
return mgp.Record()
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def add_label(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str) -> mgp.Record(o=mgp.Any):
|
||||
def add_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
|
||||
object.add_label(name)
|
||||
return mgp.Record(o=object)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str) -> mgp.Record(o=mgp.Any):
|
||||
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
|
||||
object.remove_label(name)
|
||||
return mgp.Record(o=object)
|
||||
|
||||
@@ -13,8 +13,7 @@ import mgclient
|
||||
import typing
|
||||
|
||||
|
||||
def execute_and_fetch_all(cursor: mgclient.Cursor, query: str,
|
||||
params: dict = {}) -> typing.List[tuple]:
|
||||
def execute_and_fetch_all(cursor: mgclient.Cursor, query: str, params: dict = {}) -> typing.List[tuple]:
|
||||
cursor.execute(query, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ import mgp
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def underlying_graph_is_mutable(ctx: mgp.ProcCtx,
|
||||
object: mgp.Any) -> mgp.Record(mutable=bool):
|
||||
def underlying_graph_is_mutable(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(mutable=bool):
|
||||
return mgp.Record(mutable=object.underlying_graph_is_mutable())
|
||||
|
||||
|
||||
|
||||
@@ -35,13 +35,12 @@ def detach_delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record():
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def create_edge(ctx: mgp.ProcCtx, from_vertex: mgp.Vertex,
|
||||
to_vertex: mgp.Vertex,
|
||||
edge_type: str) -> mgp.Record(e=mgp.Any):
|
||||
def create_edge(
|
||||
ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, to_vertex: mgp.Vertex, edge_type: str
|
||||
) -> mgp.Record(e=mgp.Any):
|
||||
e = None
|
||||
try:
|
||||
e = ctx.graph.create_edge(
|
||||
from_vertex, to_vertex, mgp.EdgeType(edge_type))
|
||||
e = ctx.graph.create_edge(from_vertex, to_vertex, mgp.EdgeType(edge_type))
|
||||
except RuntimeError as ex:
|
||||
return mgp.Record(e=str(ex))
|
||||
return mgp.Record(e=e)
|
||||
@@ -54,29 +53,25 @@ def delete_edge(ctx: mgp.ProcCtx, edge: mgp.Edge) -> mgp.Record():
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def set_property(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record():
|
||||
def set_property(ctx: mgp.ProcCtx, object: mgp.Any, name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record():
|
||||
object.properties.set(name, value)
|
||||
return mgp.Record()
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def add_label(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str) -> mgp.Record(o=mgp.Any):
|
||||
def add_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
|
||||
object.add_label(name)
|
||||
return mgp.Record(o=object)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str) -> mgp.Record(o=mgp.Any):
|
||||
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
|
||||
object.remove_label(name)
|
||||
return mgp.Record(o=object)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def underlying_graph_is_mutable(ctx: mgp.ProcCtx,
|
||||
object: mgp.Any) -> mgp.Record(mutable=bool):
|
||||
def underlying_graph_is_mutable(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(mutable=bool):
|
||||
return mgp.Record(mutable=object.underlying_graph_is_mutable())
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ import typing
|
||||
import mgclient
|
||||
import sys
|
||||
import pytest
|
||||
from common import (execute_and_fetch_all,
|
||||
has_one_result_row, has_n_result_row)
|
||||
from common import execute_and_fetch_all, has_one_result_row, has_n_result_row
|
||||
|
||||
|
||||
def test_is_write(connection):
|
||||
@@ -22,15 +21,19 @@ def test_is_write(connection):
|
||||
result_order = "name, signature, is_write"
|
||||
cursor = connection.cursor()
|
||||
for proc in execute_and_fetch_all(
|
||||
cursor, "CALL mg.procedures() YIELD * WITH name, signature, "
|
||||
"is_write WHERE name STARTS WITH 'write' "
|
||||
f"RETURN {result_order}"):
|
||||
cursor,
|
||||
"CALL mg.procedures() YIELD * WITH name, signature, "
|
||||
"is_write WHERE name STARTS WITH 'write' "
|
||||
f"RETURN {result_order}",
|
||||
):
|
||||
assert proc[is_write] is True
|
||||
|
||||
for proc in execute_and_fetch_all(
|
||||
cursor, "CALL mg.procedures() YIELD * WITH name, signature, "
|
||||
"is_write WHERE NOT name STARTS WITH 'write' "
|
||||
f"RETURN {result_order}"):
|
||||
cursor,
|
||||
"CALL mg.procedures() YIELD * WITH name, signature, "
|
||||
"is_write WHERE NOT name STARTS WITH 'write' "
|
||||
f"RETURN {result_order}",
|
||||
):
|
||||
assert proc[is_write] is False
|
||||
|
||||
assert cursor.description[0].name == "name"
|
||||
@@ -41,8 +44,7 @@ def test_is_write(connection):
|
||||
def test_single_vertex(connection):
|
||||
cursor = connection.cursor()
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
result = execute_and_fetch_all(
|
||||
cursor, "CALL write.create_vertex() YIELD v RETURN v")
|
||||
result = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")
|
||||
vertex = result[0][0]
|
||||
assert isinstance(vertex, mgclient.Node)
|
||||
assert has_one_result_row(cursor, "MATCH (n) RETURN n")
|
||||
@@ -50,14 +52,13 @@ def test_single_vertex(connection):
|
||||
assert vertex.properties == {}
|
||||
|
||||
def add_label(label: str):
|
||||
execute_and_fetch_all(
|
||||
cursor, f"MATCH (n) CALL write.add_label(n, '{label}') "
|
||||
"YIELD * RETURN *")
|
||||
execute_and_fetch_all(cursor, f"MATCH (n) CALL write.add_label(n, '{label}') " "YIELD * RETURN *")
|
||||
|
||||
def remove_label(label: str):
|
||||
execute_and_fetch_all(
|
||||
cursor, f"MATCH (n) CALL write.remove_label(n, '{label}') "
|
||||
"YIELD * RETURN *")
|
||||
cursor,
|
||||
f"MATCH (n) CALL write.remove_label(n, '{label}') " "YIELD * RETURN *",
|
||||
)
|
||||
|
||||
def get_vertex() -> mgclient.Node:
|
||||
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):
|
||||
nonlocal cursor
|
||||
execute_and_fetch_all(
|
||||
cursor, f"MATCH (n) CALL write.set_property(n, '{property_name}', "
|
||||
"$property) YIELD * RETURN *", {"property": property})
|
||||
cursor,
|
||||
f"MATCH (n) CALL write.set_property(n, '{property_name}', " "$property) YIELD * RETURN *",
|
||||
{"property": property},
|
||||
)
|
||||
|
||||
label_1 = "LABEL1"
|
||||
label_2 = "LABEL2"
|
||||
@@ -89,24 +92,23 @@ def test_single_vertex(connection):
|
||||
set_property(property_name, None)
|
||||
assert get_vertex().properties == {}
|
||||
|
||||
execute_and_fetch_all(
|
||||
cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1")
|
||||
execute_and_fetch_all(cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1")
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
|
||||
|
||||
def test_single_edge(connection):
|
||||
cursor = connection.cursor()
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
v1_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
|
||||
v1_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 = execute_and_fetch_all(
|
||||
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
f"MATCH (m) WHERE id(m) = {v2_id} "
|
||||
f"CALL write.create_edge(n, m, '{edge_type}') "
|
||||
"YIELD e RETURN e")[0][0]
|
||||
cursor,
|
||||
f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
f"MATCH (m) WHERE id(m) = {v2_id} "
|
||||
f"CALL write.create_edge(n, m, '{edge_type}') "
|
||||
"YIELD e RETURN e",
|
||||
)[0][0]
|
||||
|
||||
assert edge.type == edge_type
|
||||
assert edge.properties == {}
|
||||
@@ -120,9 +122,10 @@ def test_single_edge(connection):
|
||||
def set_property(property_name: str, property: typing.Any):
|
||||
nonlocal cursor
|
||||
execute_and_fetch_all(
|
||||
cursor, "MATCH ()-[e]->() "
|
||||
f"CALL write.set_property(e, '{property_name}', "
|
||||
"$property) YIELD * RETURN *", {"property": property})
|
||||
cursor,
|
||||
"MATCH ()-[e]->() " f"CALL write.set_property(e, '{property_name}', " "$property) YIELD * RETURN *",
|
||||
{"property": property},
|
||||
)
|
||||
|
||||
set_property(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}
|
||||
set_property(property_name, None)
|
||||
assert get_edge().properties == {}
|
||||
execute_and_fetch_all(
|
||||
cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1")
|
||||
execute_and_fetch_all(cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1")
|
||||
assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0)
|
||||
|
||||
|
||||
def test_detach_delete_vertex(connection):
|
||||
cursor = connection.cursor()
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
v1_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
|
||||
v1_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(
|
||||
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"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")
|
||||
execute_and_fetch_all(
|
||||
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
"CALL write.detach_delete_vertex(n) YIELD * RETURN 1")
|
||||
cursor,
|
||||
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 ()-[e]->() RETURN e", 0)
|
||||
assert has_one_result_row(
|
||||
cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n")
|
||||
assert has_one_result_row(cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n")
|
||||
|
||||
|
||||
def test_graph_mutability(connection):
|
||||
cursor = connection.cursor()
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
v1_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
|
||||
v1_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(
|
||||
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"CALL write.create_edge(n, m, 'EDGE') "
|
||||
"YIELD e RETURN e")
|
||||
"YIELD e RETURN e",
|
||||
)
|
||||
|
||||
def test_mutability(is_write: bool):
|
||||
module = "write" if is_write else "read"
|
||||
assert execute_and_fetch_all(
|
||||
cursor, f"CALL {module}.graph_is_mutable() "
|
||||
"YIELD mutable RETURN mutable")[0][0] is is_write
|
||||
assert execute_and_fetch_all(
|
||||
cursor, "MATCH (n) "
|
||||
f"CALL {module}.underlying_graph_is_mutable(n) "
|
||||
"YIELD mutable RETURN mutable")[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
|
||||
assert (
|
||||
execute_and_fetch_all(cursor, f"CALL {module}.graph_is_mutable() " "YIELD mutable RETURN mutable",)[
|
||||
0
|
||||
][0]
|
||||
is is_write
|
||||
)
|
||||
assert (
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
"MATCH (n) " f"CALL {module}.underlying_graph_is_mutable(n) " "YIELD mutable RETURN mutable",
|
||||
)[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(False)
|
||||
|
||||
@@ -20,6 +20,7 @@ from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
# Helper class and functions
|
||||
|
||||
|
||||
class TestResults:
|
||||
def __init__(self):
|
||||
self.total = 0
|
||||
@@ -39,18 +40,16 @@ class TestResults:
|
||||
|
||||
# Behave specific functions
|
||||
|
||||
|
||||
def before_all(context):
|
||||
# logging
|
||||
logging.basicConfig(level="DEBUG")
|
||||
context.log = logging.getLogger(__name__)
|
||||
|
||||
# driver
|
||||
uri = "bolt://{}:{}".format(context.config.db_host,
|
||||
context.config.db_port)
|
||||
auth_token = basic_auth(
|
||||
context.config.db_user, context.config.db_pass)
|
||||
context.driver = GraphDatabase.driver(uri, auth=auth_token,
|
||||
encrypted=False)
|
||||
uri = "bolt://{}:{}".format(context.config.db_host, context.config.db_port)
|
||||
auth_token = basic_auth(context.config.db_user, context.config.db_pass)
|
||||
context.driver = GraphDatabase.driver(uri, auth=auth_token, encrypted=False)
|
||||
|
||||
# test results
|
||||
context.test_results = TestResults()
|
||||
@@ -63,8 +62,7 @@ def before_scenario(context, scenario):
|
||||
|
||||
def after_scenario(context, scenario):
|
||||
context.test_results.add_test(scenario.status)
|
||||
if context.config.single_scenario or \
|
||||
(context.config.single_fail and scenario.status == "failed"):
|
||||
if context.config.single_scenario or (context.config.single_fail and scenario.status == "failed"):
|
||||
print("Press enter to continue")
|
||||
sys.stdin.readline()
|
||||
|
||||
@@ -87,5 +85,5 @@ def after_all(context):
|
||||
"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)
|
||||
|
||||
@@ -55,22 +55,14 @@ def main():
|
||||
add_config("--test-directory")
|
||||
|
||||
# Arguments that should be passed on to Behave
|
||||
add_argument("--db-host", default="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-user", default="memgraph",
|
||||
help="server user (default is memgraph)")
|
||||
add_argument("--db-pass", default="memgraph",
|
||||
help="server pass (default is memgraph)")
|
||||
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("--db-host", default="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-user", default="memgraph", help="server user (default is memgraph)")
|
||||
add_argument("--db-pass", default="memgraph", help="server pass (default is memgraph)")
|
||||
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")
|
||||
|
||||
# Parse arguments
|
||||
@@ -96,5 +88,5 @@ def main():
|
||||
return behave_main(behave_args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
@@ -15,11 +15,11 @@ from behave import given
|
||||
import graph
|
||||
|
||||
|
||||
@given(u'the binary-tree-1 graph')
|
||||
@given("the binary-tree-1 graph")
|
||||
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):
|
||||
graph.create_graph('binary-tree-2', context)
|
||||
graph.create_graph("binary-tree-2", context)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
def query(q, context, params={}):
|
||||
"""
|
||||
Function used to execute query on database. Query results are
|
||||
@@ -44,7 +45,7 @@ def query(q, context, params={}):
|
||||
except Exception as e:
|
||||
# exception
|
||||
context.exception = e
|
||||
context.log.info('%s', str(e))
|
||||
context.log.info("%s", str(e))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -24,234 +24,234 @@ def handle_error(context):
|
||||
@param context:
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
handle_error(context)
|
||||
|
||||
@@ -23,12 +23,12 @@ def clear_graph(context):
|
||||
database.query("MATCH (n) DETACH DELETE n", context)
|
||||
|
||||
|
||||
@given('an empty graph')
|
||||
@given("an empty graph")
|
||||
def empty_graph_step(context):
|
||||
clear_graph(context)
|
||||
|
||||
|
||||
@given('any graph')
|
||||
@given("any graph")
|
||||
def any_graph_step(context):
|
||||
clear_graph(context)
|
||||
|
||||
@@ -46,20 +46,18 @@ def create_graph(name, context):
|
||||
and sets graph properties to beginning values.
|
||||
"""
|
||||
clear_graph(context)
|
||||
path = os.path.join(context.config.test_directory, "graphs",
|
||||
name + ".cypher")
|
||||
path = os.path.join(context.config.test_directory, "graphs", name + ".cypher")
|
||||
|
||||
q_marks = ["'", '"', '`']
|
||||
q_marks = ["'", '"', "`"]
|
||||
|
||||
with open(path, 'r') as f:
|
||||
content = f.read().replace('\n', ' ')
|
||||
single_query = ''
|
||||
with open(path, "r") as f:
|
||||
content = f.read().replace("\n", " ")
|
||||
single_query = ""
|
||||
quote = None
|
||||
i = 0
|
||||
while i < len(content):
|
||||
ch = content[i]
|
||||
if ch == '\\' and i != len(content) - 1 and \
|
||||
content[i + 1] in q_marks:
|
||||
if ch == "\\" and i != len(content) - 1 and content[i + 1] in q_marks:
|
||||
single_query += ch + content[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
@@ -68,9 +66,9 @@ def create_graph(name, context):
|
||||
quote = None
|
||||
elif ch in q_marks and quote is None:
|
||||
quote = ch
|
||||
if ch == ';' and quote is None:
|
||||
if ch == ";" and quote is None:
|
||||
database.query(single_query, context)
|
||||
single_query = ''
|
||||
single_query = ""
|
||||
i += 1
|
||||
if single_query.strip() != '':
|
||||
if single_query.strip() != "":
|
||||
database.query(single_query, context)
|
||||
|
||||
@@ -29,13 +29,13 @@ def parse(el, ignore_order):
|
||||
@return:
|
||||
Parsed string of element.
|
||||
"""
|
||||
if el.startswith('(') and el.endswith(')'):
|
||||
if el.startswith("(") and el.endswith(")"):
|
||||
return parse_node(el, ignore_order)
|
||||
if el.startswith('<') and el.endswith('>'):
|
||||
if el.startswith("<") and el.endswith(">"):
|
||||
return parse_path(el, ignore_order)
|
||||
if el.startswith('{') and el.endswith('}'):
|
||||
if el.startswith("{") and el.endswith("}"):
|
||||
return parse_map(el, ignore_order)
|
||||
if el.startswith('[') and el.endswith(']'):
|
||||
if el.startswith("[") and el.endswith("]"):
|
||||
if is_list(el):
|
||||
return parse_list(el, ignore_order)
|
||||
else:
|
||||
@@ -51,7 +51,7 @@ def is_list(el):
|
||||
@return:
|
||||
true if el is list.
|
||||
"""
|
||||
if el[1] == ':':
|
||||
if el[1] == ":":
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -64,20 +64,20 @@ def parse_path(path, ignore_order):
|
||||
@return:
|
||||
parsed path
|
||||
"""
|
||||
parsed_path = '<'
|
||||
parsed_path = "<"
|
||||
dif_open_closed_brackets = 0
|
||||
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
|
||||
if dif_open_closed_brackets == 1:
|
||||
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
|
||||
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:
|
||||
parsed_path += path[i]
|
||||
parsed_path += '>'
|
||||
parsed_path += ">"
|
||||
return parsed_path
|
||||
|
||||
|
||||
@@ -89,28 +89,27 @@ def parse_node(node_str, ignore_order):
|
||||
@return:
|
||||
parsed node
|
||||
"""
|
||||
label = ''
|
||||
label = ""
|
||||
labels = []
|
||||
props_start = None
|
||||
for i in range(1, len(node_str)):
|
||||
if node_str[i] == ':' or node_str[i] == ')' or node_str[i] == '{':
|
||||
if label.startswith(':'):
|
||||
if node_str[i] == ":" or node_str[i] == ")" or node_str[i] == "{":
|
||||
if label.startswith(":"):
|
||||
labels.append(label)
|
||||
label = ''
|
||||
label = ""
|
||||
label += node_str[i]
|
||||
|
||||
if node_str[i] == '{':
|
||||
if node_str[i] == "{":
|
||||
props_start = i
|
||||
break
|
||||
|
||||
labels.sort()
|
||||
parsed_node = '('
|
||||
parsed_node = "("
|
||||
for label in labels:
|
||||
parsed_node += label
|
||||
if props_start is not None:
|
||||
parsed_node += parse_map(
|
||||
node_str[props_start:len(node_str) - 1], ignore_order)
|
||||
parsed_node += ')'
|
||||
parsed_node += parse_map(node_str[props_start : len(node_str) - 1], ignore_order)
|
||||
parsed_node += ")"
|
||||
return parsed_node
|
||||
|
||||
|
||||
@@ -123,23 +122,23 @@ def parse_map(props, ignore_order):
|
||||
parsed map
|
||||
"""
|
||||
dif_open_closed_brackets = 0
|
||||
prop = ''
|
||||
prop = ""
|
||||
list_props = []
|
||||
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))
|
||||
prop = ''
|
||||
prop = ""
|
||||
else:
|
||||
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
|
||||
elif props[i] == ')' or props[i] == '}' or props[i] == ']':
|
||||
elif props[i] == ")" or props[i] == "}" or props[i] == "]":
|
||||
dif_open_closed_brackets -= 1
|
||||
if prop != '':
|
||||
if prop != "":
|
||||
list_props.append(prop_to_str(prop, ignore_order))
|
||||
|
||||
list_props.sort()
|
||||
return '{' + ','.join(list_props) + '}'
|
||||
return "{" + ",".join(list_props) + "}"
|
||||
|
||||
|
||||
def prop_to_str(prop, ignore_order):
|
||||
@@ -152,8 +151,8 @@ def prop_to_str(prop, ignore_order):
|
||||
@return:
|
||||
parsed prop
|
||||
"""
|
||||
key = prop.split(':', 1)[0]
|
||||
val = prop.split(':', 1)[1]
|
||||
key = prop.split(":", 1)[0]
|
||||
val = prop.split(":", 1)[1]
|
||||
return key + ":" + parse(val, ignore_order)
|
||||
|
||||
|
||||
@@ -166,25 +165,25 @@ def parse_list(l, ignore_order):
|
||||
parsed list
|
||||
"""
|
||||
dif_open_closed_brackets = 0
|
||||
el = ''
|
||||
el = ""
|
||||
list_el = []
|
||||
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))
|
||||
el = ''
|
||||
el = ""
|
||||
else:
|
||||
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
|
||||
elif l[i] == ')' or l[i] == '}' or l[i] == ']':
|
||||
elif l[i] == ")" or l[i] == "}" or l[i] == "]":
|
||||
dif_open_closed_brackets -= 1
|
||||
if el != '':
|
||||
if el != "":
|
||||
list_el.append(parse(el, ignore_order))
|
||||
|
||||
if ignore_order:
|
||||
list_el.sort()
|
||||
|
||||
return '[' + ','.join(list_el) + ']'
|
||||
return "[" + ",".join(list_el) + "]"
|
||||
|
||||
|
||||
def parse_rel(rel, ignore_order):
|
||||
@@ -195,25 +194,25 @@ def parse_rel(rel, ignore_order):
|
||||
@return:
|
||||
parsed relationship
|
||||
"""
|
||||
label = ''
|
||||
label = ""
|
||||
labels = []
|
||||
props_start = None
|
||||
for i in range(1, len(rel)):
|
||||
if rel[i] == ':' or rel[i] == ']' or rel[i] == '{':
|
||||
if label.startswith(':'):
|
||||
if rel[i] == ":" or rel[i] == "]" or rel[i] == "{":
|
||||
if label.startswith(":"):
|
||||
labels.append(label)
|
||||
label = ''
|
||||
label = ""
|
||||
label += rel[i]
|
||||
|
||||
if rel[i] == '{':
|
||||
if rel[i] == "{":
|
||||
props_start = i
|
||||
break
|
||||
|
||||
labels.sort()
|
||||
parsed_rel = '['
|
||||
parsed_rel = "["
|
||||
for label in labels:
|
||||
parsed_rel += label
|
||||
if props_start is not None:
|
||||
parsed_rel += parse_map(rel[props_start:len(rel) - 1], ignore_order)
|
||||
parsed_rel += ']'
|
||||
parsed_rel += parse_map(rel[props_start : len(rel) - 1], ignore_order)
|
||||
parsed_rel += "]"
|
||||
return parsed_rel
|
||||
|
||||
@@ -17,32 +17,29 @@ from behave import given, then, step, when
|
||||
from neo4j.graph import Node, Path, Relationship
|
||||
|
||||
|
||||
@given('parameters are')
|
||||
@given("parameters are")
|
||||
def parameters_step(context):
|
||||
context.test_parameters.set_parameters_from_table(context.table)
|
||||
|
||||
|
||||
@then('parameters are')
|
||||
@then("parameters are")
|
||||
def parameters_step(context):
|
||||
context.test_parameters.set_parameters_from_table(context.table)
|
||||
|
||||
|
||||
@step('having executed')
|
||||
@step("having executed")
|
||||
def having_executed_step(context):
|
||||
context.results = database.query(
|
||||
context.text, context, context.test_parameters.get_parameters())
|
||||
context.results = database.query(context.text, context, context.test_parameters.get_parameters())
|
||||
|
||||
|
||||
@when('executing query')
|
||||
@when("executing query")
|
||||
def executing_query_step(context):
|
||||
context.results = database.query(
|
||||
context.text, context, context.test_parameters.get_parameters())
|
||||
context.results = database.query(context.text, context, context.test_parameters.get_parameters())
|
||||
|
||||
|
||||
@when('executing control query')
|
||||
@when("executing control query")
|
||||
def executing_query_step(context):
|
||||
context.results = database.query(
|
||||
context.text, context, context.test_parameters.get_parameters())
|
||||
context.results = database.query(context.text, context, context.test_parameters.get_parameters())
|
||||
|
||||
|
||||
def parse_props(props_key_value):
|
||||
@@ -93,11 +90,11 @@ def to_string(element):
|
||||
# parsing Node
|
||||
sol = "("
|
||||
if element.labels:
|
||||
sol += ':' + ': '.join(element.labels)
|
||||
sol += ":" + ": ".join(element.labels)
|
||||
|
||||
if element.keys():
|
||||
if element.labels:
|
||||
sol += ' '
|
||||
sol += " "
|
||||
sol += parse_props(element.items())
|
||||
|
||||
sol += ")"
|
||||
@@ -109,7 +106,7 @@ def to_string(element):
|
||||
if element.type:
|
||||
sol += element.type
|
||||
if element.keys():
|
||||
sol += ' '
|
||||
sol += " "
|
||||
sol += parse_props(element.items())
|
||||
sol += "]"
|
||||
return sol
|
||||
@@ -144,12 +141,12 @@ def to_string(element):
|
||||
|
||||
elif isinstance(element, list):
|
||||
# parsing list
|
||||
sol = '['
|
||||
sol = "["
|
||||
el_str = []
|
||||
for el in element:
|
||||
el_str.append(to_string(el))
|
||||
sol += ', '.join(el_str)
|
||||
sol += ']'
|
||||
sol += ", ".join(el_str)
|
||||
sol += "]"
|
||||
|
||||
return sol
|
||||
|
||||
@@ -162,23 +159,22 @@ def to_string(element):
|
||||
elif isinstance(element, dict):
|
||||
# parsing map
|
||||
if len(element) == 0:
|
||||
return '{}'
|
||||
sol = '{'
|
||||
return "{}"
|
||||
sol = "{"
|
||||
for key, val in element.items():
|
||||
sol += key + ':' + to_string(val) + ','
|
||||
sol = sol[:-1] + '}'
|
||||
sol += key + ":" + to_string(val) + ","
|
||||
sol = sol[:-1] + "}"
|
||||
return sol
|
||||
|
||||
elif isinstance(element, float):
|
||||
# parsing float, scientific
|
||||
if 'e' in str(element):
|
||||
if str(element)[-3] == '-':
|
||||
if "e" in str(element):
|
||||
if str(element)[-3] == "-":
|
||||
zeroes = int(str(element)[-2:]) - 1
|
||||
num_str = ''
|
||||
if str(element)[0] == '-':
|
||||
num_str += '-'
|
||||
num_str += '.' + zeroes * '0' + \
|
||||
str(element)[:-4].replace("-", "").replace(".", "")
|
||||
num_str = ""
|
||||
if str(element)[0] == "-":
|
||||
num_str += "-"
|
||||
num_str += "." + zeroes * "0" + str(element)[:-4].replace("-", "").replace(".", "")
|
||||
return num_str
|
||||
|
||||
return str(element)
|
||||
@@ -201,9 +197,14 @@ def get_result_rows(context, ignore_order):
|
||||
keys = result.keys()
|
||||
values = result.values()
|
||||
for i in range(0, len(keys)):
|
||||
result_rows.append(keys[i] + ":" + parser.parse(
|
||||
to_string(values[i]).replace("\n", "\\n").replace(" ", ""),
|
||||
ignore_order))
|
||||
result_rows.append(
|
||||
keys[i]
|
||||
+ ":"
|
||||
+ parser.parse(
|
||||
to_string(values[i]).replace("\n", "\\n").replace(" ", ""),
|
||||
ignore_order,
|
||||
)
|
||||
)
|
||||
return result_rows
|
||||
|
||||
|
||||
@@ -221,9 +222,7 @@ def get_expected_rows(context, ignore_order):
|
||||
expected_rows = []
|
||||
for row in context.table:
|
||||
for col in context.table.headings:
|
||||
expected_rows.append(
|
||||
col + ":" + parser.parse(row[col].replace(" ", ""),
|
||||
ignore_order))
|
||||
expected_rows.append(col + ":" + parser.parse(row[col].replace(" ", ""), ignore_order))
|
||||
return expected_rows
|
||||
|
||||
|
||||
@@ -242,13 +241,13 @@ def validate(context, ignore_order):
|
||||
|
||||
context.log.info("Expected: %s", str(expected_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)):
|
||||
if expected_rows[i] in result_rows:
|
||||
result_rows.remove(expected_rows[i])
|
||||
else:
|
||||
assert(False)
|
||||
assert False
|
||||
|
||||
|
||||
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("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)):
|
||||
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):
|
||||
validate(context, False)
|
||||
check_exception(context)
|
||||
|
||||
|
||||
@then('the result should be, in order')
|
||||
@then("the result should be, in order")
|
||||
def expected_result_step(context):
|
||||
validate_in_order(context, False)
|
||||
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):
|
||||
validate(context, True)
|
||||
check_exception(context)
|
||||
@@ -295,20 +294,20 @@ def expected_result_step(context):
|
||||
def check_exception(context):
|
||||
if context.exception is not None:
|
||||
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):
|
||||
assert(len(context.results) == 0)
|
||||
assert len(context.results) == 0
|
||||
check_exception(context)
|
||||
|
||||
|
||||
@then('the side effects should be')
|
||||
@then("the side effects should be")
|
||||
def side_effects_step(context):
|
||||
return
|
||||
|
||||
|
||||
@then('no side effects')
|
||||
@then("no side effects")
|
||||
def side_effects_step(context):
|
||||
return
|
||||
|
||||
@@ -40,15 +40,15 @@ class TestParameters:
|
||||
par = dict()
|
||||
for row in table:
|
||||
par[row[0]] = self.parse_parameters(row[1])
|
||||
if isinstance(par[row[0]], str) and par[row[0]].startswith("'") \
|
||||
and par[row[0]].endswith("'"):
|
||||
par[row[0]] = par[row[0]][1:len(par[row[0]]) - 1]
|
||||
if isinstance(par[row[0]], str) and par[row[0]].startswith("'") and par[row[0]].endswith("'"):
|
||||
par[row[0]] = par[row[0]][1 : len(par[row[0]]) - 1]
|
||||
par[table.headings[0]] = self.parse_parameters(table.headings[1])
|
||||
if isinstance(par[table.headings[0]], str) and \
|
||||
par[table.headings[0]].startswith("'") and \
|
||||
par[table.headings[0]].endswith("'"):
|
||||
par[table.headings[0]] = \
|
||||
par[table.headings[0]][1:len(par[table.headings[0]]) - 1]
|
||||
if (
|
||||
isinstance(par[table.headings[0]], str)
|
||||
and par[table.headings[0]].startswith("'")
|
||||
and par[table.headings[0]].endswith("'")
|
||||
):
|
||||
par[table.headings[0]] = par[table.headings[0]][1 : len(par[table.headings[0]]) - 1]
|
||||
|
||||
self.parameters = par
|
||||
|
||||
|
||||
@@ -37,20 +37,14 @@ QUERIES = [
|
||||
("CREATE (n {name: $name})", {"name": 5, "leftover": 42}),
|
||||
("MATCH (n), (m) CREATE (n)-[:e {when: $when}]->(m)", {"when": 42}),
|
||||
("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() "
|
||||
"ON MATCH SET n.lastSeen = timestamp() "
|
||||
"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 {test: $test}) REMOVE n.value", {"test": 48}),
|
||||
("MATCH (n), (m) REMOVE n.value, m.value", {}),
|
||||
@@ -74,7 +68,8 @@ def execute_test(memgraph_binary, tester_binary):
|
||||
storage_directory.name,
|
||||
"--audit-enabled",
|
||||
"--log-file=memgraph.log",
|
||||
"--log-level=TRACE"]
|
||||
"--log-level=TRACE",
|
||||
]
|
||||
|
||||
# Start the memgraph binary
|
||||
memgraph = subprocess.Popen(list(map(str, memgraph_args)))
|
||||
@@ -92,8 +87,13 @@ def execute_test(memgraph_binary, tester_binary):
|
||||
def execute_queries(queries):
|
||||
for query, params in queries:
|
||||
print(query, params)
|
||||
args = [tester_binary, "--query", query,
|
||||
"--params-json", json.dumps(params)]
|
||||
args = [
|
||||
tester_binary,
|
||||
"--query",
|
||||
query,
|
||||
"--params-json",
|
||||
json.dumps(params),
|
||||
]
|
||||
subprocess.run(args).check_returncode()
|
||||
|
||||
# Execute all queries
|
||||
@@ -109,10 +109,17 @@ def execute_test(memgraph_binary, tester_binary):
|
||||
# Verify the written log
|
||||
print("\033[1;36m~~ Starting log verification ~~\033[0m")
|
||||
with open(os.path.join(storage_directory.name, "audit", "audit.log")) as f:
|
||||
reader = csv.reader(f, delimiter=',', doublequote=False,
|
||||
escapechar='\\', lineterminator='\n',
|
||||
quotechar='"', quoting=csv.QUOTE_MINIMAL,
|
||||
skipinitialspace=False, strict=True)
|
||||
reader = csv.reader(
|
||||
f,
|
||||
delimiter=",",
|
||||
doublequote=False,
|
||||
escapechar="\\",
|
||||
lineterminator="\n",
|
||||
quotechar='"',
|
||||
quoting=csv.QUOTE_MINIMAL,
|
||||
skipinitialspace=False,
|
||||
strict=True,
|
||||
)
|
||||
queries = []
|
||||
for line in reader:
|
||||
timestamp, address, username, query, params = line
|
||||
@@ -120,15 +127,13 @@ def execute_test(memgraph_binary, tester_binary):
|
||||
queries.append((query, params))
|
||||
print(query, params)
|
||||
|
||||
assert queries == QUERIES, "Logged queries don't match " \
|
||||
"executed queries!"
|
||||
assert queries == QUERIES, "Logged queries don't match " "executed queries!"
|
||||
print("\033[1;36m~~ Finished log verification ~~\033[0m\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "audit", "tester")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "audit", "tester")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
|
||||
@@ -29,15 +29,8 @@ PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
|
||||
|
||||
QUERIES = [
|
||||
# CREATE
|
||||
(
|
||||
"CREATE (n)",
|
||||
("CREATE",)
|
||||
),
|
||||
(
|
||||
"MATCH (n), (m) CREATE (n)-[:e]->(m)",
|
||||
("CREATE", "MATCH")
|
||||
),
|
||||
|
||||
("CREATE (n)", ("CREATE",)),
|
||||
("MATCH (n), (m) CREATE (n)-[:e]->(m)", ("CREATE", "MATCH")),
|
||||
# DELETE
|
||||
(
|
||||
"MATCH (n) DELETE n",
|
||||
@@ -47,116 +40,43 @@ QUERIES = [
|
||||
"MATCH (n) DETACH DELETE n",
|
||||
("DELETE", "MATCH"),
|
||||
),
|
||||
|
||||
# MATCH
|
||||
(
|
||||
"MATCH (n) RETURN n",
|
||||
("MATCH",)
|
||||
),
|
||||
(
|
||||
"MATCH (n), (m) RETURN count(n), count(m)",
|
||||
("MATCH",)
|
||||
),
|
||||
|
||||
("MATCH (n) RETURN n", ("MATCH",)),
|
||||
("MATCH (n), (m) RETURN count(n), count(m)", ("MATCH",)),
|
||||
# MERGE
|
||||
(
|
||||
"MERGE (n) ON CREATE SET n.created = timestamp() "
|
||||
"ON MATCH SET n.lastSeen = timestamp() "
|
||||
"RETURN n.name, n.created, n.lastSeen",
|
||||
("MERGE",)
|
||||
("MERGE",),
|
||||
),
|
||||
|
||||
# SET
|
||||
(
|
||||
"MATCH (n) SET n.value = 0 RETURN n",
|
||||
("SET", "MATCH")
|
||||
),
|
||||
(
|
||||
"MATCH (n), (m) SET n.value = m.value",
|
||||
("SET", "MATCH")
|
||||
),
|
||||
|
||||
("MATCH (n) SET n.value = 0 RETURN n", ("SET", "MATCH")),
|
||||
("MATCH (n), (m) SET n.value = m.value", ("SET", "MATCH")),
|
||||
# REMOVE
|
||||
(
|
||||
"MATCH (n) REMOVE n.value",
|
||||
("REMOVE", "MATCH")
|
||||
),
|
||||
(
|
||||
"MATCH (n), (m) REMOVE n.value, m.value",
|
||||
("REMOVE", "MATCH")
|
||||
),
|
||||
|
||||
("MATCH (n) REMOVE n.value", ("REMOVE", "MATCH")),
|
||||
("MATCH (n), (m) REMOVE n.value, m.value", ("REMOVE", "MATCH")),
|
||||
# INDEX
|
||||
(
|
||||
"CREATE INDEX ON :User (id)",
|
||||
("INDEX",)
|
||||
),
|
||||
|
||||
("CREATE INDEX ON :User (id)", ("INDEX",)),
|
||||
# AUTH
|
||||
(
|
||||
"CREATE ROLE test_role",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"DROP ROLE test_role",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"SHOW ROLES",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"CREATE USER test_user",
|
||||
("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",)
|
||||
),
|
||||
("CREATE ROLE test_role", ("AUTH",)),
|
||||
("DROP ROLE test_role", ("AUTH",)),
|
||||
("SHOW ROLES", ("AUTH",)),
|
||||
("CREATE USER test_user", ("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 " \
|
||||
"contact your database administrator."
|
||||
UNAUTHORIZED_ERROR = "You are not authorized to execute this query! Please " "contact your database administrator."
|
||||
|
||||
|
||||
def wait_for_server(port, delay=0.1):
|
||||
@@ -166,8 +86,15 @@ def wait_for_server(port, delay=0.1):
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
def execute_tester(binary, queries, should_fail=False, failure_message="",
|
||||
username="", password="", check_failure=True):
|
||||
def execute_tester(
|
||||
binary,
|
||||
queries,
|
||||
should_fail=False,
|
||||
failure_message="",
|
||||
username="",
|
||||
password="",
|
||||
check_failure=True,
|
||||
):
|
||||
args = [binary, "--username", username, "--password", password]
|
||||
if 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):
|
||||
storage_directory = tempfile.TemporaryDirectory()
|
||||
memgraph_args = [memgraph_binary,
|
||||
"--data-directory", storage_directory.name]
|
||||
memgraph_args = [memgraph_binary, "--data-directory", storage_directory.name]
|
||||
|
||||
def execute_admin_queries(queries):
|
||||
return execute_tester(tester_binary, queries, should_fail=False,
|
||||
check_failure=True, username="admin",
|
||||
password="admin")
|
||||
return execute_tester(
|
||||
tester_binary,
|
||||
queries,
|
||||
should_fail=False,
|
||||
check_failure=True,
|
||||
username="admin",
|
||||
password="admin",
|
||||
)
|
||||
|
||||
def execute_user_queries(queries, should_fail=False, failure_message="",
|
||||
check_failure=True):
|
||||
return execute_tester(tester_binary, queries, should_fail,
|
||||
failure_message, "user", "user", check_failure)
|
||||
def execute_user_queries(queries, should_fail=False, failure_message="", check_failure=True):
|
||||
return execute_tester(
|
||||
tester_binary,
|
||||
queries,
|
||||
should_fail,
|
||||
failure_message,
|
||||
"user",
|
||||
"user",
|
||||
check_failure,
|
||||
)
|
||||
|
||||
# Start the memgraph binary
|
||||
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!"
|
||||
|
||||
# Prepare all users
|
||||
execute_admin_queries([
|
||||
"CREATE USER ADmin IDENTIFIED BY 'admin'",
|
||||
"GRANT ALL PRIVILEGES TO admIN",
|
||||
"CREATE USER usEr IDENTIFIED BY 'user'",
|
||||
])
|
||||
execute_admin_queries(
|
||||
[
|
||||
"CREATE USER ADmin IDENTIFIED BY 'admin'",
|
||||
"GRANT ALL PRIVILEGES TO admIN",
|
||||
"CREATE USER usEr IDENTIFIED BY 'user'",
|
||||
]
|
||||
)
|
||||
|
||||
# Find all existing permissions
|
||||
permissions = set()
|
||||
@@ -243,12 +182,14 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
|
||||
print("\033[1;36m~~ Starting query test ~~\033[0m")
|
||||
for mask in range(0, 2 ** len(permissions)):
|
||||
user_perms = get_permissions(permissions, mask)
|
||||
print("\033[1;34m~~ Checking queries with privileges: ",
|
||||
", ".join(user_perms), " ~~\033[0m")
|
||||
print(
|
||||
"\033[1;34m~~ Checking queries with privileges: ",
|
||||
", ".join(user_perms),
|
||||
" ~~\033[0m",
|
||||
)
|
||||
admin_queries = ["REVOKE ALL PRIVILEGES FROM uSer"]
|
||||
if len(user_perms) > 0:
|
||||
admin_queries.append(
|
||||
"GRANT {} TO User".format(", ".join(user_perms)))
|
||||
admin_queries.append("GRANT {} TO User".format(", ".join(user_perms)))
|
||||
execute_admin_queries(admin_queries)
|
||||
authorized, unauthorized = [], []
|
||||
for query, query_perms in QUERIES:
|
||||
@@ -256,35 +197,43 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
|
||||
authorized.append(query)
|
||||
else:
|
||||
unauthorized.append(query)
|
||||
execute_user_queries(authorized, check_failure=False,
|
||||
failure_message=UNAUTHORIZED_ERROR)
|
||||
execute_user_queries(unauthorized, should_fail=True,
|
||||
failure_message=UNAUTHORIZED_ERROR)
|
||||
execute_user_queries(authorized, check_failure=False, 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")
|
||||
|
||||
# Run the user/role permissions test
|
||||
print("\033[1;36m~~ Starting permissions test ~~\033[0m")
|
||||
execute_admin_queries([
|
||||
"CREATE ROLE roLe",
|
||||
"REVOKE ALL PRIVILEGES FROM uSeR",
|
||||
])
|
||||
execute_admin_queries(
|
||||
[
|
||||
"CREATE ROLE roLe",
|
||||
"REVOKE ALL PRIVILEGES FROM uSeR",
|
||||
]
|
||||
)
|
||||
execute_checker(checker_binary, [])
|
||||
for user_perm in ["GRANT", "DENY", "REVOKE"]:
|
||||
for role_perm in ["GRANT", "DENY", "REVOKE"]:
|
||||
for mapped in [True, False]:
|
||||
print("\033[1;34m~~ Checking permissions with user ",
|
||||
user_perm, ", role ", role_perm,
|
||||
"user mapped to role:", mapped, " ~~\033[0m")
|
||||
print(
|
||||
"\033[1;34m~~ Checking permissions with user ",
|
||||
user_perm,
|
||||
", role ",
|
||||
role_perm,
|
||||
"user mapped to role:",
|
||||
mapped,
|
||||
" ~~\033[0m",
|
||||
)
|
||||
if mapped:
|
||||
execute_admin_queries(["SET ROLE FOR USER TO roLE"])
|
||||
else:
|
||||
execute_admin_queries(["CLEAR ROLE FOR user"])
|
||||
user_prep = "FROM" if user_perm == "REVOKE" else "TO"
|
||||
role_prep = "FROM" if role_perm == "REVOKE" else "TO"
|
||||
execute_admin_queries([
|
||||
"{} MATCH {} user".format(user_perm, user_prep),
|
||||
"{} MATCH {} rOLe".format(role_perm, role_prep)
|
||||
])
|
||||
execute_admin_queries(
|
||||
[
|
||||
"{} MATCH {} user".format(user_perm, user_prep),
|
||||
"{} MATCH {} rOLe".format(role_perm, role_prep),
|
||||
]
|
||||
)
|
||||
expected = []
|
||||
perms = [user_perm, role_perm] if mapped else [user_perm]
|
||||
if "DENY" in perms:
|
||||
@@ -313,10 +262,8 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
|
||||
|
||||
if __name__ == "__main__":
|
||||
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "auth", "tester")
|
||||
checker_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "auth", "checker")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "auth", "tester")
|
||||
checker_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "auth", "checker")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
|
||||
@@ -40,7 +40,7 @@ def wait_for_server(port, delay=0.1):
|
||||
|
||||
|
||||
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())))
|
||||
|
||||
|
||||
@@ -52,32 +52,30 @@ def list_to_string(data):
|
||||
return ret
|
||||
|
||||
|
||||
def execute_test(
|
||||
memgraph_binary,
|
||||
dump_binary,
|
||||
test_directory,
|
||||
test_type,
|
||||
write_expected):
|
||||
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))
|
||||
def execute_test(memgraph_binary, dump_binary, test_directory, test_type, write_expected):
|
||||
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()
|
||||
if test_type == "SNAPSHOT":
|
||||
snapshots_dir = os.path.join(working_data_directory.name, "snapshots")
|
||||
os.makedirs(snapshots_dir)
|
||||
shutil.copy(os.path.join(test_directory, SNAPSHOT_FILE_NAME),
|
||||
snapshots_dir)
|
||||
shutil.copy(os.path.join(test_directory, SNAPSHOT_FILE_NAME), snapshots_dir)
|
||||
else:
|
||||
wal_dir = os.path.join(working_data_directory.name, "wal")
|
||||
os.makedirs(wal_dir)
|
||||
shutil.copy(os.path.join(test_directory, WAL_FILE_NAME), wal_dir)
|
||||
|
||||
memgraph_args = [memgraph_binary,
|
||||
"--storage-recover-on-startup",
|
||||
"--storage-properties-on-edges",
|
||||
"--data-directory", working_data_directory.name]
|
||||
memgraph_args = [
|
||||
memgraph_binary,
|
||||
"--storage-recover-on-startup",
|
||||
"--storage-properties-on-edges",
|
||||
"--data-directory",
|
||||
working_data_directory.name,
|
||||
]
|
||||
|
||||
# Start the memgraph binary
|
||||
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
|
||||
|
||||
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()
|
||||
# Write dump files
|
||||
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)
|
||||
else:
|
||||
# Compare dump files
|
||||
expected_dump_file = os.path.join(test_directory, dump_file_name)
|
||||
assert os.path.exists(expected_dump_file), \
|
||||
"Could not find expected dump path {}".format(expected_dump_file)
|
||||
assert os.path.exists(expected_dump_file), "Could not find expected dump path {}".format(expected_dump_file)
|
||||
queries_got = sorted_content(dump_output_file.name)
|
||||
queries_expected = sorted_content(expected_dump_file)
|
||||
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" \
|
||||
"{}".format(list_to_string(queries_got),
|
||||
list_to_string(queries_expected))
|
||||
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" "{}".format(
|
||||
list_to_string(queries_got), list_to_string(queries_expected)
|
||||
)
|
||||
|
||||
print("\033[1;32m~~ Test successful ~~\033[0m\n")
|
||||
|
||||
@@ -141,15 +138,17 @@ def find_test_directories(directory):
|
||||
continue
|
||||
snapshot_file = os.path.join(test_dir_path, SNAPSHOT_FILE_NAME)
|
||||
wal_file = os.path.join(test_dir_path, WAL_FILE_NAME)
|
||||
dump_snapshot_file = os.path.join(
|
||||
test_dir_path, DUMP_SNAPSHOT_FILE_NAME)
|
||||
dump_snapshot_file = os.path.join(test_dir_path, DUMP_SNAPSHOT_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)
|
||||
and os.path.isfile(wal_file) and os.path.isfile(dump_wal_file)):
|
||||
if (
|
||||
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)
|
||||
else:
|
||||
raise Exception("Missing data in test directory '{}'"
|
||||
.format(test_dir_path))
|
||||
raise Exception("Missing data in test directory '{}'".format(test_dir_path))
|
||||
return test_dirs
|
||||
|
||||
|
||||
@@ -161,26 +160,17 @@ if __name__ == "__main__":
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
parser.add_argument("--dump", default=dump_binary)
|
||||
parser.add_argument(
|
||||
'--write-expected',
|
||||
action='store_true',
|
||||
help='Overwrite the expected cypher with results from current run')
|
||||
"--write-expected",
|
||||
action="store_true",
|
||||
help="Overwrite the expected cypher with results from current run",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
test_directories = find_test_directories(TESTS_DIR)
|
||||
assert len(test_directories) > 0, "No tests have been found!"
|
||||
|
||||
for test_directory in test_directories:
|
||||
execute_test(
|
||||
args.memgraph,
|
||||
args.dump,
|
||||
test_directory,
|
||||
"SNAPSHOT",
|
||||
args.write_expected)
|
||||
execute_test(
|
||||
args.memgraph,
|
||||
args.dump,
|
||||
test_directory,
|
||||
"WAL",
|
||||
args.write_expected)
|
||||
execute_test(args.memgraph, args.dump, test_directory, "SNAPSHOT", args.write_expected)
|
||||
execute_test(args.memgraph, args.dump, test_directory, "WAL", args.write_expected)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
@@ -52,8 +52,14 @@ def wait_for_server(port, delay=0.1):
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
def execute_tester(binary, queries, username="", password="",
|
||||
auth_should_fail=False, query_should_fail=False):
|
||||
def execute_tester(
|
||||
binary,
|
||||
queries,
|
||||
username="",
|
||||
password="",
|
||||
auth_should_fail=False,
|
||||
query_should_fail=False,
|
||||
):
|
||||
if password == "":
|
||||
password = username
|
||||
args = [binary, "--username", username, "--password", password]
|
||||
@@ -76,18 +82,14 @@ class Memgraph:
|
||||
def start(self, **kwargs):
|
||||
self.stop()
|
||||
self._storage_directory = tempfile.TemporaryDirectory()
|
||||
self._auth_module = os.path.join(self._storage_directory.name,
|
||||
"ldap.py")
|
||||
self._auth_config = os.path.join(self._storage_directory.name,
|
||||
"ldap.yaml")
|
||||
script_file = os.path.join(PROJECT_DIR, "src", "auth",
|
||||
"reference_modules", "ldap.py")
|
||||
self._auth_module = os.path.join(self._storage_directory.name, "ldap.py")
|
||||
self._auth_config = os.path.join(self._storage_directory.name, "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")
|
||||
with open(script_file) as fin:
|
||||
data = fin.read()
|
||||
data = data.replace("/usr/bin/python3", virtualenv_bin)
|
||||
data = data.replace("/etc/memgraph/auth/ldap.yaml",
|
||||
self._auth_config)
|
||||
data = data.replace("/etc/memgraph/auth/ldap.yaml", self._auth_config)
|
||||
with open(self._auth_module, "w") as fout:
|
||||
fout.write(data)
|
||||
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:
|
||||
f.write(CONFIG_TEMPLATE.format(**config))
|
||||
args = [self._binary,
|
||||
"--data-directory", self._storage_directory.name,
|
||||
"--auth-module-executable",
|
||||
kwargs.pop("module_executable", self._auth_module)]
|
||||
args = [
|
||||
self._binary,
|
||||
"--data-directory",
|
||||
self._storage_directory.name,
|
||||
"--auth-module-executable",
|
||||
kwargs.pop("module_executable", self._auth_module),
|
||||
]
|
||||
for key, value in kwargs.items():
|
||||
ldap_key = "--auth-module-" + key.replace("_", "-")
|
||||
if isinstance(value, bool):
|
||||
@@ -119,8 +124,7 @@ class Memgraph:
|
||||
args.append(value)
|
||||
self._process = subprocess.Popen(args)
|
||||
time.sleep(0.1)
|
||||
assert self._process.poll() is None, "Memgraph process died " \
|
||||
"prematurely!"
|
||||
assert self._process.poll() is None, "Memgraph process died " "prematurely!"
|
||||
wait_for_server(7687)
|
||||
|
||||
def stop(self, check=True):
|
||||
@@ -137,8 +141,7 @@ class Memgraph:
|
||||
def initialize_test(memgraph, tester_binary, **kwargs):
|
||||
memgraph.start(module_executable="")
|
||||
|
||||
execute_tester(tester_binary,
|
||||
["CREATE USER root", "GRANT ALL PRIVILEGES TO root"])
|
||||
execute_tester(tester_binary, ["CREATE USER root", "GRANT ALL PRIVILEGES TO root"])
|
||||
check_login = kwargs.pop("check_login", True)
|
||||
memgraph.restart(**kwargs)
|
||||
if check_login:
|
||||
@@ -170,18 +173,15 @@ def test_role_mapping(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
|
||||
execute_tester(tester_binary, [], "alice")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
|
||||
execute_tester(tester_binary, [], "bob")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "bob",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "bob", query_should_fail=True)
|
||||
|
||||
execute_tester(tester_binary, [], "carol")
|
||||
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol", query_should_fail=True)
|
||||
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"], "dave")
|
||||
@@ -192,15 +192,13 @@ def test_role_mapping(memgraph, tester_binary):
|
||||
def test_role_removal(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary, [], "alice")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.restart(manage_roles=False)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
execute_tester(tester_binary, ["CLEAR ROLE FOR alice"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
@@ -229,28 +227,22 @@ def test_user_is_role(memgraph, tester_binary):
|
||||
|
||||
def test_user_permissions_persistancy(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
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")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_role_permissions_persistancy(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_only_authentication(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, manage_roles=False)
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
@@ -267,22 +259,16 @@ def test_wrong_suffix(memgraph, tester_binary):
|
||||
|
||||
|
||||
def test_suffix_with_spaces(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary,
|
||||
suffix=", ou= people, dc = memgraph, dc = com")
|
||||
execute_tester(tester_binary,
|
||||
["CREATE USER alice", "GRANT MATCH TO alice"], "root")
|
||||
initialize_test(memgraph, tester_binary, suffix=", ou= people, dc = memgraph, dc = com")
|
||||
execute_tester(tester_binary, ["CREATE USER alice", "GRANT MATCH TO alice"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_role_mapping_wrong_root_dn(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary,
|
||||
root_dn="ou=invalid,dc=memgraph,dc=com")
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
initialize_test(memgraph, tester_binary, root_dn="ou=invalid,dc=memgraph,dc=com")
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
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):
|
||||
initialize_test(memgraph, tester_binary, root_objectclass="person")
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
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):
|
||||
initialize_test(memgraph, tester_binary, user_attribute="cn")
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
@@ -314,8 +294,7 @@ def test_role_mapping_wrong_user_attribute(memgraph, tester_binary):
|
||||
|
||||
def test_wrong_password(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
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="root")
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
memgraph.restart(module_executable="")
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
memgraph.stop()
|
||||
|
||||
@@ -339,33 +316,25 @@ def test_password_persistancy(memgraph, tester_binary):
|
||||
def test_user_multiple_roles(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, check_login=False)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
|
||||
memgraph.restart(manage_roles=False)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
|
||||
memgraph.restart(manage_roles=False, root_dn="")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_starttls_failure(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, encryption="starttls",
|
||||
check_login=False)
|
||||
initialize_test(memgraph, tester_binary, encryption="starttls", check_login=False)
|
||||
execute_tester(tester_binary, [], "root", auth_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_ssl_failure(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, encryption="ssl",
|
||||
check_login=False)
|
||||
initialize_test(memgraph, tester_binary, encryption="ssl", check_login=False)
|
||||
execute_tester(tester_binary, [], "root", auth_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
@@ -375,22 +344,25 @@ def test_ssl_failure(memgraph, tester_binary):
|
||||
|
||||
if __name__ == "__main__":
|
||||
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "ldap", "tester")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "ldap", "tester")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
parser.add_argument("--tester", default=tester_binary)
|
||||
parser.add_argument("--openldap-dir",
|
||||
default=os.path.join(SCRIPT_DIR, "openldap-2.4.47"))
|
||||
parser.add_argument("--openldap-dir", default=os.path.join(SCRIPT_DIR, "openldap-2.4.47"))
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup Memgraph handler
|
||||
memgraph = Memgraph(args.memgraph)
|
||||
|
||||
# Start the slapd binary
|
||||
slapd_args = [os.path.join(args.openldap_dir, "exe", "libexec", "slapd"),
|
||||
"-h", "ldap://127.0.0.1:1389/", "-d", "0"]
|
||||
slapd_args = [
|
||||
os.path.join(args.openldap_dir, "exe", "libexec", "slapd"),
|
||||
"-h",
|
||||
"ldap://127.0.0.1:1389/",
|
||||
"-d",
|
||||
"0",
|
||||
]
|
||||
slapd = subprocess.Popen(slapd_args)
|
||||
time.sleep(0.1)
|
||||
assert slapd.poll() is None, "slapd process died prematurely!"
|
||||
@@ -409,8 +381,7 @@ if __name__ == "__main__":
|
||||
if slapd_stat != 0:
|
||||
print("slapd process didn't exit cleanly!")
|
||||
|
||||
assert mg_stat == 0 and slapd_stat == 0, "Some of the processes " \
|
||||
"(memgraph, slapd) crashed!"
|
||||
assert mg_stat == 0 and slapd_stat == 0, "Some of the processes " "(memgraph, slapd) crashed!"
|
||||
|
||||
# Execute tests
|
||||
names = sorted(globals().keys())
|
||||
|
||||
@@ -46,17 +46,18 @@ def list_to_string(data):
|
||||
|
||||
|
||||
def verify_lifetime(memgraph_binary, mg_import_csv_binary):
|
||||
print("\033[1;36m~~ Verifying that mg_import_csv can't be started while "
|
||||
"memgraph is running ~~\033[0m")
|
||||
print("\033[1;36m~~ Verifying that mg_import_csv can't be started while " "memgraph is running ~~\033[0m")
|
||||
storage_directory = tempfile.TemporaryDirectory()
|
||||
|
||||
# Generate common args
|
||||
common_args = ["--data-directory", storage_directory.name,
|
||||
"--storage-properties-on-edges=false"]
|
||||
common_args = [
|
||||
"--data-directory",
|
||||
storage_directory.name,
|
||||
"--storage-properties-on-edges=false",
|
||||
]
|
||||
|
||||
# Start the memgraph binary
|
||||
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + \
|
||||
common_args
|
||||
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + common_args
|
||||
memgraph = subprocess.Popen(list(map(str, memgraph_args)))
|
||||
time.sleep(0.1)
|
||||
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!"
|
||||
|
||||
# Execute mg_import_csv.
|
||||
mg_import_csv_args = [mg_import_csv_binary, "--nodes", "/dev/null"] + \
|
||||
common_args
|
||||
mg_import_csv_args = [mg_import_csv_binary, "--nodes", "/dev/null"] + common_args
|
||||
ret = subprocess.run(mg_import_csv_args)
|
||||
|
||||
# Check the return code
|
||||
if ret.returncode == 0:
|
||||
raise Exception(
|
||||
"The importer was able to run while memgraph was running!")
|
||||
raise Exception("The importer was able to run while memgraph was running!")
|
||||
|
||||
# Shutdown the memgraph binary
|
||||
memgraph.terminate()
|
||||
@@ -86,27 +85,34 @@ def verify_lifetime(memgraph_binary, mg_import_csv_binary):
|
||||
print("\033[1;32m~~ Test successful ~~\033[0m\n")
|
||||
|
||||
|
||||
def execute_test(name, test_path, test_config, memgraph_binary,
|
||||
mg_import_csv_binary, tester_binary, write_expected):
|
||||
def execute_test(
|
||||
name,
|
||||
test_path,
|
||||
test_config,
|
||||
memgraph_binary,
|
||||
mg_import_csv_binary,
|
||||
tester_binary,
|
||||
write_expected,
|
||||
):
|
||||
print("\033[1;36m~~ Executing test", name, "~~\033[0m")
|
||||
storage_directory = tempfile.TemporaryDirectory()
|
||||
|
||||
# Verify test configuration
|
||||
if ("import_should_fail" not in test_config and
|
||||
"expected" not in test_config) or \
|
||||
("import_should_fail" in test_config and
|
||||
"expected" in test_config):
|
||||
raise Exception("The test should specify either 'import_should_fail' "
|
||||
"or 'expected'!")
|
||||
if ("import_should_fail" not in test_config and "expected" not in test_config) or (
|
||||
"import_should_fail" in test_config and "expected" in test_config
|
||||
):
|
||||
raise Exception("The test should specify either 'import_should_fail' " "or 'expected'!")
|
||||
|
||||
expected_path = test_config.pop("expected", "")
|
||||
import_should_fail = test_config.pop("import_should_fail", False)
|
||||
|
||||
# Generate common args
|
||||
properties_on_edges = bool(test_config.pop("properties_on_edges", False))
|
||||
common_args = ["--data-directory", storage_directory.name,
|
||||
"--storage-properties-on-edges=" +
|
||||
str(properties_on_edges).lower()]
|
||||
common_args = [
|
||||
"--data-directory",
|
||||
storage_directory.name,
|
||||
"--storage-properties-on-edges=" + str(properties_on_edges).lower(),
|
||||
]
|
||||
|
||||
# Generate mg_import_csv args using flags specified in the test
|
||||
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 ret.returncode == 0:
|
||||
raise Exception("The import should have failed, but it "
|
||||
"succeeded instead!")
|
||||
raise Exception("The import should have failed, but it " "succeeded instead!")
|
||||
else:
|
||||
print("\033[1;32m~~ Test successful ~~\033[0m\n")
|
||||
return
|
||||
else:
|
||||
if ret.returncode != 0:
|
||||
raise Exception("The import should have succeeded, but it "
|
||||
"failed instead!")
|
||||
raise Exception("The import should have succeeded, but it " "failed instead!")
|
||||
|
||||
# Start the memgraph binary
|
||||
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + \
|
||||
common_args
|
||||
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + common_args
|
||||
memgraph = subprocess.Popen(list(map(str, memgraph_args)))
|
||||
time.sleep(0.1)
|
||||
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!"
|
||||
|
||||
# Get the contents of the database
|
||||
queries_got = extract_rows(subprocess.run(
|
||||
[tester_binary], stdout=subprocess.PIPE,
|
||||
check=True).stdout.decode("utf-8"))
|
||||
queries_got = extract_rows(
|
||||
subprocess.run([tester_binary], stdout=subprocess.PIPE, check=True).stdout.decode("utf-8")
|
||||
)
|
||||
|
||||
# Shutdown the memgraph binary
|
||||
memgraph.terminate()
|
||||
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
|
||||
|
||||
if write_expected:
|
||||
with open(os.path.join(test_path, expected_path), 'w') as expected:
|
||||
expected.write('\n'.join(queries_got))
|
||||
with open(os.path.join(test_path, expected_path), "w") as expected:
|
||||
expected.write("\n".join(queries_got))
|
||||
|
||||
else:
|
||||
if expected_path:
|
||||
@@ -173,18 +176,16 @@ def execute_test(name, test_path, test_config, memgraph_binary,
|
||||
# Verify the queries
|
||||
queries_expected.sort()
|
||||
queries_got.sort()
|
||||
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" \
|
||||
"{}".format(list_to_string(queries_got),
|
||||
list_to_string(queries_expected))
|
||||
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" "{}".format(
|
||||
list_to_string(queries_got), list_to_string(queries_expected)
|
||||
)
|
||||
print("\033[1;32m~~ Test successful ~~\033[0m\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
memgraph_binary = os.path.join(BUILD_DIR, "memgraph")
|
||||
mg_import_csv_binary = os.path.join(
|
||||
BUILD_DIR, "src", "mg_import_csv")
|
||||
tester_binary = os.path.join(
|
||||
BUILD_DIR, "tests", "integration", "mg_import_csv", "tester")
|
||||
mg_import_csv_binary = os.path.join(BUILD_DIR, "src", "mg_import_csv")
|
||||
tester_binary = os.path.join(BUILD_DIR, "tests", "integration", "mg_import_csv", "tester")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
@@ -193,7 +194,8 @@ if __name__ == "__main__":
|
||||
parser.add_argument(
|
||||
"--write-expected",
|
||||
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()
|
||||
|
||||
# First test whether the CSV importer can be started while the main
|
||||
@@ -211,7 +213,14 @@ if __name__ == "__main__":
|
||||
testcases = yaml.safe_load(f)
|
||||
for test_config in testcases:
|
||||
test_name = name + "/" + test_config.pop("name")
|
||||
execute_test(test_name, test_path, test_config, args.memgraph,
|
||||
args.mg_import_csv, args.tester, args.write_expected)
|
||||
execute_test(
|
||||
test_name,
|
||||
test_path,
|
||||
test_config,
|
||||
args.memgraph,
|
||||
args.mg_import_csv,
|
||||
args.tester,
|
||||
args.write_expected,
|
||||
)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
@@ -36,8 +36,7 @@ def execute_test(**kwargs):
|
||||
timeout = duration * 2 if "hang" not in kwargs else duration * 2 + 60
|
||||
success = False
|
||||
|
||||
server_args = [server_binary, "--interval", interval,
|
||||
"--duration", duration]
|
||||
server_args = [server_binary, "--interval", interval, "--duration", duration]
|
||||
for flag, value in kwargs.items():
|
||||
flag = "--" + flag.replace("_", "-")
|
||||
# We handle boolean flags here. The type of value must be `bool`, and
|
||||
@@ -48,9 +47,15 @@ def execute_test(**kwargs):
|
||||
else:
|
||||
server_args.extend([flag, value])
|
||||
|
||||
client_args = [client_binary, "--interval", interval,
|
||||
"--duration", duration,
|
||||
"--storage-directory", storage_directory]
|
||||
client_args = [
|
||||
client_binary,
|
||||
"--interval",
|
||||
interval,
|
||||
"--duration",
|
||||
duration,
|
||||
"--storage-directory",
|
||||
storage_directory,
|
||||
]
|
||||
if endpoint:
|
||||
client_args.extend(["--endpoint", endpoint])
|
||||
|
||||
@@ -61,8 +66,7 @@ def execute_test(**kwargs):
|
||||
assert server.poll() is None, "Server process died prematurely!"
|
||||
|
||||
try:
|
||||
subprocess.run(list(map(str, client_args)), timeout=timeout,
|
||||
check=True)
|
||||
subprocess.run(list(map(str, client_args)), timeout=timeout, check=True)
|
||||
finally:
|
||||
if server is None:
|
||||
success = True
|
||||
@@ -88,16 +92,14 @@ TESTS = [
|
||||
{"endpoint": "http://127.0.0.1:9000/nonexistant/", "no_check": True},
|
||||
{"start_server": False},
|
||||
{"startups": 4, "no_check_duration": True}, # the last 3 tests failed
|
||||
# to send any data + this test
|
||||
{"add_garbage": True}
|
||||
# to send any data + this test
|
||||
{"add_garbage": True},
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
server_binary = os.path.join(SCRIPT_DIR, "server.py")
|
||||
client_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "telemetry", "client")
|
||||
kvstore_console_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"manual", "kvstore_console")
|
||||
client_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "telemetry", "client")
|
||||
kvstore_console_binary = os.path.join(PROJECT_DIR, "build", "tests", "manual", "kvstore_console")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--client", default=client_binary)
|
||||
@@ -108,19 +110,23 @@ if __name__ == "__main__":
|
||||
storage = tempfile.TemporaryDirectory()
|
||||
|
||||
for test in TESTS:
|
||||
print("\033[1;36m~~ Executing test with arguments:",
|
||||
json.dumps(test, sort_keys=True), "~~\033[0m")
|
||||
print(
|
||||
"\033[1;36m~~ Executing test with arguments:",
|
||||
json.dumps(test, sort_keys=True),
|
||||
"~~\033[0m",
|
||||
)
|
||||
|
||||
if test.pop("add_garbage", False):
|
||||
proc = subprocess.Popen([args.kvstore_console, "--path",
|
||||
storage.name], stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL)
|
||||
proc = subprocess.Popen(
|
||||
[args.kvstore_console, "--path", storage.name],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
proc.communicate("put garbage garbage".encode("utf-8"))
|
||||
assert proc.wait() == 0
|
||||
|
||||
try:
|
||||
success = execute_test(client=args.client, server=args.server,
|
||||
storage=storage.name, **test)
|
||||
success = execute_test(client=args.client, server=args.server, storage=storage.name, **test)
|
||||
except Exception as e:
|
||||
print("\033[1;33m", e, "\033[0m", sep="")
|
||||
success = False
|
||||
|
||||
@@ -46,7 +46,7 @@ def build_handler(storage, args):
|
||||
assert self.headers["accept"] == "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"))
|
||||
|
||||
if self.path not in [args.path, args.redirect_path]:
|
||||
@@ -195,4 +195,4 @@ if __name__ == "__main__":
|
||||
verify_storage(startup, args)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -34,7 +34,8 @@ class QueryClient:
|
||||
self.default_num_workers = default_num_workers
|
||||
|
||||
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))
|
||||
|
||||
client_path = "tests/macro_benchmark/query_client"
|
||||
@@ -53,29 +54,36 @@ class QueryClient:
|
||||
output_fd, output = tempfile.mkstemp()
|
||||
os.close(output_fd)
|
||||
|
||||
client_args = ["--port", database.args.port,
|
||||
"--num-workers", str(num_workers),
|
||||
"--output", output]
|
||||
client_args = [
|
||||
"--port",
|
||||
database.args.port,
|
||||
"--num-workers",
|
||||
str(num_workers),
|
||||
"--output",
|
||||
output,
|
||||
]
|
||||
|
||||
cpu_time_start = database.database_bin.get_usage()["cpu"]
|
||||
# TODO make the timeout configurable per query or something
|
||||
return_code = self.client.run_and_wait(
|
||||
client, client_args, timeout=600, stdin=queries_path)
|
||||
return_code = self.client.run_and_wait(client, client_args, timeout=600, stdin=queries_path)
|
||||
usage = database.database_bin.get_usage()
|
||||
cpu_time_end = usage["cpu"]
|
||||
os.remove(queries_path)
|
||||
if return_code != 0:
|
||||
with open(self.client.get_stderr()) as f:
|
||||
stderr = f.read()
|
||||
self.log.error("Error while executing queries '%s'. "
|
||||
"Failed with return_code %d and stderr:\n%s",
|
||||
str(queries), return_code, stderr)
|
||||
self.log.error(
|
||||
"Error while executing queries '%s'. " "Failed with return_code %d and stderr:\n%s",
|
||||
str(queries),
|
||||
return_code,
|
||||
stderr,
|
||||
)
|
||||
raise Exception("BoltClient execution failed")
|
||||
|
||||
data = {"groups" : []}
|
||||
data = {"groups": []}
|
||||
with open(output) as 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[MAX_MEMORY] = usage["max_memory"]
|
||||
|
||||
@@ -94,7 +102,8 @@ class LongRunningClient:
|
||||
# TODO: This is quite similar to __call__ method of QueryClient. Remove
|
||||
# duplication.
|
||||
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)
|
||||
|
||||
client_path = "tests/macro_benchmark/{}".format(client)
|
||||
@@ -113,32 +122,41 @@ class LongRunningClient:
|
||||
output_fd, output = tempfile.mkstemp()
|
||||
os.close(output_fd)
|
||||
|
||||
client_args = ["--port", database.args.port,
|
||||
"--num-workers", str(num_workers),
|
||||
"--output", output,
|
||||
"--duration", str(duration),
|
||||
"--db", database.name,
|
||||
"--scenario", self.workload]
|
||||
client_args = [
|
||||
"--port",
|
||||
database.args.port,
|
||||
"--num-workers",
|
||||
str(num_workers),
|
||||
"--output",
|
||||
output,
|
||||
"--duration",
|
||||
str(duration),
|
||||
"--db",
|
||||
database.name,
|
||||
"--scenario",
|
||||
self.workload,
|
||||
]
|
||||
|
||||
return_code = self.client.run_and_wait(
|
||||
client, client_args, timeout=600, stdin=config_path)
|
||||
return_code = self.client.run_and_wait(client, client_args, timeout=600, stdin=config_path)
|
||||
os.remove(config_path)
|
||||
if return_code != 0:
|
||||
with open(self.client.get_stderr()) as f:
|
||||
stderr = f.read()
|
||||
self.log.error("Error while executing config '%s'. "
|
||||
"Failed with return_code %d and stderr:\n%s",
|
||||
str(config), return_code, stderr)
|
||||
self.log.error(
|
||||
"Error while executing config '%s'. " "Failed with return_code %d and stderr:\n%s",
|
||||
str(config),
|
||||
return_code,
|
||||
stderr,
|
||||
)
|
||||
raise Exception("BoltClient execution failed")
|
||||
|
||||
|
||||
# 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
|
||||
# becomes available.
|
||||
data = []
|
||||
with open(output) as f:
|
||||
for line in f:
|
||||
data.append(json.loads(line))
|
||||
data.append(json.loads(line))
|
||||
|
||||
os.remove(output)
|
||||
return data
|
||||
|
||||
@@ -14,9 +14,11 @@ from argparse import ArgumentParser
|
||||
|
||||
try:
|
||||
import jail
|
||||
|
||||
APOLLO = True
|
||||
except:
|
||||
import jail_faker as jail
|
||||
|
||||
APOLLO = False
|
||||
|
||||
|
||||
@@ -45,13 +47,15 @@ def get_absolute_path(path, base=""):
|
||||
def set_cpus(flag_name, process, args):
|
||||
argp = ArgumentParser()
|
||||
# named, optional arguments
|
||||
argp.add_argument("--" + flag_name, nargs="+", type=int, help="cpus that "
|
||||
"will be used by process. Obligatory on Apollo, ignored "
|
||||
"otherwise.")
|
||||
argp.add_argument(
|
||||
"--" + flag_name,
|
||||
nargs="+",
|
||||
type=int,
|
||||
help="cpus that " "will be used by process. Obligatory on Apollo, ignored " "otherwise.",
|
||||
)
|
||||
args, _ = argp.parse_known_args(args)
|
||||
attr_flag_name = flag_name.replace("-", "_")
|
||||
cpus = getattr(args, attr_flag_name)
|
||||
assert not APOLLO or cpus, \
|
||||
"flag --{} is obligatory on Apollo".format(flag_name)
|
||||
assert not APOLLO or cpus, "flag --{} is obligatory on Apollo".format(flag_name)
|
||||
if cpus:
|
||||
process.set_cpus(cpus, hyper = False)
|
||||
process.set_cpus(cpus, hyper=False)
|
||||
|
||||
@@ -36,13 +36,12 @@ class Memgraph:
|
||||
"""
|
||||
Knows how to start and stop memgraph.
|
||||
"""
|
||||
|
||||
def __init__(self, args, num_workers):
|
||||
self.log = logging.getLogger("MemgraphRunner")
|
||||
argp = ArgumentParser("MemgraphArgumentParser")
|
||||
argp.add_argument("--runner-bin",
|
||||
default=get_absolute_path("memgraph", "build"))
|
||||
argp.add_argument("--port", default="7687",
|
||||
help="Database and client port")
|
||||
argp.add_argument("--runner-bin", default=get_absolute_path("memgraph", "build"))
|
||||
argp.add_argument("--port", default="7687", help="Database and client port")
|
||||
argp.add_argument("--data-directory", default=None)
|
||||
argp.add_argument("--storage-snapshot-on-exit", action="store_true")
|
||||
argp.add_argument("--storage-recover-on-startup", action="store_true")
|
||||
@@ -55,8 +54,12 @@ class Memgraph:
|
||||
|
||||
def start(self):
|
||||
self.log.info("start")
|
||||
database_args = ["--bolt-port", self.args.port,
|
||||
"--query-execution-timeout-sec", "0"]
|
||||
database_args = [
|
||||
"--bolt-port",
|
||||
self.args.port,
|
||||
"--query-execution-timeout-sec",
|
||||
"0",
|
||||
]
|
||||
if self.num_workers:
|
||||
database_args += ["--bolt-num-workers", str(self.num_workers)]
|
||||
if self.args.data_directory:
|
||||
@@ -82,15 +85,13 @@ class Neo:
|
||||
"""
|
||||
Knows how to start and stop neo4j.
|
||||
"""
|
||||
|
||||
def __init__(self, args, config):
|
||||
self.log = logging.getLogger("NeoRunner")
|
||||
argp = ArgumentParser("NeoArgumentParser")
|
||||
argp.add_argument("--runner-bin", default=get_absolute_path(
|
||||
"neo4j/bin/neo4j", "libs"))
|
||||
argp.add_argument("--port", default="7687",
|
||||
help="Database and client port")
|
||||
argp.add_argument("--http-port", default="7474",
|
||||
help="Database and client port")
|
||||
argp.add_argument("--runner-bin", default=get_absolute_path("neo4j/bin/neo4j", "libs"))
|
||||
argp.add_argument("--port", default="7687", 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.args, _ = argp.parse_known_args(args)
|
||||
self.config = config
|
||||
@@ -105,24 +106,23 @@ class Neo:
|
||||
self.neo4j_home_path = tempfile.mkdtemp(dir="/dev/shm")
|
||||
|
||||
try:
|
||||
os.symlink(os.path.join(get_absolute_path("neo4j", "libs"), "lib"),
|
||||
os.path.join(self.neo4j_home_path, "lib"))
|
||||
os.symlink(
|
||||
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_file = os.path.join(neo4j_conf_dir, "neo4j.conf")
|
||||
os.mkdir(neo4j_conf_dir)
|
||||
shutil.copyfile(self.config, neo4j_conf_file)
|
||||
with open(neo4j_conf_file, "a") as f:
|
||||
f.write("\ndbms.connector.bolt.listen_address=:" +
|
||||
self.args.port + "\n")
|
||||
f.write("\ndbms.connector.http.listen_address=:" +
|
||||
self.args.http_port + "\n")
|
||||
f.write("\ndbms.connector.bolt.listen_address=:" + self.args.port + "\n")
|
||||
f.write("\ndbms.connector.http.listen_address=:" + self.args.http_port + "\n")
|
||||
|
||||
# environment
|
||||
cwd = os.path.dirname(self.args.runner_bin)
|
||||
env = {"NEO4J_HOME": self.neo4j_home_path}
|
||||
|
||||
self.database_bin.run(self.args.runner_bin, args=["console"],
|
||||
env=env, timeout=600, cwd=cwd)
|
||||
self.database_bin.run(self.args.runner_bin, args=["console"], env=env, timeout=600, cwd=cwd)
|
||||
except:
|
||||
shutil.rmtree(self.neo4j_home_path)
|
||||
raise Exception("Couldn't run Neo4j!")
|
||||
|
||||
@@ -9,4 +9,7 @@
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# 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
|
||||
)
|
||||
|
||||
@@ -15,6 +15,5 @@ VERTEX_COUNT = 100000
|
||||
for i in range(VERTEX_COUNT):
|
||||
print("CREATE (n%d {x: %d})" % (i, i))
|
||||
# batch CREATEs because we can't execute all at once
|
||||
if (i != 0 and i % BATCH_SIZE == 0) or \
|
||||
(i + 1 == VERTEX_COUNT):
|
||||
if (i != 0 and i % BATCH_SIZE == 0) or (i + 1 == VERTEX_COUNT):
|
||||
print(";")
|
||||
|
||||
@@ -15,6 +15,5 @@ VERTEX_COUNT = 1000000
|
||||
for i in range(VERTEX_COUNT):
|
||||
print("CREATE (n%d {x: %d})" % (i, i))
|
||||
# batch CREATEs because we can't execute all at once
|
||||
if (i != 0 and i % BATCH_SIZE == 0) or \
|
||||
(i + 1 == VERTEX_COUNT):
|
||||
if (i != 0 and i % BATCH_SIZE == 0) or (i + 1 == VERTEX_COUNT):
|
||||
print(";")
|
||||
|
||||
@@ -19,8 +19,9 @@ random.seed(1)
|
||||
for i in range(common.BFS_ITERS):
|
||||
a = int(random.random() * common.VERTEX_COUNT)
|
||||
b = int(random.random() * common.VERTEX_COUNT)
|
||||
print("MATCH (from: Node {id: %d}) WITH from "
|
||||
"MATCH (to: Node {id: %d}) WITH to "
|
||||
"MATCH path = (from)-[*bfs..%d (e, n | true)]->(to) WITH path "
|
||||
"LIMIT 10 RETURN 0;"
|
||||
% (a, b, common.PATH_LENGTH))
|
||||
print(
|
||||
"MATCH (from: Node {id: %d}) WITH from "
|
||||
"MATCH (to: Node {id: %d}) WITH to "
|
||||
"MATCH path = (from)-[*bfs..%d (e, n | true)]->(to) WITH path "
|
||||
"LIMIT 10 RETURN 0;" % (a, b, common.PATH_LENGTH)
|
||||
)
|
||||
|
||||
@@ -13,4 +13,3 @@ VERTEX_COUNT = 1000
|
||||
SPARSE_FACTOR = 10
|
||||
BFS_ITERS = 50
|
||||
PATH_LENGTH = 5000
|
||||
|
||||
|
||||
@@ -32,4 +32,3 @@ for i in range(common.VERTEX_COUNT * common.VERTEX_COUNT // common.SPARSE_FACTOR
|
||||
a = 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))
|
||||
|
||||
|
||||
@@ -11,13 +11,10 @@
|
||||
|
||||
import random
|
||||
|
||||
|
||||
def init_data(card_count, pos_count):
|
||||
print("UNWIND range(0, {} - 1) AS id "
|
||||
"CREATE (:Card {{id: id, compromised: false}});".format(
|
||||
card_count))
|
||||
print("UNWIND range(0, {} - 1) AS id "
|
||||
"CREATE (:Pos {{id: id, compromised: false}});".format(
|
||||
pos_count))
|
||||
print("UNWIND range(0, {} - 1) AS id " "CREATE (:Card {{id: id, compromised: false}});".format(card_count))
|
||||
print("UNWIND range(0, {} - 1) AS id " "CREATE (:Pos {{id: id, compromised: false}});".format(pos_count))
|
||||
|
||||
|
||||
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
|
||||
# is compromised, there is a 0.1 chance the transaction is
|
||||
# fraudulent and detected (regardless of POS).
|
||||
q = ("MATCH (c:Card {{id: {}}}), (p:Pos {{id: {}}}) "
|
||||
"CREATE (t:Transaction "
|
||||
"{{id: {}, fraud_reported: c.compromised AND (rand() < %f)}}) "
|
||||
"CREATE (c)<-[:Using]-(t)-[:At]->(p) "
|
||||
"SET c.compromised = p.compromised;" % report_pct)
|
||||
q = (
|
||||
"MATCH (c:Card {{id: {}}}), (p:Pos {{id: {}}}) "
|
||||
"CREATE (t:Transaction "
|
||||
"{{id: {}, fraud_reported: c.compromised AND (rand() < %f)}}) "
|
||||
"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):
|
||||
print(q.format(rint(card_count), rint(pos_count), i))
|
||||
|
||||
|
||||
POS_COUNT = 1000
|
||||
CARD_COUNT = 10000
|
||||
FRAUD_POS_COUNT = 20
|
||||
FRAUD_POS_COUNT = 20
|
||||
TX_COUNT = 50000
|
||||
REPORT_PCT = 0.1
|
||||
|
||||
|
||||
@@ -21,22 +21,21 @@ seed(0)
|
||||
def create_vertices(vertex_count):
|
||||
for vertex in range(vertex_count):
|
||||
print("CREATE (:Label {id: %d})" % vertex)
|
||||
if (vertex != 0 and vertex % BATCH_SIZE == 0) or \
|
||||
(vertex + 1 == vertex_count):
|
||||
if (vertex != 0 and vertex % BATCH_SIZE == 0) or (vertex + 1 == vertex_count):
|
||||
print(";")
|
||||
|
||||
|
||||
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 = []
|
||||
merges = []
|
||||
for edge in range(edge_count):
|
||||
matches.append("MATCH (a%d :Label {id: %d}), (b%d :Label {id: %d})" %
|
||||
(edge, randint(0, vertex_count - 1),
|
||||
edge, randint(0, vertex_count - 1)))
|
||||
matches.append(
|
||||
"MATCH (a%d :Label {id: %d}), (b%d :Label {id: %d})"
|
||||
% (edge, randint(0, vertex_count - 1), edge, randint(0, vertex_count - 1))
|
||||
)
|
||||
merges.append("CREATE (a%d)-[:Type]->(b%d)" % (edge, edge))
|
||||
if (edge != 0 and edge % BATCH_SIZE == 0) or \
|
||||
((edge + 1) == edge_count):
|
||||
if (edge != 0 and edge % BATCH_SIZE == 0) or ((edge + 1) == edge_count):
|
||||
print(" ".join(matches + merges))
|
||||
print(";")
|
||||
matches = []
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# licenses/APL.txt.
|
||||
|
||||
|
||||
def generate(expressions, repetitions):
|
||||
idx = 0
|
||||
|
||||
def get_alias():
|
||||
nonlocal idx
|
||||
idx += 1
|
||||
|
||||
@@ -11,11 +11,31 @@
|
||||
|
||||
import common
|
||||
|
||||
expressions = ['1 + 3', '2 - 1', '2 * 5', '5 / 2', '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']
|
||||
expressions = [
|
||||
"1 + 3",
|
||||
"2 - 1",
|
||||
"2 * 5",
|
||||
"5 / 2",
|
||||
"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))
|
||||
|
||||
@@ -18,9 +18,11 @@ from random import randint, seed
|
||||
|
||||
seed(0)
|
||||
|
||||
|
||||
def rint(upper_bound_exclusive):
|
||||
return randint(0, upper_bound_exclusive - 1)
|
||||
|
||||
|
||||
VERTEX_COUNT = 1500
|
||||
EDGE_COUNT = VERTEX_COUNT * 15
|
||||
|
||||
@@ -28,7 +30,7 @@ EDGE_COUNT = VERTEX_COUNT * 15
|
||||
LABEL_COUNT = 10
|
||||
|
||||
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
|
||||
|
||||
# some consts used in mutiple files
|
||||
@@ -38,7 +40,6 @@ PROP_PREFIX = "Prop"
|
||||
ID = "id"
|
||||
|
||||
|
||||
|
||||
def labels():
|
||||
labels = ":" + LABEL_INDEX
|
||||
for _ in range(rint(MAX_LABELS)):
|
||||
@@ -47,12 +48,11 @@ def labels():
|
||||
|
||||
|
||||
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
|
||||
are generated. Thus most labels have Prop0, and least have PropMAX_PROPS.
|
||||
"""
|
||||
props = {"%s%d" % (PROP_PREFIX, i): rint(MAX_PROP_VALUE)
|
||||
for i in range(rint(MAX_PROPS))}
|
||||
props = {"%s%d" % (PROP_PREFIX, i): rint(MAX_PROP_VALUE) for i in range(rint(MAX_PROPS))}
|
||||
props[ID] = id
|
||||
return "{" + ", ".join("%s: %s" % kv for kv in props.items()) + "}"
|
||||
|
||||
@@ -74,21 +74,20 @@ def main():
|
||||
# create vertices
|
||||
for vertex_index in range(VERTEX_COUNT):
|
||||
print("CREATE %s" % vertex(vertex_index))
|
||||
if (vertex_index != 0 and vertex_index % BATCH_SIZE == 0) or \
|
||||
vertex_index + 1 == VERTEX_COUNT:
|
||||
if (vertex_index != 0 and vertex_index % BATCH_SIZE == 0) or vertex_index + 1 == VERTEX_COUNT:
|
||||
print(";")
|
||||
print("MATCH (n) RETURN assert(count(n) = %d);" % VERTEX_COUNT)
|
||||
|
||||
# create edges stohastically
|
||||
attempts = VERTEX_COUNT ** 2
|
||||
p = EDGE_COUNT / VERTEX_COUNT ** 2
|
||||
print("MATCH (a) WITH a MATCH (b) WITH a, b WHERE rand() < %f "
|
||||
" CREATE (a)-[:EdgeType]->(b);" % p)
|
||||
attempts = VERTEX_COUNT**2
|
||||
p = EDGE_COUNT / VERTEX_COUNT**2
|
||||
print("MATCH (a) WITH a MATCH (b) WITH a, b WHERE rand() < %f " " CREATE (a)-[:EdgeType]->(b);" % p)
|
||||
sigma = (attempts * p * (1 - p)) ** 0.5
|
||||
delta = 5 * sigma
|
||||
print("MATCH (n)-[r]->() WITH count(r) AS c "
|
||||
"RETURN assert(c >= %d AND c <= %d);" % (
|
||||
EDGE_COUNT - delta, EDGE_COUNT + delta))
|
||||
print(
|
||||
"MATCH (n)-[r]->() WITH count(r) AS c "
|
||||
"RETURN assert(c >= %d AND c <= %d);" % (EDGE_COUNT - delta, EDGE_COUNT + delta)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
|
||||
from setup import LABEL_INDEX, ID, VERTEX_COUNT, rint
|
||||
|
||||
print("UNWIND range(0, 10000) AS i "
|
||||
"MATCH (n:%s {%s: %d}) RETURN n SKIP 1000000" % (
|
||||
LABEL_INDEX, ID, rint(VERTEX_COUNT)))
|
||||
print(
|
||||
"UNWIND range(0, 10000) AS i " "MATCH (n:%s {%s: %d}) RETURN n SKIP 1000000" % (LABEL_INDEX, ID, rint(VERTEX_COUNT))
|
||||
)
|
||||
|
||||
@@ -12,5 +12,4 @@
|
||||
from setup import LABEL_COUNT, LABEL_PREFIX
|
||||
|
||||
for i in range(LABEL_COUNT):
|
||||
print("UNWIND range(0, 30) AS i MATCH (n:%s%d) "
|
||||
"RETURN n SKIP 1000000;" % (LABEL_PREFIX, i))
|
||||
print("UNWIND range(0, 30) AS i MATCH (n:%s%d) " "RETURN n SKIP 1000000;" % (LABEL_PREFIX, i))
|
||||
|
||||
@@ -9,8 +9,17 @@
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# 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):
|
||||
print("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)))
|
||||
print(
|
||||
"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))
|
||||
)
|
||||
|
||||
@@ -11,5 +11,7 @@
|
||||
|
||||
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" % (
|
||||
PROP_PREFIX, rint(MAX_PROPS), rint(MAX_PROP_VALUE)))
|
||||
print(
|
||||
"UNWIND range(0, 50) AS i MATCH (n {%s%d: %d}) RETURN n SKIP 10000"
|
||||
% (PROP_PREFIX, rint(MAX_PROPS), rint(MAX_PROP_VALUE))
|
||||
)
|
||||
|
||||
@@ -21,5 +21,6 @@ def main():
|
||||
if i != 0 and i % BATCH_SIZE == 0:
|
||||
print(";")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -28,6 +28,7 @@ from signal import *
|
||||
class ProcessException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class StorageException(Exception):
|
||||
pass
|
||||
|
||||
@@ -41,10 +42,11 @@ class Process:
|
||||
self._usage = {}
|
||||
self._files = []
|
||||
|
||||
def run(self, binary, args=None, env=None, timeout=120,
|
||||
stdin="/dev/null", cwd="."):
|
||||
if args is None: args = []
|
||||
if env is None: env = {}
|
||||
def run(self, binary, args=None, env=None, timeout=120, stdin="/dev/null", cwd="."):
|
||||
if args is None:
|
||||
args = []
|
||||
if env is None:
|
||||
env = {}
|
||||
# don't start a new process if one is already running
|
||||
if self._proc != None and self._proc.returncode == None:
|
||||
raise ProcessException
|
||||
@@ -65,15 +67,14 @@ class Process:
|
||||
self._timeout = timeout
|
||||
|
||||
# start process
|
||||
self._proc = subprocess.Popen(exe, env=env, cwd=cwd,
|
||||
stdin=open(stdin, "r"))
|
||||
self._proc = subprocess.Popen(exe, env=env, cwd=cwd, stdin=open(stdin, "r"))
|
||||
|
||||
def run_and_wait(self, *args, **kwargs):
|
||||
check = kwargs.pop("check", True)
|
||||
self.run(*args, **kwargs)
|
||||
return self.wait(check)
|
||||
|
||||
def wait(self, check = True):
|
||||
def wait(self, check=True):
|
||||
if self._proc == None:
|
||||
raise ProcessException
|
||||
self._proc.wait()
|
||||
@@ -100,18 +101,17 @@ class Process:
|
||||
# this is implemented only in the real API
|
||||
def set_cpus(self, cpus, hyper=True):
|
||||
s = "out" if not hyper else ""
|
||||
sys.stderr.write("WARNING: Trying to set cpus for {} to "
|
||||
"{} with{} hyperthreading!\n".format(str(self), cpus, s))
|
||||
sys.stderr.write(
|
||||
"WARNING: Trying to set cpus for {} to " "{} with{} hyperthreading!\n".format(str(self), cpus, s)
|
||||
)
|
||||
|
||||
# this is implemented only in the real API
|
||||
def set_nproc(self, nproc):
|
||||
sys.stderr.write("WARNING: Trying to set nproc for {} to "
|
||||
"{}!\n".format(str(self), nproc))
|
||||
sys.stderr.write("WARNING: Trying to set nproc for {} to " "{}!\n".format(str(self), nproc))
|
||||
|
||||
# this is implemented only in the real API
|
||||
def set_memory(self, memory):
|
||||
sys.stderr.write("WARNING: Trying to set memory for {} to "
|
||||
"{}\n".format(str(self), memory))
|
||||
sys.stderr.write("WARNING: Trying to set memory for {} to " "{}\n".format(str(self), memory))
|
||||
|
||||
# WARNING: this won't be implemented in the real API
|
||||
def get_pid(self):
|
||||
@@ -121,7 +121,8 @@ class Process:
|
||||
|
||||
def _set_usage(self, val, name, only_value=False):
|
||||
self._usage[name] = val
|
||||
if only_value: return
|
||||
if only_value:
|
||||
return
|
||||
maxname = "max_" + name
|
||||
maxval = val
|
||||
if maxname in self._usage:
|
||||
@@ -133,7 +134,8 @@ class Process:
|
||||
self._watchdog()
|
||||
|
||||
def _update_usage(self):
|
||||
if self._proc == None: return
|
||||
if self._proc == None:
|
||||
return
|
||||
try:
|
||||
f = open("/proc/{}/stat".format(self._proc.pid), "r")
|
||||
data_stat = f.read().split()
|
||||
@@ -144,21 +146,20 @@ class Process:
|
||||
except:
|
||||
return
|
||||
# for a description of these fields see: man proc; man times
|
||||
utime, stime, cutime, cstime = map(
|
||||
lambda x: int(x) / self._ticks_per_sec, data_stat[13:17])
|
||||
utime, stime, cutime, cstime = map(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 + cutime, "cpu_user", only_value=True)
|
||||
self._set_usage(stime + cstime, "cpu_sys", only_value=True)
|
||||
self._set_usage(int(data_stat[19]), "threads")
|
||||
mem_vm, mem_res, mem_shr = map(
|
||||
lambda x: int(x) * self._page_size // 1024, data_statm[:3])
|
||||
mem_vm, mem_res, mem_shr = map(lambda x: int(x) * self._page_size // 1024, data_statm[:3])
|
||||
self._set_usage(mem_res, "memory")
|
||||
|
||||
def _watchdog(self):
|
||||
if self._proc == None or self._proc.returncode != None: return
|
||||
if time.time() - self._start_time < self._timeout: return
|
||||
sys.stderr.write("Timeout of {}s reached, sending "
|
||||
"SIGKILL to {}!\n".format(self._timeout, self))
|
||||
if self._proc == None or self._proc.returncode != None:
|
||||
return
|
||||
if time.time() - self._start_time < self._timeout:
|
||||
return
|
||||
sys.stderr.write("Timeout of {}s reached, sending " "SIGKILL to {}!\n".format(self._timeout, self))
|
||||
self.send_signal(SIGKILL)
|
||||
self.get_status()
|
||||
|
||||
@@ -172,22 +173,27 @@ PROCESSES_NUM = 8
|
||||
_processes = [Process(i) for i in range(1, PROCESSES_NUM + 1)]
|
||||
_last_process = 0
|
||||
|
||||
|
||||
def _usage_updater():
|
||||
while True:
|
||||
for proc in _processes:
|
||||
proc._do_background_tasks()
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
_thread = threading.Thread(target=_usage_updater, daemon=True)
|
||||
_thread.start()
|
||||
|
||||
|
||||
@atexit.register
|
||||
def cleanup():
|
||||
for proc in _processes:
|
||||
if proc._proc == None: continue
|
||||
if proc._proc == None:
|
||||
continue
|
||||
proc.send_signal(SIGKILL)
|
||||
proc.get_status()
|
||||
|
||||
|
||||
# end of private methods ------------------------------------------------------
|
||||
|
||||
|
||||
@@ -199,6 +205,7 @@ def get_process():
|
||||
return proc
|
||||
return None
|
||||
|
||||
|
||||
def get_host_info():
|
||||
with open("/proc/meminfo") as f:
|
||||
memdata = f.read()
|
||||
@@ -215,21 +222,24 @@ def get_host_info():
|
||||
|
||||
threads, cpus = 0, set()
|
||||
for row in cpudata.split("\n\n"):
|
||||
if not row: continue
|
||||
if not row:
|
||||
continue
|
||||
data = row.split("\n")
|
||||
core_id, physical_id = -1, -1
|
||||
for line in data:
|
||||
name, val = map(lambda x: x.strip(), line.split(":"))
|
||||
if name == "physical id": physical_id = int(val)
|
||||
elif name == "core id": core_id = int(val)
|
||||
if name == "physical id":
|
||||
physical_id = int(val)
|
||||
elif name == "core id":
|
||||
core_id = int(val)
|
||||
threads += 1
|
||||
cpus.add((core_id, physical_id))
|
||||
cpus = len(cpus)
|
||||
|
||||
hyper = True if cpus != threads else False
|
||||
|
||||
return {"cpus": cpus, "memory": memory, "hyperthreading": hyper,
|
||||
"threads": threads}
|
||||
return {"cpus": cpus, "memory": memory, "hyperthreading": hyper, "threads": threads}
|
||||
|
||||
|
||||
# placeholder function that stores a label in the real API
|
||||
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
|
||||
the appropriate subset of the above mentioned commands.
|
||||
"""
|
||||
|
||||
|
||||
def network_flush_rules(chain=None):
|
||||
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,
|
||||
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,
|
||||
action=None):
|
||||
print("Network block TCP: chain={}, src={}, dst={}, sport={}, dport={}, "
|
||||
"action={}".format(chain, src, dst, sport, dport, action))
|
||||
|
||||
|
||||
def network_block_tcp(chain=None, src=None, dst=None, sport=None, dport=None, action=None):
|
||||
print(
|
||||
"Network block TCP: chain={}, src={}, dst={}, sport={}, dport={}, "
|
||||
"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
|
||||
`network_block_tcp`, so take a look there.
|
||||
"""
|
||||
def network_unblock_tcp(chain=None,
|
||||
src=None, dst=None,
|
||||
sport=None, dport=None,
|
||||
action=None):
|
||||
print("Network unblock TCP: chain={}, src={}, dst={}, sport={}, dport={}, "
|
||||
"action={}".format(chain, src, dst, sport, dport, action))
|
||||
|
||||
|
||||
def network_unblock_tcp(chain=None, src=None, dst=None, sport=None, dport=None, action=None):
|
||||
print(
|
||||
"Network unblock TCP: chain={}, src={}, dst={}, sport={}, dport={}, "
|
||||
"action={}".format(chain, src, dst, sport, dport, action)
|
||||
)
|
||||
|
||||
|
||||
# this function is deprecated
|
||||
def store_data(data):
|
||||
pass
|
||||
|
||||
|
||||
# placeholder function that returns real data in the real API
|
||||
def get_network_usage():
|
||||
usage = {
|
||||
"lo": {
|
||||
"bytes": {"rx": 0, "tx": 0},
|
||||
"packets": {"rx": 0, "tx": 0}
|
||||
},
|
||||
"eth0": {
|
||||
"bytes": {"rx": 0, "tx": 0},
|
||||
"packets": {"rx": 0, "tx": 0}
|
||||
}
|
||||
"lo": {"bytes": {"rx": 0, "tx": 0}, "packets": {"rx": 0, "tx": 0}},
|
||||
"eth0": {"bytes": {"rx": 0, "tx": 0}, "packets": {"rx": 0, "tx": 0}},
|
||||
}
|
||||
return usage
|
||||
|
||||
@@ -38,27 +38,30 @@ class LongRunningSuite:
|
||||
duration = config["duration"]
|
||||
if self.args.duration:
|
||||
duration = self.args.duration
|
||||
log.info("Executing run for {} seconds".format(
|
||||
duration))
|
||||
log.info("Executing run for {} seconds".format(duration))
|
||||
results = runner.run(next(scenario.get("run")()), duration, config["client"])
|
||||
|
||||
runner.stop()
|
||||
|
||||
measurements = []
|
||||
summary_format = "{:>15} {:>22} {:>22}\n"
|
||||
self.summary = summary_format.format(
|
||||
"elapsed_time", "num_executed_queries", "num_executed_steps")
|
||||
self.summary = summary_format.format("elapsed_time", "num_executed_queries", "num_executed_steps")
|
||||
for result in results:
|
||||
self.summary += summary_format.format(
|
||||
result["elapsed_time"], result["num_executed_queries"],
|
||||
result["num_executed_steps"])
|
||||
measurements.append({
|
||||
"target": "throughput",
|
||||
"time": result["elapsed_time"],
|
||||
"value": result["num_executed_queries"],
|
||||
"steps": result["num_executed_steps"],
|
||||
"unit": "number of executed queries",
|
||||
"type": "throughput"})
|
||||
result["elapsed_time"],
|
||||
result["num_executed_queries"],
|
||||
result["num_executed_steps"],
|
||||
)
|
||||
measurements.append(
|
||||
{
|
||||
"target": "throughput",
|
||||
"time": result["elapsed_time"],
|
||||
"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 += "\nExecuted steps: " + str(measurements[-1]["steps"])
|
||||
return measurements
|
||||
@@ -75,8 +78,7 @@ class _LongRunningRunner:
|
||||
self.log = logging.getLogger("_LongRunningRunner")
|
||||
self.database = database
|
||||
self.query_client = QueryClient(args, num_client_workers)
|
||||
self.long_running_client = LongRunningClient(args, num_client_workers,
|
||||
workload)
|
||||
self.long_running_client = LongRunningClient(args, num_client_workers, workload)
|
||||
|
||||
def start(self):
|
||||
self.database.start()
|
||||
@@ -85,8 +87,7 @@ class _LongRunningRunner:
|
||||
return self.query_client(queries, self.database, num_client_workers)
|
||||
|
||||
def run(self, config, duration, client, num_client_workers=None):
|
||||
return self.long_running_client(
|
||||
config, self.database, duration, client, num_client_workers)
|
||||
return self.long_running_client(config, self.database, duration, client, num_client_workers)
|
||||
|
||||
def stop(self):
|
||||
self.log.info("stop")
|
||||
@@ -99,44 +100,46 @@ class MemgraphRunner(_LongRunningRunner):
|
||||
"""
|
||||
Configures memgraph database for LongRunningSuite execution.
|
||||
"""
|
||||
|
||||
def __init__(self, args):
|
||||
argp = ArgumentParser("MemgraphRunnerArgumentParser")
|
||||
argp.add_argument("--num-database-workers", type=int, default=8,
|
||||
help="Number of workers")
|
||||
argp.add_argument("--num-client-workers", type=int, default=24,
|
||||
help="Number of clients")
|
||||
argp.add_argument("--workload", type=str, default="",
|
||||
help="Type of client workload. Sets \
|
||||
scenario flag for 'TestClient'")
|
||||
argp.add_argument("--num-database-workers", type=int, default=8, help="Number of workers")
|
||||
argp.add_argument("--num-client-workers", type=int, default=24, help="Number of clients")
|
||||
argp.add_argument(
|
||||
"--workload",
|
||||
type=str,
|
||||
default="",
|
||||
help="Type of client workload. Sets \
|
||||
scenario flag for 'TestClient'",
|
||||
)
|
||||
self.args, remaining_args = argp.parse_known_args(args)
|
||||
assert not APOLLO or self.args.num_database_workers, \
|
||||
"--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_database_workers, "--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"
|
||||
database = Memgraph(remaining_args, self.args.num_database_workers)
|
||||
super(MemgraphRunner, self).__init__(
|
||||
remaining_args, database, self.args.num_client_workers,
|
||||
self.args.workload)
|
||||
super(MemgraphRunner, self).__init__(remaining_args, database, self.args.num_client_workers, self.args.workload)
|
||||
|
||||
|
||||
class NeoRunner(_LongRunningRunner):
|
||||
"""
|
||||
Configures neo4j database for QuerySuite execution.
|
||||
"""
|
||||
|
||||
def __init__(self, args):
|
||||
argp = ArgumentParser("NeoRunnerArgumentParser")
|
||||
argp.add_argument("--runner-config",
|
||||
default=get_absolute_path("config/neo4j.conf"),
|
||||
help="Path to neo config file")
|
||||
argp.add_argument("--num-client-workers", type=int, default=24,
|
||||
help="Number of clients")
|
||||
argp.add_argument("--workload", type=str, default="",
|
||||
help="Type of client workload. Sets \
|
||||
scenario flag for 'TestClient'")
|
||||
argp.add_argument(
|
||||
"--runner-config",
|
||||
default=get_absolute_path("config/neo4j.conf"),
|
||||
help="Path to neo config file",
|
||||
)
|
||||
argp.add_argument("--num-client-workers", type=int, default=24, help="Number of clients")
|
||||
argp.add_argument(
|
||||
"--workload",
|
||||
type=str,
|
||||
default="",
|
||||
help="Type of client workload. Sets \
|
||||
scenario flag for 'TestClient'",
|
||||
)
|
||||
self.args, remaining_args = argp.parse_known_args(args)
|
||||
assert not APOLLO or self.args.num_client_workers, \
|
||||
"--client-num-clients is obligatory flag on apollo"
|
||||
assert not APOLLO or self.args.num_client_workers, "--client-num-clients is obligatory flag on apollo"
|
||||
database = Neo(remaining_args, self.args.runner_config)
|
||||
super(NeoRunner, self).__init__(
|
||||
remaining_args, database, self.args.num_client_workers,
|
||||
self.args.workload)
|
||||
super(NeoRunner, self).__init__(remaining_args, database, self.args.num_client_workers, self.args.workload)
|
||||
|
||||
@@ -33,21 +33,48 @@ class _QuerySuite:
|
||||
a single Cypher query that is benchmarked, and teardown steps
|
||||
(Cypher queries) executed after the benchmark.
|
||||
"""
|
||||
|
||||
# what the QuerySuite can work with
|
||||
KNOWN_KEYS = {"config", "setup", "itersetup", "run", "iterteardown",
|
||||
"teardown", "common"}
|
||||
FORMAT = ["{:>24}", "{:>28}", "{:>16}", "{:>18}", "{:>22}",
|
||||
"{:>16}", "{:>16}", "{:>16}"]
|
||||
KNOWN_KEYS = {
|
||||
"config",
|
||||
"setup",
|
||||
"itersetup",
|
||||
"run",
|
||||
"iterteardown",
|
||||
"teardown",
|
||||
"common",
|
||||
}
|
||||
FORMAT = [
|
||||
"{:>24}",
|
||||
"{:>28}",
|
||||
"{:>16}",
|
||||
"{:>18}",
|
||||
"{:>22}",
|
||||
"{:>16}",
|
||||
"{:>16}",
|
||||
"{:>16}",
|
||||
]
|
||||
FULL_FORMAT = "".join(FORMAT) + "\n"
|
||||
headers = ["group_name", "scenario_name", "parsing_time",
|
||||
"planning_time", "plan_execution_time",
|
||||
WALL_TIME, CPU_TIME, MAX_MEMORY]
|
||||
headers = [
|
||||
"group_name",
|
||||
"scenario_name",
|
||||
"parsing_time",
|
||||
"planning_time",
|
||||
"plan_execution_time",
|
||||
WALL_TIME,
|
||||
CPU_TIME,
|
||||
MAX_MEMORY,
|
||||
]
|
||||
summary = summary_raw = FULL_FORMAT.format(*headers)
|
||||
|
||||
def __init__(self, args):
|
||||
argp = ArgumentParser("MemgraphRunnerArgumentParser")
|
||||
argp.add_argument("--perf", default=False, action="store_true",
|
||||
help="Run perf on running tests and store data")
|
||||
argp.add_argument(
|
||||
"--perf",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Run perf on running tests and store data",
|
||||
)
|
||||
self.args, remaining_args = argp.parse_known_args(args)
|
||||
|
||||
def run(self, scenario, group_name, scenario_name, runner):
|
||||
@@ -62,8 +89,7 @@ class _QuerySuite:
|
||||
r_val = runner.execute(queries(), num_client_workers)
|
||||
else:
|
||||
r_val = None
|
||||
log.info("\t%s done in %.2f seconds" % (config_name,
|
||||
time.time() - start_time))
|
||||
log.info("\t%s done in %.2f seconds" % (config_name, time.time() - start_time))
|
||||
return r_val
|
||||
|
||||
measurements = defaultdict(list)
|
||||
@@ -75,8 +101,12 @@ class _QuerySuite:
|
||||
execute("setup")
|
||||
|
||||
# warmup phase
|
||||
for _ in range(min(scenario_config.get("iterations", 1),
|
||||
scenario_config.get("warmup", 2))):
|
||||
for _ in range(
|
||||
min(
|
||||
scenario_config.get("iterations", 1),
|
||||
scenario_config.get("warmup", 2),
|
||||
)
|
||||
):
|
||||
execute("itersetup")
|
||||
execute("run")
|
||||
execute("iterteardown")
|
||||
@@ -91,15 +121,28 @@ class _QuerySuite:
|
||||
execute("itersetup")
|
||||
|
||||
if self.args.perf:
|
||||
file_directory = './perf_results/run_%d/%s/%s/' \
|
||||
% (rerun_cnt, group_name, scenario_name)
|
||||
file_directory = "./perf_results/run_%d/%s/%s/" % (
|
||||
rerun_cnt,
|
||||
group_name,
|
||||
scenario_name,
|
||||
)
|
||||
os.makedirs(file_directory, exist_ok=True)
|
||||
file_name = '%d.perf.data' % iteration
|
||||
file_name = "%d.perf.data" % iteration
|
||||
path = file_directory + file_name
|
||||
database_pid = str(runner.database.database_bin._proc.pid)
|
||||
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")
|
||||
|
||||
@@ -110,16 +153,15 @@ class _QuerySuite:
|
||||
measurements["cpu_time"].append(run_result["cpu_time"])
|
||||
measurements["max_memory"].append(run_result["max_memory"])
|
||||
|
||||
assert len(run_result["groups"]) == 1, \
|
||||
"Multiple groups in run step not yet supported"
|
||||
assert len(run_result["groups"]) == 1, "Multiple groups in run step not yet supported"
|
||||
|
||||
group = run_result["groups"][0]
|
||||
measurements["wall_time"].append(group["wall_time"])
|
||||
|
||||
for key in ["parsing_time", "plan_execution_time",
|
||||
"planning_time"]:
|
||||
for key in ["parsing_time", "plan_execution_time", "planning_time"]:
|
||||
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])
|
||||
|
||||
execute("iterteardown")
|
||||
@@ -127,27 +169,35 @@ class _QuerySuite:
|
||||
execute("teardown")
|
||||
runner.stop()
|
||||
|
||||
self.append_scenario_summary(group_name, scenario_name,
|
||||
measurements, num_iterations)
|
||||
self.append_scenario_summary(group_name, scenario_name, measurements, num_iterations)
|
||||
|
||||
# calculate mean, median and stdev of measurements
|
||||
for key in measurements:
|
||||
samples = measurements[key]
|
||||
measurements[key] = {"mean": mean(samples),
|
||||
"median": median(samples),
|
||||
"stdev": stdev(samples),
|
||||
"count": len(samples)}
|
||||
measurements[key] = {
|
||||
"mean": mean(samples),
|
||||
"median": median(samples),
|
||||
"stdev": stdev(samples),
|
||||
"count": len(samples),
|
||||
}
|
||||
measurements["group_name"] = group_name
|
||||
measurements["scenario_name"] = scenario_name
|
||||
|
||||
return measurements
|
||||
|
||||
def append_scenario_summary(self, group_name, scenario_name,
|
||||
measurement_lists, num_iterations):
|
||||
def append_scenario_summary(self, group_name, scenario_name, measurement_lists, num_iterations):
|
||||
self.summary += self.FORMAT[0].format(group_name)
|
||||
self.summary += self.FORMAT[1].format(scenario_name)
|
||||
for i, key in enumerate(("parsing_time", "planning_time",
|
||||
"plan_execution_time", WALL_TIME, CPU_TIME, MAX_MEMORY)):
|
||||
for i, key in enumerate(
|
||||
(
|
||||
"parsing_time",
|
||||
"planning_time",
|
||||
"plan_execution_time",
|
||||
WALL_TIME,
|
||||
CPU_TIME,
|
||||
MAX_MEMORY,
|
||||
)
|
||||
):
|
||||
if key not in measurement_lists:
|
||||
time = "-"
|
||||
else:
|
||||
@@ -162,11 +212,11 @@ class _QuerySuite:
|
||||
self.summary += "\n"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@@ -175,11 +225,20 @@ class QuerySuite(_QuerySuite):
|
||||
_QuerySuite.__init__(self, args)
|
||||
|
||||
def runners(self):
|
||||
return {"MemgraphRunner" : MemgraphRunner, "NeoRunner" : NeoRunner}
|
||||
return {"MemgraphRunner": MemgraphRunner, "NeoRunner": NeoRunner}
|
||||
|
||||
def groups(self):
|
||||
return ["1000_create", "unwind_create", "match", "dense_expand",
|
||||
"expression", "aggregation", "return", "update", "delete"]
|
||||
return [
|
||||
"1000_create",
|
||||
"unwind_create",
|
||||
"match",
|
||||
"dense_expand",
|
||||
"expression",
|
||||
"aggregation",
|
||||
"return",
|
||||
"update",
|
||||
"delete",
|
||||
]
|
||||
|
||||
|
||||
class QueryParallelSuite(_QuerySuite):
|
||||
@@ -187,8 +246,10 @@ class QueryParallelSuite(_QuerySuite):
|
||||
_QuerySuite.__init__(self, args)
|
||||
|
||||
def runners(self):
|
||||
return {"MemgraphRunner" : MemgraphParallelRunner, "NeoRunner" :
|
||||
NeoParallelRunner}
|
||||
return {
|
||||
"MemgraphRunner": MemgraphParallelRunner,
|
||||
"NeoRunner": NeoParallelRunner,
|
||||
}
|
||||
|
||||
def groups(self):
|
||||
return ["aggregation_parallel", "create_parallel", "bfs_parallel"]
|
||||
@@ -201,6 +262,7 @@ class _QueryRunner:
|
||||
Execution returns benchmarking data (execution times, memory
|
||||
usage etc).
|
||||
"""
|
||||
|
||||
def __init__(self, args, database, num_client_workers):
|
||||
self.log = logging.getLogger("_HarnessClientRunner")
|
||||
self.database = database
|
||||
@@ -221,6 +283,7 @@ class MemgraphRunner(_QueryRunner):
|
||||
"""
|
||||
Configures memgraph database for QuerySuite execution.
|
||||
"""
|
||||
|
||||
def __init__(self, args):
|
||||
database = Memgraph(args, 1)
|
||||
super(MemgraphRunner, self).__init__(args, database, 1)
|
||||
@@ -230,11 +293,14 @@ class NeoRunner(_QueryRunner):
|
||||
"""
|
||||
Configures neo4j database for QuerySuite execution.
|
||||
"""
|
||||
|
||||
def __init__(self, args):
|
||||
argp = ArgumentParser("NeoRunnerArgumentParser")
|
||||
argp.add_argument("--runner-config",
|
||||
default=get_absolute_path("config/neo4j.conf"),
|
||||
help="Path to neo config file")
|
||||
argp.add_argument(
|
||||
"--runner-config",
|
||||
default=get_absolute_path("config/neo4j.conf"),
|
||||
help="Path to neo config file",
|
||||
)
|
||||
self.args, remaining_args = argp.parse_known_args(args)
|
||||
database = Neo(remaining_args, self.args.runner_config)
|
||||
super(NeoRunner, self).__init__(remaining_args, database)
|
||||
@@ -244,36 +310,32 @@ class NeoParallelRunner(_QueryRunner):
|
||||
"""
|
||||
Configures neo4j database for QuerySuite execution.
|
||||
"""
|
||||
|
||||
def __init__(self, args):
|
||||
argp = ArgumentParser("NeoRunnerArgumentParser")
|
||||
argp.add_argument("--runner-config",
|
||||
default=get_absolute_path("config/neo4j.conf"),
|
||||
help="Path to neo config file")
|
||||
argp.add_argument("--num-client-workers", type=int, default=24,
|
||||
help="Number of clients")
|
||||
argp.add_argument(
|
||||
"--runner-config",
|
||||
default=get_absolute_path("config/neo4j.conf"),
|
||||
help="Path to neo config file",
|
||||
)
|
||||
argp.add_argument("--num-client-workers", type=int, default=24, help="Number of clients")
|
||||
self.args, remaining_args = argp.parse_known_args(args)
|
||||
assert not APOLLO or self.args.num_client_workers, \
|
||||
"--client-num-clients is obligatory flag on apollo"
|
||||
assert not APOLLO or self.args.num_client_workers, "--client-num-clients is obligatory flag on apollo"
|
||||
database = Neo(remaining_args, self.args.runner_config)
|
||||
super(NeoRunner, self).__init__(
|
||||
remaining_args, database, self.args.num_client_workers)
|
||||
super(NeoRunner, self).__init__(remaining_args, database, self.args.num_client_workers)
|
||||
|
||||
|
||||
class MemgraphParallelRunner(_QueryRunner):
|
||||
"""
|
||||
Configures memgraph database for QuerySuite execution.
|
||||
"""
|
||||
|
||||
def __init__(self, args):
|
||||
argp = ArgumentParser("MemgraphRunnerArgumentParser")
|
||||
argp.add_argument("--num-database-workers", type=int, default=8,
|
||||
help="Number of workers")
|
||||
argp.add_argument("--num-client-workers", type=int, default=24,
|
||||
help="Number of clients")
|
||||
argp.add_argument("--num-database-workers", type=int, default=8, help="Number of workers")
|
||||
argp.add_argument("--num-client-workers", type=int, default=24, help="Number of clients")
|
||||
self.args, remaining_args = argp.parse_known_args(args)
|
||||
assert not APOLLO or self.args.num_database_workers, \
|
||||
"--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_database_workers, "--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"
|
||||
database = Memgraph(remaining_args, self.args.num_database_workers)
|
||||
super(MemgraphParallelRunner, self).__init__(
|
||||
remaining_args, database, self.args.num_client_workers)
|
||||
super(MemgraphParallelRunner, self).__init__(remaining_args, database, self.args.num_client_workers)
|
||||
|
||||
@@ -37,8 +37,7 @@ def get_queries(gen, count):
|
||||
return ret
|
||||
|
||||
|
||||
def match_patterns(dataset, variant, group, test, is_default_variant,
|
||||
patterns):
|
||||
def match_patterns(dataset, variant, group, test, is_default_variant, patterns):
|
||||
for pattern in patterns:
|
||||
verdict = [fnmatch.fnmatchcase(dataset, pattern[0])]
|
||||
if pattern[1] != "":
|
||||
@@ -58,7 +57,7 @@ def filter_benchmarks(generators, patterns):
|
||||
pattern = patterns[i].split("/")
|
||||
if len(pattern) > 4 or len(pattern) == 0:
|
||||
raise Exception("Invalid benchmark description '" + pattern + "'!")
|
||||
pattern.extend(["", "*", "*"][len(pattern) - 1:])
|
||||
pattern.extend(["", "*", "*"][len(pattern) - 1 :])
|
||||
patterns[i] = pattern
|
||||
filtered = []
|
||||
for dataset in sorted(generators.keys()):
|
||||
@@ -68,8 +67,7 @@ def filter_benchmarks(generators, patterns):
|
||||
current = collections.defaultdict(list)
|
||||
for group in tests:
|
||||
for test_name, test_func in tests[group]:
|
||||
if match_patterns(dataset, variant, group, test_name,
|
||||
is_default_variant, patterns):
|
||||
if match_patterns(dataset, variant, group, test_name, is_default_variant, patterns):
|
||||
current[group].append((test_name, test_func))
|
||||
if len(current) > 0:
|
||||
filtered.append((generator(variant), dict(current)))
|
||||
@@ -79,43 +77,71 @@ def filter_benchmarks(generators, patterns):
|
||||
# Parse options.
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Memgraph benchmark executor.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("benchmarks", nargs="*", default="",
|
||||
help="descriptions of benchmarks that should be run; "
|
||||
"multiple descriptions can be specified to run multiple "
|
||||
"benchmarks; the description is specified as "
|
||||
"dataset/variant/group/test; Unix shell-style wildcards "
|
||||
"can be used in the descriptions; variant, group and test "
|
||||
"are optional and they can be left out; the default "
|
||||
"variant is '' which selects the default dataset variant; "
|
||||
"the default group is '*' which selects all groups; the "
|
||||
"default test is '*' which selects all tests")
|
||||
parser.add_argument("--memgraph-binary",
|
||||
default=helpers.get_binary_path("memgraph"),
|
||||
help="Memgraph binary used for benchmarking")
|
||||
parser.add_argument("--client-binary",
|
||||
default=helpers.get_binary_path("tests/mgbench/client"),
|
||||
help="client binary used for benchmarking")
|
||||
parser.add_argument("--num-workers-for-import", type=int,
|
||||
default=multiprocessing.cpu_count() // 2,
|
||||
help="number of workers used to import the dataset")
|
||||
parser.add_argument("--num-workers-for-benchmark", type=int,
|
||||
default=1,
|
||||
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")
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"benchmarks",
|
||||
nargs="*",
|
||||
default="",
|
||||
help="descriptions of benchmarks that should be run; "
|
||||
"multiple descriptions can be specified to run multiple "
|
||||
"benchmarks; the description is specified as "
|
||||
"dataset/variant/group/test; Unix shell-style wildcards "
|
||||
"can be used in the descriptions; variant, group and test "
|
||||
"are optional and they can be left out; the default "
|
||||
"variant is '' which selects the default dataset variant; "
|
||||
"the default group is '*' which selects all groups; the "
|
||||
"default test is '*' which selects all tests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--memgraph-binary",
|
||||
default=helpers.get_binary_path("memgraph"),
|
||||
help="Memgraph binary used for benchmarking",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--client-binary",
|
||||
default=helpers.get_binary_path("tests/mgbench/client"),
|
||||
help="client binary used for benchmarking",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-workers-for-import",
|
||||
type=int,
|
||||
default=multiprocessing.cpu_count() // 2,
|
||||
help="number of workers used to import the dataset",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-workers-for-benchmark",
|
||||
type=int,
|
||||
default=1,
|
||||
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()
|
||||
|
||||
# Detect available datasets.
|
||||
@@ -124,8 +150,7 @@ for key in dir(datasets):
|
||||
if key.startswith("_"):
|
||||
continue
|
||||
dataset = getattr(datasets, key)
|
||||
if not inspect.isclass(dataset) or dataset == datasets.Dataset or \
|
||||
not issubclass(dataset, datasets.Dataset):
|
||||
if not inspect.isclass(dataset) or dataset == datasets.Dataset or not issubclass(dataset, datasets.Dataset):
|
||||
continue
|
||||
tests = collections.defaultdict(list)
|
||||
for funcname in dir(dataset):
|
||||
@@ -135,8 +160,9 @@ for key in dir(datasets):
|
||||
tests[group].append((test, funcname))
|
||||
generators[dataset.NAME] = (dataset, dict(tests))
|
||||
if dataset.PROPERTIES_ON_EDGES and args.no_properties_on_edges:
|
||||
raise Exception("The \"{}\" dataset requires properties on edges, "
|
||||
"but you have disabled them!".format(dataset.NAME))
|
||||
raise Exception(
|
||||
'The "{}" dataset requires properties on edges, ' "but you have disabled them!".format(dataset.NAME)
|
||||
)
|
||||
|
||||
# List datasets if there is no specified dataset.
|
||||
if len(args.benchmarks) == 0:
|
||||
@@ -144,8 +170,11 @@ if len(args.benchmarks) == 0:
|
||||
for name in sorted(generators.keys()):
|
||||
print("Dataset:", name)
|
||||
dataset, tests = generators[name]
|
||||
print(" Variants:", ", ".join(dataset.VARIANTS),
|
||||
"(default: " + dataset.DEFAULT_VARIANT + ")")
|
||||
print(
|
||||
" Variants:",
|
||||
", ".join(dataset.VARIANTS),
|
||||
"(default: " + dataset.DEFAULT_VARIANT + ")",
|
||||
)
|
||||
for group in sorted(tests.keys()):
|
||||
print(" Group:", group)
|
||||
for test_name, test_func in tests[group]:
|
||||
@@ -165,31 +194,38 @@ benchmarks = filter_benchmarks(generators, args.benchmarks)
|
||||
|
||||
# Run all specified benchmarks.
|
||||
for dataset, tests in benchmarks:
|
||||
log.init("Preparing", dataset.NAME + "/" + dataset.get_variant(),
|
||||
"dataset")
|
||||
dataset.prepare(cache.cache_directory("datasets", dataset.NAME,
|
||||
dataset.get_variant()))
|
||||
log.init("Preparing", dataset.NAME + "/" + dataset.get_variant(), "dataset")
|
||||
dataset.prepare(cache.cache_directory("datasets", dataset.NAME, dataset.get_variant()))
|
||||
|
||||
# Prepare runners and import the dataset.
|
||||
memgraph = runners.Memgraph(args.memgraph_binary, args.temporary_directory,
|
||||
not args.no_properties_on_edges)
|
||||
memgraph = runners.Memgraph(args.memgraph_binary, args.temporary_directory, not args.no_properties_on_edges)
|
||||
client = runners.Client(args.client_binary, args.temporary_directory)
|
||||
memgraph.start_preparation()
|
||||
ret = client.execute(file_path=dataset.get_file(),
|
||||
num_workers=args.num_workers_for_import)
|
||||
ret = client.execute(file_path=dataset.get_file(), num_workers=args.num_workers_for_import)
|
||||
usage = memgraph.stop()
|
||||
|
||||
# Display import statistics.
|
||||
print()
|
||||
for row in ret:
|
||||
print("Executed", row["count"], "queries in", row["duration"],
|
||||
"seconds using", row["num_workers"],
|
||||
"workers with a total throughput of", row["throughput"],
|
||||
"queries/second.")
|
||||
print(
|
||||
"Executed",
|
||||
row["count"],
|
||||
"queries in",
|
||||
row["duration"],
|
||||
"seconds using",
|
||||
row["num_workers"],
|
||||
"workers with a total throughput of",
|
||||
row["throughput"],
|
||||
"queries/second.",
|
||||
)
|
||||
print()
|
||||
print("The database used", usage["cpu"],
|
||||
"seconds of CPU time and peaked at",
|
||||
usage["memory"] / 1024 / 1024, "MiB of RAM.")
|
||||
print(
|
||||
"The database used",
|
||||
usage["cpu"],
|
||||
"seconds of CPU time and peaked at",
|
||||
usage["memory"] / 1024 / 1024,
|
||||
"MiB of RAM.",
|
||||
)
|
||||
|
||||
# Save import results.
|
||||
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]
|
||||
cached_count = config.get_value(*config_key)
|
||||
if cached_count is None:
|
||||
print("Determining the number of queries necessary for",
|
||||
args.single_threaded_runtime_sec,
|
||||
"seconds of single-threaded runtime...")
|
||||
print(
|
||||
"Determining the number of queries necessary for",
|
||||
args.single_threaded_runtime_sec,
|
||||
"seconds of single-threaded runtime...",
|
||||
)
|
||||
# First run to prime the query caches.
|
||||
memgraph.start_benchmark()
|
||||
client.execute(queries=get_queries(func, 1), num_workers=1)
|
||||
# Get a sense of the runtime.
|
||||
count = 1
|
||||
while True:
|
||||
ret = client.execute(queries=get_queries(func, count),
|
||||
num_workers=1)
|
||||
ret = client.execute(queries=get_queries(func, count), num_workers=1)
|
||||
duration = ret[0]["duration"]
|
||||
should_execute = int(args.single_threaded_runtime_sec /
|
||||
(duration / count))
|
||||
print("executed_queries={}, total_duration={}, "
|
||||
"query_duration={}, estimated_count={}".format(
|
||||
count, duration, duration / count,
|
||||
should_execute))
|
||||
should_execute = int(args.single_threaded_runtime_sec / (duration / count))
|
||||
print(
|
||||
"executed_queries={}, total_duration={}, "
|
||||
"query_duration={}, estimated_count={}".format(
|
||||
count, duration, duration / count, should_execute
|
||||
)
|
||||
)
|
||||
# We don't have to execute the next iteration when
|
||||
# `should_execute` becomes the same order of magnitude as
|
||||
# `count * 10`.
|
||||
@@ -235,45 +273,52 @@ for dataset, tests in benchmarks:
|
||||
else:
|
||||
count = count * 10
|
||||
memgraph.stop()
|
||||
config.set_value(*config_key, value={
|
||||
"count": count,
|
||||
"duration": args.single_threaded_runtime_sec})
|
||||
config.set_value(*config_key, value={"count": count, "duration": args.single_threaded_runtime_sec})
|
||||
else:
|
||||
print("Using cached query count of", cached_count["count"],
|
||||
"queries for", cached_count["duration"],
|
||||
"seconds of single-threaded runtime.")
|
||||
count = int(cached_count["count"] *
|
||||
args.single_threaded_runtime_sec /
|
||||
cached_count["duration"])
|
||||
print(
|
||||
"Using cached query count of",
|
||||
cached_count["count"],
|
||||
"queries for",
|
||||
cached_count["duration"],
|
||||
"seconds of single-threaded runtime.",
|
||||
)
|
||||
count = int(cached_count["count"] * args.single_threaded_runtime_sec / cached_count["duration"])
|
||||
|
||||
# Benchmark run.
|
||||
print("Sample query:", get_queries(func, 1)[0][0])
|
||||
print("Executing benchmark with", count, "queries that should "
|
||||
"yield a single-threaded runtime of",
|
||||
args.single_threaded_runtime_sec, "seconds.")
|
||||
print("Queries are executed using", args.num_workers_for_benchmark,
|
||||
"concurrent clients.")
|
||||
print(
|
||||
"Executing benchmark with",
|
||||
count,
|
||||
"queries that should " "yield a single-threaded runtime of",
|
||||
args.single_threaded_runtime_sec,
|
||||
"seconds.",
|
||||
)
|
||||
print(
|
||||
"Queries are executed using",
|
||||
args.num_workers_for_benchmark,
|
||||
"concurrent clients.",
|
||||
)
|
||||
memgraph.start_benchmark()
|
||||
ret = client.execute(queries=get_queries(func, count),
|
||||
num_workers=args.num_workers_for_benchmark)[0]
|
||||
ret = client.execute(
|
||||
queries=get_queries(func, count),
|
||||
num_workers=args.num_workers_for_benchmark,
|
||||
)[0]
|
||||
usage = memgraph.stop()
|
||||
ret["database"] = usage
|
||||
|
||||
# Output summary.
|
||||
print()
|
||||
print("Executed", ret["count"], "queries in",
|
||||
ret["duration"], "seconds.")
|
||||
print("Executed", ret["count"], "queries in", ret["duration"], "seconds.")
|
||||
print("Queries have been retried", ret["retries"], "times.")
|
||||
print("Database used {:.3f} seconds of CPU time.".format(
|
||||
usage["cpu"]))
|
||||
print("Database peaked at {:.3f} MiB of memory.".format(
|
||||
usage["memory"] / 1024.0 / 1024.0))
|
||||
print("{:<31} {:>20} {:>20} {:>20}".format("Metadata:", "min",
|
||||
"avg", "max"))
|
||||
print("Database used {:.3f} seconds of CPU time.".format(usage["cpu"]))
|
||||
print("Database peaked at {:.3f} MiB of memory.".format(usage["memory"] / 1024.0 / 1024.0))
|
||||
print("{:<31} {:>20} {:>20} {:>20}".format("Metadata:", "min", "avg", "max"))
|
||||
metadata = ret["metadata"]
|
||||
for key in sorted(metadata.keys()):
|
||||
print("{name:>30}: {minimum:>20.06f} {average:>20.06f} "
|
||||
"{maximum:>20.06f}".format(name=key, **metadata[key]))
|
||||
print(
|
||||
"{name:>30}: {minimum:>20.06f} {average:>20.06f} "
|
||||
"{maximum:>20.06f}".format(name=key, **metadata[key])
|
||||
)
|
||||
log.success("Throughput: {:02f} QPS".format(ret["throughput"]))
|
||||
|
||||
# Save results.
|
||||
|
||||
@@ -85,39 +85,41 @@ def compare_results(results_from, results_to, fields):
|
||||
if group == "__import__":
|
||||
continue
|
||||
for scenario, summary_to in scenarios.items():
|
||||
summary_from = recursive_get(
|
||||
results_from, dataset, variant, group, scenario,
|
||||
value={})
|
||||
if len(summary_from) > 0 and \
|
||||
summary_to["count"] != summary_from["count"] or \
|
||||
summary_to["num_workers"] != \
|
||||
summary_from["num_workers"]:
|
||||
summary_from = recursive_get(results_from, dataset, variant, group, scenario, value={})
|
||||
if (
|
||||
len(summary_from) > 0
|
||||
and summary_to["count"] != summary_from["count"]
|
||||
or summary_to["num_workers"] != summary_from["num_workers"]
|
||||
):
|
||||
raise Exception("Incompatible results!")
|
||||
testcode = "/".join([dataset, variant, group, scenario,
|
||||
"{:02d}".format(
|
||||
summary_to["num_workers"])])
|
||||
testcode = "/".join(
|
||||
[
|
||||
dataset,
|
||||
variant,
|
||||
group,
|
||||
scenario,
|
||||
"{:02d}".format(summary_to["num_workers"]),
|
||||
]
|
||||
)
|
||||
row = {}
|
||||
performance_changed = False
|
||||
for field in fields:
|
||||
key = field["name"]
|
||||
if key in summary_to:
|
||||
row[key] = compute_diff(
|
||||
summary_from.get(key, None),
|
||||
summary_to[key])
|
||||
row[key] = compute_diff(summary_from.get(key, None), summary_to[key])
|
||||
elif key in summary_to["database"]:
|
||||
row[key] = compute_diff(
|
||||
recursive_get(summary_from, "database", key,
|
||||
value=None),
|
||||
summary_to["database"][key])
|
||||
recursive_get(summary_from, "database", key, value=None),
|
||||
summary_to["database"][key],
|
||||
)
|
||||
else:
|
||||
row[key] = compute_diff(
|
||||
recursive_get(summary_from, "metadata", key,
|
||||
"average", value=None),
|
||||
summary_to["metadata"][key]["average"])
|
||||
if "diff" not in row[key] or \
|
||||
("diff_treshold" in field and
|
||||
abs(row[key]["diff"]) >=
|
||||
field["diff_treshold"]):
|
||||
recursive_get(summary_from, "metadata", key, "average", value=None),
|
||||
summary_to["metadata"][key]["average"],
|
||||
)
|
||||
if "diff" not in row[key] or (
|
||||
"diff_treshold" in field and abs(row[key]["diff"]) >= field["diff_treshold"]
|
||||
):
|
||||
performance_changed = True
|
||||
if performance_changed:
|
||||
ret[testcode] = row
|
||||
@@ -130,8 +132,15 @@ def generate_remarkup(fields, data):
|
||||
ret += "<table>\n"
|
||||
ret += " <tr>\n"
|
||||
ret += " <th>Testcode</th>\n"
|
||||
ret += "\n".join(map(lambda x: " <th>{}</th>".format(
|
||||
x["name"].replace("_", " ").capitalize()), fields)) + "\n"
|
||||
ret += (
|
||||
"\n".join(
|
||||
map(
|
||||
lambda x: " <th>{}</th>".format(x["name"].replace("_", " ").capitalize()),
|
||||
fields,
|
||||
)
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
ret += " </tr>\n"
|
||||
for testcode in sorted(data.keys()):
|
||||
ret += " <tr>\n"
|
||||
@@ -147,12 +156,9 @@ def generate_remarkup(fields, data):
|
||||
else:
|
||||
color = "red"
|
||||
sign = "{{icon {} color={}}}".format(arrow, color)
|
||||
ret += " <td>{:.3f}{} //({:+.2%})// {}</td>\n".format(
|
||||
value, field["unit"], diff, sign)
|
||||
ret += " <td>{:.3f}{} //({:+.2%})// {}</td>\n".format(value, field["unit"], diff, sign)
|
||||
else:
|
||||
ret += " <td>{:.3f}{} //(new)// " \
|
||||
"{{icon plus color=blue}}</td>\n".format(
|
||||
value, field["unit"])
|
||||
ret += " <td>{:.3f}{} //(new)// " "{{icon plus color=blue}}</td>\n".format(value, field["unit"])
|
||||
ret += " </tr>\n"
|
||||
ret += "</table>\n"
|
||||
else:
|
||||
@@ -161,11 +167,14 @@ def generate_remarkup(fields, data):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Compare results of multiple benchmark runs.")
|
||||
parser.add_argument("--compare", action="append", nargs=2,
|
||||
metavar=("from", "to"),
|
||||
help="compare results between `from` and `to` files")
|
||||
parser = argparse.ArgumentParser(description="Compare results of multiple benchmark runs.")
|
||||
parser.add_argument(
|
||||
"--compare",
|
||||
action="append",
|
||||
nargs=2,
|
||||
metavar=("from", "to"),
|
||||
help="compare results between `from` and `to` files",
|
||||
)
|
||||
parser.add_argument("--output", default="", help="output file name")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -45,13 +45,10 @@ class Dataset:
|
||||
variant = self.DEFAULT_VARIANT
|
||||
if variant not in self.VARIANTS:
|
||||
raise ValueError("Invalid test variant!")
|
||||
if (self.FILES and variant not in self.FILES) and \
|
||||
(self.URLS and variant not in self.URLS):
|
||||
raise ValueError("The variant doesn't have a defined URL or "
|
||||
"file path!")
|
||||
if (self.FILES and variant not in self.FILES) and (self.URLS and variant not in self.URLS):
|
||||
raise ValueError("The variant doesn't have a defined URL or " "file path!")
|
||||
if variant not in self.SIZES:
|
||||
raise ValueError("The variant doesn't have a defined dataset "
|
||||
"size!")
|
||||
raise ValueError("The variant doesn't have a defined dataset " "size!")
|
||||
self._variant = variant
|
||||
if self.FILES is not None:
|
||||
self._file = self.FILES.get(variant, None)
|
||||
@@ -63,8 +60,7 @@ class Dataset:
|
||||
self._url = None
|
||||
self._size = self.SIZES[variant]
|
||||
if "vertices" not in self._size or "edges" not in self._size:
|
||||
raise ValueError("The size defined for this variant doesn't "
|
||||
"have the number of vertices and/or edges!")
|
||||
raise ValueError("The size defined for this variant doesn't " "have the number of vertices and/or edges!")
|
||||
self._num_vertices = self._size["vertices"]
|
||||
self._num_edges = self._size["edges"]
|
||||
|
||||
@@ -76,8 +72,7 @@ class Dataset:
|
||||
cached_input, exists = directory.get_file("dataset.cypher")
|
||||
if not exists:
|
||||
print("Downloading dataset file:", self._url)
|
||||
downloaded_file = helpers.download_file(
|
||||
self._url, directory.get_path())
|
||||
downloaded_file = helpers.download_file(self._url, directory.get_path())
|
||||
print("Unpacking and caching file:", downloaded_file)
|
||||
helpers.unpack_and_move_file(downloaded_file, cached_input)
|
||||
print("Using cached dataset file:", cached_input)
|
||||
@@ -137,18 +132,20 @@ class Pokec(Dataset):
|
||||
# Arango benchmarks
|
||||
|
||||
def benchmark__arango__single_vertex_read(self):
|
||||
return ("MATCH (n:User {id : $id}) RETURN n",
|
||||
{"id": self._get_random_vertex()})
|
||||
return ("MATCH (n:User {id : $id}) RETURN n", {"id": self._get_random_vertex()})
|
||||
|
||||
def benchmark__arango__single_vertex_write(self):
|
||||
return ("CREATE (n:UserTemp {id : $id}) RETURN n",
|
||||
{"id": random.randint(1, self._num_vertices * 10)})
|
||||
return (
|
||||
"CREATE (n:UserTemp {id : $id}) RETURN n",
|
||||
{"id": random.randint(1, self._num_vertices * 10)},
|
||||
)
|
||||
|
||||
def benchmark__arango__single_edge_write(self):
|
||||
vertex_from, vertex_to = self._get_random_from_to()
|
||||
return ("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})
|
||||
return (
|
||||
"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},
|
||||
)
|
||||
|
||||
def benchmark__arango__aggregate(self):
|
||||
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(*)", {})
|
||||
|
||||
def benchmark__arango__expansion_1(self):
|
||||
return ("MATCH (s:User {id: $id})-->(n:User) "
|
||||
"RETURN n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-->(n:User) " "RETURN n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__expansion_1_with_filter(self):
|
||||
return ("MATCH (s:User {id: $id})-->(n:User) "
|
||||
"WHERE n.age >= 18 "
|
||||
"RETURN n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-->(n:User) " "WHERE n.age >= 18 " "RETURN n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__expansion_2(self):
|
||||
return ("MATCH (s:User {id: $id})-->()-->(n:User) "
|
||||
"RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-->()-->(n:User) " "RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__expansion_2_with_filter(self):
|
||||
return ("MATCH (s:User {id: $id})-->()-->(n:User) "
|
||||
"WHERE n.age >= 18 "
|
||||
"RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__expansion_3(self):
|
||||
return ("MATCH (s:User {id: $id})-->()-->()-->(n:User) "
|
||||
"RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-->()-->()-->(n:User) " "RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__expansion_3_with_filter(self):
|
||||
return ("MATCH (s:User {id: $id})-->()-->()-->(n:User) "
|
||||
"WHERE n.age >= 18 "
|
||||
"RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-->()-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__expansion_4(self):
|
||||
return ("MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) "
|
||||
"RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " "RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__expansion_4_with_filter(self):
|
||||
return ("MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) "
|
||||
"WHERE n.age >= 18 "
|
||||
"RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__neighbours_2(self):
|
||||
return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) "
|
||||
"RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-[*1..2]->(n:User) " "RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__neighbours_2_with_filter(self):
|
||||
return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) "
|
||||
"WHERE n.age >= 18 "
|
||||
"RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-[*1..2]->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__neighbours_2_with_data(self):
|
||||
return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) "
|
||||
"RETURN DISTINCT n.id, n",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-[*1..2]->(n:User) " "RETURN DISTINCT n.id, n",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__neighbours_2_with_data_and_filter(self):
|
||||
return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) "
|
||||
"WHERE n.age >= 18 "
|
||||
"RETURN DISTINCT n.id, n",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (s:User {id: $id})-[*1..2]->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id, n",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__arango__shortest_path(self):
|
||||
vertex_from, vertex_to = self._get_random_from_to()
|
||||
return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m "
|
||||
"MATCH p=(n)-[*bfs..15]->(m) "
|
||||
"RETURN extract(n in nodes(p) | n.id) AS path",
|
||||
{"from": vertex_from, "to": vertex_to})
|
||||
return (
|
||||
"MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m "
|
||||
"MATCH p=(n)-[*bfs..15]->(m) "
|
||||
"RETURN extract(n in nodes(p) | n.id) AS path",
|
||||
{"from": vertex_from, "to": vertex_to},
|
||||
)
|
||||
|
||||
def benchmark__arango__shortest_path_with_filter(self):
|
||||
vertex_from, vertex_to = self._get_random_from_to()
|
||||
return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m "
|
||||
"MATCH p=(n)-[*bfs..15 (e, n | n.age >= 18)]->(m) "
|
||||
"RETURN extract(n in nodes(p) | n.id) AS path",
|
||||
{"from": vertex_from, "to": vertex_to})
|
||||
return (
|
||||
"MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m "
|
||||
"MATCH p=(n)-[*bfs..15 (e, n | n.age >= 18)]->(m) "
|
||||
"RETURN extract(n in nodes(p) | n.id) AS path",
|
||||
{"from": vertex_from, "to": vertex_to},
|
||||
)
|
||||
|
||||
# Our benchmark queries
|
||||
|
||||
def benchmark__create__edge(self):
|
||||
vertex_from, vertex_to = self._get_random_from_to()
|
||||
return ("MATCH (a:User {id: $from}), (b:User {id: $to}) "
|
||||
"CREATE (a)-[:TempEdge]->(b)",
|
||||
{"from": vertex_from, "to": vertex_to})
|
||||
return (
|
||||
"MATCH (a:User {id: $from}), (b:User {id: $to}) " "CREATE (a)-[:TempEdge]->(b)",
|
||||
{"from": vertex_from, "to": vertex_to},
|
||||
)
|
||||
|
||||
def benchmark__create__pattern(self):
|
||||
return ("CREATE ()-[:TempEdge]->()", {})
|
||||
@@ -251,9 +259,12 @@ class Pokec(Dataset):
|
||||
return ("CREATE ()", {})
|
||||
|
||||
def benchmark__create__vertex_big(self):
|
||||
return ("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})", {})
|
||||
return (
|
||||
"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})',
|
||||
{},
|
||||
)
|
||||
|
||||
def benchmark__aggregation__count(self):
|
||||
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)", {})
|
||||
|
||||
def benchmark__match__pattern_cycle(self):
|
||||
return ("MATCH (n:User {id: $id})-[e1]->(m)-[e2]->(n) "
|
||||
"RETURN e1, m, e2",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (n:User {id: $id})-[e1]->(m)-[e2]->(n) " "RETURN e1, m, e2",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__match__pattern_long(self):
|
||||
return ("MATCH (n1:User {id: $id})-[e1]->(n2)-[e2]->"
|
||||
"(n3)-[e3]->(n4)<-[e4]-(n5) "
|
||||
"RETURN n5 LIMIT 1",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (n1:User {id: $id})-[e1]->(n2)-[e2]->" "(n3)-[e3]->(n4)<-[e4]-(n5) " "RETURN n5 LIMIT 1",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__match__pattern_short(self):
|
||||
return ("MATCH (n:User {id: $id})-[e]->(m) "
|
||||
"RETURN m LIMIT 1",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"MATCH (n:User {id: $id})-[e]->(m) " "RETURN m LIMIT 1",
|
||||
{"id": self._get_random_vertex()},
|
||||
)
|
||||
|
||||
def benchmark__match__vertex_on_label_property(self):
|
||||
return ("MATCH (n:User) WITH n WHERE n.id = $id RETURN n",
|
||||
{"id": self._get_random_vertex()})
|
||||
return (
|
||||
"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):
|
||||
return ("MATCH (n:User {id: $id}) RETURN n",
|
||||
{"id": self._get_random_vertex()})
|
||||
return ("MATCH (n:User {id: $id}) RETURN n", {"id": self._get_random_vertex()})
|
||||
|
||||
def benchmark__match__vertex_on_property(self):
|
||||
return ("MATCH (n {id: $id}) RETURN n",
|
||||
{"id": self._get_random_vertex()})
|
||||
return ("MATCH (n {id: $id}) RETURN n", {"id": self._get_random_vertex()})
|
||||
|
||||
@@ -28,18 +28,21 @@ def get_binary_path(path, base=""):
|
||||
|
||||
|
||||
def download_file(url, path):
|
||||
ret = subprocess.run(["wget", "-nv", "--content-disposition", url],
|
||||
stderr=subprocess.PIPE, cwd=path, check=True)
|
||||
ret = subprocess.run(
|
||||
["wget", "-nv", "--content-disposition", url],
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=path,
|
||||
check=True,
|
||||
)
|
||||
data = ret.stderr.decode("utf-8")
|
||||
tmp = data.split("->")[1]
|
||||
name = tmp[tmp.index('"') + 1:tmp.rindex('"')]
|
||||
name = tmp[tmp.index('"') + 1 : tmp.rindex('"')]
|
||||
return os.path.join(path, name)
|
||||
|
||||
|
||||
def unpack_and_move_file(input_path, output_path):
|
||||
if input_path.endswith(".gz"):
|
||||
subprocess.run(["gunzip", input_path],
|
||||
stdout=subprocess.DEVNULL, check=True)
|
||||
subprocess.run(["gunzip", input_path], stdout=subprocess.DEVNULL, check=True)
|
||||
input_path = input_path[:-3]
|
||||
os.rename(input_path, output_path)
|
||||
|
||||
|
||||
@@ -40,8 +40,7 @@ def _convert_args_to_flags(*args, **kwargs):
|
||||
def _get_usage(pid):
|
||||
total_cpu = 0
|
||||
with open("/proc/{}/stat".format(pid)) as f:
|
||||
total_cpu = (sum(map(int, f.read().split(")")[1].split()[11:15])) /
|
||||
os.sysconf(os.sysconf_names["SC_CLK_TCK"]))
|
||||
total_cpu = sum(map(int, f.read().split(")")[1].split()[11:15])) / os.sysconf(os.sysconf_names["SC_CLK_TCK"])
|
||||
peak_rss = 0
|
||||
with open("/proc/{}/status".format(pid)) as f:
|
||||
for row in f:
|
||||
@@ -60,10 +59,8 @@ class Memgraph:
|
||||
atexit.register(self._cleanup)
|
||||
|
||||
# Determine Memgraph version
|
||||
ret = subprocess.run([memgraph_binary, "--version"],
|
||||
stdout=subprocess.PIPE, check=True)
|
||||
version = re.search(r"[0-9]+\.[0-9]+\.[0-9]+",
|
||||
ret.stdout.decode("utf-8")).group(0)
|
||||
ret = subprocess.run([memgraph_binary, "--version"], stdout=subprocess.PIPE, check=True)
|
||||
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(".")))
|
||||
|
||||
def __del__(self):
|
||||
@@ -79,8 +76,7 @@ class Memgraph:
|
||||
if self._memgraph_version >= (0, 50, 0):
|
||||
kwargs["storage_properties_on_edges"] = self._properties_on_edges
|
||||
else:
|
||||
assert self._properties_on_edges, \
|
||||
"Older versions of Memgraph can't disable properties on edges!"
|
||||
assert self._properties_on_edges, "Older versions of Memgraph can't disable properties on edges!"
|
||||
return _convert_args_to_flags(self._memgraph_binary, **kwargs)
|
||||
|
||||
def _start(self, **kwargs):
|
||||
@@ -94,8 +90,7 @@ class Memgraph:
|
||||
raise Exception("The database process died prematurely!")
|
||||
wait_for_server(7687)
|
||||
ret = self._proc_mg.poll()
|
||||
assert ret is None, "The database process died prematurely " \
|
||||
"({})!".format(ret)
|
||||
assert ret is None, "The database process died prematurely " "({})!".format(ret)
|
||||
|
||||
def _cleanup(self):
|
||||
if self._proc_mg is None:
|
||||
@@ -121,8 +116,7 @@ class Memgraph:
|
||||
|
||||
def stop(self):
|
||||
ret, usage = self._cleanup()
|
||||
assert ret == 0, "The database process exited with a non-zero " \
|
||||
"status ({})!".format(ret)
|
||||
assert ret == 0, "The database process exited with a non-zero " "status ({})!".format(ret)
|
||||
return usage
|
||||
|
||||
|
||||
@@ -135,8 +129,7 @@ class Client:
|
||||
return _convert_args_to_flags(self._client_binary, **kwargs)
|
||||
|
||||
def execute(self, queries=None, file_path=None, num_workers=1):
|
||||
if (queries is None and file_path is None) or \
|
||||
(queries is not None and file_path is not None):
|
||||
if (queries is None and file_path is None) or (queries is not None and file_path is not None):
|
||||
raise ValueError("Either queries or input_path must be specified!")
|
||||
|
||||
# TODO: check `file_path.endswith(".json")` to support advanced
|
||||
@@ -151,8 +144,7 @@ class Client:
|
||||
json.dump(query, f)
|
||||
f.write("\n")
|
||||
|
||||
args = self._get_args(input=file_path, num_workers=num_workers,
|
||||
queries_json=queries_json)
|
||||
args = self._get_args(input=file_path, num_workers=num_workers, queries_json=queries_json)
|
||||
ret = subprocess.run(args, stdout=subprocess.PIPE, check=True)
|
||||
data = ret.stdout.decode("utf-8").strip().split("\n")
|
||||
return list(map(json.loads, data))
|
||||
|
||||
@@ -12,44 +12,60 @@
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# licenses/APL.txt.
|
||||
|
||||
'''
|
||||
"""
|
||||
Large bipartite graph stress test.
|
||||
'''
|
||||
"""
|
||||
|
||||
import logging
|
||||
import multiprocessing
|
||||
import time
|
||||
import atexit
|
||||
|
||||
from common import connection_argument_parser, assert_equal, \
|
||||
OutputData, execute_till_success, \
|
||||
batch, render, SessionCache
|
||||
from common import (
|
||||
connection_argument_parser,
|
||||
assert_equal,
|
||||
OutputData,
|
||||
execute_till_success,
|
||||
batch,
|
||||
render,
|
||||
SessionCache,
|
||||
)
|
||||
|
||||
|
||||
def parse_args():
|
||||
'''
|
||||
"""
|
||||
Parses user arguments
|
||||
|
||||
:return: parsed arguments
|
||||
'''
|
||||
"""
|
||||
parser = connection_argument_parser()
|
||||
parser.add_argument('--worker-count', type=int,
|
||||
default=multiprocessing.cpu_count(),
|
||||
help='Number of concurrent workers.')
|
||||
parser.add_argument("--logging", default="INFO",
|
||||
choices=["INFO", "DEBUG", "WARNING", "ERROR"],
|
||||
help="Logging level")
|
||||
parser.add_argument('--u-count', type=int, default=100,
|
||||
help='Size of U set in the bipartite graph.')
|
||||
parser.add_argument('--v-count', type=int, default=100,
|
||||
help='Size of V set in the bipartite graph.')
|
||||
parser.add_argument('--vertex-batch-size', 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.')
|
||||
parser.add_argument(
|
||||
"--worker-count",
|
||||
type=int,
|
||||
default=multiprocessing.cpu_count(),
|
||||
help="Number of concurrent workers.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--logging",
|
||||
default="INFO",
|
||||
choices=["INFO", "DEBUG", "WARNING", "ERROR"],
|
||||
help="Logging level",
|
||||
)
|
||||
parser.add_argument("--u-count", type=int, default=100, help="Size of U set in the bipartite graph.")
|
||||
parser.add_argument("--v-count", type=int, default=100, help="Size of V set in the bipartite graph.")
|
||||
parser.add_argument(
|
||||
"--vertex-batch-size",
|
||||
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()
|
||||
|
||||
|
||||
@@ -62,18 +78,18 @@ atexit.register(SessionCache.cleanup)
|
||||
|
||||
|
||||
def create_u_v_edges(u):
|
||||
'''
|
||||
"""
|
||||
Creates nodes and checks that all nodes were created.
|
||||
create edges from one vertex in U set to all vertex of V set
|
||||
|
||||
:param worker_id: worker id
|
||||
|
||||
:return: tuple (worker_id, create execution time, time unit)
|
||||
'''
|
||||
"""
|
||||
start_time = time.time()
|
||||
session = SessionCache.argument_session(args)
|
||||
no_failures = 0
|
||||
match_u = 'MATCH (u:U {id: %d})' % u
|
||||
match_u = "MATCH (u:U {id: %d})" % u
|
||||
if args.edge_batching:
|
||||
# TODO: try to randomize execution, the execution time should
|
||||
# be smaller, add randomize flag
|
||||
@@ -83,143 +99,126 @@ def create_u_v_edges(u):
|
||||
query = match_u + "".join(match_v) + "".join(create_u)
|
||||
no_failures += execute_till_success(session, query)[1]
|
||||
else:
|
||||
no_failures += execute_till_success(
|
||||
session, match_u + ' MATCH (v:V) CREATE (u)-[:R]->(v)')[1]
|
||||
no_failures += execute_till_success(session, match_u + " MATCH (v:V) CREATE (u)-[:R]->(v)")[1]
|
||||
|
||||
end_time = time.time()
|
||||
return u, end_time - start_time, "s", no_failures
|
||||
|
||||
|
||||
def traverse_from_u_worker(u):
|
||||
'''
|
||||
"""
|
||||
Traverses edges starting from an element of U set.
|
||||
Traversed labels are: :U -> :V -> :U.
|
||||
'''
|
||||
"""
|
||||
session = SessionCache.argument_session(args)
|
||||
start_time = time.time()
|
||||
assert_equal(
|
||||
args.u_count * args.v_count - args.v_count, # cypher morphism
|
||||
session.run("MATCH (u1:U {id: %s})-[e1]->(v:V)<-[e2]-(u2:U) "
|
||||
"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")
|
||||
session.run("MATCH (u1:U {id: %s})-[e1]->(v:V)<-[e2]-(u2:U) " "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",
|
||||
)
|
||||
end_time = time.time()
|
||||
return u, end_time - start_time, 's'
|
||||
return u, end_time - start_time, "s"
|
||||
|
||||
|
||||
def traverse_from_v_worker(v):
|
||||
'''
|
||||
"""
|
||||
Traverses edges starting from an element of V set.
|
||||
Traversed labels are: :V -> :U -> :V.
|
||||
'''
|
||||
"""
|
||||
session = SessionCache.argument_session(args)
|
||||
start_time = time.time()
|
||||
assert_equal(
|
||||
args.u_count * args.v_count - args.u_count, # cypher morphism
|
||||
session.run("MATCH (v1:V {id: %s})<-[e1]-(u:U)-[e2]->(v2:V) "
|
||||
"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")
|
||||
args.u_count * args.v_count - args.u_count, # cypher morphism
|
||||
session.run("MATCH (v1:V {id: %s})<-[e1]-(u:U)-[e2]->(v2:V) " "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",
|
||||
)
|
||||
end_time = time.time()
|
||||
return v, end_time - start_time, 's'
|
||||
return v, end_time - start_time, "s"
|
||||
|
||||
|
||||
def execution_handler():
|
||||
'''
|
||||
"""
|
||||
Initializes client processes, database and starts the execution.
|
||||
'''
|
||||
"""
|
||||
# instance cleanup
|
||||
session = SessionCache.argument_session(args)
|
||||
start_time = time.time()
|
||||
|
||||
# clean existing database
|
||||
session.run('MATCH (n) DETACH DELETE n').consume()
|
||||
session.run("MATCH (n) DETACH DELETE n").consume()
|
||||
cleanup_end_time = time.time()
|
||||
output_data.add_measurement("cleanup_time",
|
||||
cleanup_end_time - start_time)
|
||||
output_data.add_measurement("cleanup_time", cleanup_end_time - start_time)
|
||||
log.info("Database is clean.")
|
||||
|
||||
# create indices
|
||||
session.run('CREATE INDEX ON :U').consume()
|
||||
session.run('CREATE INDEX ON :V').consume()
|
||||
session.run("CREATE INDEX ON :U").consume()
|
||||
session.run("CREATE INDEX ON :V").consume()
|
||||
|
||||
# create U vertices
|
||||
for b in batch(render('CREATE (:U {{id: {}}})', range(args.u_count)),
|
||||
args.vertex_batch_size):
|
||||
for b in batch(render("CREATE (:U {{id: {}}})", range(args.u_count)), args.vertex_batch_size):
|
||||
session.run(" ".join(b)).consume()
|
||||
# create V vertices
|
||||
for b in batch(render('CREATE (:V {{id: {}}})', range(args.v_count)),
|
||||
args.vertex_batch_size):
|
||||
for b in batch(render("CREATE (:V {{id: {}}})", range(args.v_count)), args.vertex_batch_size):
|
||||
session.run(" ".join(b)).consume()
|
||||
vertices_create_end_time = time.time()
|
||||
output_data.add_measurement(
|
||||
'vertices_create_time',
|
||||
vertices_create_end_time - cleanup_end_time)
|
||||
output_data.add_measurement("vertices_create_time", vertices_create_end_time - cleanup_end_time)
|
||||
log.info("All nodes created.")
|
||||
|
||||
# concurrent create execution & tests
|
||||
with multiprocessing.Pool(args.worker_count) as p:
|
||||
create_edges_start_time = time.time()
|
||||
for worker_id, create_time, time_unit, no_failures in \
|
||||
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))
|
||||
for worker_id, create_time, time_unit, no_failures in 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))
|
||||
create_edges_end_time = time.time()
|
||||
output_data.add_measurement(
|
||||
'edges_create_time',
|
||||
create_edges_end_time - create_edges_start_time)
|
||||
output_data.add_measurement("edges_create_time", create_edges_end_time - create_edges_start_time)
|
||||
|
||||
# check total number of edges
|
||||
assert_equal(
|
||||
args.v_count * args.u_count,
|
||||
session.run(
|
||||
'MATCH ()-[r]->() '
|
||||
'RETURN count(r) AS cnt').data()[0]['cnt'],
|
||||
"Total number of edges isn't correct! Expected: %s Actual: %s")
|
||||
session.run("MATCH ()-[r]->() " "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
|
||||
traverse_from_u_start_time = time.time()
|
||||
for u, traverse_u_time, time_unit in \
|
||||
p.map(traverse_from_u_worker,
|
||||
[i for i in range(args.u_count)]):
|
||||
for u, traverse_u_time, time_unit in 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))
|
||||
traverse_from_u_end_time = time.time()
|
||||
output_data.add_measurement(
|
||||
'traverse_from_u_time',
|
||||
traverse_from_u_end_time - traverse_from_u_start_time)
|
||||
"traverse_from_u_time",
|
||||
traverse_from_u_end_time - traverse_from_u_start_time,
|
||||
)
|
||||
|
||||
# check traversals starting from all elements of V
|
||||
traverse_from_v_start_time = time.time()
|
||||
for v, traverse_v_time, time_unit in \
|
||||
p.map(traverse_from_v_worker,
|
||||
[i for i in range(args.v_count)]):
|
||||
for v, traverse_v_time, time_unit in 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))
|
||||
traverse_from_v_end_time = time.time()
|
||||
output_data.add_measurement(
|
||||
'traverse_from_v_time',
|
||||
traverse_from_v_end_time - traverse_from_v_start_time)
|
||||
"traverse_from_v_time",
|
||||
traverse_from_v_end_time - traverse_from_v_start_time,
|
||||
)
|
||||
|
||||
# check total number of vertices
|
||||
assert_equal(
|
||||
args.v_count + args.u_count,
|
||||
session.run('MATCH (n) RETURN count(n) AS cnt').data()[0]['cnt'],
|
||||
"Total number of vertices isn't correct! Expected: %s Actual: %s")
|
||||
session.run("MATCH (n) RETURN count(n) AS cnt").data()[0]["cnt"],
|
||||
"Total number of vertices isn't correct! Expected: %s Actual: %s",
|
||||
)
|
||||
|
||||
# check total number of edges
|
||||
assert_equal(
|
||||
args.v_count * args.u_count,
|
||||
session.run(
|
||||
'MATCH ()-[r]->() RETURN count(r) AS cnt').data()[0]['cnt'],
|
||||
"Total number of edges isn't correct! Expected: %s Actual: %s")
|
||||
session.run("MATCH ()-[r]->() RETURN count(r) AS cnt").data()[0]["cnt"],
|
||||
"Total number of edges isn't correct! Expected: %s Actual: %s",
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
output_data.add_measurement("total_execution_time",
|
||||
end_time - start_time)
|
||||
output_data.add_measurement("total_execution_time", end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=args.logging)
|
||||
if args.logging != "DEBUG":
|
||||
logging.getLogger("neo4j").setLevel(logging.WARNING)
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
"""
|
||||
Common methods for writing graph database
|
||||
integration tests in python.
|
||||
|
||||
Only Bolt communication protocol is supported.
|
||||
'''
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
@@ -28,9 +28,9 @@ from neo4j import GraphDatabase, TRUST_ALL_CERTIFICATES
|
||||
|
||||
|
||||
class OutputData:
|
||||
'''
|
||||
"""
|
||||
Encapsulates results and info about the tests.
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# data in time format (name, time, unit)
|
||||
@@ -39,32 +39,32 @@ class OutputData:
|
||||
self._statuses = []
|
||||
|
||||
def add_measurement(self, name, time, unit="s"):
|
||||
'''
|
||||
"""
|
||||
Stores measurement.
|
||||
|
||||
:param name: str, name of measurement
|
||||
:param time: float, time value
|
||||
:param unit: str, time unit
|
||||
'''
|
||||
"""
|
||||
self._measurements.append((name, time, unit))
|
||||
|
||||
def add_status(self, name, status):
|
||||
'''
|
||||
"""
|
||||
Stores status data point.
|
||||
|
||||
:param name: str, name of data point
|
||||
:param status: printable value
|
||||
'''
|
||||
"""
|
||||
self._statuses.append((name, status))
|
||||
|
||||
def dump(self, print_f=print):
|
||||
'''
|
||||
"""
|
||||
Dumps output using the given ouput function.
|
||||
|
||||
Args:
|
||||
print_f - the function that consumes ouptput. Defaults to
|
||||
the 'print' function.
|
||||
'''
|
||||
"""
|
||||
print_f("Output data:")
|
||||
for name, status in self._statuses:
|
||||
print_f(" %s: %s" % (name, status))
|
||||
@@ -73,7 +73,7 @@ class OutputData:
|
||||
|
||||
|
||||
def execute_till_success(session, query, max_retries=1000):
|
||||
'''
|
||||
"""
|
||||
Executes a query within Bolt session until the query is
|
||||
successfully executed against the database.
|
||||
|
||||
@@ -86,7 +86,7 @@ def execute_till_success(session, query, max_retries=1000):
|
||||
:param query: query to execute
|
||||
|
||||
:return: tuple (results_data_list, number_of_failures, result_summary)
|
||||
'''
|
||||
"""
|
||||
no_failures = 0
|
||||
while True:
|
||||
try:
|
||||
@@ -97,12 +97,11 @@ def execute_till_success(session, query, max_retries=1000):
|
||||
except Exception:
|
||||
no_failures += 1
|
||||
if no_failures >= max_retries:
|
||||
raise Exception("Query '%s' failed %d times, aborting" %
|
||||
(query, max_retries))
|
||||
raise Exception("Query '%s' failed %d times, aborting" % (query, max_retries))
|
||||
|
||||
|
||||
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.
|
||||
All is lazy. The last batch can contain less elements
|
||||
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):
|
||||
'''
|
||||
"""
|
||||
Compares expected and actual values. If values are not the same terminate
|
||||
the execution.
|
||||
|
||||
@@ -142,45 +141,41 @@ def assert_equal(expected, actual, message):
|
||||
:param actual: actual value
|
||||
:param message: str, message in case that the values are not equal, must
|
||||
contain two placeholders (%s) to print the values.
|
||||
'''
|
||||
"""
|
||||
assert expected == actual, message % (expected, actual)
|
||||
|
||||
|
||||
def connection_argument_parser():
|
||||
'''
|
||||
"""
|
||||
Parses arguments related to establishing database connection like
|
||||
host, port, username, etc.
|
||||
|
||||
:return: An instance of ArgumentParser
|
||||
'''
|
||||
"""
|
||||
parser = ArgumentParser(description=__doc__)
|
||||
|
||||
parser.add_argument('--endpoint', type=str, default='127.0.0.1:7687',
|
||||
help='DBMS instance endpoint. '
|
||||
'Bolt protocol is the only option.')
|
||||
parser.add_argument('--username', type=str, default='neo4j',
|
||||
help='DBMS instance username.')
|
||||
parser.add_argument('--password', type=int, default='1234',
|
||||
help='DBMS instance password.')
|
||||
parser.add_argument('--use-ssl', action='store_true',
|
||||
help="Is SSL enabled?")
|
||||
parser.add_argument(
|
||||
"--endpoint",
|
||||
type=str,
|
||||
default="127.0.0.1:7687",
|
||||
help="DBMS instance endpoint. " "Bolt protocol is the only option.",
|
||||
)
|
||||
parser.add_argument("--username", type=str, default="neo4j", help="DBMS instance username.")
|
||||
parser.add_argument("--password", type=int, default="1234", help="DBMS instance password.")
|
||||
parser.add_argument("--use-ssl", action="store_true", help="Is SSL enabled?")
|
||||
return parser
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def bolt_session(url, auth, ssl=False):
|
||||
'''
|
||||
"""
|
||||
with wrapper around Bolt session.
|
||||
|
||||
:param url: str, e.g. "bolt://127.0.0.1:7687"
|
||||
:param auth: auth method, goes directly to the Bolt driver constructor
|
||||
:param ssl: bool, is ssl enabled
|
||||
'''
|
||||
driver = GraphDatabase.driver(
|
||||
url,
|
||||
auth=auth,
|
||||
encrypted=ssl,
|
||||
trust=TRUST_ALL_CERTIFICATES)
|
||||
"""
|
||||
driver = GraphDatabase.driver(url, auth=auth, encrypted=ssl, trust=TRUST_ALL_CERTIFICATES)
|
||||
session = driver.session()
|
||||
try:
|
||||
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
|
||||
# in bipartite for an idea how to reuse sessions.
|
||||
def argument_session(args):
|
||||
'''
|
||||
"""
|
||||
:return: Bolt session context manager based on program arguments
|
||||
'''
|
||||
return bolt_session('bolt://' + args.endpoint,
|
||||
(args.username, str(args.password)),
|
||||
args.use_ssl)
|
||||
"""
|
||||
return bolt_session("bolt://" + args.endpoint, (args.username, str(args.password)), args.use_ssl)
|
||||
|
||||
|
||||
def argument_driver(args):
|
||||
return GraphDatabase.driver(
|
||||
'bolt://' + args.endpoint,
|
||||
"bolt://" + args.endpoint,
|
||||
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
|
||||
# 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(),)
|
||||
if key in SessionCache.cache:
|
||||
return SessionCache.cache[key][1]
|
||||
driver = argument_driver(args) # |
|
||||
session = driver.session() # V
|
||||
driver = argument_driver(args) # |
|
||||
session = driver.session() # V
|
||||
SessionCache.cache[key] = (driver, session)
|
||||
return session
|
||||
|
||||
@@ -241,6 +237,7 @@ def periodically_execute(callable, args, interval, daemon=True):
|
||||
interval - time (in seconds) between two calls
|
||||
deamon - if the execution thread should be a daemon
|
||||
"""
|
||||
|
||||
def periodic_call():
|
||||
while True:
|
||||
sleep(interval)
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# licenses/APL.txt.
|
||||
|
||||
'''
|
||||
"""
|
||||
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.
|
||||
'''
|
||||
"""
|
||||
|
||||
import logging
|
||||
import multiprocessing
|
||||
@@ -28,28 +28,41 @@ from common import connection_argument_parser, argument_session
|
||||
|
||||
|
||||
def parse_args():
|
||||
'''
|
||||
"""
|
||||
Parses user arguments
|
||||
|
||||
:return: parsed arguments
|
||||
'''
|
||||
"""
|
||||
parser = connection_argument_parser()
|
||||
|
||||
# specific
|
||||
parser.add_argument('--worker-count', type=int,
|
||||
default=multiprocessing.cpu_count(),
|
||||
help='Number of concurrent workers.')
|
||||
parser.add_argument("--logging", default="INFO",
|
||||
choices=["INFO", "DEBUG", "WARNING", "ERROR"],
|
||||
help="Logging level")
|
||||
parser.add_argument('--vertex-count', type=int, default=100,
|
||||
help='Number of created vertices.')
|
||||
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')
|
||||
parser.add_argument(
|
||||
"--worker-count",
|
||||
type=int,
|
||||
default=multiprocessing.cpu_count(),
|
||||
help="Number of concurrent workers.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--logging",
|
||||
default="INFO",
|
||||
choices=["INFO", "DEBUG", "WARNING", "ERROR"],
|
||||
help="Logging level",
|
||||
)
|
||||
parser.add_argument("--vertex-count", type=int, default=100, help="Number of created vertices.")
|
||||
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()
|
||||
|
||||
|
||||
@@ -58,51 +71,49 @@ args = parse_args()
|
||||
|
||||
|
||||
def create_worker(worker_id):
|
||||
'''
|
||||
"""
|
||||
Creates nodes and checks that all nodes were created.
|
||||
|
||||
:param worker_id: worker id
|
||||
|
||||
: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)
|
||||
create_query = ''
|
||||
create_query = ""
|
||||
with argument_session(args) as session:
|
||||
# create vertices
|
||||
start_time = time.time()
|
||||
for i in range(0, args.vertex_count):
|
||||
random_number = random.randint(0, args.max_property_value - 1)
|
||||
generated_xs[random_number] += 1
|
||||
create_query += 'CREATE (:Label_T%s {x: %s}) ' % \
|
||||
(worker_id, random_number)
|
||||
create_query += "CREATE (:Label_T%s {x: %s}) " % (worker_id, random_number)
|
||||
# if full back or last item -> execute query
|
||||
if (i + 1) % args.create_pack_size == 0 or \
|
||||
i == args.vertex_count - 1:
|
||||
if (i + 1) % args.create_pack_size == 0 or i == args.vertex_count - 1:
|
||||
session.run(create_query).consume()
|
||||
create_query = ''
|
||||
create_query = ""
|
||||
create_time = time.time()
|
||||
# check total count
|
||||
result_set = session.run('MATCH (n:Label_T%s) RETURN count(n) AS cnt' %
|
||||
worker_id).data()[0]
|
||||
assert result_set['cnt'] == args.vertex_count, \
|
||||
'Create vertices Expected: %s Created: %s' % \
|
||||
(args.vertex_count, result_set['cnt'])
|
||||
result_set = session.run("MATCH (n:Label_T%s) RETURN count(n) AS cnt" % worker_id).data()[0]
|
||||
assert result_set["cnt"] == args.vertex_count, "Create vertices Expected: %s Created: %s" % (
|
||||
args.vertex_count,
|
||||
result_set["cnt"],
|
||||
)
|
||||
# check count per property value
|
||||
for i, size in generated_xs.items():
|
||||
result_set = session.run('MATCH (n:Label_T%s {x: %s}) '
|
||||
'RETURN count(n) AS cnt'
|
||||
% (worker_id, i)).data()[0]
|
||||
assert result_set['cnt'] == size, "Per x count isn't good " \
|
||||
"(Label: Label_T%s, prop x: %s" % (worker_id, i)
|
||||
result_set = session.run("MATCH (n:Label_T%s {x: %s}) " "RETURN count(n) AS cnt" % (worker_id, i)).data()[0]
|
||||
assert result_set["cnt"] == size, "Per x count isn't good " "(Label: Label_T%s, prop x: %s" % (
|
||||
worker_id,
|
||||
i,
|
||||
)
|
||||
return (worker_id, create_time - start_time, "s")
|
||||
|
||||
|
||||
def create_handler():
|
||||
'''
|
||||
"""
|
||||
Initializes processes and starts the execution.
|
||||
'''
|
||||
"""
|
||||
# instance cleanup
|
||||
with argument_session(args) as session:
|
||||
session.run("MATCH (n) DETACH DELETE n").consume()
|
||||
@@ -113,21 +124,19 @@ def create_handler():
|
||||
|
||||
# concurrent create execution & tests
|
||||
with multiprocessing.Pool(args.worker_count) as p:
|
||||
for worker_id, create_time, time_unit in \
|
||||
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))
|
||||
for worker_id, create_time, time_unit in 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))
|
||||
|
||||
# check total count
|
||||
expected_total_count = args.worker_count * args.vertex_count
|
||||
total_count = session.run(
|
||||
'MATCH (n) RETURN count(n) AS cnt').data()[0]['cnt']
|
||||
assert total_count == expected_total_count, \
|
||||
'Total vertex number: %s Expected: %s' % \
|
||||
(total_count, expected_total_count)
|
||||
total_count = session.run("MATCH (n) RETURN count(n) AS cnt").data()[0]["cnt"]
|
||||
assert total_count == expected_total_count, "Total vertex number: %s Expected: %s" % (
|
||||
total_count,
|
||||
expected_total_count,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=args.logging)
|
||||
if args.logging != "DEBUG":
|
||||
logging.getLogger("neo4j").setLevel(logging.WARNING)
|
||||
|
||||
@@ -20,9 +20,7 @@ GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY", "")
|
||||
GITHUB_SHA = os.getenv("GITHUB_SHA", "")
|
||||
GITHUB_REF = os.getenv("GITHUB_REF", "")
|
||||
|
||||
BENCH_GRAPH_SERVER_ENDPOINT = os.getenv(
|
||||
"BENCH_GRAPH_SERVER_ENDPOINT",
|
||||
"http://bench-graph-api:9001")
|
||||
BENCH_GRAPH_SERVER_ENDPOINT = os.getenv("BENCH_GRAPH_SERVER_ENDPOINT", "http://bench-graph-api:9001")
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,12 +50,12 @@ def post_measurement(args):
|
||||
"github_run_id": args.github_run_id,
|
||||
"github_run_number": args.github_run_number,
|
||||
"results": data,
|
||||
"git_branch": args.head_branch_name},
|
||||
timeout=1)
|
||||
assert req.status_code == 200, \
|
||||
f"Uploading {args.benchmark_name} data failed."
|
||||
log.info(f"{args.benchmark_name} data sent to "
|
||||
f"{BENCH_GRAPH_SERVER_ENDPOINT}")
|
||||
"git_branch": args.head_branch_name,
|
||||
},
|
||||
timeout=1,
|
||||
)
|
||||
assert req.status_code == 200, f"Uploading {args.benchmark_name} data failed."
|
||||
log.info(f"{args.benchmark_name} data sent to " f"{BENCH_GRAPH_SERVER_ENDPOINT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -4,14 +4,14 @@ import gdb
|
||||
|
||||
|
||||
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.
|
||||
# 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):
|
||||
'''Iterate all fields of value.type'''
|
||||
"""Iterate all fields of value.type"""
|
||||
types_to_process = [value.type]
|
||||
while types_to_process:
|
||||
for field in types_to_process.pop().fields():
|
||||
@@ -21,31 +21,27 @@ def _iter_fields_and_base_classes(value):
|
||||
|
||||
|
||||
def _fields(value):
|
||||
'''Return a list of value.type fields.'''
|
||||
return [f for f in _iter_fields_and_base_classes(value)
|
||||
if not f.is_base_class]
|
||||
"""Return a list of value.type fields."""
|
||||
return [f for f in _iter_fields_and_base_classes(value) if not f.is_base_class]
|
||||
|
||||
|
||||
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)]
|
||||
|
||||
|
||||
def _base_classes(value):
|
||||
'''Return a list of base classes for value.type.'''
|
||||
return [f for f in _iter_fields_and_base_classes(value)
|
||||
if f.is_base_class]
|
||||
"""Return a list of base classes for value.type."""
|
||||
return [f for f in _iter_fields_and_base_classes(value) if f.is_base_class]
|
||||
|
||||
|
||||
def _is_instance(value, type_):
|
||||
'''Return True if value is an instance of type.'''
|
||||
return value.type.unqualified() == type_ or \
|
||||
type_ in [base.type for base in _base_classes(value)]
|
||||
"""Return True if value is an instance of type."""
|
||||
return value.type.unqualified() == type_ or type_ in [base.type for base in _base_classes(value)]
|
||||
|
||||
|
||||
# Pattern for matching std::unique_ptr<T, Deleter> and std::shared_ptr<T>
|
||||
_SMART_PTR_TYPE_PATTERN = \
|
||||
re.compile('^std::(unique|shared)_ptr<(?P<pointee_type>[\w:]*)')
|
||||
_SMART_PTR_TYPE_PATTERN = re.compile("^std::(unique|shared)_ptr<(?P<pointee_type>[\w:]*)")
|
||||
|
||||
|
||||
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)
|
||||
if match is None or type_name is None:
|
||||
return bool(match)
|
||||
return type_name == match.group('pointee_type')
|
||||
return type_name == match.group("pointee_type")
|
||||
|
||||
|
||||
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
|
||||
# shared_ptr/unique_ptr via Python API.
|
||||
if _has_field(smart_ptr, '_M_ptr'):
|
||||
if _has_field(smart_ptr, "_M_ptr"):
|
||||
# shared_ptr
|
||||
return smart_ptr['_M_ptr']
|
||||
if _has_field(smart_ptr, '_M_t'):
|
||||
return smart_ptr["_M_ptr"]
|
||||
if _has_field(smart_ptr, "_M_t"):
|
||||
# unique_ptr
|
||||
smart_ptr = smart_ptr['_M_t']
|
||||
if _has_field(smart_ptr, '_M_t'):
|
||||
smart_ptr = smart_ptr["_M_t"]
|
||||
if _has_field(smart_ptr, "_M_t"):
|
||||
# Check for one more level of _M_t
|
||||
smart_ptr = smart_ptr['_M_t']
|
||||
if _has_field(smart_ptr, '_M_head_impl'):
|
||||
return smart_ptr['_M_head_impl']
|
||||
smart_ptr = smart_ptr["_M_t"]
|
||||
if _has_field(smart_ptr, "_M_head_impl"):
|
||||
return smart_ptr["_M_head_impl"]
|
||||
|
||||
|
||||
def _get_operator_input(operator):
|
||||
'''Returns the input operator of given operator, if it has any.'''
|
||||
if not _has_field(operator, 'input_'):
|
||||
"""Returns the input operator of given operator, if it has any."""
|
||||
if not _has_field(operator, "input_"):
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
super(PrintOperatorTree, self).__init__("print-operator-tree",
|
||||
gdb.COMMAND_USER,
|
||||
gdb.COMPLETE_EXPRESSION)
|
||||
super(PrintOperatorTree, self).__init__("print-operator-tree", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION)
|
||||
|
||||
def invoke(self, argument, from_tty):
|
||||
try:
|
||||
@@ -98,17 +93,16 @@ class PrintOperatorTree(gdb.Command):
|
||||
logical_operator_type = _logical_operator_type()
|
||||
if operator.type.code in (gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_REF):
|
||||
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()
|
||||
if not _is_instance(operator, logical_operator_type):
|
||||
raise gdb.GdbError("Expected a '%s', but got '%s'" %
|
||||
(logical_operator_type, operator.type))
|
||||
raise gdb.GdbError("Expected a '%s', but got '%s'" % (logical_operator_type, operator.type))
|
||||
next_op = operator.cast(operator.dynamic_type)
|
||||
tree = []
|
||||
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)
|
||||
print('\n'.join(tree))
|
||||
print("\n".join(tree))
|
||||
|
||||
|
||||
PrintOperatorTree()
|
||||
|
||||
@@ -3,43 +3,49 @@ import gdb.printing
|
||||
|
||||
|
||||
def build_memgraph_pretty_printers():
|
||||
'''Instantiate and return all memgraph pretty printer classes.'''
|
||||
pp = gdb.printing.RegexpCollectionPrettyPrinter('memgraph')
|
||||
pp.add_printer('memgraph::query::TypedValue', '^memgraph::query::TypedValue$', TypedValuePrinter)
|
||||
"""Instantiate and return all memgraph pretty printer classes."""
|
||||
pp = gdb.printing.RegexpCollectionPrettyPrinter("memgraph")
|
||||
pp.add_printer(
|
||||
"memgraph::query::TypedValue",
|
||||
"^memgraph::query::TypedValue$",
|
||||
TypedValuePrinter,
|
||||
)
|
||||
return pp
|
||||
|
||||
|
||||
class TypedValuePrinter(gdb.printing.PrettyPrinter):
|
||||
'''Pretty printer for memgraph::query::TypedValue'''
|
||||
"""Pretty printer for memgraph::query::TypedValue"""
|
||||
|
||||
def __init__(self, val):
|
||||
super(TypedValuePrinter, self).__init__('TypedValue')
|
||||
super(TypedValuePrinter, self).__init__("TypedValue")
|
||||
self.val = val
|
||||
|
||||
def to_string(self):
|
||||
def _to_str(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
|
||||
return "{%s %s}" % (value_type, self.val[val])
|
||||
|
||||
gdb.printing.register_pretty_printer(None, build_memgraph_pretty_printers(),
|
||||
replace=True)
|
||||
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(), replace=True)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/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.
|
||||
# See https://llvm.org/LICENSE.txt for license information.
|
||||
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
#
|
||||
#===-----------------------------------------------------------------------===#
|
||||
# ===-----------------------------------------------------------------------===#
|
||||
|
||||
r"""
|
||||
ClangTidy Diff Checker
|
||||
@@ -37,11 +37,11 @@ import threading
|
||||
import traceback
|
||||
|
||||
try:
|
||||
import yaml
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
yaml = None
|
||||
|
||||
is_py2 = sys.version[0] == '2'
|
||||
is_py2 = sys.version[0] == "2"
|
||||
|
||||
if is_py2:
|
||||
import Queue as queue
|
||||
@@ -50,220 +50,242 @@ else:
|
||||
|
||||
|
||||
def run_tidy(task_queue, lock, timeout):
|
||||
watchdog = None
|
||||
while True:
|
||||
command = task_queue.get()
|
||||
try:
|
||||
proc = subprocess.Popen(command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
watchdog = None
|
||||
while True:
|
||||
command = task_queue.get()
|
||||
try:
|
||||
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if timeout is not None:
|
||||
watchdog = threading.Timer(timeout, proc.kill)
|
||||
watchdog.start()
|
||||
if timeout is not None:
|
||||
watchdog = threading.Timer(timeout, proc.kill)
|
||||
watchdog.start()
|
||||
|
||||
stdout, stderr = proc.communicate()
|
||||
stdout, stderr = proc.communicate()
|
||||
|
||||
with lock:
|
||||
sys.stdout.write(stdout.decode('utf-8') + '\n')
|
||||
sys.stdout.flush()
|
||||
if stderr:
|
||||
sys.stderr.write(stderr.decode('utf-8') + '\n')
|
||||
sys.stderr.flush()
|
||||
except Exception as e:
|
||||
with lock:
|
||||
sys.stderr.write('Failed: ' + str(e) + ': '.join(command) + '\n')
|
||||
finally:
|
||||
with lock:
|
||||
if not (timeout is None or watchdog is None):
|
||||
if not watchdog.is_alive():
|
||||
sys.stderr.write('Terminated by timeout: ' +
|
||||
' '.join(command) + '\n')
|
||||
watchdog.cancel()
|
||||
task_queue.task_done()
|
||||
with lock:
|
||||
sys.stdout.write(stdout.decode("utf-8") + "\n")
|
||||
sys.stdout.flush()
|
||||
if stderr:
|
||||
sys.stderr.write(stderr.decode("utf-8") + "\n")
|
||||
sys.stderr.flush()
|
||||
except Exception as e:
|
||||
with lock:
|
||||
sys.stderr.write("Failed: " + str(e) + ": ".join(command) + "\n")
|
||||
finally:
|
||||
with lock:
|
||||
if not (timeout is None or watchdog is None):
|
||||
if not watchdog.is_alive():
|
||||
sys.stderr.write("Terminated by timeout: " + " ".join(command) + "\n")
|
||||
watchdog.cancel()
|
||||
task_queue.task_done()
|
||||
|
||||
|
||||
def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout):
|
||||
for _ in range(max_tasks):
|
||||
t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
for _ in range(max_tasks):
|
||||
t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
def merge_replacement_files(tmpdir, mergefile):
|
||||
"""Merge all replacement files in a directory into a single file"""
|
||||
# The fixes suggested by clang-tidy >= 4.0.0 are given under
|
||||
# the top level key 'Diagnostics' in the output yaml files
|
||||
mergekey = "Diagnostics"
|
||||
merged = []
|
||||
for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')):
|
||||
content = yaml.safe_load(open(replacefile, 'r'))
|
||||
if not content:
|
||||
continue # Skip empty files.
|
||||
merged.extend(content.get(mergekey, []))
|
||||
"""Merge all replacement files in a directory into a single file"""
|
||||
# The fixes suggested by clang-tidy >= 4.0.0 are given under
|
||||
# the top level key 'Diagnostics' in the output yaml files
|
||||
mergekey = "Diagnostics"
|
||||
merged = []
|
||||
for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
|
||||
content = yaml.safe_load(open(replacefile, "r"))
|
||||
if not content:
|
||||
continue # Skip empty files.
|
||||
merged.extend(content.get(mergekey, []))
|
||||
|
||||
if merged:
|
||||
# MainSourceFile: The key is required by the definition inside
|
||||
# include/clang/Tooling/ReplacementsYaml.h, but the value
|
||||
# is actually never used inside clang-apply-replacements,
|
||||
# so we set it to '' here.
|
||||
output = {'MainSourceFile': '', mergekey: merged}
|
||||
with open(mergefile, 'w') as out:
|
||||
yaml.safe_dump(output, out)
|
||||
else:
|
||||
# Empty the file:
|
||||
open(mergefile, 'w').close()
|
||||
if merged:
|
||||
# MainSourceFile: The key is required by the definition inside
|
||||
# include/clang/Tooling/ReplacementsYaml.h, but the value
|
||||
# is actually never used inside clang-apply-replacements,
|
||||
# so we set it to '' here.
|
||||
output = {"MainSourceFile": "", mergekey: merged}
|
||||
with open(mergefile, "w") as out:
|
||||
yaml.safe_dump(output, out)
|
||||
else:
|
||||
# Empty the file:
|
||||
open(mergefile, "w").close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=
|
||||
'Run clang-tidy against changed files, and '
|
||||
'output diagnostics only for modified '
|
||||
'lines.')
|
||||
parser.add_argument('-clang-tidy-binary', metavar='PATH',
|
||||
default='clang-tidy',
|
||||
help='path to clang-tidy binary')
|
||||
parser.add_argument('-p', metavar='NUM', default=0,
|
||||
help='strip the smallest prefix containing P slashes')
|
||||
parser.add_argument('-regex', metavar='PATTERN', default=None,
|
||||
help='custom pattern selecting file paths to check '
|
||||
'(case sensitive, overrides -iregex)')
|
||||
parser.add_argument('-iregex', metavar='PATTERN', default=
|
||||
r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)',
|
||||
help='custom pattern selecting file paths to check '
|
||||
'(case insensitive, overridden by -regex)')
|
||||
parser.add_argument('-j', type=int, default=1,
|
||||
help='number of tidy instances to be run in parallel.')
|
||||
parser.add_argument('-timeout', type=int, default=None,
|
||||
help='timeout per each file in seconds.')
|
||||
parser.add_argument('-fix', action='store_true', default=False,
|
||||
help='apply suggested fixes')
|
||||
parser.add_argument('-checks',
|
||||
help='checks filter, when not specified, use clang-tidy '
|
||||
'default',
|
||||
default='')
|
||||
parser.add_argument('-path', dest='build_path',
|
||||
help='Path used to read a compile command database.')
|
||||
if yaml:
|
||||
parser.add_argument('-export-fixes', metavar='FILE', dest='export_fixes',
|
||||
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('--')]
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run clang-tidy against changed files, and " "output diagnostics only for modified " "lines."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-clang-tidy-binary",
|
||||
metavar="PATH",
|
||||
default="clang-tidy",
|
||||
help="path to clang-tidy binary",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
metavar="NUM",
|
||||
default=0,
|
||||
help="strip the smallest prefix containing P slashes",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-regex",
|
||||
metavar="PATTERN",
|
||||
default=None,
|
||||
help="custom pattern selecting file paths to check " "(case sensitive, overrides -iregex)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-iregex",
|
||||
metavar="PATTERN",
|
||||
default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)",
|
||||
help="custom pattern selecting file paths to check " "(case insensitive, overridden by -regex)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
type=int,
|
||||
default=1,
|
||||
help="number of tidy instances to be run in parallel.",
|
||||
)
|
||||
parser.add_argument("-timeout", type=int, default=None, help="timeout per each file in seconds.")
|
||||
parser.add_argument("-fix", action="store_true", default=False, help="apply suggested fixes")
|
||||
parser.add_argument(
|
||||
"-checks",
|
||||
help="checks filter, when not specified, use clang-tidy " "default",
|
||||
default="",
|
||||
)
|
||||
parser.add_argument("-path", dest="build_path", help="Path used to read a compile command database.")
|
||||
if yaml:
|
||||
parser.add_argument(
|
||||
"-export-fixes",
|
||||
metavar="FILE",
|
||||
dest="export_fixes",
|
||||
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.
|
||||
filename = None
|
||||
lines_by_file = {}
|
||||
for line in sys.stdin:
|
||||
match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line)
|
||||
if match:
|
||||
filename = match.group(2)
|
||||
if filename is None:
|
||||
continue
|
||||
# Extract changed lines for each file.
|
||||
filename = None
|
||||
lines_by_file = {}
|
||||
for line in sys.stdin:
|
||||
match = re.search('^\+\+\+\ "?(.*?/){%s}([^ \t\n"]*)' % args.p, line)
|
||||
if match:
|
||||
filename = match.group(2)
|
||||
if filename is None:
|
||||
continue
|
||||
|
||||
if args.regex is not None:
|
||||
if not re.match('^%s$' % args.regex, filename):
|
||||
continue
|
||||
else:
|
||||
if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
|
||||
continue
|
||||
if args.regex is not None:
|
||||
if not re.match("^%s$" % args.regex, filename):
|
||||
continue
|
||||
else:
|
||||
if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE):
|
||||
continue
|
||||
|
||||
match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
|
||||
if match:
|
||||
start_line = int(match.group(1))
|
||||
line_count = 1
|
||||
if match.group(3):
|
||||
line_count = int(match.group(3))
|
||||
if line_count == 0:
|
||||
continue
|
||||
end_line = start_line + line_count - 1
|
||||
lines_by_file.setdefault(filename, []).append([start_line, end_line])
|
||||
match = re.search("^@@.*\+(\d+)(,(\d+))?", line)
|
||||
if match:
|
||||
start_line = int(match.group(1))
|
||||
line_count = 1
|
||||
if match.group(3):
|
||||
line_count = int(match.group(3))
|
||||
if line_count == 0:
|
||||
continue
|
||||
end_line = start_line + line_count - 1
|
||||
lines_by_file.setdefault(filename, []).append([start_line, end_line])
|
||||
|
||||
if not any(lines_by_file):
|
||||
print("No relevant changes found.")
|
||||
sys.exit(0)
|
||||
if not any(lines_by_file):
|
||||
print("No relevant changes found.")
|
||||
sys.exit(0)
|
||||
|
||||
max_task_count = args.j
|
||||
if max_task_count == 0:
|
||||
max_task_count = multiprocessing.cpu_count()
|
||||
max_task_count = min(len(lines_by_file), max_task_count)
|
||||
max_task_count = args.j
|
||||
if max_task_count == 0:
|
||||
max_task_count = multiprocessing.cpu_count()
|
||||
max_task_count = min(len(lines_by_file), max_task_count)
|
||||
|
||||
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)
|
||||
tmpdir = None
|
||||
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)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
|
||||
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.
|
||||
task_queue.join()
|
||||
# Run a pool of clang-tidy workers.
|
||||
start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout)
|
||||
|
||||
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()
|
||||
# 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)
|
||||
|
||||
if tmpdir:
|
||||
shutil.rmtree(tmpdir)
|
||||
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:
|
||||
# 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__':
|
||||
main()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/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.
|
||||
# See https://llvm.org/LICENSE.txt for license information.
|
||||
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
#
|
||||
#===-----------------------------------------------------------------------===#
|
||||
# ===-----------------------------------------------------------------------===#
|
||||
# FIXME: Integrate with clang-tidy-diff.py
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ import threading
|
||||
import traceback
|
||||
|
||||
try:
|
||||
import yaml
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
yaml = None
|
||||
|
||||
is_py2 = sys.version[0] == '2'
|
||||
is_py2 = sys.version[0] == "2"
|
||||
|
||||
if is_py2:
|
||||
import Queue as queue
|
||||
@@ -63,275 +63,327 @@ else:
|
||||
|
||||
|
||||
def find_compilation_database(path):
|
||||
"""Adjusts the directory until a compilation database is found."""
|
||||
result = './'
|
||||
while not os.path.isfile(os.path.join(result, path)):
|
||||
if os.path.realpath(result) == '/':
|
||||
print('Error: could not find compilation database.')
|
||||
sys.exit(1)
|
||||
result += '../'
|
||||
return os.path.realpath(result)
|
||||
"""Adjusts the directory until a compilation database is found."""
|
||||
result = "./"
|
||||
while not os.path.isfile(os.path.join(result, path)):
|
||||
if os.path.realpath(result) == "/":
|
||||
print("Error: could not find compilation database.")
|
||||
sys.exit(1)
|
||||
result += "../"
|
||||
return os.path.realpath(result)
|
||||
|
||||
|
||||
def make_absolute(f, directory):
|
||||
if os.path.isabs(f):
|
||||
return f
|
||||
return os.path.normpath(os.path.join(directory, f))
|
||||
if os.path.isabs(f):
|
||||
return f
|
||||
return os.path.normpath(os.path.join(directory, f))
|
||||
|
||||
|
||||
def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path,
|
||||
header_filter, allow_enabling_alpha_checkers,
|
||||
extra_arg, extra_arg_before, quiet, config):
|
||||
"""Gets a command line for clang-tidy."""
|
||||
start = [clang_tidy_binary]
|
||||
if allow_enabling_alpha_checkers:
|
||||
start.append('-allow-enabling-analyzer-alpha-checkers')
|
||||
if header_filter is not None:
|
||||
start.append('-header-filter=' + header_filter)
|
||||
if checks:
|
||||
start.append('-checks=' + checks)
|
||||
if tmpdir is not None:
|
||||
start.append('-export-fixes')
|
||||
# Get a temporary file. We immediately close the handle so clang-tidy can
|
||||
# overwrite it.
|
||||
(handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
|
||||
os.close(handle)
|
||||
start.append(name)
|
||||
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 get_tidy_invocation(
|
||||
f,
|
||||
clang_tidy_binary,
|
||||
checks,
|
||||
tmpdir,
|
||||
build_path,
|
||||
header_filter,
|
||||
allow_enabling_alpha_checkers,
|
||||
extra_arg,
|
||||
extra_arg_before,
|
||||
quiet,
|
||||
config,
|
||||
):
|
||||
"""Gets a command line for clang-tidy."""
|
||||
start = [clang_tidy_binary]
|
||||
if allow_enabling_alpha_checkers:
|
||||
start.append("-allow-enabling-analyzer-alpha-checkers")
|
||||
if header_filter is not None:
|
||||
start.append("-header-filter=" + header_filter)
|
||||
if checks:
|
||||
start.append("-checks=" + checks)
|
||||
if tmpdir is not None:
|
||||
start.append("-export-fixes")
|
||||
# Get a temporary file. We immediately close the handle so clang-tidy can
|
||||
# overwrite it.
|
||||
(handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir)
|
||||
os.close(handle)
|
||||
start.append(name)
|
||||
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):
|
||||
"""Merge all replacement files in a directory into a single file"""
|
||||
# The fixes suggested by clang-tidy >= 4.0.0 are given under
|
||||
# the top level key 'Diagnostics' in the output yaml files
|
||||
mergekey = "Diagnostics"
|
||||
merged=[]
|
||||
for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')):
|
||||
content = yaml.safe_load(open(replacefile, 'r'))
|
||||
if not content:
|
||||
continue # Skip empty files.
|
||||
merged.extend(content.get(mergekey, []))
|
||||
"""Merge all replacement files in a directory into a single file"""
|
||||
# The fixes suggested by clang-tidy >= 4.0.0 are given under
|
||||
# the top level key 'Diagnostics' in the output yaml files
|
||||
mergekey = "Diagnostics"
|
||||
merged = []
|
||||
for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
|
||||
content = yaml.safe_load(open(replacefile, "r"))
|
||||
if not content:
|
||||
continue # Skip empty files.
|
||||
merged.extend(content.get(mergekey, []))
|
||||
|
||||
if merged:
|
||||
# MainSourceFile: The key is required by the definition inside
|
||||
# include/clang/Tooling/ReplacementsYaml.h, but the value
|
||||
# is actually never used inside clang-apply-replacements,
|
||||
# so we set it to '' here.
|
||||
output = {'MainSourceFile': '', mergekey: merged}
|
||||
with open(mergefile, 'w') as out:
|
||||
yaml.safe_dump(output, out)
|
||||
else:
|
||||
# Empty the file:
|
||||
open(mergefile, 'w').close()
|
||||
if merged:
|
||||
# MainSourceFile: The key is required by the definition inside
|
||||
# include/clang/Tooling/ReplacementsYaml.h, but the value
|
||||
# is actually never used inside clang-apply-replacements,
|
||||
# so we set it to '' here.
|
||||
output = {"MainSourceFile": "", mergekey: merged}
|
||||
with open(mergefile, "w") as out:
|
||||
yaml.safe_dump(output, out)
|
||||
else:
|
||||
# Empty the file:
|
||||
open(mergefile, "w").close()
|
||||
|
||||
|
||||
def check_clang_apply_replacements_binary(args):
|
||||
"""Checks if invoking supplied clang-apply-replacements binary works."""
|
||||
try:
|
||||
subprocess.check_call([args.clang_apply_replacements_binary, '--version'])
|
||||
except:
|
||||
print('Unable to run clang-apply-replacements. Is clang-apply-replacements '
|
||||
'binary correctly specified?', file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
"""Checks if invoking supplied clang-apply-replacements binary works."""
|
||||
try:
|
||||
subprocess.check_call([args.clang_apply_replacements_binary, "--version"])
|
||||
except:
|
||||
print(
|
||||
"Unable to run clang-apply-replacements. Is clang-apply-replacements " "binary correctly specified?",
|
||||
file=sys.stderr,
|
||||
)
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def apply_fixes(args, tmpdir):
|
||||
"""Calls clang-apply-fixes on a given directory."""
|
||||
invocation = [args.clang_apply_replacements_binary]
|
||||
if args.format:
|
||||
invocation.append('-format')
|
||||
if args.style:
|
||||
invocation.append('-style=' + args.style)
|
||||
invocation.append(tmpdir)
|
||||
subprocess.call(invocation)
|
||||
"""Calls clang-apply-fixes on a given directory."""
|
||||
invocation = [args.clang_apply_replacements_binary]
|
||||
if args.format:
|
||||
invocation.append("-format")
|
||||
if args.style:
|
||||
invocation.append("-style=" + args.style)
|
||||
invocation.append(tmpdir)
|
||||
subprocess.call(invocation)
|
||||
|
||||
|
||||
def run_tidy(args, tmpdir, build_path, queue, lock, failed_files):
|
||||
"""Takes filenames out of queue and runs clang-tidy on them."""
|
||||
while True:
|
||||
name = queue.get()
|
||||
invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks,
|
||||
tmpdir, build_path, args.header_filter,
|
||||
args.allow_enabling_alpha_checkers,
|
||||
args.extra_arg, args.extra_arg_before,
|
||||
args.quiet, args.config)
|
||||
"""Takes filenames out of queue and runs clang-tidy on them."""
|
||||
while True:
|
||||
name = queue.get()
|
||||
invocation = get_tidy_invocation(
|
||||
name,
|
||||
args.clang_tidy_binary,
|
||||
args.checks,
|
||||
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)
|
||||
output, err = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
failed_files.append(name)
|
||||
with lock:
|
||||
sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8'))
|
||||
if len(err) > 0:
|
||||
sys.stdout.flush()
|
||||
sys.stderr.write(err.decode('utf-8'))
|
||||
queue.task_done()
|
||||
proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
output, err = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
failed_files.append(name)
|
||||
with lock:
|
||||
sys.stdout.write(" ".join(invocation) + "\n" + output.decode("utf-8"))
|
||||
if len(err) > 0:
|
||||
sys.stdout.flush()
|
||||
sys.stderr.write(err.decode("utf-8"))
|
||||
queue.task_done()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
|
||||
'in a compilation database. Requires '
|
||||
'clang-tidy and clang-apply-replacements in '
|
||||
'$PATH.')
|
||||
parser.add_argument('-allow-enabling-alpha-checkers',
|
||||
action='store_true', help='allow alpha checkers from '
|
||||
'clang-analyzer.')
|
||||
parser.add_argument('-clang-tidy-binary', metavar='PATH',
|
||||
default='clang-tidy-11',
|
||||
help='path to clang-tidy binary')
|
||||
parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
|
||||
default='clang-apply-replacements-11',
|
||||
help='path to clang-apply-replacements binary')
|
||||
parser.add_argument('-checks', default=None,
|
||||
help='checks filter, when not specified, use clang-tidy '
|
||||
'default')
|
||||
parser.add_argument('-config', default=None,
|
||||
help='Specifies a configuration in YAML/JSON format: '
|
||||
' -config="{Checks: \'*\', '
|
||||
' CheckOptions: [{key: x, '
|
||||
' value: y}]}" '
|
||||
'When the value is empty, clang-tidy will '
|
||||
'attempt to find a file named .clang-tidy for '
|
||||
'each source file in its parent directories.')
|
||||
parser.add_argument('-header-filter', default=None,
|
||||
help='regular expression matching the names of the '
|
||||
'headers to output diagnostics from. Diagnostics from '
|
||||
'the main file of each translation unit are always '
|
||||
'displayed.')
|
||||
if yaml:
|
||||
parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes',
|
||||
help='Create a yaml file to store suggested fixes in, '
|
||||
'which can be applied with clang-apply-replacements.')
|
||||
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()
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Runs clang-tidy over all files "
|
||||
"in a compilation database. Requires "
|
||||
"clang-tidy and clang-apply-replacements in "
|
||||
"$PATH."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-allow-enabling-alpha-checkers",
|
||||
action="store_true",
|
||||
help="allow alpha checkers from " "clang-analyzer.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-clang-tidy-binary",
|
||||
metavar="PATH",
|
||||
default="clang-tidy-11",
|
||||
help="path to clang-tidy binary",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-clang-apply-replacements-binary",
|
||||
metavar="PATH",
|
||||
default="clang-apply-replacements-11",
|
||||
help="path to clang-apply-replacements binary",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-checks",
|
||||
default=None,
|
||||
help="checks filter, when not specified, use clang-tidy " "default",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-config",
|
||||
default=None,
|
||||
help="Specifies a configuration in YAML/JSON format: "
|
||||
" -config=\"{Checks: '*', "
|
||||
" CheckOptions: [{key: x, "
|
||||
' value: y}]}" '
|
||||
"When the value is empty, clang-tidy will "
|
||||
"attempt to find a file named .clang-tidy for "
|
||||
"each source file in its parent directories.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-header-filter",
|
||||
default=None,
|
||||
help="regular expression matching the names of the "
|
||||
"headers to output diagnostics from. Diagnostics from "
|
||||
"the main file of each translation unit are always "
|
||||
"displayed.",
|
||||
)
|
||||
if yaml:
|
||||
parser.add_argument(
|
||||
"-export-fixes",
|
||||
metavar="filename",
|
||||
dest="export_fixes",
|
||||
help="Create a yaml file to store suggested fixes in, "
|
||||
"which can be applied with clang-apply-replacements.",
|
||||
)
|
||||
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:
|
||||
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)
|
||||
if args.build_path is not None:
|
||||
build_path = args.build_path
|
||||
else:
|
||||
subprocess.check_call(invocation)
|
||||
except:
|
||||
print("Unable to run clang-tidy.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# Find our database
|
||||
build_path = find_compilation_database(db_path)
|
||||
|
||||
# Load the database and extract all files.
|
||||
database = json.load(open(os.path.join(build_path, db_path)))
|
||||
files = [make_absolute(entry['file'], entry['directory'])
|
||||
for entry in database]
|
||||
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:
|
||||
subprocess.check_call(invocation)
|
||||
except:
|
||||
print("Unable to run clang-tidy.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
max_task = args.j
|
||||
if max_task == 0:
|
||||
max_task = multiprocessing.cpu_count()
|
||||
# Load the database and extract all files.
|
||||
database = json.load(open(os.path.join(build_path, db_path)))
|
||||
files = [make_absolute(entry["file"], entry["directory"]) for entry in database]
|
||||
|
||||
tmpdir = None
|
||||
if args.fix or (yaml and args.export_fixes):
|
||||
check_clang_apply_replacements_binary(args)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
max_task = args.j
|
||||
if max_task == 0:
|
||||
max_task = multiprocessing.cpu_count()
|
||||
|
||||
# Build up a big regexy filter from all command line arguments.
|
||||
file_name_re = re.compile('|'.join(args.files))
|
||||
tmpdir = None
|
||||
if args.fix or (yaml and args.export_fixes):
|
||||
check_clang_apply_replacements_binary(args)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
|
||||
return_code = 0
|
||||
try:
|
||||
# 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()
|
||||
# Build up a big regexy filter from all command line arguments.
|
||||
file_name_re = re.compile("|".join(args.files))
|
||||
|
||||
# Fill the queue with files.
|
||||
for name in files:
|
||||
if file_name_re.search(name):
|
||||
task_queue.put(name)
|
||||
return_code = 0
|
||||
try:
|
||||
# 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()
|
||||
|
||||
# Wait for all threads to be done.
|
||||
task_queue.join()
|
||||
if len(failed_files):
|
||||
return_code = 1
|
||||
# Fill the queue with files.
|
||||
for name in files:
|
||||
if file_name_re.search(name):
|
||||
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:
|
||||
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
|
||||
|
||||
if tmpdir:
|
||||
shutil.rmtree(tmpdir)
|
||||
sys.exit(return_code)
|
||||
shutil.rmtree(tmpdir)
|
||||
sys.exit(return_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user