diff --git a/test/test_graph/test_diff.py b/test/test_graph/test_diff.py index d9b48a701..587281872 100644 --- a/test/test_graph/test_diff.py +++ b/test/test_graph/test_diff.py @@ -1,12 +1,23 @@ -from test.utils import GraphHelper -from typing import TYPE_CHECKING, Set +from dataclasses import dataclass, field +from test.utils import ( + COLLAPSED_BNODE, + BNodeHandling, + GHQuad, + GHTriple, + GraphHelper, + MarksType, + MarkType, +) +from typing import TYPE_CHECKING, Collection, Set, Tuple, Type, Union, cast import pytest +from _pytest.mark.structures import ParameterSet import rdflib from rdflib import Graph from rdflib.compare import graph_diff -from rdflib.namespace import FOAF, RDF +from rdflib.graph import ConjunctiveGraph, Dataset +from rdflib.namespace import FOAF, RDF, Namespace from rdflib.term import BNode, Literal if TYPE_CHECKING: @@ -15,7 +26,7 @@ """Test for graph_diff - much more extensive testing would certainly be possible""" -_TripleSetT = Set["_TripleType"] +_TripleSetType = Set["_TripleType"] class TestDiff: @@ -48,7 +59,7 @@ def test_subsets(self) -> None: triples only in `g0`, and that there are triples that occur in both `g0` and `g1`, and that there are triples only in `g1`. """ - g0_ts: _TripleSetT = set() + g0_ts: _TripleSetType = set() bnode = BNode() g0_ts.update( { @@ -59,7 +70,7 @@ def test_subsets(self) -> None: g0 = Graph() g0 += g0_ts - g1_ts: _TripleSetT = set() + g1_ts: _TripleSetType = set() bnode = BNode() g1_ts.update( { @@ -76,3 +87,154 @@ def test_subsets(self) -> None: assert in_first == set() assert len(in_second) > 0 assert len(in_both) > 0 + + +_ElementSetType = Union[Collection[GHTriple], Collection[GHQuad]] + +_ElementSetTypeOrStr = Union[_ElementSetType, str] + + +@dataclass +class GraphDiffCase: + graph_type: Type[Graph] + format: str + lhs: str + rhs: str + expected_result: Tuple[ + _ElementSetTypeOrStr, _ElementSetTypeOrStr, _ElementSetTypeOrStr + ] + marks: MarkType = field(default_factory=lambda: cast(MarksType, list())) + + def as_element_set(self, value: _ElementSetTypeOrStr) -> _ElementSetType: + if isinstance(value, str): + graph = self.graph_type() + graph.parse(data=value, format=self.format) + if isinstance(graph, ConjunctiveGraph): + return GraphHelper.quad_set(graph, BNodeHandling.COLLAPSE) + else: + return GraphHelper.triple_set(graph, BNodeHandling.COLLAPSE) + return value + + def expected_in_both_set(self) -> _ElementSetType: + return self.as_element_set(self.expected_result[0]) + + def expected_in_lhs_set(self) -> _ElementSetType: + return self.as_element_set(self.expected_result[1]) + + def expected_in_rhs_set(self) -> _ElementSetType: + return self.as_element_set(self.expected_result[2]) + + def as_params(self) -> ParameterSet: + return pytest.param(self, marks=self.marks) + + +EGSCHEME = Namespace("example:") + + +@pytest.mark.parametrize( + "test_case", + [ + GraphDiffCase( + Graph, + format="turtle", + lhs=""" + @prefix eg: . + _:a _:b _:c . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + rhs=""" + @prefix eg: . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + expected_result=( + """ + @prefix eg: . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + {(COLLAPSED_BNODE, COLLAPSED_BNODE, COLLAPSED_BNODE)}, + "", + ), + ), + GraphDiffCase( + Graph, + format="turtle", + lhs=""" + @prefix eg: . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + rhs=""" + @prefix eg: . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + expected_result=( + """ + @prefix eg: . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + "", + "", + ), + ), + GraphDiffCase( + Dataset, + format="trig", + lhs=""" + @prefix eg: . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + rhs=""" + @prefix eg: . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + expected_result=( + """ + @prefix eg: . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + "", + "", + ), + marks=pytest.mark.xfail( + reason="quads are not supported", raises=ValueError + ), + ).as_params(), + ], +) +def test_assert_sets_equal(test_case: GraphDiffCase): + """ + GraphHelper.sets_equals and related functions work correctly in both + positive and negative cases. + """ + lhs_graph: Graph = test_case.graph_type() + lhs_graph.parse(data=test_case.lhs, format=test_case.format) + + rhs_graph: Graph = test_case.graph_type() + rhs_graph.parse(data=test_case.rhs, format=test_case.format) + + in_both, in_lhs, in_rhs = graph_diff(lhs_graph, rhs_graph) + in_both_set = GraphHelper.triple_or_quad_set(in_both, BNodeHandling.COLLAPSE) + in_lhs_set = GraphHelper.triple_or_quad_set(in_lhs, BNodeHandling.COLLAPSE) + in_rhs_set = GraphHelper.triple_or_quad_set(in_rhs, BNodeHandling.COLLAPSE) + + assert test_case.expected_in_both_set() == in_both_set + assert test_case.expected_in_lhs_set() == in_lhs_set + assert test_case.expected_in_rhs_set() == in_rhs_set + + # Diff should be symetric + in_rboth, in_rlhs, in_rrhs = graph_diff(rhs_graph, lhs_graph) + in_rboth_set = GraphHelper.triple_or_quad_set(in_rboth, BNodeHandling.COLLAPSE) + in_rlhs_set = GraphHelper.triple_or_quad_set(in_rlhs, BNodeHandling.COLLAPSE) + in_rrhs_set = GraphHelper.triple_or_quad_set(in_rrhs, BNodeHandling.COLLAPSE) + + assert test_case.expected_in_both_set() == in_rboth_set + assert test_case.expected_in_rhs_set() == in_rlhs_set + assert test_case.expected_in_lhs_set() == in_rrhs_set diff --git a/test/test_graph/test_graph_cbd.py b/test/test_graph/test_graph_cbd.py index 4b8985eda..66861241a 100644 --- a/test/test_graph/test_graph_cbd.py +++ b/test/test_graph/test_graph_cbd.py @@ -1,5 +1,5 @@ from test.data import TEST_DATA_DIR -from test.utils import GraphHelper +from test.utils import BNodeHandling, GraphHelper import pytest @@ -130,7 +130,7 @@ def test_cbd_example(): query = "http://example.com/aReallyGreatBook" GraphHelper.assert_isomorphic(g.cbd(URIRef(query)), g_cbd) - GraphHelper.assert_sets_equals(g.cbd(URIRef(query)), g_cbd, exclude_blanks=True) + GraphHelper.assert_sets_equals(g.cbd(URIRef(query)), g_cbd, BNodeHandling.COLLAPSE) assert len(g.cbd(URIRef(query))) == ( 21 ), "cbd() for aReallyGreatBook should return 21 triples" diff --git a/test/test_roundtrip.py b/test/test_roundtrip.py index 632b3add1..58d7480cb 100644 --- a/test/test_roundtrip.py +++ b/test/test_roundtrip.py @@ -3,7 +3,7 @@ import os.path from pathlib import Path from test.data import TEST_DATA_DIR -from test.utils import GraphHelper +from test.utils import BNodeHandling, GraphHelper from typing import Callable, Iterable, List, Optional, Set, Tuple, Type, Union from xml.sax import SAXParseException @@ -285,9 +285,13 @@ def roundtrip( GraphHelper.assert_isomorphic(g1, g2) if checks is not None: if Check.SET_EQUALS in checks: - GraphHelper.assert_sets_equals(g1, g2, exclude_blanks=False) + GraphHelper.assert_sets_equals( + g1, g2, bnode_handling=BNodeHandling.COLLAPSE + ) if Check.SET_EQUALS_WITHOUT_BLANKS in checks: - GraphHelper.assert_sets_equals(g1, g2, exclude_blanks=True) + GraphHelper.assert_sets_equals( + g1, g2, bnode_handling=BNodeHandling.COLLAPSE + ) if logger.isEnabledFor(logging.DEBUG): logger.debug("OK") diff --git a/test/utils/__init__.py b/test/utils/__init__.py index 56af5e5ff..67935cc76 100644 --- a/test/utils/__init__.py +++ b/test/utils/__init__.py @@ -7,18 +7,14 @@ from __future__ import print_function -import email.message +import enum import pprint import random -import unittest -from contextlib import AbstractContextManager, contextmanager -from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler +from contextlib import contextmanager +from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import PurePath, PureWindowsPath from threading import Thread -from traceback import print_exc -from types import TracebackType from typing import ( - TYPE_CHECKING, Any, Callable, Collection, @@ -28,7 +24,6 @@ Iterable, Iterator, List, - NamedTuple, Optional, Set, Tuple, @@ -37,10 +32,7 @@ Union, cast, ) -from unittest.mock import MagicMock, Mock -from urllib.error import HTTPError -from urllib.parse import ParseResult, parse_qs, unquote, urlparse -from urllib.request import urlopen +from urllib.parse import unquote, urlparse import pytest from _pytest.mark.structures import Mark, MarkDecorator, ParameterSet @@ -105,6 +97,16 @@ def ctx_http_server( GHQuadSet = Set[GHQuad] GHQuadFrozenSet = FrozenSet[GHQuad] +NodeT = TypeVar("NodeT", bound=GHNode) + +COLLAPSED_BNODE = URIRef("urn:fdc:rdflib.github.io:20220522:collapsed-bnode") + + +class BNodeHandling(str, enum.Enum): + COMPARE = "compare" # Compare BNodes as normal + EXCLUDE = "exclude" # Exclude blanks from comparison + COLLAPSE = "collapse" # Collapse all blank nodes to one IRI + class GraphHelper: """ @@ -112,57 +114,102 @@ class GraphHelper: """ @classmethod - def node(cls, node: Node, exclude_blanks: bool = False) -> GHNode: + def add_triples( + cls, graph: Graph, triples: Iterable[Tuple[Node, Node, Node]] + ) -> Graph: + for triple in triples: + graph.add(triple) + return graph + + @classmethod + def node( + cls, node: Node, bnode_handling: BNodeHandling = BNodeHandling.COMPARE + ) -> GHNode: """ Return the identifier of the provided node. """ if isinstance(node, Graph): - xset = cast(GHNode, cls.triple_or_quad_set(node, exclude_blanks)) + xset = cast(GHNode, cls.triple_or_quad_set(node, bnode_handling)) return xset return cast(Identifier, node) @classmethod def nodes( - cls, nodes: Tuple[Node, ...], exclude_blanks: bool = False + cls, + nodes: Tuple[Node, ...], + bnode_handling: BNodeHandling = BNodeHandling.COMPARE, ) -> Tuple[GHNode, ...]: """ Return the identifiers of the provided nodes. """ result = [] for node in nodes: - result.append(cls.node(node, exclude_blanks)) + result.append(cls.node(node, bnode_handling)) + return tuple(result) + + @classmethod + def _contains_bnodes(cls, nodes: Tuple[GHNode, ...]) -> bool: + """ + Return true if any of the nodes are BNodes. + """ + for node in nodes: + if isinstance(node, BNode): + return True + return False + + @classmethod + def _collapse_bnodes(cls, nodes: Tuple[NodeT, ...]) -> Tuple[NodeT, ...]: + """ + Return BNodes as COLLAPSED_BNODE + """ + result: List[NodeT] = [] + for node in nodes: + if isinstance(node, BNode): + result.append(cast(NodeT, COLLAPSED_BNODE)) + else: + result.append(node) return tuple(result) @classmethod def triple_set( - cls, graph: Graph, exclude_blanks: bool = False + cls, graph: Graph, bnode_handling: BNodeHandling = BNodeHandling.COMPARE ) -> GHTripleFrozenSet: result: GHTripleSet = set() for sn, pn, on in graph.triples((None, None, None)): - s, p, o = cls.nodes((sn, pn, on), exclude_blanks) - if exclude_blanks and ( - isinstance(s, BNode) or isinstance(p, BNode) or isinstance(o, BNode) + s, p, o = cls.nodes((sn, pn, on), bnode_handling) + if bnode_handling == BNodeHandling.EXCLUDE and cls._contains_bnodes( + (s, p, o) ): continue + elif bnode_handling == BNodeHandling.COLLAPSE: + s, p, o = cls._collapse_bnodes((s, p, o)) + # if bnode_handling == BNodeHandling.EXCLUDE ( + # isinstance(s, BNode) or isinstance(p, BNode) or isinstance(o, BNode) + # ): + # continue result.add((s, p, o)) return frozenset(result) @classmethod def triple_sets( - cls, graphs: Iterable[Graph], exclude_blanks: bool = False + cls, + graphs: Iterable[Graph], + bnode_handling: BNodeHandling = BNodeHandling.COMPARE, ) -> List[GHTripleFrozenSet]: """ Extracts the set of all triples from the supplied Graph. """ result: List[GHTripleFrozenSet] = [] for graph in graphs: - result.append(cls.triple_set(graph, exclude_blanks)) + result.append(cls.triple_set(graph, bnode_handling)) return result @classmethod def quad_set( - cls, graph: ConjunctiveGraph, exclude_blanks: bool = False + cls, + graph: ConjunctiveGraph, + bnode_handling: BNodeHandling = BNodeHandling.COMPARE, ) -> GHQuadFrozenSet: """ Extracts the set of all quads from the supplied ConjunctiveGraph. @@ -180,73 +227,90 @@ def quad_set( gn_id = gn.identifier # type: ignore[assignment] else: raise ValueError(f"invalid graph type {type(graph)}: {graph!r}") - s, p, o = cls.nodes((sn, pn, on), exclude_blanks) - if exclude_blanks and ( - isinstance(s, BNode) or isinstance(p, BNode) or isinstance(o, BNode) + s, p, o = cls.nodes((sn, pn, on), bnode_handling) + if bnode_handling == BNodeHandling.EXCLUDE and cls._contains_bnodes( + (s, p, o, gn_id) ): continue + elif bnode_handling == BNodeHandling.COLLAPSE: + s, p, o, gn_id = cast(GHQuad, cls._collapse_bnodes((s, p, o, gn_id))) quad: GHQuad = (s, p, o, gn_id) result.add(quad) return frozenset(result) @classmethod def triple_or_quad_set( - cls, graph: Graph, exclude_blanks: bool = False + cls, graph: Graph, bnode_handling: BNodeHandling = BNodeHandling.COMPARE ) -> Union[GHQuadFrozenSet, GHTripleFrozenSet]: """ Extracts quad or triple sets depending on whether or not the graph is ConjunctiveGraph or a normal Graph. """ if isinstance(graph, ConjunctiveGraph): - return cls.quad_set(graph, exclude_blanks) - return cls.triple_set(graph, exclude_blanks) + return cls.quad_set(graph, bnode_handling) + return cls.triple_set(graph, bnode_handling) @classmethod def assert_triple_sets_equals( - cls, lhs: Graph, rhs: Graph, exclude_blanks: bool = False + cls, + lhs: Graph, + rhs: Graph, + bnode_handling: BNodeHandling = BNodeHandling.COMPARE, + negate: bool = False, ) -> None: """ Asserts that the triple sets in the two graphs are equal. """ - lhs_set = cls.triple_set(lhs, exclude_blanks) if isinstance(lhs, Graph) else lhs - rhs_set = cls.triple_set(rhs, exclude_blanks) if isinstance(rhs, Graph) else rhs - assert lhs_set == rhs_set + lhs_set = cls.triple_set(lhs, bnode_handling) if isinstance(lhs, Graph) else lhs + rhs_set = cls.triple_set(rhs, bnode_handling) if isinstance(rhs, Graph) else rhs + if not negate: + assert lhs_set == rhs_set + else: + assert lhs_set != rhs_set @classmethod def assert_quad_sets_equals( cls, lhs: Union[ConjunctiveGraph, GHQuadSet], rhs: Union[ConjunctiveGraph, GHQuadSet], - exclude_blanks: bool = False, + bnode_handling: BNodeHandling = BNodeHandling.COMPARE, + negate: bool = False, ) -> None: """ Asserts that the quads sets in the two graphs are equal. """ - lhs_set = cls.quad_set(lhs, exclude_blanks) if isinstance(lhs, Graph) else lhs - rhs_set = cls.quad_set(rhs, exclude_blanks) if isinstance(rhs, Graph) else rhs - assert lhs_set == rhs_set + lhs_set = cls.quad_set(lhs, bnode_handling) if isinstance(lhs, Graph) else lhs + rhs_set = cls.quad_set(rhs, bnode_handling) if isinstance(rhs, Graph) else rhs + if not negate: + assert lhs_set == rhs_set + else: + assert lhs_set != rhs_set @classmethod def assert_sets_equals( cls, lhs: Union[Graph, GHTripleSet, GHQuadSet], rhs: Union[Graph, GHTripleSet, GHQuadSet], - exclude_blanks: bool = False, + bnode_handling: BNodeHandling = BNodeHandling.COMPARE, + negate: bool = False, ) -> None: """ Asserts that that ther quad or triple sets from the two graphs are equal. """ lhs_set = ( - cls.triple_or_quad_set(lhs, exclude_blanks) + cls.triple_or_quad_set(lhs, bnode_handling) if isinstance(lhs, Graph) else lhs ) rhs_set = ( - cls.triple_or_quad_set(rhs, exclude_blanks) + cls.triple_or_quad_set(rhs, bnode_handling) if isinstance(rhs, Graph) else rhs ) - assert lhs_set == rhs_set + if not negate: + assert lhs_set == rhs_set + else: + assert lhs_set != rhs_set @classmethod def format_set( @@ -281,7 +345,7 @@ def assert_isomorphic( This asserts that the two graphs are isomorphic, providing a nicely formatted error message if they are not. """ - + # TODO FIXME: This should possibly raise an error when used on a ConjunctiveGraph def format_report(message: Optional[str] = None) -> str: in_both, in_lhs, in_rhs = rdflib.compare.graph_diff(lhs, rhs) preamle = "" if message is None else f"{message}\n" @@ -296,6 +360,38 @@ def format_report(message: Optional[str] = None) -> str: assert rdflib.compare.isomorphic(lhs, rhs), format_report(message) + @classmethod + def assert_cgraph_isomorphic( + cls, + lhs: ConjunctiveGraph, + rhs: ConjunctiveGraph, + exclude_bnodes: bool, + message: Optional[str] = None, + ) -> None: + def get_contexts(cgraph: ConjunctiveGraph) -> Dict[URIRef, Graph]: + result = {} + for context in cgraph.contexts(): + if isinstance(context.identifier, BNode): + if exclude_bnodes: + continue + else: + raise AssertionError("BNode labelled graphs not supported") + elif isinstance(context.identifier, URIRef): + result[context.identifier] = context + else: + raise AssertionError( + f"unsupported context identifier {context.identifier}" + ) + return result + + lhs_contexts = get_contexts(lhs) + rhs_contexts = get_contexts(rhs) + assert ( + lhs_contexts.keys() == rhs_contexts.keys() + ), f"must have same context ids in LHS and RHS (exclude_bnodes={exclude_bnodes})" + for id, lhs_context in lhs_contexts.items(): + cls.assert_isomorphic(lhs_context, rhs_contexts[id], message) + @classmethod def strip_literal_datatypes(cls, graph: Graph, datatypes: Set[URIRef]) -> None: """ @@ -353,11 +449,24 @@ def file_uri_to_path( ParamsT = TypeVar("ParamsT", bound=tuple) -Marks = Collection[Union[Mark, MarkDecorator]] +MarksType = Collection[Union[MarkDecorator, Mark]] +MarkListType = List[Union[MarkDecorator, Mark]] +MarkType = Union[MarkDecorator, MarksType] + +MarkerType = Callable[..., Optional[MarkType]] + + +def marks_to_list(mark: MarkType) -> MarkListType: + if isinstance(mark, (MarkDecorator, Mark)): + return [mark] + elif isinstance(mark, list): + return mark + return list(*mark) def pytest_mark_filter( - param_sets: Iterable[Union[ParamsT, ParameterSet]], mark_dict: Dict[ParamsT, Marks] + param_sets: Iterable[Union[ParamsT, ParameterSet]], + mark_dict: Dict[ParamsT, MarksType], ) -> Generator[ParameterSet, None, None]: """ Adds marks to test parameters. Useful for adding xfails to test parameters. @@ -370,12 +479,14 @@ def pytest_mark_filter( id=param_set.id, marks=[ *param_set.marks, - *mark_dict.get(cast(ParamsT, param_set.values), cast(Marks, ())), + *mark_dict.get( + cast(ParamsT, param_set.values), cast(MarksType, ()) + ), ], ) else: yield pytest.param( - *param_set, marks=mark_dict.get(param_set, cast(Marks, ())) + *param_set, marks=mark_dict.get(param_set, cast(MarksType, ())) ) diff --git a/test/utils/test/test_testutils.py b/test/utils/test/test_testutils.py index 2485a404c..a624c4456 100644 --- a/test/utils/test/test_testutils.py +++ b/test/utils/test/test_testutils.py @@ -1,12 +1,19 @@ import os +from contextlib import ExitStack from dataclasses import dataclass from pathlib import PurePosixPath, PureWindowsPath -from test.utils import GraphHelper, affix_tuples, file_uri_to_path -from typing import Any, List, Optional, Tuple, Union +from test.utils import ( + COLLAPSED_BNODE, + BNodeHandling, + GraphHelper, + affix_tuples, + file_uri_to_path, +) +from typing import Any, List, Optional, Tuple, Type, Union import pytest -from rdflib.graph import ConjunctiveGraph, Graph +from rdflib.graph import ConjunctiveGraph, Dataset, Graph from rdflib.term import URIRef @@ -99,7 +106,7 @@ def test_paths( class SetsEqualTestCase: equal: bool format: Union[str, Tuple[str, str]] - ignore_blanks: bool + bnode_handling: BNodeHandling lhs: str rhs: str @@ -122,7 +129,7 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=False, format="turtle", - ignore_blanks=False, + bnode_handling=BNodeHandling.COMPARE, lhs=""" @prefix eg: . _:a _:b _:c . @@ -138,7 +145,7 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=True, format="turtle", - ignore_blanks=True, + bnode_handling=BNodeHandling.EXCLUDE, lhs=""" @prefix eg: . _:a _:b _:c . @@ -154,7 +161,25 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=True, format="turtle", - ignore_blanks=False, + bnode_handling=BNodeHandling.COLLAPSE, + lhs=""" + @prefix eg: . + _:a _:b _:c . + _:z _:b _:c . + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + rhs=f""" + @prefix eg: . + <{COLLAPSED_BNODE}> <{COLLAPSED_BNODE}> <{COLLAPSED_BNODE}>. + eg:o0 eg:p0 eg:s0 . + eg:o1 eg:p1 eg:s1 . + """, + ), + SetsEqualTestCase( + equal=True, + format="turtle", + bnode_handling=BNodeHandling.COMPARE, lhs=""" . . @@ -168,7 +193,7 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=False, format="turtle", - ignore_blanks=False, + bnode_handling=BNodeHandling.COMPARE, lhs=""" . . @@ -183,7 +208,7 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=True, format=("nquads", "trig"), - ignore_blanks=True, + bnode_handling=BNodeHandling.EXCLUDE, lhs=""" . . @@ -199,7 +224,7 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=True, format=("nquads", "trig"), - ignore_blanks=True, + bnode_handling=BNodeHandling.COMPARE, lhs=""" . . @@ -219,7 +244,7 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=True, format="n3", - ignore_blanks=False, + bnode_handling=BNodeHandling.COMPARE, lhs=""" { } {}. """, @@ -231,7 +256,7 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=True, format="n3", - ignore_blanks=False, + bnode_handling=BNodeHandling.COMPARE, lhs=""" { } {}. """, @@ -243,7 +268,7 @@ def rhs_format(self) -> str: SetsEqualTestCase( equal=True, format="n3", - ignore_blanks=False, + bnode_handling=BNodeHandling.COMPARE, lhs=""" { { } } {}. """, @@ -278,44 +303,44 @@ def test_assert_sets_equal(test_case: SetsEqualTestCase): graph: Graph cgraph: ConjunctiveGraph for graph, cgraph in ((lhs_graph, lhs_cgraph), (rhs_graph, rhs_cgraph)): - GraphHelper.assert_sets_equals(graph, graph, True) - GraphHelper.assert_sets_equals(cgraph, cgraph, True) - GraphHelper.assert_triple_sets_equals(graph, graph, True) - GraphHelper.assert_triple_sets_equals(cgraph, cgraph, True) - GraphHelper.assert_quad_sets_equals(cgraph, cgraph, True) + GraphHelper.assert_sets_equals(graph, graph, BNodeHandling.COLLAPSE) + GraphHelper.assert_sets_equals(cgraph, cgraph, BNodeHandling.COLLAPSE) + GraphHelper.assert_triple_sets_equals(graph, graph, BNodeHandling.COLLAPSE) + GraphHelper.assert_triple_sets_equals(cgraph, cgraph, BNodeHandling.COLLAPSE) + GraphHelper.assert_quad_sets_equals(cgraph, cgraph, BNodeHandling.COLLAPSE) if not test_case.equal: with pytest.raises(AssertionError): GraphHelper.assert_sets_equals( - lhs_graph, rhs_graph, test_case.ignore_blanks + lhs_graph, rhs_graph, test_case.bnode_handling ) with pytest.raises(AssertionError): GraphHelper.assert_sets_equals( - lhs_cgraph, rhs_cgraph, test_case.ignore_blanks + lhs_cgraph, rhs_cgraph, test_case.bnode_handling ) with pytest.raises(AssertionError): GraphHelper.assert_triple_sets_equals( - lhs_graph, rhs_graph, test_case.ignore_blanks + lhs_graph, rhs_graph, test_case.bnode_handling ) with pytest.raises(AssertionError): GraphHelper.assert_triple_sets_equals( - lhs_cgraph, rhs_cgraph, test_case.ignore_blanks + lhs_cgraph, rhs_cgraph, test_case.bnode_handling ) with pytest.raises(AssertionError): GraphHelper.assert_quad_sets_equals( - lhs_cgraph, rhs_cgraph, test_case.ignore_blanks + lhs_cgraph, rhs_cgraph, test_case.bnode_handling ) else: - GraphHelper.assert_sets_equals(lhs_graph, rhs_graph, test_case.ignore_blanks) - GraphHelper.assert_sets_equals(lhs_cgraph, rhs_cgraph, test_case.ignore_blanks) + GraphHelper.assert_sets_equals(lhs_graph, rhs_graph, test_case.bnode_handling) + GraphHelper.assert_sets_equals(lhs_cgraph, rhs_cgraph, test_case.bnode_handling) GraphHelper.assert_triple_sets_equals( - lhs_graph, rhs_graph, test_case.ignore_blanks + lhs_graph, rhs_graph, test_case.bnode_handling ) GraphHelper.assert_triple_sets_equals( - lhs_cgraph, rhs_cgraph, test_case.ignore_blanks + lhs_cgraph, rhs_cgraph, test_case.bnode_handling ) GraphHelper.assert_quad_sets_equals( - lhs_cgraph, rhs_cgraph, test_case.ignore_blanks + lhs_cgraph, rhs_cgraph, test_case.bnode_handling ) @@ -357,3 +382,104 @@ def test_prefix_tuples( expected_result: List[Tuple[Any, ...]], ) -> None: assert expected_result == list(affix_tuples(prefix, tuples, suffix)) + + +@pytest.mark.parametrize( + ["graph_type", "format", "lhs", "rhs", "expected_result"], + [ + ( + Dataset, + "trig", + """ + @prefix eg: . + + _:b0 eg:p0 eg:o0. + eg:s1 eg:p1 eg:o1. + + eg:g0 { + _:g0b0 eg:g0p0 eg:g0o0. + eg:g0s1 eg:g0p1 eg:g0o1. + } + """, + """ + @prefix eg: . + + _:b1 eg:p0 eg:o0. + eg:s1 eg:p1 eg:o1. + + eg:g0 { + _:g0b1 eg:g0p0 eg:g0o0. + eg:g0s1 eg:g0p1 eg:g0o1. + } + """, + None, + ), + ( + Dataset, + "trig", + """ + @prefix eg: . + + eg:g0 { + _:b0 eg:g0p0 eg:g0o0. + eg:g0s1 eg:g0p1 eg:g0o1. + } + """, + """ + @prefix eg: . + + eg:g0 { + _:b1 eg:g0p0 eg:g0o1. + eg:g0s1 eg:g0p1 eg:g0o1. + } + """, + AssertionError, + ), + ( + Dataset, + "trig", + """ + @prefix eg: . + + eg:g0 { + _:b0 eg:g0p0 eg:g0o0. + eg:g0s1 eg:g0p1 eg:g0o1. + } + """, + """ + @prefix eg: . + + eg:g0 { + _:b1 eg:g0p0 eg:g0o0. + eg:g0s1 eg:g0p1 eg:g0o1. + } + + eg:g1 { + _:b0 eg:g1p0 eg:g1o0. + eg:g1s1 eg:g1p1 eg:g1o1. + } + """, + AssertionError, + ), + ], +) +def test_assert_cgraph_isomorphic( + graph_type: Type[ConjunctiveGraph], + format: str, + lhs: str, + rhs: str, + expected_result: Union[None, Type[Exception]], +) -> None: + lhs_graph = graph_type() + lhs_graph.parse(data=lhs, format=format) + rhs_graph = graph_type() + rhs_graph.parse(data=rhs, format=format) + catcher: Optional[pytest.ExceptionInfo[Exception]] = None + with ExitStack() as xstack: + if expected_result is not None: + catcher = xstack.enter_context(pytest.raises(expected_result)) + GraphHelper.assert_cgraph_isomorphic(lhs_graph, rhs_graph, exclude_bnodes=True) + if expected_result is None: + assert catcher is None + else: + assert catcher is not None