From 97d45ab1d87e73414f6d077530924dc7bf512272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ante=20Pu=C5=A1i=C4=87?= Date: Tue, 7 Mar 2023 15:41:19 +0100 Subject: [PATCH] Add Python query module API mock (#757) --- include/_mgp_mock.py | 343 ++++ include/mgp_mock.py | 1655 ++++++++++++++++++ src/CMakeLists.txt | 4 + tests/e2e/CMakeLists.txt | 1 + tests/e2e/mock_api/CMakeLists.txt | 8 + tests/e2e/mock_api/common.py | 14 + tests/e2e/mock_api/procedures/CMakeLists.txt | 10 + tests/e2e/mock_api/procedures/edge.py | 85 + tests/e2e/mock_api/procedures/edge_type.py | 33 + tests/e2e/mock_api/procedures/graph.py | 102 ++ tests/e2e/mock_api/procedures/label.py | 52 + tests/e2e/mock_api/procedures/path.py | 65 + tests/e2e/mock_api/procedures/properties.py | 184 ++ tests/e2e/mock_api/procedures/record.py | 18 + tests/e2e/mock_api/procedures/test_utils.py | 160 ++ tests/e2e/mock_api/procedures/vertex.py | 101 ++ tests/e2e/mock_api/procedures/vertices.py | 32 + tests/e2e/mock_api/test_compare_mock.py | 195 +++ tests/e2e/mock_api/workloads.yaml | 83 + tests/setup.sh | 1 + tools/requirements.txt | 1 + 21 files changed, 3147 insertions(+) create mode 100644 include/_mgp_mock.py create mode 100644 include/mgp_mock.py create mode 100644 tests/e2e/mock_api/CMakeLists.txt create mode 100644 tests/e2e/mock_api/common.py create mode 100644 tests/e2e/mock_api/procedures/CMakeLists.txt create mode 100644 tests/e2e/mock_api/procedures/edge.py create mode 100644 tests/e2e/mock_api/procedures/edge_type.py create mode 100644 tests/e2e/mock_api/procedures/graph.py create mode 100644 tests/e2e/mock_api/procedures/label.py create mode 100644 tests/e2e/mock_api/procedures/path.py create mode 100644 tests/e2e/mock_api/procedures/properties.py create mode 100644 tests/e2e/mock_api/procedures/record.py create mode 100644 tests/e2e/mock_api/procedures/test_utils.py create mode 100644 tests/e2e/mock_api/procedures/vertex.py create mode 100644 tests/e2e/mock_api/procedures/vertices.py create mode 100644 tests/e2e/mock_api/test_compare_mock.py create mode 100644 tests/e2e/mock_api/workloads.yaml diff --git a/include/_mgp_mock.py b/include/_mgp_mock.py new file mode 100644 index 000000000..5e940b3ee --- /dev/null +++ b/include/_mgp_mock.py @@ -0,0 +1,343 @@ +import typing +from enum import Enum + +import networkx as nx + +NX_LABEL_ATTR = "labels" +NX_TYPE_ATTR = "type" + +SOURCE_TYPE_KAFKA = "SOURCE_TYPE_KAFKA" +SOURCE_TYPE_PULSAR = "SOURCE_TYPE_PULSAR" + +""" +This module provides helpers for the mock Python API, much like _mgp.py does for mgp.py. +""" + + +class InvalidArgumentError(Exception): + """ + Signals that some of the arguments have invalid values. + """ + + pass + + +class ImmutableObjectError(Exception): + pass + + +class LogicErrorError(Exception): + pass + + +class DeletedObjectError(Exception): + pass + + +class EdgeConstants(Enum): + I_START = 0 + I_END = 1 + I_KEY = 2 + + +class Graph: + """Wrapper around a NetworkX MultiDiGraph instance.""" + + __slots__ = ("nx", "_highest_vertex_id", "_highest_edge_id", "_valid") + + def __init__(self, graph: nx.MultiDiGraph) -> None: + if not isinstance(graph, nx.MultiDiGraph): + raise TypeError(f"Expected 'networkx.classes.multidigraph.MultiDiGraph', got '{type(graph)}'") + + self.nx = graph + self._highest_vertex_id = None + self._highest_edge_id = None + self._valid = True + + @property + def vertex_ids(self): + return self.nx.nodes + + def vertex_is_isolate(self, vertex_id: int) -> bool: + return nx.is_isolate(self.nx, vertex_id) + + @property + def vertices(self): + return (Vertex(node_id, self) for node_id in self.nx.nodes) + + def has_node(self, node_id): + return self.nx.has_node(node_id) + + @property + def edges(self): + return self.nx.edges + + def is_valid(self) -> bool: + return self._valid + + def get_vertex_by_id(self, vertex_id: int) -> "Vertex": + return Vertex(vertex_id, self) + + def invalidate(self): + self._valid = False + + def is_immutable(self) -> bool: + return nx.is_frozen(self.nx) + + def make_immutable(self): + self.nx = nx.freeze(self.nx) + + def _new_vertex_id(self): + if self._highest_vertex_id is None: + self._highest_vertex_id = max(vertex_id for vertex_id in self.nx.nodes) + + return self._highest_vertex_id + 1 + + def _new_edge_id(self): + if self._highest_edge_id is None: + self._highest_edge_id = max(edge[EdgeConstants.I_KEY.value] for edge in self.nx.edges(keys=True)) + + return self._highest_edge_id + 1 + + def create_vertex(self) -> "Vertex": + vertex_id = self._new_vertex_id() + + self.nx.add_node(vertex_id) + self._highest_vertex_id = vertex_id + + return Vertex(vertex_id, self) + + def create_edge(self, from_vertex: "Vertex", to_vertex: "Vertex", edge_type: str) -> "Edge": + if from_vertex.is_deleted() or to_vertex.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + edge_id = self._new_edge_id() + + from_id = from_vertex.id + to_id = to_vertex.id + + self.nx.add_edge(from_id, to_id, key=edge_id, type=edge_type) + self._highest_edge_id = edge_id + + return Edge((from_id, to_id, edge_id), self) + + def delete_vertex(self, vertex_id: int): + self.nx.remove_node(vertex_id) + + def delete_edge(self, from_vertex_id: int, to_vertex_id: int, edge_id: int): + self.nx.remove_edge(from_vertex_id, to_vertex_id, edge_id) + + @property + def highest_vertex_id(self) -> int: + if self._highest_vertex_id is None: + self._highest_vertex_id = max(vertex_id for vertex_id in self.nx.nodes) + 1 + + return self._highest_vertex_id + + @property + def highest_edge_id(self) -> int: + if self._highest_edge_id is None: + self._highest_edge_id = max(edge[EdgeConstants.I_KEY.value] for edge in self.nx.edges(keys=True)) + + return self._highest_edge_id + 1 + + +class Vertex: + """Represents a graph vertex.""" + + __slots__ = ("_id", "_graph") + + def __init__(self, id: int, graph: Graph) -> None: + if not isinstance(id, int): + raise TypeError(f"Expected 'int', got '{type(id)}'") + + if not isinstance(graph, Graph): + raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(graph)}'") + + if not graph.nx.has_node(id): + raise IndexError(f"Unable to find vertex with ID {id}.") + + self._id = id + self._graph = graph + + def is_valid(self) -> bool: + return self._graph.is_valid() + + def is_deleted(self) -> bool: + return not self._graph.nx.has_node(self._id) and self._id <= self._graph.highest_vertex_id + + @property + def underlying_graph(self) -> Graph: + return self._graph + + def underlying_graph_is_mutable(self) -> bool: + return not nx.is_frozen(self._graph.nx) + + @property + def labels(self) -> typing.List[int]: + return self._graph.nx.nodes[self._id][NX_LABEL_ATTR].split(":") + + def add_label(self, label: str) -> None: + if nx.is_frozen(self._graph.nx): + raise ImmutableObjectError("Cannot modify immutable object.") + + self._graph.nx.nodes[self._id][NX_LABEL_ATTR] += f":{label}" + + def remove_label(self, label: str) -> None: + if nx.is_frozen(self._graph.nx): + raise ImmutableObjectError("Cannot modify immutable object.") + + labels = self._graph.nx.nodes[self._id][NX_LABEL_ATTR] + if labels.startswith(f"{label}:"): + labels = "\n" + labels # pseudo-string starter + self._graph.nx.nodes[self._id][NX_LABEL_ATTR] = labels.replace(f"\n{label}:", "") + elif labels.endswith(f":{label}"): + labels += "\n" # pseudo-string terminator + self._graph.nx.nodes[self._id][NX_LABEL_ATTR] = labels.replace(f":{label}\n", "") + else: + self._graph.nx.nodes[self._id][NX_LABEL_ATTR] = labels.replace(f":{label}:", ":") + + @property + def id(self) -> int: + return self._id + + @property + def properties(self): + return ( + (key, value) + for key, value in self._graph.nx.nodes[self._id].items() + if key not in (NX_LABEL_ATTR, NX_TYPE_ATTR) + ) + + def get_property(self, property_name: str): + return self._graph.nx.nodes[self._id][property_name] + + def set_property(self, property_name: str, value: object): + self._graph.nx.nodes[self._id][property_name] = value + + @property + def in_edges(self) -> typing.Iterable["Edge"]: + return [Edge(edge, self._graph) for edge in self._graph.nx.in_edges(self._id, keys=True)] + + @property + def out_edges(self) -> typing.Iterable["Edge"]: + return [Edge(edge, self._graph) for edge in self._graph.nx.out_edges(self._id, keys=True)] + + +class Edge: + """Represents a graph edge.""" + + __slots__ = ("_edge", "_graph") + + def __init__(self, edge: typing.Tuple[int, int, int], graph: Graph) -> None: + if not isinstance(edge, typing.Tuple): + raise TypeError(f"Expected 'Tuple', got '{type(edge)}'") + + if not isinstance(graph, Graph): + raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(graph)}'") + + if not graph.nx.has_edge(*edge): + raise IndexError(f"Unable to find edge with ID {edge[EdgeConstants.I_KEY.value]}.") + + self._edge = edge + self._graph = graph + + def is_valid(self) -> bool: + return self._graph.is_valid() + + def is_deleted(self) -> bool: + return ( + not self._graph.nx.has_edge(*self._edge) + and self._edge[EdgeConstants.I_KEY.value] <= self._graph.highest_edge_id + ) + + def underlying_graph_is_mutable(self) -> bool: + return not nx.is_frozen(self._graph.nx) + + @property + def id(self) -> int: + return self._edge[EdgeConstants.I_KEY.value] + + @property + def edge(self) -> typing.Tuple[int, int, int]: + return self._edge + + @property + def start_id(self) -> int: + return self._edge[EdgeConstants.I_START.value] + + @property + def end_id(self) -> int: + return self._edge[EdgeConstants.I_END.value] + + def get_type_name(self): + return self._graph.nx.get_edge_data(*self._edge)[NX_TYPE_ATTR] + + def from_vertex(self) -> Vertex: + return Vertex(self.start_id, self._graph) + + def to_vertex(self) -> Vertex: + return Vertex(self.end_id, self._graph) + + @property + def properties(self): + return ( + (key, value) + for key, value in self._graph.nx.edges[self._edge].items() + if key not in (NX_LABEL_ATTR, NX_TYPE_ATTR) + ) + + def get_property(self, property_name: str): + return self._graph.nx.edges[self._edge][property_name] + + def set_property(self, property_name: str, value: object): + self._graph.nx.edges[self._edge][property_name] = value + + +class Path: + """Represents a path comprised of `Vertex` and `Edge` instances.""" + + __slots__ = ("_vertices", "_edges", "_graph") + __create_key = object() + + def __init__(self, create_key, vertex_id: int, graph: Graph) -> None: + assert create_key == Path.__create_key, "Path objects must be created using Path.make_with_start" + + self._vertices = [vertex_id] + self._edges = [] + self._graph = graph + + @classmethod + def make_with_start(cls, vertex: Vertex) -> "Path": + if not isinstance(vertex, Vertex): + raise TypeError(f"Expected 'Vertex', got '{type(vertex)}'") + + if not isinstance(vertex.underlying_graph, Graph): + raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(vertex.underlying_graph)}'") + + if not vertex.underlying_graph.nx.has_node(vertex._id): + raise IndexError(f"Unable to find vertex with ID {vertex._id}.") + + return Path(cls.__create_key, vertex._id, vertex.underlying_graph) + + def is_valid(self) -> bool: + return self._graph.is_valid() + + def underlying_graph_is_mutable(self) -> bool: + return not nx.is_frozen(self._graph.nx) + + def expand(self, edge: Edge): + if edge.start_id != self._vertices[-1]: + raise LogicErrorError("Logic error.") + + self._vertices.append(edge.end_id) + self._edges.append((edge.start_id, edge.end_id, edge.id)) + + def vertex_at(self, index: int) -> Vertex: + return Vertex(self._vertices[index], self._graph) + + def edge_at(self, index: int) -> Edge: + return Edge(self._edges[index], self._graph) + + def size(self) -> int: + return len(self._edges) diff --git a/include/mgp_mock.py b/include/mgp_mock.py new file mode 100644 index 000000000..45be0edbd --- /dev/null +++ b/include/mgp_mock.py @@ -0,0 +1,1655 @@ +# Copyright 2023 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. + +""" +This module provides a mock Python API for easy development of custom openCypher procedures. +The API's interface is fully consistent with the Python API in mgp.py. Because of that, you +can develop procedures without setting Memgraph up for each run, and run them from Memgraph +when they're implemented in full. +""" + +import datetime +import inspect +import sys +import typing +from collections import namedtuple +from copy import deepcopy +from functools import wraps + +import _mgp_mock + + +class InvalidContextError(Exception): + """ + Signals using a graph element instance outside of the registered procedure. + """ + + pass + + +class UnknownError(Exception): + """ + Signals unspecified failure. + """ + + pass + + +class LogicErrorError(Exception): + """ + Signals faulty logic within the program such as violating logical + preconditions or class invariants and may be preventable. + """ + + pass + + +class DeletedObjectError(Exception): + """ + Signals accessing an already deleted object. + """ + + pass + + +class InvalidArgumentError(Exception): + """ + Signals that some of the arguments have invalid values. + """ + + pass + + +class ImmutableObjectError(Exception): + """ + Signals modification of an immutable object. + """ + + pass + + +class ValueConversionError(Exception): + """ + Signals that conversion between python and cypher values failed. + """ + + pass + + +class Label: + """A vertex label.""" + + __slots__ = ("_name",) + + def __init__(self, name: str): + self._name = name + + @property + def name(self) -> str: + """ + Get the name of the label. + + Returns: + A string with the name of the label. + + Example: + ```label.name``` + """ + return self._name + + def __eq__(self, other) -> bool: + if isinstance(other, Label): + return self._name == other.name + + if isinstance(other, str): + return self._name == other + + return NotImplemented + + +# Named property value of a Vertex or an Edge. +# It would be better to use typing.NamedTuple with typed fields, but that is +# not available in Python 3.5. +Property = namedtuple("Property", ("name", "value")) + + +class Properties: + """Collection of the properties of a vertex or an edge.""" + + __slots__ = ("_vertex_or_edge", "_len") + + def __init__(self, vertex_or_edge): + if not isinstance(vertex_or_edge, (_mgp_mock.Vertex, _mgp_mock.Edge)): + raise TypeError(f"Expected _mgp_mock.Vertex or _mgp_mock.Edge, got {type(vertex_or_edge)}") + + self._len = None + self._vertex_or_edge = vertex_or_edge + + def __deepcopy__(self, memo): + # In line with the Python API, this is the same as the shallow copy. + return Properties(self._vertex_or_edge) + + def get(self, property_name: str, default=None) -> object: + """ + Get the value of the property with the given name, otherwise return the default value. + + Args: + property_name: String with the property name. + default: The value to return if there is no `property_name` property. + + Returns: + The value associated with `property_name` or, if there’s no such property, the `default` argument. + + Raises: + InvalidContextError: If the edge or vertex is out of context. + DeletedObjectError: If the edge has been deleted. + + Examples: + ``` + vertex.properties.get(property_name) + edge.properties.get(property_name) + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + try: + return self[property_name] + except KeyError: + return default + + def set(self, property_name: str, value: object) -> None: + """ + Set the value of the given property. If `value` is `None`, the property is removed. + + Args: + property_name: String with the property name. + value: The new value of the `property_name` property. + + Raises: + ImmutableObjectError: If the object is immutable. + DeletedObjectError: If the edge has been deleted. + ValueConversionError: If `value` is vertex, edge or path. + + Examples: + ``` + vertex.properties.set(property_name, value) + edge.properties.set(property_name, value) + ``` + """ + if not self._vertex_or_edge.underlying_graph_is_mutable(): + raise ImmutableObjectError("Cannot modify immutable object.") + + self[property_name] = value + + def items(self) -> typing.Iterable[Property]: + """ + Iterate over the properties. Doesn’t return a dynamic view of the properties, but copies the + current properties. + + Returns: + Iterable `Property` of names and values. + + Raises: + InvalidContextError: If the edge or vertex is out of context. + DeletedObjectError: If the edge or vertex has been deleted. + + Examples: + ``` + items = vertex.properties.items() + for it in items: + name = it.name + value = it.value + ``` + ``` + items = edge.properties.items() + for it in items: + name = it.name + value = it.value + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + if self._vertex_or_edge.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + vertex_or_edge_properties = self._vertex_or_edge.properties + + for property in vertex_or_edge_properties: + yield Property(*property) + + def keys(self) -> typing.Iterable[str]: + """ + Iterate over property names. Doesn’t return a dynamic view of the property names, but copies the + name of the current properties. + + Returns: + `Iterable` of strings that represent the property names (keys). + + Raises: + InvalidContextError: If edge or vertex is out of context. + DeletedObjectError: If the edge or vertex has been deleted. + + Examples: + ``` + graph.vertex.properties.keys() + graph.edge.properties.keys() + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + for item in self.items(): + yield item.name + + def values(self) -> typing.Iterable[object]: + """ + Iterate over property values. Doesn’t return a dynamic view of the property values, but copies the + values of the current properties. + + Returns: + `Iterable` of property values. + + Raises: + InvalidContextError: If edge or vertex is out of context. + DeletedObjectError: If the edge or vertex has been deleted. + + Examples: + ``` + vertex.properties.values() + edge.properties.values() + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + for item in self.items(): + yield item.value + + def __len__(self) -> int: + """ + Get the count of the vertex or edge’s properties. + + Returns: + The count of the stored properties. + + Raises: + InvalidContextError: If the edge or vertex is out of context. + DeletedObjectError: If the edge or vertex has been deleted. + + Examples: + ``` + len(vertex.properties) + len(edge.properties) + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + if self._len is None: + self._len = sum(1 for _ in self.items()) + + return self._len + + def __iter__(self) -> typing.Iterable[str]: + """ + Iterate over property names. + + Returns: + `Iterable` of strings that represent property names. + + Raises: + InvalidContextError: If edge or vertex is out of context. + DeletedObjectError: If the edge or vertex has been deleted. + + Examples: + ``` + iter(vertex.properties) + iter(edge.properties) + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + for item in self.items(): + yield item.name + + def __getitem__(self, property_name: str) -> object: + """ + Get the value of the property with the given name, otherwise raise a KeyError. + + Args: + property_name: String with the property name. + + Returns: + Value of the named property. + + Raises: + InvalidContextError: If edge or vertex is out of context. + DeletedObjectError: If the edge or vertex has been deleted. + + Examples: + ``` + vertex.properties[property_name] + edge.properties[property_name] + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + if self._vertex_or_edge.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + return self._vertex_or_edge.get_property(property_name) + + def __setitem__(self, property_name: str, value: object) -> None: + """ + Set the value of the given property. If `value` is `None`, the property + is removed. + + Args: + property_name: String with the property name. + value: Object that represents the value to be set. + + Raises: + InvalidContextError: If the edge or vertex is out of context. + ImmutableObjectError: If the object is immutable. + DeletedObjectError: If the edge or vertex has been deleted. + ValueConversionError: If `value` is vertex, edge or path. + + Examples: + ``` + vertex.properties[property_name] = value + edge.properties[property_name] = value + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + if not self._vertex_or_edge.underlying_graph_is_mutable(): + raise ImmutableObjectError("Cannot modify immutable object.") + + if isinstance(value, (Vertex, Edge, Path)): + raise ValueConversionError("Value conversion failed") + + if self._vertex_or_edge.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + self._vertex_or_edge.set_property(property_name, value) + + def __contains__(self, property_name: str) -> bool: + """ + Check if there is a property with the given name. + + Args: + property_name: String with the property name. + + Returns: + Boolean value that represents whether a property with the given name exists. + + Raises: + InvalidContextError: If edge or vertex is out of context. + DeletedObjectError: If the edge or vertex has been deleted. + + Examples: + ``` + if property_name in vertex.properties: + ``` + ``` + if property_name in edge.properties: + ``` + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + try: + _ = self[property_name] + return True + except KeyError: + return False + + +class EdgeType: + """Type of an Edge.""" + + __slots__ = ("_name",) + + def __init__(self, name): + self._name = name + + @property + def name(self) -> str: + """ + Get the name of an EdgeType. + + Returns: + The string with the name of the EdgeType. + + Example: + ```edge.type.name``` + """ + return self._name + + def __eq__(self, other) -> bool: + if isinstance(other, EdgeType): + return self.name == other.name + + if isinstance(other, str): + return self.name == other + + return NotImplemented + + +if sys.version_info >= (3, 5, 2): + EdgeId = typing.NewType("EdgeId", int) +else: + EdgeId = int + + +class Edge: + """Represents a graph edge. + + Access to an Edge is only valid during a single execution of a procedure in + a query. You should not globally store an instance of an Edge. Using an + invalid Edge instance will raise an InvalidContextError. + """ + + __slots__ = ("_edge",) + + def __init__(self, edge): + if not isinstance(edge, _mgp_mock.Edge): + raise TypeError(f"Expected '_mgp_mock.Edge', got '{type(edge)}'") + + self._edge = edge + + def __deepcopy__(self, memo): + # In line with the Python API, this is the same as the shallow copy. + return Edge(self._edge) + + def is_valid(self) -> bool: + """ + Check if the `Edge` is in a valid context, i.e. if it may be used. + + Returns: + A `bool` value that represents whether the edge is in a valid context. + + Examples: + ```edge.is_valid()``` + """ + return self._edge.is_valid() + + def underlying_graph_is_mutable(self) -> bool: + """ + Check if the underlying `Graph` is mutable. + + Returns: + A `bool` value that represents whether the graph is mutable. + + Raises: + InvalidContextError: If the context is not valid. + + Examples: + ```edge.underlying_graph_is_mutable()``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return self._edge.underlying_graph_is_mutable() + + @property + def id(self) -> EdgeId: + """ + Get the ID of the edge. + + Returns: + An `EdgeId` representing the edge’s ID. + + Raises: + InvalidContextError: If edge is out of context. + + Examples: + ```edge.id``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return self._edge.id + + @property + def type(self) -> EdgeType: + """ + Get the type of the `Edge`. + + Returns: + `EdgeType` representing the edge’s type. + + Raises: + InvalidContextError: If edge is out of context. + + Examples: + ```edge.type``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return EdgeType(self._edge.get_type_name()) + + @property + def from_vertex(self) -> "Vertex": + """ + Get the source (tail) vertex of the edge. + + Returns: + `Vertex` that is the edge’s source/tail. + + Raises: + InvalidContextError: If edge is out of context. + + Examples: + ```edge.from_vertex``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return Vertex(self._edge.from_vertex()) + + @property + def to_vertex(self) -> "Vertex": + """ + Get the destination (head) vertex of the edge. + + Returns: + `Vertex` that is the edge’s destination/head. + + Raises: + InvalidContextError: If edge is out of context. + + Examples: + ```edge.to_vertex``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return Vertex(self._edge.to_vertex()) + + @property + def properties(self) -> Properties: + """ + Get the edge’s properties. + + Returns: + `Properties` containing all properties of the edge. + + Raises: + InvalidContextError: If edge is out of context. + + Examples: + ```edge.properties``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return Properties(self._edge) + + def __eq__(self, other) -> bool: + if not self.is_valid(): + raise InvalidContextError() + + if not isinstance(other, Edge): + return NotImplemented + + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + + +if sys.version_info >= (3, 5, 2): + VertexId = typing.NewType("VertexId", int) +else: + VertexId = int + + +class Vertex: + """Represents a graph vertex. + + Access to a Vertex is only valid during a single execution of a procedure + in a query. You should not globally store an instance of a Vertex. Using an + invalid Vertex instance will raise an InvalidContextError. + """ + + __slots__ = ("_vertex",) + + def __init__(self, vertex): + if not isinstance(vertex, _mgp_mock.Vertex): + raise TypeError(f"Expected '_mgp_mock.Vertex', got '{type(vertex)}'") + + self._vertex = vertex + + def __deepcopy__(self, memo): + # In line with the Python API, this is the same as the shallow copy. + return Vertex(self._vertex) + + def is_valid(self) -> bool: + """ + Check if the vertex is in a valid context, i.e. if it may be used. + + Returns: + A `bool` value that represents whether the vertex is in a valid context. + + Examples: + ```vertex.is_valid()``` + """ + return self._vertex.is_valid() + + def underlying_graph_is_mutable(self) -> bool: + """ + Check if the underlying graph is mutable. + + Returns: + A `bool` value that represents whether the graph is mutable. + + Raises: + InvalidContextError: If the context is not valid. + + Examples: + ```edge.underlying_graph_is_mutable()``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return self._vertex.underlying_graph_is_mutable() + + @property + def id(self) -> VertexId: + """ + Get the ID of the vertex. + + Returns: + A `VertexId` representing the vertex’s ID. + + Raises: + InvalidContextError: If vertex is out of context. + + Examples: + ```vertex.id``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return self._vertex.id + + @property + def labels(self) -> typing.Tuple[Label]: + """ + Get the labels of the vertex. + + Returns: + A tuple of `Label` instances representing individual labels. + + Raises: + InvalidContextError: If vertex is out of context. + DeletedObjectError: If `Vertex` has been deleted. + + Examples: + ```vertex.labels``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._vertex.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + return tuple(Label(label) for label in self._vertex.labels) + + def add_label(self, label: str) -> None: + """ + Add the given label to the vertex. + + Args: + label: The label (`str`) to be added. + + Raises: + InvalidContextError: If `Vertex` is out of context. + ImmutableObjectError: If `Vertex` is immutable. + DeletedObjectError: If `Vertex` has been deleted. + + Examples: + ```vertex.add_label(label)``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._vertex.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + return self._vertex.add_label(label) + + def remove_label(self, label: str) -> None: + """ + Remove the given label from the vertex. + + Args: + label: The label (`str`) to be removed. + + Raises: + InvalidContextError: If `Vertex` is out of context. + ImmutableObjectError: If `Vertex` is immutable. + DeletedObjectError: If `Vertex` has been deleted. + + Examples: + ```vertex.remove_label(label)``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._vertex.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + return self._vertex.remove_label(label) + + @property + def properties(self) -> Properties: + """ + Get the properties of the vertex. + + Returns: + The `Properties` of the vertex. + + Raises: + InvalidContextError: If `Vertex` is out of context. + + Examples: + ```vertex.properties``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return Properties(self._vertex) + + @property + def in_edges(self) -> typing.Iterable[Edge]: + """ + Iterate over the inbound edges of the vertex. + Doesn’t return a dynamic view of the edges, but copies the current inbound edges. + + Returns: + An `Iterable` of all `Edge` objects directed towards the vertex. + + Raises: + InvalidContextError: If `Vertex` is out of context. + DeletedObjectError: If `Vertex` has been deleted. + + Examples: + ```for edge in vertex.in_edges:``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._vertex.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + for edge in self._vertex.in_edges: + yield Edge(edge) + + @property + def out_edges(self) -> typing.Iterable[Edge]: + """ + Iterate over the outbound edges of the vertex. + Doesn’t return a dynamic view of the edges, but copies the current outbound edges. + + Returns: + An `Iterable` of all `Edge` objects directed outwards from the vertex. + + Raises: + InvalidContextError: If `Vertex` is out of context. + DeletedObjectError: If `Vertex` has been deleted. + + Examples: + ```for edge in vertex.in_edges:``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._vertex.is_deleted(): + raise DeletedObjectError("Accessing deleted object.") + + for edge in self._vertex.out_edges: + yield Edge(edge) + + def __eq__(self, other) -> bool: + if not self.is_valid(): + raise InvalidContextError() + + if not isinstance(other, Vertex): + return NotImplemented + + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + + +class Path: + """Represents a path comprised of `Vertex` and `Edge` instances.""" + + __slots__ = ("_path", "_vertices", "_edges") + + def __init__(self, starting_vertex_or_path: typing.Union[_mgp_mock.Path, Vertex]): + """Initialize with a starting `Vertex`. + + Raises: + InvalidContextError: If the given vertex is invalid. + """ + # Accepting _mgp.Path is just for internal usage. + if isinstance(starting_vertex_or_path, _mgp_mock.Path): + self._path = starting_vertex_or_path + + elif isinstance(starting_vertex_or_path, Vertex): + # For consistency with the Python API, the `_vertex` attribute isn’t a “public” property. + vertex = starting_vertex_or_path._vertex + if not vertex.is_valid(): + raise InvalidContextError() + + self._path = _mgp_mock.Path.make_with_start(vertex) + + else: + raise TypeError(f"Expected 'Vertex' or '_mgp_mock.Path', got '{type(starting_vertex_or_path)}'") + + self._vertices = None + self._edges = None + + def __copy__(self): + if not self.is_valid(): + raise InvalidContextError() + + assert len(self.vertices) >= 1 + + path = Path(self.vertices[0]) + for e in self.edges: + path.expand(e) + + return path + + def __deepcopy__(self, memo): + # In line with the Python API, this is the same as the shallow copy. + try: + return Path(memo[id(self._path)]) + except KeyError: + pass + path = self.__copy__() + memo[id(self._path)] = path._path + return path + + def is_valid(self) -> bool: + """ + Check if the path is in a valid context, i.e. if it may be used. + + Returns: + A `bool` value that represents whether the path is in a valid context. + + Examples: + ```path.is_valid()``` + """ + return self._path.is_valid() + + def expand(self, edge: Edge): + """ + Append an edge continuing from the last vertex on the path. + The destination (head) of the given edge will become the last vertex in the path. + + Args: + edge: The `Edge` to be added to the path. + + Raises: + InvalidContextError: If using an invalid `Path` instance or if the given `Edge` is invalid. + LogicErrorError: If the current last vertex in the path is not part of the given edge. + + Examples: + ```path.expand(edge)``` + """ + if not isinstance(edge, Edge): + raise TypeError(f"Expected 'Edge', got '{type(edge)}'") + + if not self.is_valid() or not edge.is_valid(): + raise InvalidContextError() + + # For consistency with the Python API, the `_edge` attribute isn’t a “public” property. + self._path.expand(edge._edge) + + self._vertices = None + self._edges = None + + @property + def vertices(self) -> typing.Tuple[Vertex, ...]: + """ + Get the path’s vertices in a fixed order. + + Returns: + A `Tuple` of the path’s vertices (`Vertex`) ordered from the start to the end of the path. + + Raises: + InvalidContextError: If using an invalid Path instance. + + Examples: + ```path.vertices``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._vertices is None: + num_vertices = self._path.size() + 1 + self._vertices = tuple(Vertex(self._path.vertex_at(i)) for i in range(num_vertices)) + + return self._vertices + + @property + def edges(self) -> typing.Tuple[Edge, ...]: + """ + Get the path’s edges in a fixed order. + + Returns: + A `Tuple` of the path’s edges (`Edges`) ordered from the start to the end of the path. + + Raises: + InvalidContextError: If using an invalid Path instance. + + Examples: + ```path.vertices``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._edges is None: + num_edges = self._path.size() + self._edges = tuple(Edge(self._path.edge_at(i)) for i in range(num_edges)) + + return self._edges + + +class Vertices: + """An iterable structure of the vertices in a graph.""" + + __slots__ = ("_graph", "_len") + + def __init__(self, graph): + if not isinstance(graph, _mgp_mock.Graph): + raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(graph)}'") + + self._graph = graph + self._len = None + + def __deepcopy__(self, memo): + # In line with the Python API, this is the same as the shallow copy. + return Vertices(self._graph) + + def is_valid(self) -> bool: + """ + Check if the `Vertices` object is in a valid context, i.e. if it may be used. + + Returns: + A `bool` value that represents whether the object is in a valid context. + + Examples: + ```vertices.is_valid()``` + """ + return self._graph.is_valid() + + def __iter__(self) -> typing.Iterable[Vertex]: + """ + Iterate over a graph’s vertices. + + Returns: + An `Iterable` of `Vertex` objects. + + Raises: + InvalidContextError: If context is invalid. + + Examples: + ``` + for vertex in graph.vertices: + ``` + ``` + iter(graph.vertices) + ``` + """ + if not self.is_valid(): + raise InvalidContextError() + + for vertex in self._graph.vertices: + yield Vertex(vertex) + + def __contains__(self, vertex: Vertex): + """ + Check if the given vertex is one of the graph vertices. + + Args: + vertex: The `Vertex` to be checked. + + Returns: + A Boolean value that represents whether the given vertex is one of the graph vertices. + + Raises: + InvalidContextError: If the `Vertices` instance or the givern vertex is not in a valid context. + + Examples: + ```if vertex in graph.vertices:``` + """ + if not self.is_valid() or not vertex.is_valid(): + raise InvalidContextError() + + return self._graph.has_node(vertex.id) + + def __len__(self): + """ + Get the count of the graph vertices. + + Returns: + The count of the vertices in the graph. + + Raises: + InvalidContextError: If the `Vertices` instance is not in a valid context. + + Examples: + ```len(graph.vertices)``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if not self._len: + self._len = sum(1 for _ in self) + + return self._len + + +class Record: + """Represents a record as returned by a query procedure. + Records are comprised of key (field name) - value pairs.""" + + __slots__ = ("fields",) + + def __init__(self, **kwargs): + """Initialize with {name}={value} fields in kwargs.""" + self.fields = kwargs + + def __str__(self): + return str(self.fields) + + +class Graph: + """The graph that stands in for Memgraph’s graph.""" + + __slots__ = ("_graph",) + + def __init__(self, graph): + if not isinstance(graph, _mgp_mock.Graph): + raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(graph)}'") + + self._graph = graph + + def __deepcopy__(self, memo): + # In line with the Python API, this is the same as the shallow copy. + return Graph(self._graph) + + def is_valid(self) -> bool: + """ + Check if the graph is in a valid context, i.e. if it may be used. + + Returns: + A `bool` value that represents whether the graph is in a valid context. + + Examples: + ```graph.is_valid()``` + """ + return self._graph.is_valid() + + def get_vertex_by_id(self, vertex_id: VertexId) -> Vertex: + """ + Return the graph vertex with the given vertex_id. + Access to a `Vertex` is only valid during a single execution of a + procedure in a query. You should not globally store the returned + vertex. + + Args: + vertex_id: A Memgraph vertex ID (`Vertex ID`) + + Returns: + The `Vertex` with the given ID. + + Raises: + IndexError: If unable to find the given vertex_id. + InvalidContextError: If context is invalid. + + Examples: + ```graph.get_vertex_by_id(vertex_id)``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return Vertex(self._graph.get_vertex_by_id(vertex_id)) + + @property + def vertices(self) -> Vertices: + """ + Get all graph vertices. + + Access to a `Vertex` is only valid during a single execution of a + query procedure. You should not globally store the returned `Vertex` + instances. + + Returns: + `Vertices` in the graph. + + Raises: + InvalidContextError: If context is invalid. + + Examples: + Iteration over all graph vertices. + + ``` + graph = context.graph + for vertex in graph.vertices: + ``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return Vertices(self._graph) + + def is_mutable(self) -> bool: + """ + Check if the graph is mutable, i.e. if it can be modified. + + Returns: + A `bool` value that represents whether the graph is mutable. + + Raises: + InvalidContextError: If the graph is not in a valid context. + + Examples: + ```graph.is_mutable()``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return not self._graph.is_immutable() + + def create_vertex(self) -> Vertex: + """ + Create an empty vertex. + + Returns: + The created `Vertex`. + + Raises: + InvalidContextError: If the graph is not in a valid context. + ImmutableObjectError: If the graph is immutable. + + Examples: + Creating an empty vertex: + ```vertex = graph.create_vertex()``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._graph.is_immutable(): + raise ImmutableObjectError("Cannot modify immutable object.") + + return Vertex(self._graph.create_vertex()) + + def delete_vertex(self, vertex: Vertex) -> None: + """ + Delete the given vertex if it’s isolated, i.e. if there are no edges connected to it. + + Args: + vertex: The `Vertex` to be deleted. + + Raises: + InvalidContextError: If the graph is not in a valid context. + ImmutableObjectError: If the graph is immutable. + LogicErrorError: If the vertex is not isolated. + + Examples: + ```graph.delete_vertex(vertex)``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._graph.is_immutable(): + raise ImmutableObjectError("Cannot modify immutable object.") + + if not self._graph.vertex_is_isolate(vertex.id): + raise LogicErrorError("Logic error.") + + self._graph.delete_vertex(vertex.id) + + def detach_delete_vertex(self, vertex: Vertex) -> None: + """ + Delete the given vertex together with all connected edges. + + Args: + vertex: The `Vertex` to be deleted. + + Raises: + InvalidContextError: If the graph is not in a valid context. + ImmutableObjectError: If the graph is immutable. + + Examples: + ```graph.detach_delete_vertex(vertex)``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._graph.is_immutable(): + raise ImmutableObjectError("Cannot modify immutable object.") + + self._graph.delete_vertex(vertex.id) + + def create_edge(self, from_vertex: Vertex, to_vertex: Vertex, edge_type: EdgeType) -> Edge: + """ + Create an empty edge. + + Args: + from_vertex: The source (tail) `Vertex`. + to_vertex: The destination (head) `Vertex`. + edge_type: `EdgeType` specifying the new edge’s type. + + Returns: + The created `Edge`. + + Raises: + InvalidContextError: If the graph is not in a valid context. + ImmutableObjectError: If the graph is immutable. + DeletedObjectError: If `from_vertex` or `to_vertex` have been deleted. + + Examples: + ```edge = graph.create_edge(from_vertex, vertex, edge_type)``` + """ + + if not self.is_valid(): + raise InvalidContextError() + + if self._graph.is_immutable(): + raise ImmutableObjectError("Cannot modify immutable object.") + + new_edge = self._graph.create_edge(from_vertex._vertex, to_vertex._vertex, edge_type.name) + return Edge(new_edge) + + def delete_edge(self, edge: Edge) -> None: + """ + Delete the given edge. + + Args: + edge: The `Edge` to be deleted. + + Raises: + InvalidContextError: If the graph is not in a valid context. + ImmutableObjectError: If the graph is immutable. + + Examples: + ```graph.delete_edge(edge)``` + """ + if not self.is_valid(): + raise InvalidContextError() + + if self._graph.is_immutable(): + raise ImmutableObjectError("Cannot modify immutable object.") + + self._graph.delete_edge(edge.from_vertex.id, edge.to_vertex.id, edge.id) + + +class AbortError(Exception): + """Signals that the procedure was asked to abort its execution.""" + + pass + + +class ProcCtx: + """The context of the procedure being executed. + + Access to a `ProcCtx` is only valid during a single execution of a query procedure. + You should not globally store a `ProcCtx` instance. + """ + + __slots__ = ("_graph",) + + def __init__(self, graph): + if not isinstance(graph, (_mgp_mock.Graph, _mgp_mock.nx.MultiDiGraph)): + raise TypeError(f"Expected '_mgp_mock.Graph' or 'networkx.MultiDiGraph', got '{type(graph)}'") + + self._graph = Graph(graph) if isinstance(graph, _mgp_mock.Graph) else Graph(_mgp_mock.Graph(graph)) + + def is_valid(self) -> bool: + """ + Check if the context is valid, i.e. if the contained structures may be used. + + Returns: + A `bool` value that represents whether the context is valid. + + Examples: + ```context.is_valid()``` + """ + return self._graph.is_valid() + + @property + def graph(self) -> Graph: + """ + Access the graph. + + Returns: + A `Graph` object representing the graph. + + Raises: + InvalidContextError: If the procedure context is not valid. + + Examples: + ```context.graph``` + """ + if not self.is_valid(): + raise InvalidContextError() + + return self._graph + + +# Additional typing support + +Number = typing.Union[int, float] + +Map = typing.Union[dict, Edge, Vertex] + +Date = datetime.date + +LocalTime = datetime.time + +LocalDateTime = datetime.datetime + +Duration = datetime.timedelta + +Any = typing.Union[bool, str, Number, Map, list, Date, LocalTime, LocalDateTime, Duration] + +List = typing.List + +Nullable = typing.Optional + + +# Procedure registration + + +def raise_if_does_not_meet_requirements(func: typing.Callable[..., Record]): + if not callable(func): + raise TypeError(f"Expected a callable object, got an instance of '{type(func)}'") + if inspect.iscoroutinefunction(func): + raise TypeError("Callable must not be 'async def' function") + if sys.version_info >= (3, 6): + if inspect.isasyncgenfunction(func): + raise TypeError("Callable must not be 'async def' function") + if inspect.isgeneratorfunction(func): + raise NotImplementedError("Generator functions are not supported") + + +def _register_proc(func: typing.Callable[..., Record], is_write: bool): + raise_if_does_not_meet_requirements(func) + + sig = inspect.signature(func) + + params = tuple(sig.parameters.values()) + if params and params[0].annotation is ProcCtx: + + @wraps(func) + def wrapper(ctx, *args): + result_record = None + + if is_write: + ctx_copy = ProcCtx(deepcopy(ctx._graph._graph.nx)) + + result_record = func(ctx_copy, *args) + + ctx._graph._graph = deepcopy(ctx_copy._graph._graph) + + # Invalidate context after execution + ctx_copy._graph._graph.invalidate() + else: + ctx._graph._graph.make_immutable() + + result_record = func(ctx, *args) + + # Invalidate context after execution + ctx._graph._graph.invalidate() + + return result_record + + else: + + @wraps(func) + def wrapper(*args): + return func(*args) + + if sig.return_annotation is not sig.empty: + record = sig.return_annotation + + if not isinstance(record, Record): + raise TypeError(f"Expected '{func.__name__}' to return 'mgp.Record', got '{type(record)}'") + + return wrapper + + +def read_proc(func: typing.Callable[..., Record]): + """ + Register a function as a Memgraph read-only procedure. + + The `func` needs to be a callable and optionally take `ProcCtx` as its first argument. + Other parameters of `func` will be bound to the passed arguments. + The full signature of `func` needs to be annotated with types. The return type must + be `Record(field_name=type, ...)`, and the procedure must produce either a complete + Record or None. Multiple records can be produced by returning an iterable of them. + Registering generator functions is currently not supported. + + Example: + ``` + import mgp_mock + + @mgp_mock.read_proc + def procedure(context: mgp_mock.ProcCtx, + required_arg: mgp_mock.Nullable[mgp_mock.Any], + optional_arg: mgp_mock.Nullable[mgp_mock.Any] = None + ) -> mgp_mock.Record(result=str, args=list): + args = [required_arg, optional_arg] + # Multiple rows can be produced by returning an iterable of mgp_mock.Record: + return mgp_mock.Record(args=args, result="Hello World!") + ``` + + The above example procedure returns two fields: `args` and `result`. + * `args` is a copy of the arguments passed to the procedure. + * `result` is "Hello World!". + + Any errors can be reported by raising an Exception. + """ + return _register_proc(func, False) + + +def write_proc(func: typing.Callable[..., Record]): + """ + Register a function as a Memgraph write procedure. + + The `func` needs to be a callable and optionally take `ProcCtx` as its first argument. + Other parameters of `func` will be bound to the passed arguments. + The full signature of `func` needs to be annotated with types. The return type must + be `Record(field_name=type, ...)`, and the procedure must produce either a complete + Record or None. Multiple records can be produced by returning an iterable of them. + Registering generator functions is currently not supported. + + Example: + ``` + import mgp_mock + + @mgp_mock.write_proc + def procedure(context: mgp_mock.ProcCtx, + required_arg: str, + optional_arg: mgp_mock.Nullable[str] = None + ) -> mgp_mock.Record(result=mgp_mock.Vertex): + vertex = context.graph.create_vertex() + vertex_properties = vertex.properties + vertex_properties["required_arg"] = required_arg + if optional_arg is not None: + vertex_properties["optional_arg"] = optional_arg + + return mgp.Record(result=vertex) + ``` + + The above example procedure returns a newly created vertex that has + up to 2 properties: + * `required_arg` is always present and its value is the first + argument of the procedure. + * `optional_arg` is present if the second argument of the procedure + is not `null`. + + Any errors can be reported by raising an Exception. + """ + return _register_proc(func, True) + + +class FuncCtx: + """The context of the function being executed. + + Access to a `FuncCtx` is only valid during a single execution of a transformation. + You should not globally store a `FuncCtx` instance. + The graph object within `FuncCtx` is not mutable. + """ + + __slots__ = ("_graph",) + + def __init__(self, graph): + if not isinstance(graph, (_mgp_mock.Graph, _mgp_mock.nx.MultiDiGraph)): + raise TypeError(f"Expected '_mgp_mock.Graph' or 'networkx.MultiDiGraph', got '{type(graph)}'") + + self._graph = Graph(graph) if isinstance(graph, _mgp_mock.Graph) else Graph(_mgp_mock.Graph(graph)) + + def is_valid(self) -> bool: + """ + Check if the context is valid, i.e. if the contained structures may be used. + + Returns: + A `bool` value that represents whether the context is valid. + + Examples: + ```context.is_valid()``` + """ + return self._graph.is_valid() + + +def function(func: typing.Callable): + """ + Register a function as a Memgraph function. + + The `func` needs to be a callable and optionally take `ProcCtx` as its first argument. + Other parameters of `func` will be bound to the passed arguments. + Only the function arguments need to be annotated with types. The return type doesn’t + need to be specified, but it has to be within `mgp_mock.Any`. + Registering generator functions is currently not supported. + + Example: + ``` + import mgp_mock + + @mgp_mock.function + def procedure(context: mgp_mock.FuncCtx, + required_arg: str, + optional_arg: mgp_mock.Nullable[str] = None + ): + return_args = [required_arg] + if optional_arg is not None: + return_args.append(optional_arg) + # Return any result whose type is within mgp_mock.Any + return return_args + ``` + + The above example function returns a list of the passed parameters: + * `required_arg` is always present and its value is the first + argument of the procedure. + * `optional_arg` is present if the second argument of the procedure + is not `null`. + + Any errors can be reported by raising an Exception. + """ + raise_if_does_not_meet_requirements(func) + + sig = inspect.signature(func) + + params = tuple(sig.parameters.values()) + if params and params[0].annotation is FuncCtx: + + @wraps(func) + def wrapper(ctx, *args): + ctx._graph._graph.make_immutable() + + result = func(ctx, *args) + + # Invalidate context after execution + ctx._graph._graph.invalidate() + + return result + + else: + + @wraps(func) + def wrapper(*args): + return func(*args) + + return wrapper + + +def _wrap_exceptions(): + def wrap_function(func): + @wraps(func) + def wrapped_func(*args, **kwargs): + try: + return func(*args, **kwargs) + except _mgp_mock.LogicErrorError as e: + raise LogicErrorError(e) + except _mgp_mock.ImmutableObjectError as e: + raise ImmutableObjectError(e) + + return wrapped_func + + def wrap_prop_func(func): + return None if func is None else wrap_function(func) + + def wrap_member_functions(cls: type): + for name, obj in inspect.getmembers(cls): + if inspect.isfunction(obj): + setattr(cls, name, wrap_function(obj)) + elif isinstance(obj, property): + setattr( + cls, + name, + property( + wrap_prop_func(obj.fget), + wrap_prop_func(obj.fset), + wrap_prop_func(obj.fdel), + obj.__doc__, + ), + ) + + def defined_in_this_module(obj: object): + return getattr(obj, "__module__", "") == __name__ + + module = sys.modules[__name__] + for name, obj in inspect.getmembers(module): + if not defined_in_this_module(obj): + continue + if inspect.isclass(obj): + wrap_member_functions(obj) + elif inspect.isfunction(obj) and not name.startswith("_"): + setattr(module, name, wrap_function(obj)) + + +_wrap_exceptions() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b35434d42..d6f58c642 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -106,6 +106,10 @@ install(PROGRAMS $ # Install Python source for supporting our embedded Python. install(FILES ${CMAKE_SOURCE_DIR}/include/mgp.py DESTINATION lib/memgraph/python_support) +install(FILES ${CMAKE_SOURCE_DIR}/include/mgp_mock.py + DESTINATION lib/memgraph/python_support) +install(FILES ${CMAKE_SOURCE_DIR}/include/_mgp_mock.py + DESTINATION lib/memgraph/python_support) # Install the includes file for writing custom procedures in C and C++> install(FILES ${CMAKE_SOURCE_DIR}/include/mg_procedure.h diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index 0a5bdc77e..1d2eee643 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -44,6 +44,7 @@ add_subdirectory(module_file_manager) add_subdirectory(monitoring_server) add_subdirectory(lba_procedures) add_subdirectory(python_query_modules_reloading) +add_subdirectory(mock_api) copy_e2e_python_files(pytest_runner pytest_runner.sh "") file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/tests/e2e/mock_api/CMakeLists.txt b/tests/e2e/mock_api/CMakeLists.txt new file mode 100644 index 000000000..aa170dc62 --- /dev/null +++ b/tests/e2e/mock_api/CMakeLists.txt @@ -0,0 +1,8 @@ +function(copy_mock_python_api_e2e_files FILE_NAME) + copy_e2e_python_files(mock_python_api ${FILE_NAME}) +endfunction() + +add_subdirectory(procedures) + +copy_mock_python_api_e2e_files(common.py) +copy_mock_python_api_e2e_files(test_compare_mock.py) diff --git a/tests/e2e/mock_api/common.py b/tests/e2e/mock_api/common.py new file mode 100644 index 000000000..28c911ac9 --- /dev/null +++ b/tests/e2e/mock_api/common.py @@ -0,0 +1,14 @@ +import typing + +import mgclient + + +def connect(**kwargs) -> mgclient.Connection: + connection = mgclient.connect(host="localhost", port=7687, **kwargs) + connection.autocommit = True + return connection + + +def execute_and_fetch_results_dict(cursor, query) -> typing.Dict: + cursor.execute(query) + return cursor.fetchall()[0][0] diff --git a/tests/e2e/mock_api/procedures/CMakeLists.txt b/tests/e2e/mock_api/procedures/CMakeLists.txt new file mode 100644 index 000000000..6a70e7e95 --- /dev/null +++ b/tests/e2e/mock_api/procedures/CMakeLists.txt @@ -0,0 +1,10 @@ +copy_mock_python_api_e2e_files(test_utils.py) +copy_mock_python_api_e2e_files(edge_type.py) +copy_mock_python_api_e2e_files(edge.py) +copy_mock_python_api_e2e_files(graph.py) +copy_mock_python_api_e2e_files(label.py) +copy_mock_python_api_e2e_files(path.py) +copy_mock_python_api_e2e_files(properties.py) +copy_mock_python_api_e2e_files(record.py) +copy_mock_python_api_e2e_files(vertex.py) +copy_mock_python_api_e2e_files(vertices.py) diff --git a/tests/e2e/mock_api/procedures/edge.py b/tests/e2e/mock_api/procedures/edge.py new file mode 100644 index 000000000..2e1a7e1b5 --- /dev/null +++ b/tests/e2e/mock_api/procedures/edge.py @@ -0,0 +1,85 @@ +import mgp +import mgp_mock +import test_utils + + +@mgp.read_proc +def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=False) + results = dict() + + TARGET_EDGE_1_ID = 9 + TARGET_EDGE_2_ID = 37 + + target_edge_1 = test_utils.get_edge(ctx, permanent_id=TARGET_EDGE_1_ID) + target_edge_2 = test_utils.get_edge(ctx, permanent_id=TARGET_EDGE_2_ID) + target_mock_edge_1 = test_utils.get_mock_edge(mock_ctx, id=TARGET_EDGE_1_ID) + target_mock_edge_2 = test_utils.get_mock_edge(mock_ctx, id=TARGET_EDGE_2_ID) + + results["is_valid"] = test_utils.all_equal( + target_edge_1.is_valid(), + target_mock_edge_1.is_valid(), + True, + ) + + results["underlying_graph_is_mutable"] = test_utils.all_equal( + target_edge_1.underlying_graph_is_mutable(), + target_mock_edge_1.underlying_graph_is_mutable(), + False, + ) + + results["id"] = test_utils.all_equal( + isinstance(target_edge_1.id, int), + isinstance(target_mock_edge_1.id, int), + True, + ) + + results["type"] = test_utils.all_equal( + target_edge_1.type.name, + target_mock_edge_1.type.name, + "HAS_TEAM", + ) + + results["from_vertex"] = test_utils.all_equal( + isinstance(target_edge_1.from_vertex, mgp.Vertex), + isinstance(target_mock_edge_1.from_vertex, mgp_mock.Vertex), + True, + ) + + results["to_vertex"] = test_utils.all_equal( + isinstance(target_edge_1.to_vertex, mgp.Vertex), + isinstance(target_mock_edge_1.to_vertex, mgp_mock.Vertex), + True, + ) + + results["properties"] = test_utils.all_equal( + isinstance(target_edge_1.properties, mgp.Properties), + isinstance(target_mock_edge_1.properties, mgp_mock.Properties), + True, + ) and test_utils.all_equal( + {prop.name: prop.value for prop in target_edge_1.properties.items()}, + {prop.name: prop.value for prop in target_mock_edge_1.properties.items()}, + {"permanent_id": 9}, + ) + + results["__eq__"] = test_utils.all_equal( + target_edge_1 == target_edge_1, + target_mock_edge_1 == target_mock_edge_1, + True, + ) and test_utils.all_equal( + target_edge_1 != target_edge_1, + target_mock_edge_1 != target_mock_edge_1, + False, + ) + + results["__ne__"] = test_utils.all_equal( + target_edge_1 != target_edge_2, + target_mock_edge_1 != target_mock_edge_2, + True, + ) and test_utils.all_equal( + target_edge_1 == target_edge_2, + target_mock_edge_1 == target_mock_edge_2, + False, + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/procedures/edge_type.py b/tests/e2e/mock_api/procedures/edge_type.py new file mode 100644 index 000000000..f2989c3d8 --- /dev/null +++ b/tests/e2e/mock_api/procedures/edge_type.py @@ -0,0 +1,33 @@ +import mgp +import test_utils + + +@mgp.read_proc +def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=False) + results = dict() + + TARGET_EDGE_ID = 0 + + target_edge_type = test_utils.get_edge(ctx, permanent_id=TARGET_EDGE_ID).type + target_mock_edge_type = test_utils.get_mock_edge(mock_ctx, id=TARGET_EDGE_ID).type + + results["name"] = test_utils.all_equal( + target_edge_type.name, + target_mock_edge_type.name, + "IS_PART_OF", + ) + + results["__eq__"] = test_utils.all_equal( + target_edge_type == target_edge_type, + target_edge_type == "IS_PART_OF", + target_mock_edge_type == target_mock_edge_type, + target_mock_edge_type == "IS_PART_OF", + ) + + results["__ne__"] = test_utils.all_equal( + target_edge_type != "HAS_TEAM", + target_mock_edge_type != "HAS_TEAM", + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/procedures/graph.py b/tests/e2e/mock_api/procedures/graph.py new file mode 100644 index 000000000..174f6595b --- /dev/null +++ b/tests/e2e/mock_api/procedures/graph.py @@ -0,0 +1,102 @@ +import mgp +import mgp_mock +import test_utils + + +@mgp.write_proc +def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + VERTEX_ID = 6 + + mock_ctx = test_utils.get_mock_proc_ctx(is_write=True) + results = dict() + + results["is_valid"] = test_utils.all_equal( + ctx.graph.is_valid(), + mock_ctx.graph.is_valid(), + True, + ) + + results["get_vertex_by_id"] = test_utils.all_equal( + test_utils.get_vertex(ctx, permanent_id=VERTEX_ID).properties["permanent_id"], + mock_ctx.graph.get_vertex_by_id(VERTEX_ID).properties["permanent_id"], + VERTEX_ID, + ) + + results["vertices"] = test_utils.all_equal( + len(ctx.graph.vertices), + len(mock_ctx.graph.vertices), + 27, + ) + + results["is_mutable"] = test_utils.all_equal( + ctx.graph.is_mutable(), + mock_ctx.graph.is_mutable(), + True, + ) + + new_mock_vertex = mock_ctx.graph.create_vertex() + new_mock_vertex_id = new_mock_vertex.id + results["create_vertex"] = test_utils.all_equal( + new_mock_vertex_id in [v.id for v in mock_ctx.graph.vertices], + True, + ) + + mock_ctx.graph.delete_vertex(new_mock_vertex) + results["delete_vertex"] = test_utils.all_equal( + new_mock_vertex_id not in [v.id for v in mock_ctx.graph.vertices], + True, + ) + + mock_vertex_to_delete = mock_ctx.graph.get_vertex_by_id(VERTEX_ID) + mock_ctx.graph.detach_delete_vertex(mock_vertex_to_delete) + results["detach_delete_vertex"] = test_utils.all_equal( + VERTEX_ID not in [v.properties["permanent_id"] for v in mock_ctx.graph.vertices], + True, + ) + + MAX_EDGE_ID = 37 + + START_ID = 10 + END1_ID = 13 + END2_ID = 14 + start_mock_vertex, end1_mock_vertex, end2_mock_vertex = ( + mock_ctx.graph.get_vertex_by_id(START_ID), + mock_ctx.graph.get_vertex_by_id(END1_ID), + mock_ctx.graph.get_vertex_by_id(END2_ID), + ) + EDGE_TYPE = "CONNECTED_TO" + mock_edge_type = mgp_mock.EdgeType(EDGE_TYPE) + new_mock_edge = mock_ctx.graph.create_edge(start_mock_vertex, end1_mock_vertex, mock_edge_type) + new_mock_edge_id = new_mock_edge.id + results["create_edge"] = test_utils.all_equal( + new_mock_edge_id, + MAX_EDGE_ID + 1, + ) + + mock_ctx.graph.delete_edge(new_mock_edge) + results["delete_edge"] = test_utils.all_equal( + new_mock_edge_id not in [e.id for e in start_mock_vertex.out_edges], + True, + ) + + another_mock_edge = mock_ctx.graph.create_edge(start_mock_vertex, end2_mock_vertex, mock_edge_type) + results["edge_id_assignment"] = test_utils.all_equal( + another_mock_edge.id, + MAX_EDGE_ID + 2, + ) + + return mgp.Record(results_dict=results) + + +@mgp.read_proc +def test_read_proc_mutability(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=False) + results = dict() + + results["is_not_mutable"] = test_utils.all_equal( + ctx.graph.is_mutable(), + mock_ctx.graph.is_mutable(), + False, + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/procedures/label.py b/tests/e2e/mock_api/procedures/label.py new file mode 100644 index 000000000..6f1f5ca90 --- /dev/null +++ b/tests/e2e/mock_api/procedures/label.py @@ -0,0 +1,52 @@ +import mgp +import test_utils + + +@mgp.read_proc +def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=False) + results = dict() + + TARGET_LABELLED_NODE_ID = 5 + + target_vertex = test_utils.get_vertex(ctx, permanent_id=TARGET_LABELLED_NODE_ID) + target_mock_vertex = mock_ctx.graph.get_vertex_by_id(TARGET_LABELLED_NODE_ID) + + label_1, label_2 = sorted(target_vertex.labels, key=lambda l: l.name) # ("Company", "Startup") + mock_label_1, mock_label_2 = sorted(target_mock_vertex.labels, key=lambda l: l.name) # ditto + + results["name"] = test_utils.all_equal( + (label_1.name, label_2.name), + (mock_label_1.name, mock_label_2.name), + ("Company", "Startup"), + ) + + results["__eq__"] = test_utils.all_equal( + label_1 == label_1, + label_1 == "Company", + mock_label_1 == mock_label_1, + mock_label_1 == "Company", + True, + ) and test_utils.all_equal( + label_1 == label_2, + label_1 == "Startup", + mock_label_1 == mock_label_2, + mock_label_1 == "Startup", + False, + ) + + results["__ne__"] = test_utils.all_equal( + label_1 != label_2, + label_1 != "Startup", + mock_label_1 != mock_label_2, + mock_label_1 != "Startup", + True, + ) and test_utils.all_equal( + label_1 != label_1, + label_1 != "Company", + mock_label_1 != mock_label_1, + mock_label_1 != "Company", + False, + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/procedures/path.py b/tests/e2e/mock_api/procedures/path.py new file mode 100644 index 000000000..3e2499394 --- /dev/null +++ b/tests/e2e/mock_api/procedures/path.py @@ -0,0 +1,65 @@ +import copy + +import mgp +import mgp_mock +import test_utils + + +@mgp.read_proc +def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=False) + results = dict() + + START_ID = 0 + start_vertex = test_utils.get_vertex(ctx, permanent_id=START_ID) + mock_start_vertex = mock_ctx.graph.get_vertex_by_id(START_ID) + path = mgp.Path(start_vertex) + mock_path = mgp_mock.Path(mock_start_vertex) + + results["is_valid"] = test_utils.all_equal( + path.is_valid(), + mock_path.is_valid(), + True, + ) + + EDGE_ID = 0 + edge_to_add = test_utils.get_edge(ctx, permanent_id=EDGE_ID) + mock_edge_to_add = test_utils.get_mock_edge(mock_ctx, id=EDGE_ID) + path.expand(edge_to_add) + mock_path.expand(mock_edge_to_add) + results["expand"] = test_utils.all_equal( + (len(path.vertices), len(path.edges)), + (len(mock_path.vertices), len(mock_path.edges)), + (2, 1), + ) + + NEXT_ID = 1 + results["vertices"] = test_utils.all_equal( + all(isinstance(vertex, mgp.Vertex) for vertex in path.vertices), + all(isinstance(vertex, mgp_mock.Vertex) for vertex in mock_path.vertices), + True, + ) and test_utils.all_equal( + [vertex.properties["permanent_id"] for vertex in path.vertices], + [vertex.properties["permanent_id"] for vertex in mock_path.vertices], + [START_ID, NEXT_ID], + ) + + results["edges"] = test_utils.all_equal( + all(isinstance(edge, mgp.Edge) for edge in path.edges), + all(isinstance(edge, mgp_mock.Edge) for edge in mock_path.edges), + True, + ) and test_utils.all_equal( + [edge.properties["permanent_id"] for edge in path.edges], + [edge.properties["permanent_id"] for edge in mock_path.edges], + [0], + ) + + path_copy = copy.copy(path) + mock_path_copy = copy.copy(mock_path) + results["__copy__"] = test_utils.all_equal( + path_copy.is_valid(), + mock_path_copy.is_valid(), + True, + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/procedures/properties.py b/tests/e2e/mock_api/procedures/properties.py new file mode 100644 index 000000000..5f6a30ce8 --- /dev/null +++ b/tests/e2e/mock_api/procedures/properties.py @@ -0,0 +1,184 @@ +import mgp +import test_utils + + +@mgp.write_proc +def compare_apis_on_vertex(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=True) + results = dict() + + TARGET_ID = 0 + target_vertex = test_utils.get_vertex(ctx, permanent_id=TARGET_ID) + target_mock_vertex = mock_ctx.graph.get_vertex_by_id(TARGET_ID) + + properties = target_vertex.properties + mock_properties = target_mock_vertex.properties + + results["get"] = test_utils.all_equal( + properties.get("name"), + mock_properties.get("name"), + "Peter", + ) + results["get[default]"] = test_utils.all_equal( + properties.get("YoE", default="N/A"), + mock_properties.get("YoE", default="N/A"), + "N/A", + ) + + properties.set("education", "PhD") + mock_properties.set("education", "PhD") + results["set"] = test_utils.all_equal( + properties.get("education"), + mock_properties.get("education"), + "PhD", + ) + + results["items"] = test_utils.all_equal( + {prop.name: prop.value for prop in properties.items()}, + {prop.name: prop.value for prop in mock_properties.items()}, + {"name": "Peter", "surname": "Yang", "education": "PhD", "permanent_id": 0}, + ) + + results["keys"] = test_utils.all_equal( + {key for key in properties.keys()}, + {key for key in mock_properties.keys()}, + {"name", "surname", "education", "permanent_id"}, + ) + + results["values"] = test_utils.all_equal( + {val for val in properties.values()}, + {val for val in mock_properties.values()}, + {"Peter", "Yang", "PhD", 0}, + ) + + results["__len__"] = test_utils.all_equal( + len(properties), + len(mock_properties), + 4, + ) + + results["__iter__"] = test_utils.all_equal( + {name for name in properties}, + {name for name in mock_properties}, + {"name", "surname", "education", "permanent_id"}, + ) + + results["__getitem__"] = test_utils.all_equal( + {properties[name] for name in properties}, + {mock_properties[name] for name in mock_properties}, + {"Peter", "Yang", "PhD", 0}, + ) + + properties["YoE"] = 6 + mock_properties["YoE"] = 6 + results["__setitem__"] = test_utils.all_equal( + properties["YoE"], + mock_properties["YoE"], + 6, + ) + + results["__contains__"] = test_utils.all_equal( + "YoE" in properties, + "age" not in properties, + "YoE" in mock_properties, + "age" not in mock_properties, + True, + ) and test_utils.all_equal( + "YoE" not in properties, + "age" in properties, + "YoE" not in mock_properties, + "age" in mock_properties, + False, + ) + + return mgp.Record(results_dict=results) + + +@mgp.write_proc +def compare_apis_on_edge(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=True) + results = dict() + + TARGET_EDGE_ID = 37 + + target_edge_properties = test_utils.get_edge(ctx, permanent_id=TARGET_EDGE_ID).properties + target_mock_edge_properties = test_utils.get_mock_edge(mock_ctx, id=TARGET_EDGE_ID).properties + + results["get"] = test_utils.all_equal( + target_edge_properties.get("importance"), + target_mock_edge_properties.get("importance"), + "HIGH", + ) + results["get[default]"] = test_utils.all_equal( + target_edge_properties.get("priority", default="N/A"), + target_mock_edge_properties.get("priority", default="N/A"), + "N/A", + ) + + target_edge_properties.set("priority", "MEDIUM") + target_mock_edge_properties.set("priority", "MEDIUM") + results["set"] = test_utils.all_equal( + target_edge_properties.get("priority"), + target_mock_edge_properties.get("priority"), + "MEDIUM", + ) + + results["items"] = test_utils.all_equal( + {prop.name: prop.value for prop in target_edge_properties.items()}, + {prop.name: prop.value for prop in target_mock_edge_properties.items()}, + {"importance": "HIGH", "priority": "MEDIUM", "permanent_id": 37}, + ) + + results["keys"] = test_utils.all_equal( + {key for key in target_edge_properties.keys()}, + {key for key in target_mock_edge_properties.keys()}, + {"importance", "priority", "permanent_id"}, + ) + + results["values"] = test_utils.all_equal( + {val for val in target_edge_properties.values()}, + {val for val in target_mock_edge_properties.values()}, + {"HIGH", "MEDIUM", 37}, + ) + + results["__len__"] = test_utils.all_equal( + len(target_edge_properties), + len(target_mock_edge_properties), + 3, + ) + + results["__iter__"] = test_utils.all_equal( + {name for name in target_edge_properties}, + {name for name in target_mock_edge_properties}, + {"importance", "priority", "permanent_id"}, + ) + + results["__getitem__"] = test_utils.all_equal( + {target_edge_properties[name] for name in target_edge_properties}, + {target_mock_edge_properties[name] for name in target_mock_edge_properties}, + {"HIGH", "MEDIUM", 37}, + ) + + target_edge_properties["priority"] = "LOW" + target_mock_edge_properties["priority"] = "LOW" + results["__setitem__"] = test_utils.all_equal( + target_edge_properties["priority"], + target_mock_edge_properties["priority"], + "LOW", + ) + + results["__contains__"] = test_utils.all_equal( + "priority" in target_edge_properties, + "status" not in target_edge_properties, + "priority" in target_mock_edge_properties, + "status" not in target_mock_edge_properties, + True, + ) and test_utils.all_equal( + "priority" not in target_edge_properties, + "status" in target_edge_properties, + "priority" not in target_mock_edge_properties, + "status" in target_mock_edge_properties, + False, + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/procedures/record.py b/tests/e2e/mock_api/procedures/record.py new file mode 100644 index 000000000..5fbb5af84 --- /dev/null +++ b/tests/e2e/mock_api/procedures/record.py @@ -0,0 +1,18 @@ +import mgp +import mgp_mock +import test_utils + + +@mgp.read_proc +def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + results = dict() + + record = mgp.Record(a=1, b=2.0, c="3") + mock_record = mgp_mock.Record(a=1, b=2.0, c="3") + + results["fields"] = test_utils.all_equal( + record.fields, + mock_record.fields, + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/procedures/test_utils.py b/tests/e2e/mock_api/procedures/test_utils.py new file mode 100644 index 000000000..f48e889ab --- /dev/null +++ b/tests/e2e/mock_api/procedures/test_utils.py @@ -0,0 +1,160 @@ +from itertools import groupby + +import _mgp_mock +import mgp +import mgp_mock +import networkx as nx + + +def all_equal(*args): + """Returns True if all the elements are equal to each other + (source: https://docs.python.org/3/library/itertools.html#itertools-recipes)""" + g = groupby(args) + return next(g, True) and not next(g, False) + + +def get_mock_proc_ctx(is_write: bool) -> mgp_mock.ProcCtx: + GRAPH_DATA = [ + (0, 1, 0), + (5, 1, 9), + (5, 1, 37), + (10, 1, 15), + (1, 2, 4), + (1, 3, 5), + (1, 4, 6), + (0, 5, 1), + (10, 5, 16), + (22, 5, 33), + (1, 6, 7), + (6, 7, 12), + (11, 7, 18), + (13, 7, 26), + (26, 7, 35), + (6, 8, 13), + (26, 8, 36), + (0, 9, 2), + (10, 9, 17), + (22, 9, 34), + (1, 11, 8), + (9, 12, 14), + (14, 13, 27), + (0, 14, 3), + (5, 14, 11), + (12, 14, 22), + (13, 15, 23), + (13, 16, 24), + (13, 17, 25), + (11, 18, 19), + (11, 19, 20), + (11, 20, 21), + (5, 21, 10), + (22, 21, 32), + (21, 23, 28), + (21, 24, 29), + (21, 25, 30), + (21, 26, 31), + ] + NODE_INFO = { + 0: {"labels": "Person", "name": "Peter", "surname": "Yang", "permanent_id": 0}, + 1: {"labels": "Team", "name": "Engineering", "permanent_id": 1}, + 2: {"labels": "Repository", "name": "Memgraph", "permanent_id": 2}, + 3: {"labels": "Repository", "name": "MAGE", "permanent_id": 3}, + 4: {"labels": "Repository", "name": "GQLAlchemy", "permanent_id": 4}, + 5: {"labels": "Company:Startup", "name": "Memgraph", "permanent_id": 5}, + 6: {"labels": "File", "name": "welcome_to_engineering.txt", "permanent_id": 6}, + 7: {"labels": "Storage", "name": "Google Drive", "permanent_id": 7}, + 8: {"labels": "Storage", "name": "Notion", "permanent_id": 8}, + 9: {"labels": "File", "name": "welcome_to_memgraph.txt", "permanent_id": 9}, + 10: {"labels": "Person", "name": "Carl", "permanent_id": 10}, + 11: {"labels": "Folder", "name": "engineering_folder", "permanent_id": 11}, + 12: {"labels": "Person", "name": "Anna", "permanent_id": 12}, + 13: {"labels": "Folder", "name": "operations_folder", "permanent_id": 13}, + 14: {"labels": "Team", "name": "Operations", "permanent_id": 14}, + 15: {"labels": "File", "name": "operations101.txt", "permanent_id": 15}, + 16: {"labels": "File", "name": "expenses2022.csv", "permanent_id": 16}, + 17: {"labels": "File", "name": "salaries2022.csv", "permanent_id": 17}, + 18: {"labels": "File", "name": "engineering101.txt", "permanent_id": 18}, + 19: {"labels": "File", "name": "working_with_github.txt", "permanent_id": 19}, + 20: {"labels": "File", "name": "working_with_notion.txt", "permanent_id": 20}, + 21: {"labels": "Team", "name": "Marketing", "permanent_id": 21}, + 22: {"labels": "Person", "name": "Julie", "permanent_id": 22}, + 23: {"labels": "Account", "name": "Facebook", "permanent_id": 23}, + 24: {"labels": "Account", "name": "LinkedIn", "permanent_id": 24}, + 25: {"labels": "Account", "name": "HackerNews", "permanent_id": 25}, + 26: {"labels": "File", "name": "welcome_to_marketing.txt", "permanent_id": 26}, + } + EDGE_INFO = { + (0, 1, 0): {"type": "IS_PART_OF", "permanent_id": 0}, + (0, 5, 1): {"type": "IS_PART_OF", "permanent_id": 1}, + (0, 9, 2): {"type": "HAS_ACCESS_TO", "permanent_id": 2}, + (0, 14, 3): {"type": "IS_PART_OF", "permanent_id": 3}, + (1, 2, 4): {"type": "HAS_ACCESS_TO", "permanent_id": 4}, + (1, 3, 5): {"type": "HAS_ACCESS_TO", "permanent_id": 5}, + (1, 4, 6): {"type": "HAS_ACCESS_TO", "permanent_id": 6}, + (1, 6, 7): {"type": "HAS_ACCESS_TO", "permanent_id": 7}, + (1, 11, 8): {"type": "HAS_ACCESS_TO", "permanent_id": 8}, + (5, 1, 9): {"type": "HAS_TEAM", "permanent_id": 9}, + (5, 1, 37): {"type": "HAS_TEAM_2", "importance": "HIGH", "permanent_id": 37}, + (5, 14, 11): {"type": "HAS_TEAM", "permanent_id": 11}, + (5, 21, 10): {"type": "HAS_TEAM", "permanent_id": 10}, + (6, 7, 12): {"type": "IS_STORED_IN", "permanent_id": 12}, + (6, 8, 13): {"type": "IS_STORED_IN", "permanent_id": 13}, + (9, 12, 14): {"type": "CREATED_BY", "permanent_id": 14}, + (10, 1, 15): {"type": "IS_PART_OF", "permanent_id": 15}, + (10, 5, 16): {"type": "IS_PART_OF", "permanent_id": 16}, + (10, 9, 17): {"type": "HAS_ACCESS_TO", "permanent_id": 17}, + (11, 7, 18): {"type": "IS_STORED_IN", "permanent_id": 18}, + (11, 18, 19): {"type": "HAS_ACCESS_TO", "permanent_id": 19}, + (11, 19, 20): {"type": "HAS_ACCESS_TO", "permanent_id": 20}, + (11, 20, 21): {"type": "HAS_ACCESS_TO", "permanent_id": 21}, + (12, 14, 22): {"type": "IS_PART_OF", "permanent_id": 22}, + (13, 7, 26): {"type": "IS_STORED_IN", "permanent_id": 26}, + (13, 15, 23): {"type": "HAS_ACCESS_TO", "permanent_id": 23}, + (13, 16, 24): {"type": "HAS_ACCESS_TO", "permanent_id": 24}, + (13, 17, 25): {"type": "HAS_ACCESS_TO", "permanent_id": 25}, + (14, 13, 27): {"type": "HAS_ACCESS_TO", "permanent_id": 27}, + (21, 23, 28): {"type": "HAS_ACCESS_TO", "permanent_id": 28}, + (21, 24, 29): {"type": "HAS_ACCESS_TO", "permanent_id": 29}, + (21, 25, 30): {"type": "HAS_ACCESS_TO", "permanent_id": 30}, + (21, 26, 31): {"type": "HAS_ACCESS_TO", "permanent_id": 31}, + (22, 5, 33): {"type": "IS_PART_OF", "permanent_id": 33}, + (22, 9, 34): {"type": "HAS_ACCESS_TO", "permanent_id": 34}, + (22, 21, 32): {"type": "IS_PART_OF", "permanent_id": 32}, + (26, 7, 35): {"type": "IS_STORED_IN", "permanent_id": 35}, + (26, 8, 36): {"type": "IS_STORED_IN", "permanent_id": 36}, + } + + example_graph = nx.MultiDiGraph(GRAPH_DATA) + nx.set_node_attributes(example_graph, NODE_INFO) + nx.set_edge_attributes(example_graph, EDGE_INFO) + + if not is_write: + example_graph = nx.freeze(example_graph) + + return mgp_mock.ProcCtx(_mgp_mock.Graph(example_graph)) + + +def get_vertex(ctx, permanent_id: int) -> mgp.Vertex: + for vertex in ctx.graph.vertices: + if vertex.properties["permanent_id"] == permanent_id: + return vertex + + return None + + +def get_edge(ctx: mgp.ProcCtx, permanent_id: int) -> mgp.Edge: + for vertex in ctx.graph.vertices: + for edge in vertex.out_edges: + if edge.properties["permanent_id"] == permanent_id: + return edge + + return None + + +def get_mock_edge(ctx: mgp_mock.ProcCtx, id: int) -> mgp_mock.Edge: + for vertex in ctx.graph.vertices: + for edge in vertex.out_edges: + if edge.id == id: + return edge + + return None diff --git a/tests/e2e/mock_api/procedures/vertex.py b/tests/e2e/mock_api/procedures/vertex.py new file mode 100644 index 000000000..dd76e27f7 --- /dev/null +++ b/tests/e2e/mock_api/procedures/vertex.py @@ -0,0 +1,101 @@ +import typing + +import mgp +import mgp_mock +import test_utils + + +@mgp.read_proc +def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=False) + results = dict() + + ID = 1 + + target_vertex = test_utils.get_vertex(ctx, permanent_id=ID) + target_mock_vertex = mock_ctx.graph.get_vertex_by_id(ID) + + results["is_valid"] = test_utils.all_equal( + target_vertex.is_valid(), + target_mock_vertex.is_valid(), + True, + ) + + results["underlying_graph_is_mutable"] = test_utils.all_equal( + target_vertex.underlying_graph_is_mutable(), + target_mock_vertex.underlying_graph_is_mutable(), + False, + ) + + results["id"] = test_utils.all_equal( + isinstance(target_vertex.id, int), + isinstance(target_mock_vertex.id, int), + True, + ) + + results["labels"] = test_utils.all_equal( + isinstance(target_vertex.labels, typing.Tuple), + isinstance(target_mock_vertex.labels, typing.Tuple), + True, + ) and test_utils.all_equal( + {label.name for label in target_vertex.labels}, + {mock_label.name for mock_label in target_mock_vertex.labels}, + {"Team"}, + ) + + results["properties"] = test_utils.all_equal( + isinstance(target_vertex.properties, mgp.Properties), + isinstance(target_mock_vertex.properties, mgp_mock.Properties), + True, + ) and test_utils.all_equal( + {prop for prop in target_vertex.properties}, + {mock_prop for mock_prop in target_mock_vertex.properties}, + {"name", "permanent_id"}, + ) + + results["in_edges"] = test_utils.all_equal( + all(isinstance(edge, mgp.Edge) for edge in target_vertex.in_edges), + all(isinstance(edge, mgp_mock.Edge) for edge in target_mock_vertex.in_edges), + True, + ) and test_utils.all_equal( + {edge.properties["permanent_id"] for edge in target_vertex.in_edges}, + {edge.properties["permanent_id"] for edge in target_mock_vertex.in_edges}, + {0, 9, 15, 37}, + ) + + results["out_edges"] = test_utils.all_equal( + all(isinstance(edge, mgp.Edge) for edge in target_vertex.out_edges), + all(isinstance(edge, mgp_mock.Edge) for edge in target_mock_vertex.out_edges), + True, + ) and test_utils.all_equal( + {edge.properties["permanent_id"] for edge in target_vertex.out_edges}, + {edge.properties["permanent_id"] for edge in target_mock_vertex.out_edges}, + {4, 5, 6, 7, 8}, + ) + + ID_2 = 2 + + target_vertex_2 = test_utils.get_vertex(ctx, permanent_id=ID_2) + target_mock_vertex_2 = mock_ctx.graph.get_vertex_by_id(ID_2) + + results["__eq__"] = test_utils.all_equal( + target_vertex == target_vertex, + target_mock_vertex == target_mock_vertex, + True, + ) and test_utils.all_equal( + target_vertex == target_vertex_2, + target_mock_vertex == target_mock_vertex_2, + False, + ) + + results["__ne__"] = test_utils.all_equal( + target_vertex != target_vertex_2, + target_mock_vertex != target_mock_vertex_2, + True, + ) and test_utils.all_equal( + target_vertex != target_vertex, + target_mock_vertex != target_mock_vertex, + False, + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/procedures/vertices.py b/tests/e2e/mock_api/procedures/vertices.py new file mode 100644 index 000000000..b2cb19fdb --- /dev/null +++ b/tests/e2e/mock_api/procedures/vertices.py @@ -0,0 +1,32 @@ +import mgp +import mgp_mock +import test_utils + + +@mgp.read_proc +def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): + mock_ctx = test_utils.get_mock_proc_ctx(is_write=False) + results = dict() + + vertices = ctx.graph.vertices + mock_vertices = mock_ctx.graph.vertices + + results["is_valid"] = test_utils.all_equal( + vertices.is_valid(), + mock_vertices.is_valid(), + True, + ) + + results["__iter__"] = test_utils.all_equal( + all(isinstance(vertex, mgp.Vertex) for vertex in vertices), + all(isinstance(vertex, mgp_mock.Vertex) for vertex in mock_vertices), + True, + ) + + results["__len__"] = test_utils.all_equal( + len(vertices), + len(mock_vertices), + 27, + ) + + return mgp.Record(results_dict=results) diff --git a/tests/e2e/mock_api/test_compare_mock.py b/tests/e2e/mock_api/test_compare_mock.py new file mode 100644 index 000000000..1f49913c8 --- /dev/null +++ b/tests/e2e/mock_api/test_compare_mock.py @@ -0,0 +1,195 @@ +import sys + +import pytest +from common import connect, execute_and_fetch_results_dict + + +def test_label(): + expected_results = { + "name": True, + "__eq__": True, + "__ne__": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict( + cursor, "CALL label.compare_apis() YIELD results_dict RETURN results_dict;" + ) + + assert results == expected_results + + +def test_properties_on_vertex(): + expected_results = { + "get": True, + "get[default]": True, + "set": True, + "items": True, + "keys": True, + "values": True, + "__len__": True, + "__iter__": True, + "__getitem__": True, + "__setitem__": True, + "__contains__": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict( + cursor, "CALL properties.compare_apis_on_vertex() YIELD results_dict RETURN results_dict;" + ) + + assert results == expected_results + + +def test_properties_on_edge(): + expected_results = { + "get": True, + "get[default]": True, + "set": True, + "items": True, + "keys": True, + "values": True, + "__len__": True, + "__iter__": True, + "__getitem__": True, + "__setitem__": True, + "__contains__": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict( + cursor, "CALL properties.compare_apis_on_edge() YIELD results_dict RETURN results_dict;" + ) + + assert results == expected_results + + +def test_edge_type(): + expected_results = { + "name": True, + "__eq__": True, + "__ne__": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict( + cursor, "CALL edge_type.compare_apis() YIELD results_dict RETURN results_dict;" + ) + + assert results == expected_results + + +def test_edge(): + expected_results = { + "is_valid": True, + "underlying_graph_is_mutable": True, + "id": True, + "type": True, + "from_vertex": True, + "to_vertex": True, + "properties": True, + "__eq__": True, + "__ne__": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict(cursor, "CALL edge.compare_apis() YIELD results_dict RETURN results_dict;") + + assert results == expected_results + + +def test_vertex(): + expected_results = { + "is_valid": True, + "underlying_graph_is_mutable": True, + "id": True, + "labels": True, + "properties": True, + "in_edges": True, + "out_edges": True, + "__eq__": True, + "__ne__": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict( + cursor, "CALL vertex.compare_apis() YIELD results_dict RETURN results_dict;" + ) + + assert results == expected_results + + +def test_path(): + expected_results = { + "__copy__": True, + "is_valid": True, + "expand": True, + "vertices": True, + "edges": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict(cursor, "CALL path.compare_apis() YIELD results_dict RETURN results_dict;") + + assert results == expected_results + + +def test_record(): + expected_results = { + "fields": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict( + cursor, "CALL record.compare_apis() YIELD results_dict RETURN results_dict;" + ) + + assert results == expected_results + + +def test_vertices(): + expected_results = { + "is_valid": True, + "__iter__": True, + "__len__": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict( + cursor, "CALL vertices.compare_apis() YIELD results_dict RETURN results_dict;" + ) + + assert results == expected_results + + +def test_graph(): + expected_results = { + "create_edge": True, + "create_vertex": True, + "delete_edge": True, + "delete_vertex": True, + "detach_delete_vertex": True, + "edge_id_assignment": True, + "get_vertex_by_id": True, + "is_mutable": True, + "is_not_mutable": True, + "is_valid": True, + "vertices": True, + } + + cursor = connect().cursor() + results = execute_and_fetch_results_dict( + cursor, "CALL graph.compare_apis() YIELD results_dict RETURN results_dict;" + ) + results.update( + execute_and_fetch_results_dict( + cursor, "CALL graph.test_read_proc_mutability() YIELD results_dict RETURN results_dict;" + ) + ) + + assert results == expected_results + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/mock_api/workloads.yaml b/tests/e2e/mock_api/workloads.yaml new file mode 100644 index 000000000..165477479 --- /dev/null +++ b/tests/e2e/mock_api/workloads.yaml @@ -0,0 +1,83 @@ +compare_mock: &compare_mock + cluster: + main: + args: ["--bolt-port", "7687", "--log-level=TRACE", "--also-log-to-stderr"] + log_file: "test-compare-mock-e2e.log" + setup_queries: + - "CREATE INDEX ON :__mg_vertex__(__mg_id__);" + - "CREATE (:__mg_vertex__:`Person` {__mg_id__: 0, `name`: 'Peter', `surname`: 'Yang'});" + - "CREATE (:__mg_vertex__:`Team` {__mg_id__: 1, `name`: 'Engineering'});" + - "CREATE (:__mg_vertex__:`Repository` {__mg_id__: 2, `name`: 'Memgraph'});" + - "CREATE (:__mg_vertex__:`Repository` {__mg_id__: 3, `name`: 'MAGE'});" + - "CREATE (:__mg_vertex__:`Repository` {__mg_id__: 4, `name`: 'GQLAlchemy'});" + - "CREATE (:__mg_vertex__:`Company`:`Startup` {__mg_id__: 5, `name`: 'Memgraph'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 6, `name`: 'welcome_to_engineering.txt'});" + - "CREATE (:__mg_vertex__:`Storage` {__mg_id__: 7, `name`: 'Google Drive'});" + - "CREATE (:__mg_vertex__:`Storage` {__mg_id__: 8, `name`: 'Notion'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 9, `name`: 'welcome_to_memgraph.txt'});" + - "CREATE (:__mg_vertex__:`Person` {__mg_id__: 10, `name`: 'Carl'});" + - "CREATE (:__mg_vertex__:`Folder` {__mg_id__: 11, `name`: 'engineering_folder'});" + - "CREATE (:__mg_vertex__:`Person` {__mg_id__: 12, `name`: 'Anna'});" + - "CREATE (:__mg_vertex__:`Folder` {__mg_id__: 13, `name`: 'operations_folder'});" + - "CREATE (:__mg_vertex__:`Team` {__mg_id__: 14, `name`: 'Operations'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 15, `name`: 'operations101.txt'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 16, `name`: 'expenses2022.csv'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 17, `name`: 'salaries2022.csv'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 18, `name`: 'engineering101.txt'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 19, `name`: 'working_with_github.txt'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 20, `name`: 'working_with_notion.txt'});" + - "CREATE (:__mg_vertex__:`Team` {__mg_id__: 21, `name`: 'Marketing'});" + - "CREATE (:__mg_vertex__:`Person` {__mg_id__: 22, `name`: 'Julie'});" + - "CREATE (:__mg_vertex__:`Account` {__mg_id__: 23, `name`: 'Facebook'});" + - "CREATE (:__mg_vertex__:`Account` {__mg_id__: 24, `name`: 'LinkedIn'});" + - "CREATE (:__mg_vertex__:`Account` {__mg_id__: 25, `name`: 'HackerNews'});" + - "CREATE (:__mg_vertex__:`File` {__mg_id__: 26, `name`: 'welcome_to_marketing.txt'});" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 0}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 5 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 1}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 9 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 2}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 14 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 3}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 2 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 4}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 3 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 5}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 4 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 6}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 6 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 7}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 11 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 8}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 1 CREATE (u)-[:`HAS_TEAM` {`permanent_id`: 9}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 21 CREATE (u)-[:`HAS_TEAM` {`permanent_id`: 10}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 14 CREATE (u)-[:`HAS_TEAM` {`permanent_id`: 11}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 12}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 8 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 13}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 9 AND v.__mg_id__ = 12 CREATE (u)-[:`CREATED_BY` {`permanent_id`: 14}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 1 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 15}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 5 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 16}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 9 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 17}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 7 CREATE(u)-[:`IS_STORED_IN` {`permanent_id`: 18}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 18 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 19}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 19 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 20}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 20 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 21}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 14 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 22}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 15 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 23}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 16 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 24}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 17 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 25}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 7 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 26}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 27}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 21 AND v.__mg_id__ = 23 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 28}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 21 AND v.__mg_id__ = 24 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 29}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 21 AND v.__mg_id__ = 25 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 30}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 21 AND v.__mg_id__ = 26 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 31}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 22 AND v.__mg_id__ = 21 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 32}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 22 AND v.__mg_id__ = 5 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 33}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 22 AND v.__mg_id__ = 9 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 34}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 26 AND v.__mg_id__ = 7 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 35}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 26 AND v.__mg_id__ = 8 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 36}]->(v);" + - "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 1 CREATE (u)-[:`HAS_TEAM_2` {`importance`: 'HIGH', `permanent_id`: 37}]->(v);" + - "DROP INDEX ON :__mg_vertex__(__mg_id__);" + - "MATCH (u) SET u.permanent_id = u.__mg_id__;" + - "MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;" + validation_queries: [] + +workloads: + - name: "test-compare-mock" # should be the same as the python file + binary: "tests/e2e/pytest_runner.sh" + proc: "tests/e2e/mock_api/procedures/" + args: ["mock_api/test_compare_mock.py"] + <<: *compare_mock diff --git a/tests/setup.sh b/tests/setup.sh index a74ee6ff9..463ea557b 100755 --- a/tests/setup.sh +++ b/tests/setup.sh @@ -15,6 +15,7 @@ PIP_DEPS=( "pytest==6.2.3" "pyyaml==5.4.1" "six==1.15.0" + "networkx==2.4" ) cd "$DIR" diff --git a/tools/requirements.txt b/tools/requirements.txt index 7dae50101..d506d9947 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -6,3 +6,4 @@ python-dateutil==2.6.1 pytz==2017.2 six==1.11.0 tabulate==0.8.1 +networkx==2.4