Compare commits

..

10 Commits

Author SHA1 Message Date
János Benjamin Antal
309d03287d Use CentOS 9 instead of 8 2022-07-12 16:47:35 +02:00
Jure Bajic
3dd2657320 Create mgp python package (#433) 2022-07-12 10:54:23 +02:00
gvolfing
6fe474282a Modify logaical operators to conform openCyper regarding checking against NULL in CASE expressions (#432)
* Make `IfOperator` return the `else_expression_` in case of `NULL`

* Add gql_behave tests

* Add gql_behave test to specifically check for the case when the test expression itself is null
2022-07-11 15:00:29 +02:00
gvolfing
7fc0fb6520 Implement ToString function for temporal datatypes (#429)
* Modify `toString` to be able to handle `Date`, `LocalTime`, `LocalDateTime` and `Duration`

* Add unit tests

* Make `operator<<` use the `ToString()` implementations

* Add tests to verify the correctness of negative durations

* Add more tests to look for cases when the individual duration entities overflow.
2022-07-11 13:44:27 +02:00
Jeremy B
063e297e1e Avoid usage of time.sleep (#434)
e2e python: added tooling function around `time.sleep()` that stops as soon as condition is fulfilled and will raise assert if timeout is reached
2022-07-08 10:47:18 +02:00
Ante Javor
86b1688192 Rewrite Python API comments and snippets (#420)
* Update comments
2022-07-07 15:05:56 +02:00
Jeremy B
f629de7e60 Save replication settings (#415)
* Storage takes care of the saving of setting when a new replica is added

* Restore replicas at startup

* Modify interactive_mg_runner + memgraph to support that data-directory can be configured in CONTEXT

* Extend e2e test

* Correct typo

* Add flag to config to specify when replication should be stored (true by default when starting Memgraph)

* Remove un-necessary "--" in yaml file

* Make sure Memgraph stops if a replica can't be restored.

* Add UT covering the parsing  of ReplicaStatus to/from json

* Add assert in e2e script to check that a port is free before using it

* Add test covering crash on Jepsen

* Make sure applciaiton crashes if it starts on corrupted replications' info

Starting with a non-reponsive replica is allowed.

* Add temporary startup flag: this is needed so jepsen do not automatically restore replica on startup of main. This will be removed in T0835
2022-07-07 13:30:28 +02:00
Jeremy B
b737e53456 Remove sync with timeout (#423)
* Remove timout when registering a sync replica

* Simplify jepsen configuration file

* Remove timeout from jepsen configuration

* Add unit test

* Remove TimeoutDispatcher
2022-07-05 09:40:50 +02:00
János Benjamin Antal
10ca68bb2a Remove CODEOWNERS (#427)
Co-authored-by: Marko Budiselić <marko.budiselic@memgraph.com>
2022-07-04 16:42:36 +02:00
Jure Bajic
bfbd8538d4 Update docker release process (#421)
* Fix release directory

* Update release process

* Fix debian arm path
2022-07-04 16:10:33 +02:00
138 changed files with 4858 additions and 3720 deletions

View File

@@ -174,5 +174,5 @@ jobs:
- name: "Upload package"
uses: actions/upload-artifact@v2
with:
name: debian-11
path: build/output/debian-11/memgraph*.deb
name: debian-11-arm
path: build/output/debian-11-arm/memgraph*.deb

View File

@@ -1,4 +1,4 @@
name: Release CentOS 8
name: Release CentOS 9
on:
workflow_dispatch:
@@ -8,7 +8,7 @@ on:
jobs:
community_build:
name: "Community build"
runs-on: [self-hosted, Linux, X64, CentOS8]
runs-on: [self-hosted, Linux, X64, CentOS9]
env:
THREADS: 24
MEMGRAPH_ENTERPRISE_LICENSE: ${{ secrets.MEMGRAPH_ENTERPRISE_LICENSE }}
@@ -47,7 +47,7 @@ jobs:
coverage_build:
name: "Coverage build"
runs-on: [self-hosted, Linux, X64, CentOS8]
runs-on: [self-hosted, Linux, X64, CentOS9]
env:
THREADS: 24
MEMGRAPH_ENTERPRISE_LICENSE: ${{ secrets.MEMGRAPH_ENTERPRISE_LICENSE }}
@@ -104,7 +104,7 @@ jobs:
debug_build:
name: "Debug build"
runs-on: [self-hosted, Linux, X64, CentOS8]
runs-on: [self-hosted, Linux, X64, CentOS9]
env:
THREADS: 24
MEMGRAPH_ENTERPRISE_LICENSE: ${{ secrets.MEMGRAPH_ENTERPRISE_LICENSE }}
@@ -180,7 +180,7 @@ jobs:
release_build:
name: "Release build"
runs-on: [self-hosted, Linux, X64, CentOS8]
runs-on: [self-hosted, Linux, X64, CentOS9]
env:
THREADS: 24
MEMGRAPH_ENTERPRISE_LICENSE: ${{ secrets.MEMGRAPH_ENTERPRISE_LICENSE }}

View File

@@ -4,8 +4,12 @@ on:
workflow_dispatch:
inputs:
version:
description: "Memgraph binary version to publish on Dockerhub."
description: "Memgraph binary version to publish on DockerHub."
required: true
force_release:
type: boolean
required: false
default: false
jobs:
docker_publish:
@@ -36,6 +40,22 @@ jobs:
curl -L https://download.memgraph.com/memgraph/v${{ github.event.inputs.version }}/debian-11/memgraph_${{ github.event.inputs.version }}-1_amd64.deb > memgraph-amd64.deb
curl -L https://download.memgraph.com/memgraph/v${{ github.event.inputs.version }}/debian-11-aarch64/memgraph_${{ github.event.inputs.version }}-1_arm64.deb > memgraph-arm64.deb
- name: Check if specified version is already pushed
run: |
EXISTS=$(docker manifest inspect $DOCKER_ORGANIZATION_NAME/$DOCKER_REPOSITORY_NAME:${{ github.event.inputs.version }} > /dev/null; echo $?)
echo $EXISTS
if [[ ${EXISTS} -eq 0 ]]; then
echo 'The specified version has been already released to DockerHub.'
if [[ ${{ github.event.inputs.force_release }} = true ]]; then
echo 'Forcing the release!'
else
echo 'Stopping the release!'
exit 1
fi
else
echo 'All good the specified version has not been release to DockerHub.'
fi
- name: Build & push docker images
run: |
cd release/docker

2
.gitignore vendored
View File

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

View File

@@ -1 +0,0 @@
* @antaljanosbenjamin @kostasrim

View File

@@ -18,16 +18,14 @@ 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"):
@@ -48,7 +46,8 @@ 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"]
@@ -76,9 +75,8 @@ 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
@@ -91,7 +89,8 @@ 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"
@@ -99,16 +98,13 @@ 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)

File diff suppressed because it is too large Load Diff

3
init
View File

@@ -139,3 +139,6 @@ done;
# Install precommit hook
python3 -m pip install pre-commit
python3 -m pre_commit install
# Link `include/mgp.py` with `release/mgp/mgp.py`
ln -v -f include/mgp.py release/mgp/mgp.py

View File

@@ -7,11 +7,13 @@ 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.
@@ -35,7 +37,7 @@ def procedure(
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
@@ -49,13 +51,15 @@ def procedure(
# 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

View File

@@ -4,17 +4,15 @@ 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
@@ -25,14 +23,16 @@ _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,8 +41,10 @@ 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.
@@ -55,20 +57,19 @@ def analyze(context: mgp.ProcCtx, analyses: mgp.Nullable[List[str]] = None) -> m
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.
@@ -90,40 +91,36 @@ def analyze_subgraph(
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):
@@ -135,15 +132,20 @@ 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]
@@ -153,120 +155,126 @@ def _analyze_graph(context: mgp.ProcCtx, g: nx.MultiDiGraph, analyses: List[str]
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

View File

@@ -1,22 +1,20 @@
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
@@ -26,7 +24,8 @@ 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)
@@ -41,7 +40,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
@@ -72,26 +71,31 @@ 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
@@ -118,14 +122,18 @@ 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
@@ -147,7 +155,7 @@ class UnhashableProperties(collections.abc.Mapping):
class MemgraphNodeDict(collections.abc.Mapping):
__slots__ = ("_ctx",)
__slots__ = ('_ctx',)
def __init__(self, ctx):
self._ctx = ctx
@@ -179,7 +187,8 @@ 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
@@ -192,30 +201,23 @@ 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)
@@ -229,29 +231,33 @@ 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
@@ -264,7 +270,8 @@ 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

View File

@@ -1,20 +1,23 @@
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.
@@ -38,7 +41,7 @@ def get_components(
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])

View File

@@ -104,9 +104,7 @@ def retry(retry_limit, timeout=100):
except Exception:
time.sleep(timeout)
return func(*args, **kwargs)
return wrapper
return inner_func
@@ -165,15 +163,8 @@ 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",
@@ -182,9 +173,7 @@ 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()
@@ -267,27 +256,14 @@ 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="",
)

3
release/mgp/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.venv
dist
mgp.py

201
release/mgp/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

4
release/mgp/README.md Normal file
View File

@@ -0,0 +1,4 @@
# mgp
PyPi package used for type hinting when creating MAGE modules. The get started
using MAGE repository checkout the repository here: https://github.com/memgraph/mage.

255
release/mgp/_mgp.py Normal file
View File

@@ -0,0 +1,255 @@
from typing import Any
class MgpIterable:
def get() -> Any:
pass
def next() -> Any:
pass
class Vertex:
def is_valid() -> bool: # type: ignore
pass
def underlying_graph_is_mutable() -> bool: # type: ignore
pass
def iter_properties() -> MgpIterable: # type: ignore
pass
def get_property(self, property_name: str) -> "Property": # type: ignore
pass
def set_property(self, property_name: str, value: Any) -> "Property": # type: ignore
pass
def get_id() -> "VertexId": # type: ignore
pass
def label_at(self, index: int) -> "Label": # type: ignore
pass
def labels_count() -> int: # type: ignore
pass
def add_label(self, label: Any):
pass
def remove_label(self, label: Any):
pass
def iter_in_edges() -> MgpIterable: # type: ignore
pass
def iter_out_edges() -> MgpIterable: # type: ignore
pass
class Edge:
def is_valid() -> bool: # type: ignore
pass
def underlying_graph_is_mutable() -> bool: # type: ignore
pass
def iter_properties() -> MgpIterable: # type: ignore
pass
def get_property(self, property_name: str) -> "Property": # type: ignore
pass
def set_property(self, property_name: str, valuse: Any) -> "Property": # type: ignore
pass
def get_type_name() -> str: # type: ignore
pass
def get_id() -> "EdgeId": # type: ignore
pass
def from_vertex() -> Vertex: # type: ignore
pass
def to_vertex() -> Vertex: # type: ignore
pass
class Path:
def is_valid() -> bool: # type: ignore
pass
@staticmethod
def make_with_start(vertex: Vertex) -> "Path": # type: ignore
pass
class Graph:
def is_valid() -> bool: # type: ignore
pass
class CypherType:
pass
class Message:
def is_valid() -> bool: # type: ignore
pass
def source_type() -> str: # type: ignore
pass
def topic_name() -> str: # type: ignore
pass
def key() -> bytes: # type: ignore
pass
def timestamp() -> int: # type: ignore
pass
def offset() -> int: # type: ignore
pass
def payload() -> bytes: # type: ignore
pass
class Messages:
def is_valid() -> bool: # type: ignore
pass
def message_at(self, id: int) -> Message: # type: ignore
pass
def total_messages() -> int: # type: ignore
pass
class UnknownError(Exception):
pass
class UnableToAllocateError(Exception):
pass
class InsufficientBufferError(Exception):
pass
class OutOfRangeError(Exception):
pass
class LogicErrorError(Exception):
pass
class DeletedObjectError(Exception):
pass
class InvalidArgumentError(Exception):
pass
class KeyAlreadyExistsError(Exception):
pass
class ImmutableObjectError(Exception):
pass
class ValueConversionError(Exception):
pass
class SerializationError(Exception):
pass
def type_nullable(elem: Any):
pass
def type_list(elem: Any):
pass
def type_bool():
pass
def type_string():
pass
def type_int():
pass
def type_float():
pass
def type_number():
pass
def type_map():
pass
def type_node():
pass
def type_relationship():
pass
def type_path():
pass
def type_date():
pass
def type_local_time():
pass
def type_local_date_time():
pass
def type_duration():
pass
def type_any():
pass
class _MODULE:
@staticmethod
def add_read_procedure(wrapper):
pass
@staticmethod
def add_write_procedure(wrapper):
pass
@staticmethod
def add_transformation(wrapper):
pass
@staticmethod
def add_function(wrapper):
pass

View File

@@ -0,0 +1,22 @@
# How to publish new versions
## Prerequisites
1. Installed poetry
```
pip install poetry
```
2. Set up [API tokens](https://pypi.org/help/#apitoken)
3. Be a collaborator on [pypi](https://pypi.org/project/mgp/)
## Making changes
1. Make changes to the package
2. Bump version in `pyproject.tml`
3. `poetry build`
4. `poetry publish`
## Why is this not automatized?
Because someone always has to manually bump up the version in `pyproject.toml`
## Why does `_mgp.py` exists?
Because we are mocking here all the types that are created by Memgraph
in order to fix typing errors in `mgp.py`.

View File

@@ -0,0 +1,23 @@
[tool.poetry]
name = "mgp"
version = "1.0.0"
description = "Memgraph's module for developing MAGE modules. Used only for type hinting!"
authors = [
"MasterMedo <mislav.vuletic@gmail.com>",
"jbajic <jure.bajic@memgraph.io>",
"katarinasupe <katarina.supe@memgraph.io>",
"antejavor <ante.javor@memgraph.io>",
"antaljanosbenjamin <benjamin.antal@memgraph.io>",
]
license = "Apache-2.0"
readme = "README.md"
include = ["mgp.py", "_mgp.py"]
[tool.poetry.dependencies]
python = "^3.7"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@@ -84,8 +84,6 @@ std::string PermissionToString(Permission permission) {
return "MODULE_WRITE";
case Permission::WEBSOCKET:
return "WEBSOCKET";
case Permission::LABELS:
return "LABELS";
}
}
@@ -185,107 +183,19 @@ 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;
}
@@ -297,9 +207,7 @@ Role Role::Deserialize(const nlohmann::json &data) {
throw AuthException("Couldn't load role data!");
}
auto permissions = Permissions::Deserialize(data["permissions"]);
auto labelPermissions = LabelPermissions::Deserialize(data["labelPermissions"]);
return {data["rolename"], permissions, labelPermissions};
return {data["rolename"], permissions};
}
bool operator==(const Role &first, const Role &second) {
@@ -311,13 +219,6 @@ 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_);
@@ -370,8 +271,6 @@ 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();
@@ -384,7 +283,6 @@ 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;
}
@@ -397,14 +295,11 @@ User User::Deserialize(const nlohmann::json &data) {
throw AuthException("Couldn't load user data!");
}
auto permissions = Permissions::Deserialize(data["permissions"]);
auto labelPermissions = LabelPermissions::Deserialize(data["labelPermissions"]);
return {data["username"], data["password_hash"], permissions, labelPermissions};
return {data["username"], data["password_hash"], permissions};
}
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

View File

@@ -12,7 +12,6 @@
#include <string>
#include <json/json.hpp>
#include <unordered_set>
namespace memgraph::auth {
// These permissions must have values that are applicable for usage in a
@@ -39,8 +38,7 @@ enum class Permission : uint64_t {
STREAM = 1U << 17U,
MODULE_READ = 1U << 18U,
MODULE_WRITE = 1U << 19U,
WEBSOCKET = 1U << 20U,
LABELS = 1U << 21U
WEBSOCKET = 1U << 20U
};
// clang-format on
@@ -90,52 +88,16 @@ 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.
@@ -146,7 +108,6 @@ class Role final {
private:
std::string rolename_;
Permissions permissions_;
LabelPermissions labelPermissions_;
};
bool operator==(const Role &first, const Role &second);
@@ -158,9 +119,6 @@ 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);
@@ -180,8 +138,6 @@ class User final {
const Role *role() const;
LabelPermissions &labelPermissions();
nlohmann::json Serialize() const;
/// @throw AuthException if unable to deserialize.
@@ -194,9 +150,7 @@ 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

View File

@@ -18,24 +18,19 @@ 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.
@@ -45,12 +40,14 @@ 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": ""}
@@ -59,32 +56,25 @@ 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": ""}

View File

@@ -57,8 +57,6 @@ 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

View File

@@ -216,6 +216,11 @@ DEFINE_bool(telemetry_enabled, false,
"the database runtime (vertex and edge counts and resource usage) "
"to allow for easier improvement of the product.");
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
DEFINE_bool(storage_restore_replicas_on_startup, true,
"Controls replicas should be restored automatically."); // TODO(42jeremy) this must be removed once T0835
// is implemented.
// Streams flags
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
DEFINE_uint32(
@@ -501,7 +506,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,9 +752,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
}
void GrantPrivilege(const std::string &user_or_role,
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) {
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
// TODO (mferencevic): should we first check that the
// privilege is granted/denied/revoked before
// unconditionally granting/denying/revoking it?
@@ -758,9 +762,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
}
void DenyPrivilege(const std::string &user_or_role,
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) {
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
// TODO (mferencevic): should we first check that the
// privilege is granted/denied/revoked before
// unconditionally granting/denying/revoking it?
@@ -769,9 +772,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
}
void RevokePrivilege(const std::string &user_or_role,
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) {
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
// TODO (mferencevic): should we first check that the
// privilege is granted/denied/revoked before
// unconditionally granting/denying/revoking it?
@@ -782,8 +784,7 @@ 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 std::vector<std::string> &labels, const TEditFun &edit_fun) {
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges, const TEditFun &edit_fun) {
if (!std::regex_match(user_or_role, name_regex_)) {
throw memgraph::query::QueryRuntimeException("Invalid user or role name.");
}
@@ -803,17 +804,11 @@ 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) {
@@ -1205,7 +1200,8 @@ int main(int argc, char **argv) {
.snapshot_retention_count = FLAGS_storage_snapshot_retention_count,
.wal_file_size_kibibytes = FLAGS_storage_wal_file_size_kib,
.wal_file_flush_every_n_tx = FLAGS_storage_wal_file_flush_every_n_tx,
.snapshot_on_exit = FLAGS_storage_snapshot_on_exit},
.snapshot_on_exit = FLAGS_storage_snapshot_on_exit,
.restore_replicas_on_startup = FLAGS_storage_restore_replicas_on_startup},
.transaction = {.isolation_level = ParseIsolationLevel()}};
if (FLAGS_storage_snapshot_interval_sec == 0) {
if (FLAGS_storage_wal_enabled) {

View File

@@ -2239,11 +2239,9 @@ 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
@@ -2255,7 +2253,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 labels)
websocket)
(:serialize))
#>cpp
AuthQuery() = default;
@@ -2266,14 +2264,13 @@ cpp<#
#>cpp
AuthQuery(Action action, std::string user, std::string role,
std::string user_or_role, Expression *password,
std::vector<std::string> labels ,std::vector<Privilege> privileges)
std::vector<Privilege> privileges)
: action_(action),
user_(user),
role_(role),
user_or_role_(user_or_role),
password_(password),
labels_(labels),
privileges_(privileges){}
privileges_(privileges) {}
cpp<#)
(:private
#>cpp
@@ -2298,8 +2295,7 @@ 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::LABELS};
AuthQuery::Privilege::WEBSOCKET};
cpp<#
(lcp:define-class info-query (query)
@@ -2379,10 +2375,7 @@ cpp<#
(port "Expression *" :initval "nullptr" :scope :public
:slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Expression"))
(sync_mode "SyncMode" :scope :public)
(timeout "Expression *" :initval "nullptr" :scope :public
:slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Expression")))
(sync_mode "SyncMode" :scope :public))
(:public
(lcp:define-enum action

View File

@@ -275,18 +275,7 @@ antlrcpp::Any CypherMainVisitor::visitRegisterReplica(MemgraphCypher::RegisterRe
replication_query->replica_name_ = ctx->replicaName()->symbolicName()->accept(this).as<std::string>();
if (ctx->SYNC()) {
replication_query->sync_mode_ = memgraph::query::ReplicationQuery::SyncMode::SYNC;
if (ctx->WITH() && ctx->TIMEOUT()) {
if (ctx->timeout->numberLiteral()) {
// we accept both double and integer literals
replication_query->timeout_ = ctx->timeout->accept(this);
} else {
throw SemanticException("Timeout should be a integer or double literal!");
}
}
} else if (ctx->ASYNC()) {
if (ctx->WITH() && ctx->TIMEOUT()) {
throw SyntaxException("Timeout can be set only for the SYNC replication mode!");
}
replication_query->sync_mode_ = memgraph::query::ReplicationQuery::SyncMode::ASYNC;
}
@@ -1285,11 +1274,7 @@ 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()) {
if (privilege->LABELS()) {
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
} else {
auth->privileges_.push_back(privilege->accept(this));
}
auth->privileges_.push_back(privilege->accept(this));
}
} else {
/* grant all privileges */
@@ -1307,11 +1292,7 @@ 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()) {
if (privilege->LABELS()) {
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
} else {
auth->privileges_.push_back(privilege->accept(this));
}
auth->privileges_.push_back(privilege->accept(this));
}
} else {
/* deny all privileges */
@@ -1329,11 +1310,7 @@ 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()) {
if (privilege->LABELS()) {
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
} else {
auth->privileges_.push_back(privilege->accept(this));
}
auth->privileges_.push_back(privilege->accept(this));
}
} else {
/* revoke all privileges */
@@ -1342,22 +1319,6 @@ 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
*/
@@ -1383,10 +1344,6 @@ 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!");
}

View File

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

View File

@@ -56,7 +56,6 @@ memgraphCypherKeyword : cypherKeyword
| IDENTIFIED
| ISOLATION
| KAFKA
| LABELS
| LEVEL
| LOAD
| LOCK
@@ -255,15 +254,10 @@ 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 ;
@@ -282,7 +276,6 @@ replicaName : symbolicName ;
socketAddress : literal ;
registerReplica : REGISTER REPLICA replicaName ( SYNC | ASYNC )
( WITH TIMEOUT timeout=literal ) ?
TO socketAddress ;
dropReplica : DROP REPLICA replicaName ;

View File

@@ -66,7 +66,6 @@ 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 ;

View File

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

View File

@@ -882,21 +882,36 @@ TypedValue Id(const TypedValue *args, int64_t nargs, const FunctionContext &ctx)
}
TypedValue ToString(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
FType<Or<Null, String, Number, Bool>>("toString", args, nargs);
FType<Or<Null, String, Number, Date, LocalTime, LocalDateTime, Duration, Bool>>("toString", args, nargs);
const auto &arg = args[0];
if (arg.IsNull()) {
return TypedValue(ctx.memory);
} else if (arg.IsString()) {
}
if (arg.IsString()) {
return TypedValue(arg, ctx.memory);
} else if (arg.IsInt()) {
}
if (arg.IsInt()) {
// TODO: This is making a pointless copy of std::string, we may want to
// use a different conversion to string
return TypedValue(std::to_string(arg.ValueInt()), ctx.memory);
} else if (arg.IsDouble()) {
return TypedValue(std::to_string(arg.ValueDouble()), ctx.memory);
} else {
return TypedValue(arg.ValueBool() ? "true" : "false", ctx.memory);
}
if (arg.IsDouble()) {
return TypedValue(std::to_string(arg.ValueDouble()), ctx.memory);
}
if (arg.IsDate()) {
return TypedValue(arg.ValueDate().ToString(), ctx.memory);
}
if (arg.IsLocalTime()) {
return TypedValue(arg.ValueLocalTime().ToString(), ctx.memory);
}
if (arg.IsLocalDateTime()) {
return TypedValue(arg.ValueLocalDateTime().ToString(), ctx.memory);
}
if (arg.IsDuration()) {
return TypedValue(arg.ValueDuration().ToString(), ctx.memory);
}
return TypedValue(arg.ValueBool() ? "true" : "false", ctx.memory);
}
TypedValue Timestamp(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {

View File

@@ -111,7 +111,7 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
TypedValue Visit(IfOperator &if_operator) override {
auto condition = if_operator.condition_->Accept(*this);
if (condition.IsNull()) {
return if_operator.then_expression_->Accept(*this);
return if_operator.else_expression_->Accept(*this);
}
if (condition.type() != TypedValue::Type::Bool) {
// At the moment IfOperator is used only in CASE construct.

View File

@@ -44,6 +44,7 @@
#include "query/trigger.hpp"
#include "query/typed_value.hpp"
#include "storage/v2/property_value.hpp"
#include "storage/v2/replication/enums.hpp"
#include "utils/algorithm.hpp"
#include "utils/csv_parsing.hpp"
#include "utils/event_counter.hpp"
@@ -160,7 +161,7 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler {
/// @throw QueryRuntimeException if an error ocurred.
void RegisterReplica(const std::string &name, const std::string &socket_address,
const ReplicationQuery::SyncMode sync_mode, const std::optional<double> timeout,
const ReplicationQuery::SyncMode sync_mode,
const std::chrono::seconds replica_check_frequency) override {
if (db_->GetReplicationRole() == storage::ReplicationRole::REPLICA) {
// replica can't register another replica
@@ -183,9 +184,9 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler {
io::network::Endpoint::ParseSocketOrIpAddress(socket_address, query::kDefaultReplicationPort);
if (maybe_ip_and_port) {
auto [ip, port] = *maybe_ip_and_port;
auto ret = db_->RegisterReplica(
name, {std::move(ip), port}, repl_mode,
{.timeout = timeout, .replica_check_frequency = replica_check_frequency, .ssl = std::nullopt});
auto ret = db_->RegisterReplica(name, {std::move(ip), port}, repl_mode,
storage::replication::RegistrationMode::MUST_BE_INSTANTLY_VALID,
{.replica_check_frequency = replica_check_frequency, .ssl = std::nullopt});
if (ret.HasError()) {
throw QueryRuntimeException(fmt::format("Couldn't register replica '{}'!", name));
}
@@ -228,9 +229,6 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler {
replica.sync_mode = ReplicationQuery::SyncMode::ASYNC;
break;
}
if (repl_info.timeout) {
replica.timeout = *repl_info.timeout;
}
replica.current_timestamp_of_replica = repl_info.timestamp_info.current_timestamp_of_replica;
replica.current_number_of_timestamp_behind_master =
@@ -282,8 +280,6 @@ 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;
@@ -298,8 +294,7 @@ 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_) {
@@ -314,7 +309,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>>();
@@ -389,20 +384,20 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
};
return callback;
case AuthQuery::Action::GRANT_PRIVILEGE:
callback.fn = [auth, user_or_role, privileges, labels] {
auth->GrantPrivilege(user_or_role, privileges, labels);
callback.fn = [auth, user_or_role, privileges] {
auth->GrantPrivilege(user_or_role, privileges);
return std::vector<std::vector<TypedValue>>();
};
return callback;
case AuthQuery::Action::DENY_PRIVILEGE:
callback.fn = [auth, user_or_role, privileges, labels] {
auth->DenyPrivilege(user_or_role, privileges, labels);
callback.fn = [auth, user_or_role, privileges] {
auth->DenyPrivilege(user_or_role, privileges);
return std::vector<std::vector<TypedValue>>();
};
return callback;
case AuthQuery::Action::REVOKE_PRIVILEGE: {
callback.fn = [auth, user_or_role, privileges, labels] {
auth->RevokePrivilege(user_or_role, privileges, labels);
callback.fn = [auth, user_or_role, privileges] {
auth->RevokePrivilege(user_or_role, privileges);
return std::vector<std::vector<TypedValue>>();
};
return callback;
@@ -490,22 +485,11 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
const auto &name = repl_query->replica_name_;
const auto &sync_mode = repl_query->sync_mode_;
auto socket_address = repl_query->socket_address_->Accept(evaluator);
auto timeout = EvaluateOptionalExpression(repl_query->timeout_, &evaluator);
const auto replica_check_frequency = interpreter_context->config.replication_replica_check_frequency;
std::optional<double> maybe_timeout;
if (timeout.IsDouble()) {
maybe_timeout = timeout.ValueDouble();
} else if (timeout.IsInt()) {
maybe_timeout = static_cast<double>(timeout.ValueInt());
}
if (maybe_timeout && *maybe_timeout <= 0.0) {
throw utils::BasicException("Parameter TIMEOUT must be strictly greater than 0.");
}
callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, name, socket_address, sync_mode,
maybe_timeout, replica_check_frequency]() mutable {
handler.RegisterReplica(name, std::string(socket_address.ValueString()), sync_mode, maybe_timeout,
replica_check_frequency);
replica_check_frequency]() mutable {
handler.RegisterReplica(name, std::string(socket_address.ValueString()), sync_mode, replica_check_frequency);
return std::vector<std::vector<TypedValue>>();
};
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::REGISTER_REPLICA,
@@ -525,13 +509,9 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
}
case ReplicationQuery::Action::SHOW_REPLICAS: {
callback.header = {"name",
"socket_address",
"sync_mode",
"timeout",
"current_timestamp_of_replica",
"number_of_timestamp_behind_master",
"state"};
callback.header = {
"name", "socket_address", "sync_mode", "current_timestamp_of_replica", "number_of_timestamp_behind_master",
"state"};
callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, replica_nfields = callback.header.size()] {
const auto &replicas = handler.ShowReplicas();
auto typed_replicas = std::vector<std::vector<TypedValue>>{};
@@ -552,12 +532,6 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
break;
}
if (replica.timeout) {
typed_replica.emplace_back(TypedValue(*replica.timeout));
} else {
typed_replica.emplace_back(TypedValue());
}
typed_replica.emplace_back(TypedValue(static_cast<int64_t>(replica.current_timestamp_of_replica)));
typed_replica.emplace_back(
TypedValue(static_cast<int64_t>(replica.current_number_of_timestamp_behind_master)));

View File

@@ -99,16 +99,14 @@ 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,
const std::vector<std::string> &labels) = 0;
virtual void GrantPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges) = 0;
/// @throw QueryRuntimeException if an error ocurred.
virtual void DenyPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
const std::vector<std::string> &labels) = 0;
virtual void DenyPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges) = 0;
/// @throw QueryRuntimeException if an error ocurred.
virtual void RevokePrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
const std::vector<std::string> &labels) = 0;
virtual void RevokePrivilege(const std::string &user_or_role,
const std::vector<AuthQuery::Privilege> &privileges) = 0;
};
enum class QueryHandlerResult { COMMIT, ABORT, NOTHING };
@@ -142,7 +140,7 @@ class ReplicationQueryHandler {
/// @throw QueryRuntimeException if an error ocurred.
virtual void RegisterReplica(const std::string &name, const std::string &socket_address,
const ReplicationQuery::SyncMode sync_mode, const std::optional<double> timeout,
ReplicationQuery::SyncMode sync_mode,
const std::chrono::seconds replica_check_frequency) = 0;
/// @throw QueryRuntimeException if an error ocurred.

View File

@@ -619,10 +619,10 @@ void Streams::Drop(const std::string &stream_name) {
// no running Test function for this consumer, therefore it can be erased.
std::visit([&](const auto &stream_data) { stream_data.stream_source->Lock(); }, it->second);
locked_streams->erase(it);
if (!storage_.Delete(stream_name)) {
throw StreamsException("Couldn't delete stream '{}' from persistent store!", stream_name);
}
locked_streams->erase(it);
// TODO(antaljanosbenjamin) Release the transformation
}

View File

@@ -188,7 +188,7 @@ class Streams final {
void Persist(StreamStatus<TStream> &&status) {
const std::string stream_name = status.name;
if (!storage_.Put(stream_name, nlohmann::json(std::move(status)).dump())) {
throw StreamsException{"Couldn't persist steam data for stream '{}'", stream_name};
throw StreamsException{"Couldn't persist stream data for stream '{}'", stream_name};
}
}

View File

@@ -13,7 +13,6 @@ set(storage_v2_src_files
storage.cpp)
##### Replication #####
define_add_lcp(add_lcp_storage lcp_storage_cpp_files generated_lcp_storage_files)
add_lcp_storage(replication/rpc.lcp SLK_SERIALIZE)
@@ -26,10 +25,10 @@ set(storage_v2_src_files
replication/replication_server.cpp
replication/serialization.cpp
replication/slk.cpp
replication/replication_persistence_helper.cpp
${lcp_storage_cpp_files})
#######################
find_package(gflags REQUIRED)
find_package(Threads REQUIRED)

View File

@@ -49,7 +49,7 @@ struct Config {
uint64_t wal_file_flush_every_n_tx{100000};
bool snapshot_on_exit{false};
bool restore_replicas_on_startup{false};
} durability;
struct Transaction {

View File

@@ -22,6 +22,7 @@ static const std::string kSnapshotDirectory{"snapshots"};
static const std::string kWalDirectory{"wal"};
static const std::string kBackupDirectory{".backup"};
static const std::string kLockFile{".lock"};
static const std::string kReplicationDirectory{"replication"};
// This is the prefix used for Snapshot and WAL filenames. It is a timestamp
// format that equals to: YYYYmmddHHMMSSffffff

View File

@@ -10,12 +10,13 @@
// licenses/APL.txt.
#pragma once
#include <chrono>
#include <optional>
#include <string>
namespace memgraph::storage::replication {
struct ReplicationClientConfig {
std::optional<double> timeout;
// The default delay between main checking/pinging replicas is 1s because
// that seems like a reasonable timeframe in which main should notice a
// replica is down.
@@ -24,6 +25,8 @@ struct ReplicationClientConfig {
struct SSL {
std::string key_file = "";
std::string cert_file = "";
friend bool operator==(const SSL &, const SSL &) = default;
};
std::optional<SSL> ssl;

View File

@@ -16,4 +16,6 @@ namespace memgraph::storage::replication {
enum class ReplicationMode : std::uint8_t { SYNC, ASYNC };
enum class ReplicaState : std::uint8_t { READY, REPLICATING, RECOVERY, INVALID };
enum class RegistrationMode : std::uint8_t { MUST_BE_INSTANTLY_VALID, CAN_BE_INVALID };
} // namespace memgraph::storage::replication

View File

@@ -43,12 +43,6 @@ Storage::ReplicationClient::ReplicationClient(std::string name, Storage *storage
rpc_client_.emplace(endpoint, &*rpc_context_);
TryInitializeClientSync();
if (config.timeout && replica_state_ != replication::ReplicaState::INVALID) {
MG_ASSERT(*config.timeout > 0);
timeout_.emplace(*config.timeout);
timeout_dispatcher_.emplace();
}
// Help the user to get the most accurate replica state possible.
if (config.replica_check_frequency > std::chrono::seconds(0)) {
replica_checker_.Run("Replica Checker", config.replica_check_frequency, [&] { FrequentCheck(); });
@@ -239,41 +233,6 @@ void Storage::ReplicationClient::FinalizeTransactionReplication() {
if (mode_ == replication::ReplicationMode::ASYNC) {
thread_pool_.AddTask([this] { this->FinalizeTransactionReplicationInternal(); });
} else if (timeout_) {
MG_ASSERT(mode_ == replication::ReplicationMode::SYNC, "Only SYNC replica can have a timeout.");
MG_ASSERT(timeout_dispatcher_, "Timeout thread is missing");
timeout_dispatcher_->WaitForTaskToFinish();
timeout_dispatcher_->active = true;
thread_pool_.AddTask([&, this] {
this->FinalizeTransactionReplicationInternal();
std::unique_lock main_guard(timeout_dispatcher_->main_lock);
// TimerThread can finish waiting for timeout
timeout_dispatcher_->active = false;
// Notify the main thread
timeout_dispatcher_->main_cv.notify_one();
});
timeout_dispatcher_->StartTimeoutTask(*timeout_);
// Wait until one of the threads notifies us that they finished executing
// Both threads should first set the active flag to false
{
std::unique_lock main_guard(timeout_dispatcher_->main_lock);
timeout_dispatcher_->main_cv.wait(main_guard, [&] { return !timeout_dispatcher_->active.load(); });
}
// TODO (antonio2368): Document and/or polish SEMI-SYNC to ASYNC fallback.
if (replica_state_ == replication::ReplicaState::REPLICATING) {
mode_ = replication::ReplicationMode::ASYNC;
timeout_.reset();
// This can only happen if we timeouted so we are sure that
// Timeout task finished
// We need to delete timeout dispatcher AFTER the replication
// finished because it tries to acquire the timeout lock
// and acces the `active` variable`
thread_pool_.AddTask([this] { timeout_dispatcher_.reset(); });
}
} else {
FinalizeTransactionReplicationInternal();
}
@@ -566,30 +525,6 @@ Storage::TimestampInfo Storage::ReplicationClient::GetTimestampInfo() {
return info;
}
////// TimeoutDispatcher //////
void Storage::ReplicationClient::TimeoutDispatcher::WaitForTaskToFinish() {
// Wait for the previous timeout task to finish
std::unique_lock main_guard(main_lock);
main_cv.wait(main_guard, [&] { return finished; });
}
void Storage::ReplicationClient::TimeoutDispatcher::StartTimeoutTask(const double timeout) {
timeout_pool.AddTask([timeout, this] {
finished = false;
using std::chrono::steady_clock;
const auto timeout_duration =
std::chrono::duration_cast<steady_clock::duration>(std::chrono::duration<double>(timeout));
const auto end_time = steady_clock::now() + timeout_duration;
while (active && (steady_clock::now() < end_time)) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::unique_lock main_guard(main_lock);
finished = true;
active = false;
main_cv.notify_one();
});
}
////// ReplicaStream //////
Storage::ReplicationClient::ReplicaStream::ReplicaStream(ReplicationClient *self,
const uint64_t previous_commit_timestamp,

View File

@@ -120,8 +120,6 @@ class Storage::ReplicationClient {
auto Mode() const { return mode_; }
auto Timeout() const { return timeout_; }
const auto &Endpoint() const { return rpc_client_->Endpoint(); }
Storage::TimestampInfo GetTimestampInfo();
@@ -158,30 +156,6 @@ class Storage::ReplicationClient {
std::optional<ReplicaStream> replica_stream_;
replication::ReplicationMode mode_{replication::ReplicationMode::SYNC};
// Dispatcher class for timeout tasks
struct TimeoutDispatcher {
explicit TimeoutDispatcher(){};
void WaitForTaskToFinish();
void StartTimeoutTask(double timeout);
// If the Timeout task should continue waiting
std::atomic<bool> active{false};
std::mutex main_lock;
std::condition_variable main_cv;
private:
// if the Timeout task finished executing
bool finished{true};
utils::ThreadPool timeout_pool{1};
};
std::optional<double> timeout_;
std::optional<TimeoutDispatcher> timeout_dispatcher_;
utils::SpinLock client_lock_;
// This thread pool is used for background tasks so we don't
// block the main storage thread

View File

@@ -0,0 +1,83 @@
// Copyright 2022 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include "storage/v2/replication/replication_persistence_helper.hpp"
#include "utils/logging.hpp"
namespace {
const std::string kReplicaName = "replica_name";
const std::string kIpAddress = "replica_ip_address";
const std::string kPort = "replica_port";
const std::string kSyncMode = "replica_sync_mode";
const std::string kCheckFrequency = "replica_check_frequency";
const std::string kSSLKeyFile = "replica_ssl_key_file";
const std::string kSSLCertFile = "replica_ssl_cert_file";
} // namespace
namespace memgraph::storage::replication {
nlohmann::json ReplicaStatusToJSON(ReplicaStatus &&status) {
auto data = nlohmann::json::object();
data[kReplicaName] = std::move(status.name);
data[kIpAddress] = std::move(status.ip_address);
data[kPort] = status.port;
data[kSyncMode] = status.sync_mode;
data[kCheckFrequency] = status.replica_check_frequency.count();
if (status.ssl.has_value()) {
data[kSSLKeyFile] = std::move(status.ssl->key_file);
data[kSSLCertFile] = std::move(status.ssl->cert_file);
} else {
data[kSSLKeyFile] = nullptr;
data[kSSLCertFile] = nullptr;
}
return data;
}
std::optional<ReplicaStatus> JSONToReplicaStatus(nlohmann::json &&data) {
ReplicaStatus replica_status;
const auto get_failed_message = [](const std::string_view message, const std::string_view nested_message) {
return fmt::format("Failed to deserialize replica's configuration: {} : {}", message, nested_message);
};
try {
data.at(kReplicaName).get_to(replica_status.name);
data.at(kIpAddress).get_to(replica_status.ip_address);
data.at(kPort).get_to(replica_status.port);
data.at(kSyncMode).get_to(replica_status.sync_mode);
replica_status.replica_check_frequency = std::chrono::seconds(data.at(kCheckFrequency));
const auto &key_file = data.at(kSSLKeyFile);
const auto &cert_file = data.at(kSSLCertFile);
MG_ASSERT(key_file.is_null() == cert_file.is_null());
if (!key_file.is_null()) {
replica_status.ssl = replication::ReplicationClientConfig::SSL{};
data.at(kSSLKeyFile).get_to(replica_status.ssl->key_file);
data.at(kSSLCertFile).get_to(replica_status.ssl->cert_file);
}
} catch (const nlohmann::json::type_error &exception) {
spdlog::error(get_failed_message("Invalid type conversion", exception.what()));
return std::nullopt;
} catch (const nlohmann::json::out_of_range &exception) {
spdlog::error(get_failed_message("Non existing field", exception.what()));
return std::nullopt;
}
return replica_status;
}
} // namespace memgraph::storage::replication

View File

@@ -0,0 +1,40 @@
// Copyright 2022 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#pragma once
#include <chrono>
#include <compare>
#include <optional>
#include <string>
#include <json/json.hpp>
#include "storage/v2/replication/config.hpp"
#include "storage/v2/replication/enums.hpp"
namespace memgraph::storage::replication {
struct ReplicaStatus {
std::string name;
std::string ip_address;
uint16_t port;
ReplicationMode sync_mode;
std::chrono::seconds replica_check_frequency;
std::optional<ReplicationClientConfig::SSL> ssl;
friend bool operator==(const ReplicaStatus &, const ReplicaStatus &) = default;
};
nlohmann::json ReplicaStatusToJSON(ReplicaStatus &&status);
std::optional<ReplicaStatus> JSONToReplicaStatus(nlohmann::json &&data);
} // namespace memgraph::storage::replication

View File

@@ -17,6 +17,7 @@
#include <variant>
#include <gflags/gflags.h>
#include <spdlog/spdlog.h>
#include "io/network/endpoint.hpp"
#include "storage/v2/durability/durability.hpp"
@@ -28,6 +29,8 @@
#include "storage/v2/indices.hpp"
#include "storage/v2/mvcc.hpp"
#include "storage/v2/replication/config.hpp"
#include "storage/v2/replication/enums.hpp"
#include "storage/v2/replication/replication_persistence_helper.hpp"
#include "storage/v2/transaction.hpp"
#include "storage/v2/vertex_accessor.hpp"
#include "utils/file.hpp"
@@ -50,6 +53,19 @@ using OOMExceptionEnabler = utils::MemoryTracker::OutOfMemoryExceptionEnabler;
namespace {
inline constexpr uint16_t kEpochHistoryRetention = 1000;
std::string RegisterReplicaErrorToString(Storage::RegisterReplicaError error) {
switch (error) {
case Storage::RegisterReplicaError::NAME_EXISTS:
return "NAME_EXISTS";
case Storage::RegisterReplicaError::END_POINT_EXISTS:
return "END_POINT_EXISTS";
case Storage::RegisterReplicaError::CONNECTION_FAILED:
return "CONNECTION_FAILED";
case Storage::RegisterReplicaError::COULD_NOT_BE_PERSISTED:
return "COULD_NOT_BE_PERSISTED";
}
}
} // namespace
auto AdvanceToVisibleVertex(utils::SkipList<Vertex>::Iterator it, utils::SkipList<Vertex>::Iterator end,
@@ -400,6 +416,16 @@ Storage::Storage(Config config)
} else {
commit_log_.emplace(timestamp_);
}
if (config_.durability.restore_replicas_on_startup) {
spdlog::info("Replica's configuration will be stored and will be automatically restored in case of a crash.");
utils::EnsureDirOrDie(config_.durability.storage_directory / durability::kReplicationDirectory);
storage_ =
std::make_unique<kvstore::KVStore>(config_.durability.storage_directory / durability::kReplicationDirectory);
RestoreReplicas();
} else {
spdlog::warn("Replicas' configuration will NOT be stored. When the server restarts, replicas will be forgotten.");
}
}
Storage::~Storage() {
@@ -1882,7 +1908,7 @@ bool Storage::SetMainReplicationRole() {
utils::BasicResult<Storage::RegisterReplicaError> Storage::RegisterReplica(
std::string name, io::network::Endpoint endpoint, const replication::ReplicationMode replication_mode,
const replication::ReplicationClientConfig &config) {
const replication::RegistrationMode registration_mode, const replication::ReplicationClientConfig &config) {
MG_ASSERT(replication_role_.load() == ReplicationRole::MAIN, "Only main instance can register a replica!");
const bool name_exists = replication_clients_.WithLock([&](auto &clients) {
@@ -1902,12 +1928,28 @@ utils::BasicResult<Storage::RegisterReplicaError> Storage::RegisterReplica(
return RegisterReplicaError::END_POINT_EXISTS;
}
MG_ASSERT(replication_mode == replication::ReplicationMode::SYNC || !config.timeout,
"Only SYNC mode can have a timeout set");
if (ShouldStoreAndRestoreReplicas()) {
auto data = replication::ReplicaStatusToJSON(
replication::ReplicaStatus{.name = name,
.ip_address = endpoint.address,
.port = endpoint.port,
.sync_mode = replication_mode,
.replica_check_frequency = config.replica_check_frequency,
.ssl = config.ssl});
if (!storage_->Put(name, data.dump())) {
spdlog::error("Error when saving replica {} in settings.", name);
return RegisterReplicaError::COULD_NOT_BE_PERSISTED;
}
}
auto client = std::make_unique<ReplicationClient>(std::move(name), this, endpoint, replication_mode, config);
if (client->State() == replication::ReplicaState::INVALID) {
return RegisterReplicaError::CONNECTION_FAILED;
if (replication::RegistrationMode::CAN_BE_INVALID != registration_mode) {
return RegisterReplicaError::CONNECTION_FAILED;
}
spdlog::warn("Connection failed when registering replica {}. Replica will still be registered.", client->Name());
}
return replication_clients_.WithLock([&](auto &clients) -> utils::BasicResult<Storage::RegisterReplicaError> {
@@ -1928,8 +1970,15 @@ utils::BasicResult<Storage::RegisterReplicaError> Storage::RegisterReplica(
});
}
bool Storage::UnregisterReplica(const std::string_view name) {
bool Storage::UnregisterReplica(const std::string &name) {
MG_ASSERT(replication_role_.load() == ReplicationRole::MAIN, "Only main instance can unregister a replica!");
if (ShouldStoreAndRestoreReplicas()) {
if (!storage_->Delete(name)) {
spdlog::error("Error when removing replica {} from settings.", name);
return false;
}
}
return replication_clients_.WithLock([&](auto &clients) {
return std::erase_if(clients, [&](const auto &client) { return client->Name() == name; });
});
@@ -1952,11 +2001,10 @@ std::vector<Storage::ReplicaInfo> Storage::ReplicasInfo() {
return replication_clients_.WithLock([](auto &clients) {
std::vector<Storage::ReplicaInfo> replica_info;
replica_info.reserve(clients.size());
std::transform(clients.begin(), clients.end(), std::back_inserter(replica_info),
[](const auto &client) -> ReplicaInfo {
return {client->Name(), client->Mode(), client->Timeout(),
client->Endpoint(), client->State(), client->GetTimestampInfo()};
});
std::transform(
clients.begin(), clients.end(), std::back_inserter(replica_info), [](const auto &client) -> ReplicaInfo {
return {client->Name(), client->Mode(), client->Endpoint(), client->State(), client->GetTimestampInfo()};
});
return replica_info;
});
}
@@ -1966,4 +2014,41 @@ void Storage::SetIsolationLevel(IsolationLevel isolation_level) {
isolation_level_ = isolation_level;
}
void Storage::RestoreReplicas() {
MG_ASSERT(memgraph::storage::ReplicationRole::MAIN == GetReplicationRole());
if (!ShouldStoreAndRestoreReplicas()) {
return;
}
spdlog::info("Restoring replicas.");
for (const auto &[replica_name, replica_data] : *storage_) {
spdlog::info("Restoring replica {}.", replica_name);
const auto maybe_replica_status = replication::JSONToReplicaStatus(nlohmann::json::parse(replica_data));
if (!maybe_replica_status.has_value()) {
LOG_FATAL("Cannot parse previously saved configuration of replica {}.", replica_name);
}
auto replica_status = *maybe_replica_status;
MG_ASSERT(replica_status.name == replica_name, "Expected replica name is '{}', but got '{}'", replica_status.name,
replica_name);
auto ret =
RegisterReplica(std::move(replica_status.name), {std::move(replica_status.ip_address), replica_status.port},
replica_status.sync_mode, replication::RegistrationMode::CAN_BE_INVALID,
{
.replica_check_frequency = replica_status.replica_check_frequency,
.ssl = replica_status.ssl,
});
if (ret.HasError()) {
MG_ASSERT(RegisterReplicaError::CONNECTION_FAILED != ret.GetError());
LOG_FATAL("Failure when restoring replica {}: {}.", replica_name, RegisterReplicaErrorToString(ret.GetError()));
}
spdlog::info("Replica {} restored.", replica_name);
}
}
bool Storage::ShouldStoreAndRestoreReplicas() const { return nullptr != storage_; }
} // namespace memgraph::storage

View File

@@ -18,6 +18,7 @@
#include <variant>
#include "io/network/endpoint.hpp"
#include "kvstore/kvstore.hpp"
#include "storage/v2/commit_log.hpp"
#include "storage/v2/config.hpp"
#include "storage/v2/constraints.hpp"
@@ -411,15 +412,20 @@ class Storage final {
bool SetMainReplicationRole();
enum class RegisterReplicaError : uint8_t { NAME_EXISTS, END_POINT_EXISTS, CONNECTION_FAILED };
enum class RegisterReplicaError : uint8_t {
NAME_EXISTS,
END_POINT_EXISTS,
CONNECTION_FAILED,
COULD_NOT_BE_PERSISTED
};
/// @pre The instance should have a MAIN role
/// @pre Timeout can only be set for SYNC replication
utils::BasicResult<RegisterReplicaError, void> RegisterReplica(
std::string name, io::network::Endpoint endpoint, replication::ReplicationMode replication_mode,
const replication::ReplicationClientConfig &config = {});
replication::RegistrationMode registration_mode, const replication::ReplicationClientConfig &config = {});
/// @pre The instance should have a MAIN role
bool UnregisterReplica(std::string_view name);
bool UnregisterReplica(const std::string &name);
std::optional<replication::ReplicaState> GetReplicaState(std::string_view name);
@@ -433,7 +439,6 @@ class Storage final {
struct ReplicaInfo {
std::string name;
replication::ReplicationMode mode;
std::optional<double> timeout;
io::network::Endpoint endpoint;
replication::ReplicaState state;
TimestampInfo timestamp_info;
@@ -475,6 +480,10 @@ class Storage final {
uint64_t CommitTimestamp(std::optional<uint64_t> desired_commit_timestamp = {});
void RestoreReplicas();
bool ShouldStoreAndRestoreReplicas() const;
// Main storage lock.
//
// Accessors take a shared lock when starting, so it is possible to block
@@ -535,6 +544,7 @@ class Storage final {
std::filesystem::path wal_directory_;
std::filesystem::path lock_file_path_;
utils::OutputFile lock_file_handle_;
std::unique_ptr<kvstore::KVStore> storage_;
utils::Scheduler snapshot_runner_;
utils::SpinLock snapshot_lock_;

View File

@@ -15,6 +15,7 @@
#include <chrono>
#include <ctime>
#include <limits>
#include <string>
#include <string_view>
#include "utils/exceptions.hpp"
@@ -175,6 +176,10 @@ int64_t Date::MicrosecondsSinceEpoch() const {
int64_t Date::DaysSinceEpoch() const { return utils::DaysSinceEpoch(year, month, day).count(); }
std::string Date::ToString() const {
return fmt::format("{:0>4}-{:0>2}-{:0>2}", year, static_cast<int>(month), static_cast<int>(day));
}
size_t DateHash::operator()(const Date &date) const {
utils::HashCombine<uint64_t, uint64_t> hasher;
size_t result = hasher(0, date.year);
@@ -377,6 +382,15 @@ int64_t LocalTime::NanosecondsSinceEpoch() const {
return chrono::duration_cast<chrono::nanoseconds>(SumLocalTimeParts()).count();
}
std::string LocalTime::ToString() const {
using milli = std::chrono::milliseconds;
using micro = std::chrono::microseconds;
const auto subseconds = milli(millisecond) + micro(microsecond);
return fmt::format("{:0>2}:{:0>2}:{:0>2}.{:0>6}", static_cast<int>(hour), static_cast<int>(minute),
static_cast<int>(second), subseconds.count());
}
size_t LocalTimeHash::operator()(const LocalTime &local_time) const {
utils::HashCombine<uint64_t, uint64_t> hasher;
size_t result = hasher(0, local_time.hour);
@@ -486,6 +500,8 @@ int64_t LocalDateTime::SubSecondsAsNanoseconds() const {
return (milli_as_nanos + micros_as_nanos).count();
}
std::string LocalDateTime::ToString() const { return date.ToString() + 'T' + local_time.ToString(); }
LocalDateTime::LocalDateTime(const DateParameters &date_parameters, const LocalTimeParameters &local_time_parameters)
: date(date_parameters), local_time(local_time_parameters) {}
@@ -699,6 +715,23 @@ int64_t Duration::SubSecondsAsNanoseconds() const {
return chrono::duration_cast<chrono::nanoseconds>(micros - secs).count();
}
std::string Duration::ToString() const {
// Format [nD]T[nH]:[nM]:[nS].
namespace chrono = std::chrono;
auto micros = chrono::microseconds(microseconds);
const auto dd = GetAndSubtractDuration<chrono::days>(micros);
const auto h = GetAndSubtractDuration<chrono::hours>(micros);
const auto m = GetAndSubtractDuration<chrono::minutes>(micros);
const auto s = GetAndSubtractDuration<chrono::seconds>(micros);
auto first_half = fmt::format("P{}DT{}H{}M", dd, h, m);
auto second_half = fmt::format("{}.{:0>6}S", s, std::abs(micros.count()));
if (s == 0 && micros.count() < 0) {
return first_half + '-' + second_half;
}
return first_half + second_half;
}
Duration Duration::operator-() const {
if (microseconds == std::numeric_limits<decltype(microseconds)>::min()) [[unlikely]] {
throw temporal::InvalidArgumentException("Duration arithmetic overflows");

View File

@@ -87,20 +87,9 @@ struct Duration {
int64_t SubDaysAsNanoseconds() const;
int64_t SubSecondsAsNanoseconds() const;
friend std::ostream &operator<<(std::ostream &os, const Duration &dur) {
// Format [nD]T[nH]:[nM]:[nS].
namespace chrono = std::chrono;
auto micros = chrono::microseconds(dur.microseconds);
const auto dd = GetAndSubtractDuration<chrono::days>(micros);
const auto h = GetAndSubtractDuration<chrono::hours>(micros);
const auto m = GetAndSubtractDuration<chrono::minutes>(micros);
const auto s = GetAndSubtractDuration<chrono::seconds>(micros);
os << fmt::format("P{}DT{}H{}M", dd, h, m);
if (s == 0 && micros.count() < 0) {
os << '-';
}
return os << fmt::format("{}.{:0>6}S", s, std::abs(micros.count()));
}
std::string ToString() const;
friend std::ostream &operator<<(std::ostream &os, const Duration &dur) { return os << dur.ToString(); }
Duration operator-() const;
@@ -155,13 +144,11 @@ struct Date {
explicit Date(int64_t microseconds);
explicit Date(const DateParameters &date_parameters);
friend std::ostream &operator<<(std::ostream &os, const Date &date) {
return os << fmt::format("{:0>2}-{:0>2}-{:0>2}", date.year, static_cast<int>(date.month),
static_cast<int>(date.day));
}
friend std::ostream &operator<<(std::ostream &os, const Date &date) { return os << date.ToString(); }
int64_t MicrosecondsSinceEpoch() const;
int64_t DaysSinceEpoch() const;
std::string ToString() const;
friend Date operator+(const Date &date, const Duration &dur) {
namespace chrono = std::chrono;
@@ -217,17 +204,11 @@ struct LocalTime {
// Epoch means the start of the day, i,e, midnight
int64_t MicrosecondsSinceEpoch() const;
int64_t NanosecondsSinceEpoch() const;
std::string ToString() const;
auto operator<=>(const LocalTime &) const = default;
friend std::ostream &operator<<(std::ostream &os, const LocalTime &lt) {
namespace chrono = std::chrono;
using milli = chrono::milliseconds;
using micro = chrono::microseconds;
const auto subseconds = milli(lt.millisecond) + micro(lt.microsecond);
return os << fmt::format("{:0>2}:{:0>2}:{:0>2}.{:0>6}", static_cast<int>(lt.hour), static_cast<int>(lt.minute),
static_cast<int>(lt.second), subseconds.count());
}
friend std::ostream &operator<<(std::ostream &os, const LocalTime &lt) { return os << lt.ToString(); }
friend LocalTime operator+(const LocalTime &local_time, const Duration &dur) {
namespace chrono = std::chrono;
@@ -279,13 +260,11 @@ struct LocalDateTime {
int64_t MicrosecondsSinceEpoch() const;
int64_t SecondsSinceEpoch() const; // seconds since epoch
int64_t SubSecondsAsNanoseconds() const;
std::string ToString() const;
auto operator<=>(const LocalDateTime &) const = default;
friend std::ostream &operator<<(std::ostream &os, const LocalDateTime &ldt) {
os << ldt.date << 'T' << ldt.local_time;
return os;
}
friend std::ostream &operator<<(std::ostream &os, const LocalDateTime &ldt) { return os << ldt.ToString(); }
friend LocalDateTime operator+(const LocalDateTime &dt, const Duration &dur) {
const auto local_date_time_as_duration = Duration(dt.MicrosecondsSinceEpoch());

View File

@@ -15,31 +15,33 @@
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!')

View File

@@ -14,7 +14,9 @@
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
@@ -24,11 +26,10 @@ 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()
@@ -41,7 +42,8 @@ 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

View File

@@ -15,7 +15,6 @@
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])
@@ -23,19 +22,17 @@ 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:

View File

@@ -36,6 +36,7 @@ import os
import subprocess
from argparse import ArgumentParser
from pathlib import Path
import tempfile
import time
import sys
from inspect import signature
@@ -67,7 +68,7 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
"log_file": "main.log",
"setup_queries": [
"REGISTER REPLICA replica1 SYNC TO '127.0.0.1:10001'",
"REGISTER REPLICA replica2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
"REGISTER REPLICA replica2 SYNC TO '127.0.0.1:10002'",
],
},
}
@@ -103,7 +104,7 @@ def is_port_in_use(port: int) -> bool:
return s.connect_ex(("localhost", port)) == 0
def _start_instance(name, args, log_file, queries, use_ssl, procdir):
def _start_instance(name, args, log_file, queries, use_ssl, procdir, data_directory):
assert (
name not in MEMGRAPH_INSTANCES.keys()
), "If this raises, you are trying to start an instance with the same name than one already running."
@@ -113,7 +114,8 @@ def _start_instance(name, args, log_file, queries, use_ssl, procdir):
mg_instance = MemgraphInstanceRunner(MEMGRAPH_BINARY, use_ssl)
MEMGRAPH_INSTANCES[name] = mg_instance
log_file_path = os.path.join(BUILD_DIR, "logs", log_file)
binary_args = args + ["--log-file", log_file_path]
data_directory_path = os.path.join(BUILD_DIR, data_directory)
binary_args = args + ["--log-file", log_file_path] + ["--data-directory", data_directory_path]
if len(procdir) != 0:
binary_args.append("--query-modules-directory=" + procdir)
@@ -175,8 +177,13 @@ def start_instance(context, name, procdir):
if "ssl" in value:
use_ssl = bool(value["ssl"])
value.pop("ssl")
data_directory = ""
if "data_directory" in value:
data_directory = value["data_directory"]
else:
data_directory = tempfile.TemporaryDirectory().name
instance = _start_instance(name, args, log_file, queries, use_ssl, procdir)
instance = _start_instance(name, args, log_file, queries, use_ssl, procdir, data_directory)
mg_instances[name] = instance
assert len(mg_instances) == 1

View File

@@ -106,7 +106,6 @@ 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()

View File

@@ -76,11 +76,8 @@ class MemgraphInstanceRunner:
self.stop()
self.args = copy.deepcopy(args)
self.args = [replace_paths(arg) for arg in self.args]
self.data_directory = tempfile.TemporaryDirectory()
args_mg = [
self.binary_path,
"--data-directory",
self.data_directory.name,
"--storage-wal-enabled",
"--storage-snapshot-interval-sec",
"300",

16
tests/e2e/mg_utils.py Normal file
View File

@@ -0,0 +1,16 @@
import time
def mg_sleep_and_assert(expected_value, function_to_retrieve_data, max_duration=20, time_between_attempt=0.05):
result = function_to_retrieve_data()
start_time = time.time()
while result != expected_value:
current_time = time.time()
duration = current_time - start_time
if duration > max_duration:
assert False, " mg_sleep_and_assert has tried for too long and did not get the expected result!"
time.sleep(time_between_attempt)
result = function_to_retrieve_data()
return result

View File

@@ -5,7 +5,7 @@ monitoring_port: &monitoring_port "7444"
template_cluster: &template_cluster
cluster:
monitoring:
args: ["--bolt-port=7687", "--log-level=TRACE", "--"]
args: ["--bolt-port=7687", "--log-level=TRACE"]
log_file: "monitoring-websocket-e2e.log"
template_cluster_ssl: &template_cluster_ssl
cluster:
@@ -21,7 +21,6 @@ template_cluster_ssl: &template_cluster_ssl
*cert_file,
"--bolt-key-file",
*key_file,
"--",
]
log_file: "monitoring-websocket-ssl-e2e.log"
ssl: true

View File

@@ -12,3 +12,4 @@ copy_e2e_python_files(replication_show show.py)
copy_e2e_python_files(replication_show show_while_creating_invalid_state.py)
copy_e2e_python_files_from_parent_folder(replication_show ".." memgraph.py)
copy_e2e_python_files_from_parent_folder(replication_show ".." interactive_mg_runner.py)
copy_e2e_python_files_from_parent_folder(replication_show ".." mg_utils.py)

View File

@@ -15,6 +15,7 @@ import pytest
import time
from common import execute_and_fetch_all
from mg_utils import mg_sleep_and_assert
@pytest.mark.parametrize(
@@ -36,20 +37,19 @@ def test_show_replicas(connection):
"name",
"socket_address",
"sync_mode",
"timeout",
"current_timestamp_of_replica",
"number_of_timestamp_behind_master",
"state",
}
actual_column_names = {x.name for x in cursor.description}
assert expected_column_names == actual_column_names
assert actual_column_names == expected_column_names
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
}
assert expected_data == actual_data
assert actual_data == expected_data
def test_show_replicas_while_inserting_data(connection):
@@ -68,43 +68,43 @@ def test_show_replicas_while_inserting_data(connection):
"name",
"socket_address",
"sync_mode",
"timeout",
"current_timestamp_of_replica",
"number_of_timestamp_behind_master",
"state",
}
actual_column_names = {x.name for x in cursor.description}
assert expected_column_names == actual_column_names
assert actual_column_names == expected_column_names
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
}
assert expected_data == actual_data
assert actual_data == expected_data
# 1/
execute_and_fetch_all(cursor, "CREATE (n1:Number {name: 'forty_two', value:42});")
time.sleep(1)
# 2/
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 2.0, 4, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 1.0, 4, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", None, 4, 0, "ready"),
("replica_1", "127.0.0.1:10001", "sync", 4, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 4, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 4, 0, "ready"),
}
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
print("actual_data=" + str(actual_data))
print("expected_data=" + str(expected_data))
assert expected_data == actual_data
def retrieve_data():
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
actual_data = mg_sleep_and_assert(expected_data, retrieve_data)
assert actual_data == expected_data
# 3/
res = execute_and_fetch_all(cursor, "MATCH (node) return node;")
assert 1 == len(res)
assert len(res) == 1
# 4/
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
assert expected_data == actual_data
assert actual_data == expected_data
if __name__ == "__main__":

View File

@@ -13,11 +13,12 @@ import sys
import os
import pytest
import time
from common import execute_and_fetch_all
from mg_utils import mg_sleep_and_assert
import interactive_mg_runner
import mgclient
import tempfile
interactive_mg_runner.SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
interactive_mg_runner.PROJECT_DIR = os.path.normpath(
@@ -51,8 +52,8 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
"log_file": "main.log",
"setup_queries": [
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001';",
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002';",
"REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001';",
"REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002';",
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003';",
"REGISTER REPLICA replica_4 ASYNC TO '127.0.0.1:10004';",
],
@@ -78,32 +79,31 @@ def test_show_replicas(connection):
"name",
"socket_address",
"sync_mode",
"timeout",
"current_timestamp_of_replica",
"number_of_timestamp_behind_master",
"state",
}
actual_column_names = {x.name for x in cursor.description}
assert EXPECTED_COLUMN_NAMES == actual_column_names
assert actual_column_names == EXPECTED_COLUMN_NAMES
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 2, 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "ready"),
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", 0, 0, "ready"),
}
assert expected_data == actual_data
assert actual_data == expected_data
# 2/
execute_and_fetch_all(cursor, "DROP REPLICA replica_2")
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "ready"),
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", 0, 0, "ready"),
}
assert expected_data == actual_data
assert actual_data == expected_data
# 3/
interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "replica_1")
@@ -111,53 +111,296 @@ def test_show_replicas(connection):
interactive_mg_runner.stop(MEMGRAPH_INSTANCES_DESCRIPTION, "replica_4")
# We leave some time for the main to realise the replicas are down.
time.sleep(2)
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
def retrieve_data():
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "invalid"),
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "invalid"),
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "invalid"),
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "invalid"),
("replica_3", "127.0.0.1:10003", "async", 0, 0, "invalid"),
("replica_4", "127.0.0.1:10004", "async", 0, 0, "invalid"),
}
assert expected_data == actual_data
actual_data = mg_sleep_and_assert(expected_data, retrieve_data)
assert actual_data == expected_data
def test_add_replica_invalid_timeout(connection):
# Goal of this test is to check the registration of replica with invalid timeout raises an exception
def test_basic_recovery(connection):
# Goal of this test is to check the recovery of main.
# 0/ We start all replicas manually: we want to be able to kill them ourselves without relying on external tooling to kill processes.
# 1/ We check that all replicas have the correct state: they should all be ready.
# 2/ We kill main.
# 3/ We re-start main.
# 4/ We check that all replicas have the correct state: they should all be ready.
# 5/ Drop one replica.
# 6/ We add some data to main, then kill it and restart.
# 7/ We check that all replicas but one have the expected data.
# 8/ We kill another replica.
# 9/ We add some data to main.
# 10/ We re-add the two replicas droped/killed and check the data.
# 11/ We kill another replica.
# 12/ Add some more data to main.
# 13/ Check the states of replicas.
# 0/
data_directory = tempfile.TemporaryDirectory()
CONFIGURATION = {
"replica_1": {
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
"log_file": "replica1.log",
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"],
},
"replica_2": {
"args": ["--bolt-port", "7689", "--log-level=TRACE"],
"log_file": "replica2.log",
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
},
"replica_3": {
"args": ["--bolt-port", "7690", "--log-level=TRACE"],
"log_file": "replica3.log",
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10003;"],
},
"replica_4": {
"args": ["--bolt-port", "7691", "--log-level=TRACE"],
"log_file": "replica4.log",
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10004;"],
},
"main": {
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
"args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
"log_file": "main.log",
"setup_queries": [],
"data_directory": f"{data_directory.name}",
},
}
interactive_mg_runner.start_all(CONFIGURATION)
cursor = connection(7687, "main").cursor()
# We want to execute manually and not via the configuration, otherwise re-starting main would also execute these registration.
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001';")
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002';")
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003';")
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_4 ASYNC TO '127.0.0.1:10004';")
# 1/
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", 0, 0, "ready"),
}
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
assert actual_data == expected_data
def check_roles():
assert "main" == interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("SHOW REPLICATION ROLE;")[0][0]
for index in range(1, 4):
assert (
"replica"
== interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query("SHOW REPLICATION ROLE;")[0][0]
)
check_roles()
# 2/
interactive_mg_runner.kill(CONFIGURATION, "main")
# 3/
interactive_mg_runner.start(CONFIGURATION, "main")
cursor = connection(7687, "main").cursor()
check_roles()
# 4/
def retrieve_data():
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
actual_data = mg_sleep_and_assert(expected_data, retrieve_data)
assert actual_data == expected_data
# 5/
execute_and_fetch_all(cursor, "DROP REPLICA replica_2;")
# 6/
execute_and_fetch_all(cursor, "CREATE (p1:Number {name:'Magic', value:42})")
interactive_mg_runner.kill(CONFIGURATION, "main")
interactive_mg_runner.start(CONFIGURATION, "main")
cursor = connection(7687, "main").cursor()
check_roles()
# 7/
QUERY_TO_CHECK = "MATCH (node) return node;"
res_from_main = execute_and_fetch_all(cursor, QUERY_TO_CHECK)
assert len(res_from_main) == 1
for index in (1, 3, 4):
assert res_from_main == interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query(QUERY_TO_CHECK)
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 2, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 2, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", 2, 0, "ready"),
}
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
assert actual_data == expected_data
# Replica_2 was dropped, we check it does not have the data from main.
assert len(interactive_mg_runner.MEMGRAPH_INSTANCES["replica_2"].query(QUERY_TO_CHECK)) == 0
# 8/
interactive_mg_runner.kill(CONFIGURATION, "replica_3")
# 9/
execute_and_fetch_all(cursor, "CREATE (p1:Number {name:'Magic_again', value:43})")
res_from_main = execute_and_fetch_all(cursor, QUERY_TO_CHECK)
assert len(res_from_main) == 2
# 10/
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002';")
interactive_mg_runner.start(CONFIGURATION, "replica_3")
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 6, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 6, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 6, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", 6, 0, "ready"),
}
def retrieve_data2():
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
actual_data = mg_sleep_and_assert(expected_data, retrieve_data2)
assert actual_data == expected_data
for index in (1, 2, 3, 4):
assert interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query(QUERY_TO_CHECK) == res_from_main
# 11/
interactive_mg_runner.kill(CONFIGURATION, "replica_1")
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "invalid"),
("replica_2", "127.0.0.1:10002", "sync", 6, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 6, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", 6, 0, "ready"),
}
def retrieve_data3():
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
actual_data = mg_sleep_and_assert(expected_data, retrieve_data3)
assert actual_data == expected_data
# 12/
execute_and_fetch_all(cursor, "CREATE (p1:Number {name:'Magic_again_again', value:44})")
res_from_main = execute_and_fetch_all(cursor, QUERY_TO_CHECK)
assert len(res_from_main) == 3
for index in (2, 3, 4):
assert interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query(QUERY_TO_CHECK) == res_from_main
# 13/
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "invalid"),
("replica_2", "127.0.0.1:10002", "sync", 9, 0, "ready"),
("replica_3", "127.0.0.1:10003", "async", 9, 0, "ready"),
("replica_4", "127.0.0.1:10004", "async", 9, 0, "ready"),
}
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
assert actual_data == expected_data
def test_conflict_at_startup(connection):
# Goal of this test is to check starting up several instance with different replicas' configuration directory works as expected.
# main_1 and main_2 have different directory.
data_directory1 = tempfile.TemporaryDirectory()
data_directory2 = tempfile.TemporaryDirectory()
CONFIGURATION = {
"main_1": {
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
"log_file": "main1.log",
"setup_queries": [],
"data_directory": f"{data_directory1.name}",
},
"main_2": {
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
"log_file": "main2.log",
"setup_queries": [],
"data_directory": f"{data_directory2.name}",
},
}
interactive_mg_runner.start_all(CONFIGURATION)
cursor_1 = connection(7687, "main_1").cursor()
cursor_2 = connection(7688, "main_2").cursor()
assert execute_and_fetch_all(cursor_1, "SHOW REPLICATION ROLE;")[0][0] == "main"
assert execute_and_fetch_all(cursor_2, "SHOW REPLICATION ROLE;")[0][0] == "main"
def test_basic_recovery_when_replica_is_kill_when_main_is_down(connection):
# Goal of this test is to check the recovery of main.
# 0/ We start all replicas manually: we want to be able to kill them ourselves without relying on external tooling to kill processes.
# 1/ We check that all replicas have the correct state: they should all be ready.
# 2/ We kill main then kill a replica.
# 3/ We re-start main: it should be able to restart.
# 4/ Check status of replica: replica_2 is invalid.
data_directory = tempfile.TemporaryDirectory()
CONFIGURATION = {
"replica_1": {
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
"log_file": "replica1.log",
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"],
},
"replica_2": {
"args": ["--bolt-port", "7689", "--log-level=TRACE"],
"log_file": "replica2.log",
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
},
"main": {
"args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
"log_file": "main.log",
"setup_queries": [],
"data_directory": f"{data_directory.name}",
},
}
interactive_mg_runner.start_all(CONFIGURATION)
cursor = connection(7687, "main").cursor()
# We want to execute manually and not via the configuration, otherwise re-starting main would also execute these registration.
interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001';")
interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002';")
with pytest.raises(mgclient.DatabaseError):
execute_and_fetch_all(
cursor,
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 0 TO '127.0.0.1:10001';",
)
# 1/
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
}
actual_data = set(interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("SHOW REPLICAS;"))
with pytest.raises(mgclient.DatabaseError):
execute_and_fetch_all(
cursor,
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT -5 TO '127.0.0.1:10001';",
)
assert actual_data == expected_data
actual_data = execute_and_fetch_all(cursor, "SHOW REPLICAS;")
assert 0 == len(actual_data)
def check_roles():
assert "main" == interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("SHOW REPLICATION ROLE;")[0][0]
for index in range(1, 2):
assert (
"replica"
== interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query("SHOW REPLICATION ROLE;")[0][0]
)
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10001';")
actual_data = execute_and_fetch_all(cursor, "SHOW REPLICAS;")
assert 1 == len(actual_data)
check_roles()
# 2/
interactive_mg_runner.kill(CONFIGURATION, "main")
interactive_mg_runner.kill(CONFIGURATION, "replica_2")
# 3/
interactive_mg_runner.start(CONFIGURATION, "main")
# 4/
expected_data = {
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "invalid"),
}
actual_data = set(interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("SHOW REPLICAS;"))
assert actual_data == expected_data
if __name__ == "__main__":

View File

@@ -29,8 +29,8 @@ template_cluster: &template_cluster
args: ["--bolt-port", "7687", "--log-level=TRACE"]
log_file: "replication-e2e-main.log"
setup_queries: [
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001'",
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
"REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001'",
"REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002'",
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003'"
]
<<: *template_validation_queries
@@ -69,8 +69,8 @@ workloads:
args: ["--bolt-port", "7687", "--log-level=TRACE"]
log_file: "replication-e2e-main.log"
setup_queries: [
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001'",
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
"REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001'",
"REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002'",
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003'"
]
validation_queries: []

View File

@@ -4,7 +4,7 @@ bolt_port: &bolt_port "7687"
template_cluster: &template_cluster
cluster:
server:
args: ["--bolt-port=7687", "--log-level=TRACE", "--"]
args: ["--bolt-port=7687", "--log-level=TRACE"]
log_file: "server-connection-e2e.log"
template_cluster_ssl: &template_cluster_ssl
cluster:
@@ -18,7 +18,6 @@ template_cluster_ssl: &template_cluster_ssl
*cert_file,
"--bolt-key-file",
*key_file,
"--",
]
log_file: "server-connection-ssl-e2e.log"
ssl: true

View File

@@ -9,3 +9,5 @@ copy_streams_e2e_python_files(streams_owner_tests.py)
copy_streams_e2e_python_files(pulsar_streams_tests.py)
add_subdirectory(transformations)
copy_e2e_python_files_from_parent_folder(streams ".." mg_utils.py)

View File

@@ -13,6 +13,7 @@ import mgclient
import pytest
import time
from mg_utils import mg_sleep_and_assert
from multiprocessing import Manager, Process, Value
# These are the indices of the different values in the result of SHOW STREAM
@@ -115,10 +116,7 @@ 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}")
@@ -159,12 +157,7 @@ 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
@@ -325,42 +318,24 @@ 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
)
@@ -415,32 +390,20 @@ 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
)
@@ -465,8 +428,10 @@ def test_start_stream_with_batch_limit(connection, stream_creator, messages_send
thread_stream_running = Process(target=start_new_stream_with_limit, daemon=True, args=(STREAM_NAME, BATCH_LIMIT))
thread_stream_running.start()
time.sleep(2)
assert get_is_running(cursor, STREAM_NAME)
def is_running():
return get_is_running(cursor, STREAM_NAME)
assert mg_sleep_and_assert(True, is_running)
messages_sender(BATCH_LIMIT - 1)
@@ -476,10 +441,8 @@ def test_start_stream_with_batch_limit(connection, stream_creator, messages_send
# We send a last message to reach the batch_limit
messages_sender(1)
time.sleep(2)
# We check that the stream has correctly stoped.
assert not get_is_running(cursor, STREAM_NAME)
assert not mg_sleep_and_assert(False, is_running)
def test_start_stream_with_batch_limit_timeout(connection, stream_creator):
@@ -505,10 +468,7 @@ 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 (
@@ -524,10 +484,7 @@ 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()
@@ -548,8 +505,11 @@ def test_start_stream_with_batch_limit_while_check_running(
# 1/
thread_stream_check = Process(target=start_check_stream, daemon=True, args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT))
thread_stream_check.start()
time.sleep(2)
assert get_is_running(cursor, STREAM_NAME)
def is_running():
return get_is_running(cursor, STREAM_NAME)
assert mg_sleep_and_assert(True, is_running)
with pytest.raises(mgclient.DatabaseError):
start_stream_with_limit(cursor, STREAM_NAME, BATCH_LIMIT, timeout=TIMEOUT)
@@ -562,18 +522,15 @@ 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)
assert get_is_running(cursor, STREAM_NAME)
assert mg_sleep_and_assert(True, is_running)
message_sender(SIMPLE_MSG)
time.sleep(2)
assert not get_is_running(cursor, STREAM_NAME)
assert not mg_sleep_and_assert(False, is_running)
def test_check_while_stream_with_batch_limit_running(connection, stream_creator, message_sender):
@@ -587,10 +544,7 @@ 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
@@ -602,42 +556,34 @@ 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()
time.sleep(2)
assert get_is_running(cursor, STREAM_NAME)
def is_running():
return get_is_running(cursor, STREAM_NAME)
assert mg_sleep_and_assert(True, is_running)
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!"
message_sender(SIMPLE_MSG)
time.sleep(2)
assert not get_is_running(cursor, STREAM_NAME)
assert not mg_sleep_and_assert(False, is_running)
# 2/
thread_stream_check = Process(target=start_check_stream, daemon=True, args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT))
start_time = time.time()
thread_stream_check.start()
time.sleep(2)
assert get_is_running(cursor, STREAM_NAME)
assert mg_sleep_and_assert(True, is_running)
message_sender(SIMPLE_MSG)
time.sleep(2)
end_time = time.time()
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
assert not get_is_running(cursor, STREAM_NAME)
assert not mg_sleep_and_assert(False, is_running)
def test_start_stream_with_batch_limit_with_invalid_batch_limit(connection, stream_creator):
@@ -686,10 +632,7 @@ 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!"
@@ -699,10 +642,7 @@ 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!"

View File

@@ -37,22 +37,29 @@ 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
@@ -73,5 +80,6 @@ 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

View File

@@ -15,15 +15,13 @@ import sys
import pytest
import mgclient
import time
from mg_utils import mg_sleep_and_assert
from multiprocessing import Process, Value
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)
@@ -466,14 +464,13 @@ 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)
def is_running():
return common.get_is_running(cursor, stream_name)
assert mg_sleep_and_assert(True, is_running)
message_sender(common.SIMPLE_MSG)
thread_stream_check.join()

View File

@@ -18,20 +18,13 @@ 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}"'}
)
@@ -44,7 +37,6 @@ def test_simple(pulsar_client, pulsar_topics, connection, transformation):
f"CREATE PULSAR STREAM test TOPICS '{','.join(pulsar_topics)}' TRANSFORM {transformation}",
)
common.start_stream(cursor, "test")
time.sleep(5)
for topic in pulsar_topics:
producer = pulsar_client.create_producer(
@@ -73,8 +65,6 @@ def test_separate_consumers(pulsar_client, pulsar_topics, connection, transforma
for stream_name in stream_names:
common.start_stream(cursor, stream_name)
time.sleep(5)
for topic in pulsar_topics:
producer = pulsar_client.create_producer(topic, send_timeout_millis=60000)
producer.send(common.SIMPLE_MSG)
@@ -96,7 +86,6 @@ def test_start_from_latest_messages(pulsar_client, pulsar_topics, connection):
f"CREATE PULSAR STREAM test TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple",
)
common.start_stream(cursor, "test")
time.sleep(1)
def assert_message_not_consumed(message):
vertices_with_msg = common.execute_and_fetch_all(
@@ -107,8 +96,7 @@ 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)
@@ -162,11 +150,9 @@ def test_check_stream(pulsar_client, pulsar_topics, connection, transformation):
f"CREATE PULSAR STREAM test TOPICS {pulsar_topics[0]} TRANSFORM {transformation} BATCH_SIZE {BATCH_SIZE}",
)
common.start_stream(cursor, "test")
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)
@@ -272,8 +258,7 @@ 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):
@@ -318,17 +303,14 @@ def test_restart_after_error(pulsar_client, pulsar_topics, connection):
)
common.start_stream(cursor, "test_stream")
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"))
common.start_stream(cursor, "test_stream")
time.sleep(1)
producer.send(b"CREATE (n:VERTEX { id : 42 })")
assert common.check_one_result_row(cursor, "MATCH (n:VERTEX { id : 42 }) RETURN n")
@@ -343,7 +325,6 @@ def test_service_url(pulsar_client, pulsar_topics, connection, transformation):
f"CREATE PULSAR STREAM test TOPICS {','.join(pulsar_topics)} TRANSFORM {transformation} SERVICE_URL '{LOCAL}'",
)
common.start_stream(cursor, "test")
time.sleep(5)
for topic in pulsar_topics:
producer = pulsar_client.create_producer(
@@ -362,8 +343,7 @@ 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):
@@ -398,8 +378,7 @@ 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):
@@ -415,8 +394,7 @@ 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):
@@ -434,8 +412,7 @@ 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):
@@ -453,8 +430,7 @@ 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):

View File

@@ -15,7 +15,6 @@ import time
import mgclient
import common
def get_cursor_with_user(username):
connection = common.connect(username=username, password="")
return connection.cursor()
@@ -23,21 +22,23 @@ 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)
@@ -45,9 +46,11 @@ 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")
@@ -55,9 +58,11 @@ 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):
@@ -68,16 +73,12 @@ 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):
@@ -92,10 +93,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
@@ -103,19 +104,24 @@ 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):
@@ -129,12 +135,13 @@ 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(
stream_cursor,
"CREATE KAFKA STREAM insufficient_test " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
)
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.start_stream(stream_cursor, "insufficient_test")
time.sleep(1)
@@ -142,9 +149,11 @@ 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__":

View File

@@ -23,10 +23,7 @@ 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
@@ -47,17 +44,13 @@ 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}"}
)
)

View File

@@ -59,9 +59,7 @@ 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()):

View File

@@ -11,7 +11,6 @@
import mgp
@mgp.write_proc
def create_vertex(ctx: mgp.ProcCtx, id: mgp.Any) -> mgp.Record(v=mgp.Any):
v = None
@@ -37,14 +36,15 @@ 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,20 +61,19 @@ 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)

View File

@@ -13,7 +13,8 @@ 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()

View File

@@ -13,7 +13,8 @@ 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())

View File

@@ -35,12 +35,13 @@ 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)
@@ -53,25 +54,29 @@ 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())

View File

@@ -13,7 +13,8 @@ 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):
@@ -21,19 +22,15 @@ 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"
@@ -44,7 +41,8 @@ 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")
@@ -52,13 +50,14 @@ 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]
@@ -66,10 +65,8 @@ 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"
@@ -92,23 +89,24 @@ 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 == {}
@@ -122,10 +120,9 @@ 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}
@@ -133,68 +130,60 @@ 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)

View File

@@ -20,7 +20,6 @@ from neo4j import GraphDatabase, basic_auth
# Helper class and functions
class TestResults:
def __init__(self):
self.total = 0
@@ -40,16 +39,18 @@ 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()
@@ -62,7 +63,8 @@ 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()
@@ -85,5 +87,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)

View File

@@ -55,14 +55,22 @@ 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
@@ -88,5 +96,5 @@ def main():
return behave_main(behave_args)
if __name__ == "__main__":
if __name__ == '__main__':
sys.exit(main())

View File

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

View File

@@ -11,7 +11,6 @@
# -*- coding: utf-8 -*-
def query(q, context, params={}):
"""
Function used to execute query on database. Query results are
@@ -45,7 +44,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()

View File

@@ -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("a TypeError should be raised at compile time: IncomparableValues")
@then(u'a TypeError should be raised at compile time: IncomparableValues')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: RequiresDirectedRelationship")
@then(u'a SyntaxError should be raised at compile time: RequiresDirectedRelationship')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidRelationshipPattern")
@then(u'a SyntaxError should be raised at compile time: InvalidRelationshipPattern')
def syntax_error(context):
handle_error(context)
@then("a TypeError should be raised at runtime: MapElementAccessByNonString")
@then(u'a TypeError should be raised at runtime: MapElementAccessByNonString')
def type_error(context):
handle_error(context)
@then("a ConstraintVerificationFailed should be raised at runtime: DeleteConnectedNode")
@then(u'a ConstraintVerificationFailed should be raised at runtime: DeleteConnectedNode')
def step(context):
handle_error(context)
@then("a TypeError should be raised at runtime: ListElementAccessByNonInteger")
@then(u'a TypeError should be raised at runtime: ListElementAccessByNonInteger')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidArgumentType")
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentType')
def step(context):
handle_error(context)
@then("a TypeError should be raised at runtime: InvalidElementAccess")
@then(u'a TypeError should be raised at runtime: InvalidElementAccess')
def step(context):
handle_error(context)
@then("a ArgumentError should be raised at runtime: NumberOutOfRange")
@then(u'a ArgumentError should be raised at runtime: NumberOutOfRange')
def step(context):
handle_error(context)
@then("a TypeError should be raised at runtime: InvalidArgumentValue")
@then(u'a TypeError should be raised at runtime: InvalidArgumentValue')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: VariableAlreadyBound")
@then(u'a SyntaxError should be raised at compile time: VariableAlreadyBound')
def step(context):
handle_error(context)
@then("a TypeError should be raised at runtime: IncomparableValues")
@then(u'a TypeError should be raised at runtime: IncomparableValues')
def step(context):
handle_error(context)
@then("a TypeError should be raised at runtime: PropertyAccessOnNonMap")
@then(u'a TypeError should be raised at runtime: PropertyAccessOnNonMap')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidUnicodeLiteral")
@then(u'a SyntaxError should be raised at compile time: InvalidUnicodeLiteral')
def step(context):
handle_error(context)
@then("a SemanticError should be raised at compile time: MergeReadOwnWrites")
@then(u'a SemanticError should be raised at compile time: MergeReadOwnWrites')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidAggregation")
@then(u'a SyntaxError should be raised at compile time: InvalidAggregation')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: NoExpressionAlias")
@then(u'a SyntaxError should be raised at compile time: NoExpressionAlias')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: UndefinedVariable")
@then(u'a SyntaxError should be raised at compile time: UndefinedVariable')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: VariableTypeConflict")
@then(u'a SyntaxError should be raised at compile time: VariableTypeConflict')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: DifferentColumnsInUnion")
@then(u'a SyntaxError should be raised at compile time: DifferentColumnsInUnion')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidClauseComposition")
@then(u'a SyntaxError should be raised at compile time: InvalidClauseComposition')
def step(context):
handle_error(context)
@then("a TypeError should be raised at compile time: InvalidPropertyType")
@then(u'a TypeError should be raised at compile time: InvalidPropertyType')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: ColumnNameConflict")
@then(u'a SyntaxError should be raised at compile time: ColumnNameConflict')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: NoVariablesInScope")
@then(u'a SyntaxError should be raised at compile time: NoVariablesInScope')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidDelete")
@then(u'a SyntaxError should be raised at compile time: InvalidDelete')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: NegativeIntegerArgument")
@then(u'a SyntaxError should be raised at compile time: NegativeIntegerArgument')
def step(context):
handle_error(context)
@then("a EntityNotFound should be raised at runtime: DeletedEntityAccess")
@then(u'a EntityNotFound should be raised at runtime: DeletedEntityAccess')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: RelationshipUniquenessViolation")
@then(u'a SyntaxError should be raised at compile time: RelationshipUniquenessViolation')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: CreatingVarLength")
@then(u'a SyntaxError should be raised at compile time: CreatingVarLength')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidParameterUse")
@then(u'a SyntaxError should be raised at compile time: InvalidParameterUse')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: FloatingPointOverflow")
@then(u'a SyntaxError should be raised at compile time: FloatingPointOverflow')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time InvalidArgumentExpression")
@then(u'a SyntaxError should be raised at compile time InvalidArgumentExpression')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time InvalidUnicodeCharacter")
@then(u'a SyntaxError should be raised at compile time InvalidUnicodeCharacter')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: NonConstantExpression")
@then(u'a SyntaxError should be raised at compile time: NonConstantExpression')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: NoSingleRelationshipType")
@then(u'a SyntaxError should be raised at compile time: NoSingleRelationshipType')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: UnknownFunction")
@then(u'a SyntaxError should be raised at compile time: UnknownFunction')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidNumberLiteral")
@then(u'a SyntaxError should be raised at compile time: InvalidNumberLiteral')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidArgumentExpression")
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentExpression')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidUnicodeCharacter")
@then(u'a SyntaxError should be raised at compile time: InvalidUnicodeCharacter')
def step(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidArgumentPassingMode")
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentPassingMode')
def step_impl(context):
handle_error(context)
@then("a SyntaxError should be raised at compile time: InvalidNumberOfArguments")
@then(u'a SyntaxError should be raised at compile time: InvalidNumberOfArguments')
def step_impl(context):
handle_error(context)
@then("a ParameterMissing should be raised at compile time: MissingParameter")
@then(u'a ParameterMissing should be raised at compile time: MissingParameter')
def step_impl(context):
handle_error(context)
@then("a ProcedureError should be raised at compile time: ProcedureNotFound")
@then(u'a ProcedureError should be raised at compile time: ProcedureNotFound')
def step_impl(context):
handle_error(context)

View File

@@ -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,18 +46,20 @@ 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:
@@ -66,9 +68,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)

View File

@@ -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,27 +89,28 @@ 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
@@ -122,23 +123,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):
@@ -151,8 +152,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)
@@ -165,25 +166,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):
@@ -194,25 +195,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

View File

@@ -17,29 +17,32 @@ 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):
@@ -90,11 +93,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 += ")"
@@ -106,7 +109,7 @@ def to_string(element):
if element.type:
sol += element.type
if element.keys():
sol += " "
sol += ' '
sol += parse_props(element.items())
sol += "]"
return sol
@@ -141,12 +144,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
@@ -159,22 +162,23 @@ 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)
@@ -197,14 +201,9 @@ 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
@@ -222,7 +221,9 @@ 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
@@ -241,13 +242,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):
@@ -266,26 +267,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)
@@ -294,20 +295,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

View File

@@ -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

View File

@@ -57,3 +57,43 @@ Feature: Case
Then the result should be:
| z |
| ['nottwo', 'two', 'nottwo'] |
Scenario: Simple CASE nullcheck does not have match:
Given an empty graph
When executing query:
"""
WITH 2 AS name RETURN CASE name WHEN 3 THEN 'something went wrong' WHEN null THEN "doesn't work" ELSE 'works' END
"""
Then the result should be:
| CASE name WHEN 3 THEN 'something went wrong' WHEN null THEN "doesn't work" ELSE 'works' END |
| 'works' |
Scenario: Simple CASE nullcheck does have match:
Given an empty graph
When executing query:
"""
WITH 2 AS name RETURN CASE name WHEN 2 THEN 'works' WHEN null THEN "doesn't work" ELSE 'something went wrong' END
"""
Then the result should be:
| CASE name WHEN 2 THEN 'works' WHEN null THEN "doesn't work" ELSE 'something went wrong' END |
| 'works' |
Scenario: Generic CASE nullcheck does have match:
Given an empty graph
When executing query:
"""
WITH 2 AS name RETURN CASE WHEN name is NULL THEN "doesn't work" WHEN name = 2 THEN "works" ELSE "something went wrong" END
"""
Then the result should be:
| CASE WHEN name is NULL THEN "doesn't work" WHEN name = 2 THEN "works" ELSE "something went wrong" END |
| 'works' |
Scenario: Generic CASE expression is null:
Given an empty graph
When executing query:
"""
WITH null AS name RETURN CASE name WHEN null THEN "doesn't work" WHEN 2 THEN "doesn't work" ELSE 'works' END
"""
Then the result should be:
| CASE name WHEN null THEN "doesn't work" WHEN 2 THEN "doesn't work" ELSE 'works' END |
| 'works' |

View File

@@ -37,14 +37,20 @@ 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", {}),
@@ -68,8 +74,7 @@ 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)))
@@ -87,13 +92,8 @@ 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,17 +109,10 @@ 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
@@ -127,13 +120,15 @@ 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)

View File

@@ -29,8 +29,15 @@ 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",
@@ -40,43 +47,116 @@ 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):
@@ -86,15 +166,8 @@ 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")
@@ -127,28 +200,18 @@ 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)))
@@ -164,13 +227,11 @@ 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()
@@ -182,14 +243,12 @@ 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:
@@ -197,43 +256,35 @@ 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:
@@ -262,8 +313,10 @@ 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)

View File

@@ -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,30 +52,32 @@ 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)
@@ -102,21 +104,22 @@ def execute_test(memgraph_binary, dump_binary, test_directory, test_type, write_
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")
@@ -138,17 +141,15 @@ 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
@@ -160,17 +161,26 @@ 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)

View File

@@ -52,14 +52,8 @@ 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]
@@ -82,14 +76,18 @@ 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)
@@ -108,13 +106,10 @@ 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):
@@ -124,7 +119,8 @@ 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):
@@ -141,7 +137,8 @@ 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:
@@ -173,15 +170,18 @@ 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,13 +192,15 @@ 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()
@@ -227,22 +229,28 @@ 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()
@@ -259,16 +267,22 @@ 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()
@@ -276,8 +290,11 @@ 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()
@@ -285,8 +302,11 @@ 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()
@@ -294,7 +314,8 @@ 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()
@@ -305,10 +326,12 @@ 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()
@@ -316,25 +339,33 @@ 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()
@@ -344,25 +375,22 @@ 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!"
@@ -381,7 +409,8 @@ 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())

View File

@@ -46,18 +46,17 @@ 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!"
@@ -71,12 +70,14 @@ 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()
@@ -85,34 +86,27 @@ 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
@@ -131,16 +125,19 @@ def execute_test(
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!"
@@ -154,17 +151,17 @@ def execute_test(
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:
@@ -176,16 +173,18 @@ def execute_test(
# 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)
@@ -194,8 +193,7 @@ 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
@@ -213,14 +211,7 @@ 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)

View File

@@ -36,7 +36,8 @@ 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
@@ -47,15 +48,9 @@ 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])
@@ -66,7 +61,8 @@ 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
@@ -92,14 +88,16 @@ 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)
@@ -110,23 +108,19 @@ 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

View File

@@ -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

View File

@@ -3,4 +3,4 @@
"n3" {:replication-role :replica :replication-mode :async :port 10000}}
{"n1" {:replication-role :main}
"n2" {:replication-role :replica :replication-mode :async :port 10000}
"n3" {:replication-role :replica :replication-mode :sync :port 10000 :timeout 3}}]
"n3" {:replication-role :replica :replication-mode :sync :port 10000}}]

View File

@@ -31,7 +31,7 @@
[node-config]
(case (:replication-mode node-config)
:async "ASYNC"
:sync (str "SYNC" (when-let [timeout (:timeout node-config)] (str " WITH TIMEOUT " timeout)))))
:sync "SYNC" ))
(defn create-register-replica-query
[name node-config]

View File

@@ -129,13 +129,7 @@
:replication-mode
(str "Invalid node configuration. "
"Every replication node requires "
":replication-mode to be defined."))
(throw-if-key-missing-in-any
(filter #(= (:replication-mode %) :sync) replica-nodes-configs)
:timeout
(str "Invalid node confiruation. "
"Every SYNC replication node requires "
":timeout to be defined."))))
":replication-mode to be defined."))))
(map (fn [node-config] (resolve-all-node-hostnames
(merge

View File

@@ -25,7 +25,8 @@
:--storage-recover-on-startup
:--storage-wal-enabled
:--storage-snapshot-interval-sec 300
:--storage-properties-on-edges))
:--storage-properties-on-edges
:--storage-restore-replicas-on-startup false))
(defn stop-node!
[test node]

View File

@@ -34,8 +34,7 @@ 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"
@@ -54,36 +53,29 @@ 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"]
@@ -102,8 +94,7 @@ 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)
@@ -122,41 +113,32 @@ 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

View File

@@ -14,11 +14,9 @@ from argparse import ArgumentParser
try:
import jail
APOLLO = True
except:
import jail_faker as jail
APOLLO = False
@@ -47,15 +45,13 @@ 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)

View File

@@ -36,12 +36,13 @@ 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")
@@ -54,12 +55,8 @@ 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:
@@ -85,13 +82,15 @@ 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
@@ -106,23 +105,24 @@ 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!")

Some files were not shown because too many files have changed in this diff Show More