diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/graph/ac/test_null.py | 10 | ||||
-rw-r--r-- | test/graph/test_graph.py | 55 | ||||
-rw-r--r-- | test/graph/test_nodes.py | 30 | ||||
-rw-r--r-- | test/graph/test_resolve.py | 181 | ||||
-rw-r--r-- | test/query/__init__.py | 0 | ||||
-rw-r--r-- | test/query/ast/__init__.py | 0 | ||||
-rw-r--r-- | test/query/ast/test_filter_.py | 480 | ||||
-rw-r--r-- | test/query/test_validator.py | 261 | ||||
-rw-r--r-- | test/triple_store/sparql/__init__.py | 0 | ||||
-rw-r--r-- | test/triple_store/sparql/test_parse_filter.py | 727 | ||||
-rw-r--r-- | test/triple_store/sparql/test_sparql.py (renamed from test/triple_store/test_sparql.py) | 92 | ||||
-rw-r--r-- | test/triple_store/test_base.py | 3 | ||||
-rw-r--r-- | test/utils/test_commons.py | 17 |
13 files changed, 1837 insertions, 19 deletions
diff --git a/test/graph/ac/test_null.py b/test/graph/ac/test_null.py index f39c9be..c863943 100644 --- a/test/graph/ac/test_null.py +++ b/test/graph/ac/test_null.py @@ -10,6 +10,7 @@ import unittest # bsie imports from bsfs import schema as _schema from bsfs.namespace import ns +from bsfs.query import ast from bsfs.triple_store import SparqlStore from bsfs.utils import URI @@ -93,6 +94,15 @@ class TestNullAC(unittest.TestCase): ac = NullAC(self.backend, self.user) self.assertSetEqual(self.ent_ids, ac.createable(self.ent_type, self.ent_ids)) + def test_filter_read(self): + query = ast.filter.Or( + ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234')), + ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#4321')), + ast.filter.Any(ns.bse.author, ast.filter.Equals('Me, Myself, and I'))) + ac = NullAC(self.backend, self.user) + self.assertEqual(query, ac.filter_read(self.ent_type, query)) + return query + ## main ## diff --git a/test/graph/test_graph.py b/test/graph/test_graph.py index 33cf6aa..8503d5b 100644 --- a/test/graph/test_graph.py +++ b/test/graph/test_graph.py @@ -9,10 +9,11 @@ import unittest # bsie imports from bsfs import schema +from bsfs.graph.nodes import Nodes from bsfs.namespace import ns +from bsfs.query import ast from bsfs.triple_store import SparqlStore from bsfs.utils import URI, errors -from bsfs.graph.nodes import Nodes # objects to test from bsfs.graph.graph import Graph @@ -192,6 +193,58 @@ class TestGraph(unittest.TestCase): bsfs:unique "true"^^xsd:boolean . ''')) + def test_get(self): + # setup + graph = Graph(self.backend, self.user) + graph.migrate(schema.Schema.from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Tag rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean . + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + + ''')) + # add some instances + ents = graph.nodes(ns.bsfs.Entity, {URI('http://example.com/entity#1234'), URI('http://example.com/entity#4321')}) + tags = graph.nodes(ns.bsfs.Tag, {URI('http://example.com/tag#1234'), URI('http://example.com/tag#4321')}) + # add some node links + ents.set(ns.bse.tag, tags) + # add some literals + graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'hello world') + graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'foo') + graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'foobar') + graph.node(ns.bsfs.Tag, URI('http://example.com/tag#1234')).set(ns.bse.comment, 'foo') + graph.node(ns.bsfs.Tag, URI('http://example.com/tag#4321')).set(ns.bse.comment, 'bar') + + # get exception for invalid query + self.assertRaises(errors.ConsistencyError, graph.get, ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Equals('hello world'))) + + # query returns nodes + self.assertEqual(graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags))), ents) + self.assertEqual(graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('foo'))), + graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234'))) + self.assertEqual(graph.get(ns.bsfs.Node, ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('foo'))), + graph.nodes(ns.bsfs.Node, {URI('http://example.com/entity#1234'), URI('http://example.com/tag#1234')})) + self.assertEqual(graph.get(ns.bsfs.Entity, ast.filter.Or( + ast.filter.Any(ns.bse.comment, ast.filter.EndsWith('bar')), + ast.filter.Any(ns.bse.tag, ast.filter.All(ns.bse.comment, ast.filter.Equals('bar'))))), + ents) + + + ## main ## diff --git a/test/graph/test_nodes.py b/test/graph/test_nodes.py index 43e7f6f..11ae46d 100644 --- a/test/graph/test_nodes.py +++ b/test/graph/test_nodes.py @@ -72,6 +72,20 @@ class TestNodes(unittest.TestCase): bsfs:unique "true"^^xsd:boolean . ''') + self.schema_triples = { + # schema hierarchy + (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bsm.t_created), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.author), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef('http://bsfs.ai/schema/Tag#representative'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + } # Nodes constructor args self.user = URI('http://example.com/me') # set args @@ -160,7 +174,7 @@ class TestNodes(unittest.TestCase): time_triples = list(self.backend._graph.objects(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri))) t_ent_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0 # check triples - self.assertSetEqual(set(self.backend._graph), { + self.assertSetEqual(set(self.backend._graph), self.schema_triples | { # entity definitions (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), @@ -171,7 +185,7 @@ class TestNodes(unittest.TestCase): # existing nodes remain unchanged self.assertSetEqual(self.ent_ids, nodes._ensure_nodes(self.ent_type, self.ent_ids)) - self.assertSetEqual(set(self.backend._graph), { + self.assertSetEqual(set(self.backend._graph), self.schema_triples | { # entity definitions (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), @@ -186,7 +200,7 @@ class TestNodes(unittest.TestCase): time_triples = list(self.backend._graph.objects(rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(self.t_created.uri))) t_tag_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0 # check triples - self.assertSetEqual(set(self.backend._graph), { + self.assertSetEqual(set(self.backend._graph), self.schema_triples | { # previous triples (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), @@ -202,7 +216,7 @@ class TestNodes(unittest.TestCase): def test___set(self): # setup nodes = Nodes(self.backend, self.user, self.ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}) - self.assertSetEqual(set(self.backend._graph), set()) + self.assertSetEqual(set(self.backend._graph), self.schema_triples | set()) set_ = nodes._Nodes__set # node_type must match predicate's domain @@ -217,7 +231,7 @@ class TestNodes(unittest.TestCase): time_triples = list(self.backend._graph.objects(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri))) t_ent_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0 # verify triples - self.assertSetEqual(set(self.backend._graph), { + self.assertSetEqual(set(self.backend._graph), self.schema_triples | { # entity definitions (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), @@ -236,7 +250,7 @@ class TestNodes(unittest.TestCase): time_triples = list(self.backend._graph.objects(rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(self.t_created.uri))) t_tag_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0 # verify triples - self.assertSetEqual(set(self.backend._graph), { + self.assertSetEqual(set(self.backend._graph), self.schema_triples | { # previous values (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')), @@ -265,7 +279,7 @@ class TestNodes(unittest.TestCase): Nodes(self.backend, self.user, self.ent_type, self.ent_ids)) def test_set(self): - self.assertSetEqual(set(self.backend._graph), set()) + self.assertSetEqual(set(self.backend._graph), self.schema_triples | set()) nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids) # can set literal values self.assertEqual(nodes, nodes.set(self.p_filesize.uri, 1234)) @@ -312,7 +326,7 @@ class TestNodes(unittest.TestCase): def test_set_from_iterable(self): - self.assertSetEqual(set(self.backend._graph), set()) + self.assertSetEqual(set(self.backend._graph), self.schema_triples | set()) nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids) # can set literal and node values simultaneously self.assertEqual(nodes, nodes.set_from_iterable({ diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py new file mode 100644 index 0000000..5bc99e4 --- /dev/null +++ b/test/graph/test_resolve.py @@ -0,0 +1,181 @@ +""" + +Part of the bsfs test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import unittest + +# bsie imports +from bsfs import schema as bsc +from bsfs.graph import Graph, nodes +from bsfs.namespace import ns +from bsfs.query import ast +from bsfs.triple_store import SparqlStore +from bsfs.utils import URI, errors + +# objects to test +from bsfs.graph.resolve import Filter + + +## code ## + +class TestFilter(unittest.TestCase): + """ + + NOTE: The Filter resolver is relatively simple as it only checks and changes + ast.filter.Is instances. Hence, we don't test all methods individually but + all of them with respect to ast.filter.Is elements. + + """ + + def test_call(self): + schema = bsc.Schema.from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Tag rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Literal . + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + + bse:filesize rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "false"^^xsd:boolean . + + bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean . + ''') + backend = SparqlStore.Open() + backend.schema = schema + graph = Graph(backend, URI('http://example.com/me')) + ents = graph.nodes(ns.bsfs.Entity, + {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}) + tags = graph.nodes(ns.bsfs.Tag, + {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')}) + invalid = nodes.Nodes(None, '', schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), + {'http://example.com/you/invalid#1234', 'http://example.com/you/invalid#4321'}) + resolver = Filter(schema) + + # immediate Is + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.Is(ents)), + ast.filter.Or( + ast.filter.Is('http://example.com/me/entity#1234'), + ast.filter.Is('http://example.com/me/entity#4321') + )) + # only resolves nodes instances, not URIs + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.Is('http://example.com/me/entity#1234')), + ast.filter.Is('http://example.com/me/entity#1234')) + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.Is(1234)), + ast.filter.Is(1234)) + + # within And (also checks _value) + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Is(ents), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')), + )), + ast.filter.And( + ast.filter.Or( + ast.filter.Is('http://example.com/me/entity#1234'), + ast.filter.Is('http://example.com/me/entity#4321')), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')) + )) + # within Or (checks _bounded) + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is(ents), + ast.filter.Any(ns.bse.filesize, ast.filter.LessThan(5)), + )), + ast.filter.Or( + ast.filter.Or( + ast.filter.Is('http://example.com/me/entity#1234'), + ast.filter.Is('http://example.com/me/entity#4321')), + ast.filter.Any(ns.bse.filesize, ast.filter.LessThan(5)) + )) + + # Any-branched Is + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.tag, ast.filter.Is(tags))), + ast.filter.Any(ns.bse.tag, ast.filter.Or( + ast.filter.Is('http://example.com/me/tag#1234'), + ast.filter.Is('http://example.com/me/tag#4321')), + )) + # All-branched Is + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.All(ns.bse.tag, ast.filter.Is(tags))), + ast.filter.All(ns.bse.tag, ast.filter.Or( + ast.filter.Is('http://example.com/me/tag#1234'), + ast.filter.Is('http://example.com/me/tag#4321')), + )) + # Negated predicate + self.assertEqual(resolver(schema.node(ns.bsfs.Tag), + ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True), ast.filter.Is(ents))), + ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True), ast.filter.Or( + ast.filter.Is('http://example.com/me/entity#1234'), + ast.filter.Is('http://example.com/me/entity#4321')), + )) + + # negated Is + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.Not(ast.filter.Is(ents))), + ast.filter.Not( + ast.filter.Or( + ast.filter.Is('http://example.com/me/entity#1234'), + ast.filter.Is('http://example.com/me/entity#4321')), + )) + + # for sake of completeness: Has + self.assertEqual(resolver(schema.node(ns.bsfs.Entity), + ast.filter.Has(ns.bse.comment)), + ast.filter.Has(ns.bse.comment)) + # route errors + self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Predicate(ns.bse.comment)) + self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Any(ast.filter.PredicateExpression(), ast.filter.Equals('foo'))) + self.assertRaises(errors.UnreachableError, resolver._one_of, ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate))) + + # check schema consistency + self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Is(invalid)) + # check immediate type compatibility + self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Is(ents)) + self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Entity), + ast.filter.Is(tags)) + # check type compatibility through branches + self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Any(ns.bse.comment, ast.filter.Is(tags))) + self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Any(ns.bse.invalid, ast.filter.Is(tags))) + self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Any(ast.filter.OneOf(ns.bse.comment, ns.bse.tag), ast.filter.Is(tags))) + self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Any(ast.filter.OneOf(ns.bse.comment, ns.bse.filesize), ast.filter.Is(tags))) + self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag), + ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True), ast.filter.Is(tags))) + + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## diff --git a/test/query/__init__.py b/test/query/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/query/__init__.py diff --git a/test/query/ast/__init__.py b/test/query/ast/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/query/ast/__init__.py diff --git a/test/query/ast/test_filter_.py b/test/query/ast/test_filter_.py new file mode 100644 index 0000000..4f69bdc --- /dev/null +++ b/test/query/ast/test_filter_.py @@ -0,0 +1,480 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import unittest + +# bsfs imports +from bsfs.namespace import ns +from bsfs.utils import URI + +# objects to test +from bsfs.query.ast.filter_ import _Expression, FilterExpression, PredicateExpression +from bsfs.query.ast.filter_ import _Branch, Any, All +from bsfs.query.ast.filter_ import _Agg, And, Or +from bsfs.query.ast.filter_ import Not, Has +from bsfs.query.ast.filter_ import _Value, Is, Equals, Substring, StartsWith, EndsWith +from bsfs.query.ast.filter_ import _Bounded, LessThan, GreaterThan +from bsfs.query.ast.filter_ import Predicate, OneOf +from bsfs.query.ast.filter_ import IsIn, IsNotIn + + +## code ## + +class TestExpression(unittest.TestCase): + def test_essentials(self): + # comparison + self.assertEqual(_Expression(), _Expression()) + self.assertEqual(FilterExpression(), FilterExpression()) + self.assertEqual(PredicateExpression(), PredicateExpression()) + self.assertEqual(hash(_Expression()), hash(_Expression())) + self.assertEqual(hash(FilterExpression()), hash(FilterExpression())) + self.assertEqual(hash(PredicateExpression()), hash(PredicateExpression())) + # comparison respects type + self.assertNotEqual(FilterExpression(), _Expression()) + self.assertNotEqual(_Expression(), PredicateExpression()) + self.assertNotEqual(PredicateExpression(), FilterExpression()) + self.assertNotEqual(hash(FilterExpression()), hash(_Expression())) + self.assertNotEqual(hash(_Expression()), hash(PredicateExpression())) + self.assertNotEqual(hash(PredicateExpression()), hash(FilterExpression())) + # string conversion + self.assertEqual(str(_Expression()), '_Expression()') + self.assertEqual(str(FilterExpression()), 'FilterExpression()') + self.assertEqual(str(PredicateExpression()), 'PredicateExpression()') + self.assertEqual(repr(_Expression()), '_Expression()') + self.assertEqual(repr(FilterExpression()), 'FilterExpression()') + self.assertEqual(repr(PredicateExpression()), 'PredicateExpression()') + + +class TestBranch(unittest.TestCase): # _Branch, Any, All + def test_essentials(self): + pred = PredicateExpression() + expr = FilterExpression() + + # comparison respects type + self.assertNotEqual(_Branch(pred, expr), Any(pred, expr)) + self.assertNotEqual(Any(pred, expr), All(pred, expr)) + self.assertNotEqual(All(pred, expr), _Branch(pred, expr)) + self.assertNotEqual(hash(_Branch(pred, expr)), hash(Any(pred, expr))) + self.assertNotEqual(hash(Any(pred, expr)), hash(All(pred, expr))) + self.assertNotEqual(hash(All(pred, expr)), hash(_Branch(pred, expr))) + + for cls in (_Branch, Any, All): + # comparison + self.assertEqual(cls(pred, expr), cls(pred, expr)) + self.assertEqual(hash(cls(pred, expr)), hash(cls(pred, expr))) + # comparison respects predicate + self.assertNotEqual(cls(ns.bse.filename, expr), cls(ns.bse.filesize, expr)) + self.assertNotEqual(hash(cls(ns.bse.filename, expr)), hash(cls(ns.bse.filesize, expr))) + # comparison respects expression + self.assertNotEqual(cls(pred, Equals('hello')), cls(pred, Equals('world'))) + self.assertNotEqual(hash(cls(pred, Equals('hello'))), hash(cls(pred, Equals('world')))) + + # string conversion + self.assertEqual(str(_Branch(pred, expr)), f'_Branch({pred}, {expr})') + self.assertEqual(repr(_Branch(pred, expr)), f'_Branch({pred}, {expr})') + self.assertEqual(str(Any(pred, expr)), f'Any({pred}, {expr})') + self.assertEqual(repr(Any(pred, expr)), f'Any({pred}, {expr})') + self.assertEqual(str(All(pred, expr)), f'All({pred}, {expr})') + self.assertEqual(repr(All(pred, expr)), f'All({pred}, {expr})') + + def test_members(self): + class Foo(): pass + pred = PredicateExpression() + expr = FilterExpression() + + for cls in (_Branch, Any, All): + # predicate returns member + self.assertEqual(cls(PredicateExpression(), expr).predicate, PredicateExpression()) + # can pass an URI + self.assertEqual(cls(ns.bse.filename, expr).predicate, Predicate(ns.bse.filename)) + # can pass a PredicateExpression + self.assertEqual(cls(Predicate(ns.bse.filename), expr).predicate, Predicate(ns.bse.filename)) + # must pass an URI or PredicateExpression + self.assertRaises(TypeError, cls, Foo(), expr) + # expression returns member + self.assertEqual(cls(pred, Equals('hello')).expr, Equals('hello')) + # expression must be a FilterExpression + self.assertRaises(TypeError, cls, ns.bse.filename, 'hello') + self.assertRaises(TypeError, cls, ns.bse.filename, 1234) + self.assertRaises(TypeError, cls, ns.bse.filename, Foo()) + + +class TestAgg(unittest.TestCase): # _Agg, And, Or + def test_essentials(self): + expr = {Equals('hello'), Equals('world')} + + # comparison respects type + self.assertNotEqual(_Agg(expr), And(expr)) + self.assertNotEqual(And(expr), Or(expr)) + self.assertNotEqual(Or(expr), _Agg(expr)) + self.assertNotEqual(hash(_Agg(expr)), hash(And(expr))) + self.assertNotEqual(hash(And(expr)), hash(Or(expr))) + self.assertNotEqual(hash(Or(expr)), hash(_Agg(expr))) + + for cls in (_Agg, And, Or): + # comparison + self.assertEqual(cls(expr), cls(expr)) + self.assertEqual(hash(cls(expr)), hash(cls(expr))) + # comparison respects expression + self.assertNotEqual(cls(expr), cls(Equals('world'))) + self.assertNotEqual(hash(cls(expr)), hash(cls(Equals('world')))) + self.assertNotEqual(cls(Equals('hello')), cls(Equals('world'))) + self.assertNotEqual(hash(cls(Equals('hello'))), hash(cls(Equals('world')))) + + # string conversion + self.assertEqual(str(_Agg(Equals('hello'))), '_Agg({Equals(hello)})') + self.assertEqual(repr(_Agg(Equals('hello'))), '_Agg({Equals(hello)})') + self.assertEqual(str(And(Equals('hello'))), 'And({Equals(hello)})') + self.assertEqual(repr(And(Equals('hello'))), 'And({Equals(hello)})') + self.assertEqual(str(Or(Equals('hello'))), 'Or({Equals(hello)})') + self.assertEqual(repr(Or(Equals('hello'))), 'Or({Equals(hello)})') + + def test_expression(self): + class Foo(): pass + + for cls in (_Agg, And, Or): + # can pass expressions as arguments + self.assertSetEqual(cls(Equals('hello'), Equals('world')).expr, {Equals('hello'), Equals('world')}) + # can pass one expressions as argument + self.assertSetEqual(cls(Equals('hello')).expr, {Equals('hello')}) + # can pass expressions as iterator + self.assertSetEqual(cls(iter((Equals('hello'), Equals('world')))).expr, {Equals('hello'), Equals('world')}) + # can pass expressions as generator + def gen(): + yield Equals('hello') + yield Equals('world') + self.assertSetEqual(cls(gen()).expr, {Equals('hello'), Equals('world')}) + # can pass expressions as list-like + self.assertSetEqual(cls((Equals('hello'), Equals('world'))).expr, {Equals('hello'), Equals('world')}) + # can pass one expression as list-like + self.assertSetEqual(cls([Equals('hello')]).expr, {Equals('hello')}) + # must pass expressions + self.assertRaises(TypeError, cls, Foo(), Foo()) + self.assertRaises(TypeError, cls, [Foo(), Foo()]) + + # iter + self.assertSetEqual(set(iter(cls(Equals('hello'), Equals('world')))), {Equals('hello'), Equals('world')}) + # contains + self.assertIn(Equals('world'), cls(Equals('hello'), Equals('world'))) + self.assertNotIn(Equals('foo'), cls(Equals('hello'), Equals('world'))) + # len + self.assertEqual(len(cls(Equals('hello'), Equals('world'))), 2) + self.assertEqual(len(cls(Equals('hello'), Equals('world'), Equals('foo'))), 3) + + + +class TestNot(unittest.TestCase): + def test_essentials(self): + expr = FilterExpression() + # comparison + self.assertEqual(Not(expr), Not(expr)) + self.assertEqual(hash(Not(expr)), hash(Not(expr))) + # comparison respects type + self.assertNotEqual(Not(expr), FilterExpression()) + self.assertNotEqual(hash(Not(expr)), hash(FilterExpression())) + # comparison respects expression + self.assertNotEqual(Not(Equals('hello')), Not(Equals('world'))) + self.assertNotEqual(hash(Not(Equals('hello'))), hash(Not(Equals('world')))) + # string conversion + self.assertEqual(str(Not(Equals('hello'))), 'Not(Equals(hello))') + self.assertEqual(repr(Not(Equals('hello'))), 'Not(Equals(hello))') + + def test_expression(self): + # Not requires an expression argument + self.assertRaises(TypeError, Not) + # expression must be a FilterExpression + self.assertRaises(TypeError, Not, 'hello') + self.assertRaises(TypeError, Not, 1234) + self.assertRaises(TypeError, Not, Predicate(ns.bse.filesize)) + # member returns expression + self.assertEqual(Not(Equals('hello')).expr, Equals('hello')) + + +class TestHas(unittest.TestCase): + def test_essentials(self): + pred = PredicateExpression() + count = FilterExpression() + # comparison + self.assertEqual(Has(pred, count), Has(pred, count)) + self.assertEqual(hash(Has(pred, count)), hash(Has(pred, count))) + # comparison respects type + self.assertNotEqual(Has(pred, count), FilterExpression()) + self.assertNotEqual(hash(Has(pred, count)), hash(FilterExpression())) + # comparison respects predicate + self.assertNotEqual(Has(pred, count), Has(Predicate(ns.bse.filesize), count)) + self.assertNotEqual(hash(Has(pred, count)), hash(Has(Predicate(ns.bse.filesize), count))) + # comparison respects count + self.assertNotEqual(Has(pred, count), Has(pred, LessThan(5))) + self.assertNotEqual(hash(Has(pred, count)), hash(Has(pred, LessThan(5)))) + # string conversion + self.assertEqual(str(Has(Predicate(ns.bse.filesize), LessThan(5))), + f'Has(Predicate({ns.bse.filesize}, False), LessThan(5.0, True))') + self.assertEqual(repr(Has(Predicate(ns.bse.filesize), LessThan(5))), + f'Has(Predicate({ns.bse.filesize}, False), LessThan(5.0, True))') + + def test_members(self): + pred = PredicateExpression() + count = FilterExpression() + # member returns expression + # predicate must be an URI or a PredicateExpression + self.assertEqual(Has(ns.bse.filesize, count).predicate, Predicate(ns.bse.filesize)) + self.assertEqual(Has(Predicate(ns.bse.filesize), count).predicate, Predicate(ns.bse.filesize)) + self.assertRaises(TypeError, Has, 1234, FilterExpression()) + self.assertRaises(TypeError, Has, FilterExpression(), FilterExpression()) + # member returns count + # count must be None, an integer, or a FilterExpression + self.assertEqual(Has(pred).count, GreaterThan(1, False)) + self.assertEqual(Has(pred, LessThan(5)).count, LessThan(5)) + self.assertEqual(Has(pred, 5).count, Equals(5)) + self.assertRaises(TypeError, Has, pred, 'hello') + self.assertRaises(TypeError, Has, pred, Predicate(ns.bse.filesize)) + + + +class TestValue(unittest.TestCase): + def test_essentials(self): + # comparison respects type + self.assertNotEqual(_Value('hello'), Equals('hello')) + self.assertNotEqual(Equals('hello'), Is('hello')) + self.assertNotEqual(Is('hello'), Substring('hello')) + self.assertNotEqual(Substring('hello'), StartsWith('hello')) + self.assertNotEqual(StartsWith('hello'), EndsWith('hello')) + self.assertNotEqual(EndsWith('hello'), _Value('hello')) + self.assertNotEqual(hash(_Value('hello')), hash(Equals('hello'))) + self.assertNotEqual(hash(Equals('hello')), hash(Is('hello'))) + self.assertNotEqual(hash(Is('hello')), hash(Substring('hello'))) + self.assertNotEqual(hash(Substring('hello')), hash(StartsWith('hello'))) + self.assertNotEqual(hash(StartsWith('hello')), hash(EndsWith('hello'))) + self.assertNotEqual(hash(EndsWith('hello')), hash(_Value('hello'))) + + for cls in (_Value, Is, Equals, Substring, StartsWith, EndsWith): + # comparison + self.assertEqual(cls('hello'), cls('hello')) + self.assertEqual(hash(cls('hello')), hash(cls('hello'))) + # comparison respects value + self.assertNotEqual(cls('hello'), cls('world')) + self.assertNotEqual(hash(cls('hello')), hash(cls('world'))) + + # string conversion + self.assertEqual(str(_Value('hello')), '_Value(hello)') + self.assertEqual(repr(_Value('hello')), '_Value(hello)') + self.assertEqual(str(Is('hello')), 'Is(hello)') + self.assertEqual(repr(Is('hello')), 'Is(hello)') + self.assertEqual(str(Equals('hello')), 'Equals(hello)') + self.assertEqual(repr(Equals('hello')), 'Equals(hello)') + self.assertEqual(str(Substring('hello')), 'Substring(hello)') + self.assertEqual(repr(Substring('hello')), 'Substring(hello)') + self.assertEqual(str(StartsWith('hello')), 'StartsWith(hello)') + self.assertEqual(repr(StartsWith('hello')), 'StartsWith(hello)') + self.assertEqual(str(EndsWith('hello')), 'EndsWith(hello)') + self.assertEqual(repr(EndsWith('hello')), 'EndsWith(hello)') + + def test_value(self): + class Foo(): pass + for cls in (_Value, Is, Equals, Substring, StartsWith, EndsWith): + # value can be anything + # value returns member + f = Foo() + self.assertEqual(cls('hello').value, 'hello') + self.assertEqual(cls(1234).value, 1234) + self.assertEqual(cls(f).value, f) + + +class TestBounded(unittest.TestCase): + def test_essentials(self): + # comparison respects type + self.assertNotEqual(_Bounded(1234), LessThan(1234)) + self.assertNotEqual(LessThan(1234), GreaterThan(1234)) + self.assertNotEqual(GreaterThan(1234), _Bounded(1234)) + self.assertNotEqual(hash(_Bounded(1234)), hash(LessThan(1234))) + self.assertNotEqual(hash(LessThan(1234)), hash(GreaterThan(1234))) + self.assertNotEqual(hash(GreaterThan(1234)), hash(_Bounded(1234))) + + for cls in (_Bounded, LessThan, GreaterThan): + # comparison + self.assertEqual(cls(1234), cls(1234)) + self.assertEqual(hash(cls(1234)), hash(cls(1234))) + # comparison respects threshold + self.assertNotEqual(cls(1234), cls(4321)) + self.assertNotEqual(hash(cls(1234)), hash(cls(4321))) + # comparison respects strict + self.assertNotEqual(cls(1234, True), cls(1234, False)) + self.assertNotEqual(hash(cls(1234, True)), hash(cls(1234, False))) + + # string conversion + self.assertEqual(str(_Bounded(1234, False)), '_Bounded(1234.0, False)') + self.assertEqual(repr(_Bounded(1234, False)), '_Bounded(1234.0, False)') + self.assertEqual(str(LessThan(1234, False)), 'LessThan(1234.0, False)') + self.assertEqual(repr(LessThan(1234, False)), 'LessThan(1234.0, False)') + self.assertEqual(str(GreaterThan(1234, False)), 'GreaterThan(1234.0, False)') + self.assertEqual(repr(GreaterThan(1234, False)), 'GreaterThan(1234.0, False)') + + def test_members(self): + class Foo(): pass + for cls in (_Bounded, LessThan, GreaterThan): + # threshold becomes float + self.assertEqual(cls(1.234).threshold, 1.234) + self.assertEqual(cls(1234).threshold, 1234.0) + self.assertEqual(cls('1234').threshold, 1234) + self.assertRaises(TypeError, cls, Foo()) + # strict becomes bool + self.assertEqual(cls(1234, True).strict, True) + self.assertEqual(cls(1234, False).strict, False) + self.assertEqual(cls(1234, Foo()).strict, True) + + +class TestPredicate(unittest.TestCase): + def test_essentials(self): + # comparison + self.assertEqual(Predicate(ns.bse.filesize), Predicate(ns.bse.filesize)) + self.assertEqual(hash(Predicate(ns.bse.filesize)), hash(Predicate(ns.bse.filesize))) + # comparison respects type + self.assertNotEqual(Predicate(ns.bse.filesize), PredicateExpression()) + self.assertNotEqual(hash(Predicate(ns.bse.filesize)), hash(PredicateExpression())) + # comparison respects predicate + self.assertNotEqual(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)) + self.assertNotEqual(hash(Predicate(ns.bse.filesize)), hash(Predicate(ns.bse.filename))) + # comparison respects reverse + self.assertNotEqual(Predicate(ns.bse.filesize, True), Predicate(ns.bse.filesize, False)) + self.assertNotEqual(hash(Predicate(ns.bse.filesize, True)), hash(Predicate(ns.bse.filesize, False))) + # string conversion + self.assertEqual(str(Predicate(ns.bse.filesize)), f'Predicate({ns.bse.filesize}, False)') + self.assertEqual(str(Predicate(ns.bse.filesize, True)), + f'Predicate({ns.bse.filesize}, True)') + self.assertEqual(repr(Predicate(ns.bse.filesize)), f'Predicate({ns.bse.filesize}, False)') + self.assertEqual(repr(Predicate(ns.bse.filesize, True)), + f'Predicate({ns.bse.filesize}, True)') + + def test_members(self): + # member returns predicate + # predicate must be an URI + self.assertEqual(Predicate(ns.bse.filesize).predicate, ns.bse.filesize) + self.assertEqual(Predicate(URI('hello world')).predicate, URI('hello world')) + self.assertRaises(TypeError, Predicate, 1234) + self.assertRaises(TypeError, Predicate, FilterExpression()) + self.assertRaises(TypeError, Predicate, FilterExpression()) + # reverse becomes a boolean + self.assertEqual(Predicate(ns.bse.filesize, True).reverse, True) + self.assertEqual(Predicate(ns.bse.filesize, False).reverse, False) + self.assertEqual(Predicate(ns.bse.filesize, 'abc').reverse, True) + + +class TestOneOf(unittest.TestCase): + def test_essentials(self): + expr = {Predicate(ns.bse.filename), Predicate(ns.bse.filesize)} + # comparison + self.assertEqual(OneOf(expr), OneOf(expr)) + self.assertEqual(hash(OneOf(expr)), hash(OneOf(expr))) + # comparison respects type + self.assertNotEqual(OneOf(expr), PredicateExpression()) + self.assertNotEqual(hash(OneOf(expr)), hash(PredicateExpression())) + # comparison respects expression + self.assertNotEqual(OneOf(expr), OneOf(Predicate(ns.bse.filename))) + self.assertNotEqual(hash(OneOf(expr)), hash(OneOf(Predicate(ns.bse.filename)))) + # string conversion + self.assertEqual(str(OneOf(Predicate(ns.bse.filesize))), + f'OneOf({{Predicate({ns.bse.filesize}, False)}})') + self.assertEqual(repr(OneOf(Predicate(ns.bse.filesize))), + f'OneOf({{Predicate({ns.bse.filesize}, False)}})') + + def test_expression(self): + class Foo(): pass + # can pass expressions as arguments + self.assertSetEqual(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)).expr, + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # can pass one expressions as argument + self.assertSetEqual(OneOf(Predicate(ns.bse.filesize)).expr, + {Predicate(ns.bse.filesize)}) + # can pass expressions as iterator + self.assertSetEqual(OneOf(iter((Predicate(ns.bse.filesize), Predicate(ns.bse.filename)))).expr, + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # can pass expressions as generator + def gen(): + yield Predicate(ns.bse.filesize) + yield Predicate(ns.bse.filename) + self.assertSetEqual(OneOf(gen()).expr, + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # can pass expressions as list-like + self.assertSetEqual(OneOf((Predicate(ns.bse.filesize), Predicate(ns.bse.filename))).expr, + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # can pass one expression as list-like + self.assertSetEqual(OneOf([Predicate(ns.bse.filesize)]).expr, + {Predicate(ns.bse.filesize)}) + # must pass expressions + self.assertRaises(TypeError, OneOf, Foo(), Foo()) + self.assertRaises(TypeError, OneOf, [Foo(), Foo()]) + # must pass at least one expression + self.assertRaises(AttributeError, OneOf) + + # iter + self.assertSetEqual(set(iter(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename)))), + {Predicate(ns.bse.filesize), Predicate(ns.bse.filename)}) + # contains + self.assertIn(Predicate(ns.bse.filesize), + OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))) + self.assertNotIn(Predicate(ns.bse.tag), + OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))) + # len + self.assertEqual(len(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename))), 2) + self.assertEqual(len(OneOf(Predicate(ns.bse.filesize), Predicate(ns.bse.filename), Predicate(ns.bse.tag))), 3) + + + def testIsIn(self): + # can pass expressions as arguments + self.assertEqual(IsIn('http://example.com/entity#1234', 'http://example.com/entity#4321'), + Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) + # can pass one expression as argument + self.assertEqual(IsIn('http://example.com/entity#1234'), + Or(Is('http://example.com/entity#1234'))) + # can pass expressions as iterator + self.assertEqual(IsIn(iter(('http://example.com/entity#1234', 'http://example.com/entity#4321'))), + Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) + # can pass expressions as generator + def gen(): + yield 'http://example.com/entity#1234' + yield 'http://example.com/entity#4321' + self.assertEqual(IsIn(gen()), + Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) + # can pass expressions as list-like + self.assertEqual(IsIn(['http://example.com/entity#1234', 'http://example.com/entity#4321']), + Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321'))) + # can pass one expression as list-like + self.assertEqual(IsIn(['http://example.com/entity#1234']), + Or(Is('http://example.com/entity#1234'))) + + + def testIsNotIn(self): + # can pass expressions as arguments + self.assertEqual(IsNotIn('http://example.com/entity#1234', 'http://example.com/entity#4321'), + Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) + # can pass one expression as argument + self.assertEqual(IsNotIn('http://example.com/entity#1234'), + Not(Or(Is('http://example.com/entity#1234')))) + # can pass expressions as iterator + self.assertEqual(IsNotIn(iter(('http://example.com/entity#1234', 'http://example.com/entity#4321'))), + Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) + # can pass expressions as generator + def gen(): + yield 'http://example.com/entity#1234' + yield 'http://example.com/entity#4321' + self.assertEqual(IsNotIn(gen()), + Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) + # can pass expressions as list-like + self.assertEqual(IsNotIn(['http://example.com/entity#1234', 'http://example.com/entity#4321']), + Not(Or(Is('http://example.com/entity#1234'), Is('http://example.com/entity#4321')))) + # can pass one expression as list-like + self.assertEqual(IsNotIn(['http://example.com/entity#1234']), + Not(Or(Is('http://example.com/entity#1234')))) + + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## diff --git a/test/query/test_validator.py b/test/query/test_validator.py new file mode 100644 index 0000000..4f8364a --- /dev/null +++ b/test/query/test_validator.py @@ -0,0 +1,261 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import unittest + +# bsfs imports +from bsfs import schema as _schema +from bsfs.namespace import ns +from bsfs.query import ast +from bsfs.utils import errors + +# objects to test +from bsfs.query.validator import Filter + + +## code ## + +class TestFilter(unittest.TestCase): + def setUp(self): + self.schema = _schema.Schema.from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:URI rdfs:subClassOf bsfs:Literal . + + bsfs:Tag rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Literal . + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + + bse:filesize rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + + bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean . + + bse:label rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Tag ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + + bse:buddy rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + ''') + self.validate = Filter(self.schema) + + def test_call(self): + # root_type must be a _schema.Node + self.assertRaises(TypeError, self.validate, 1234, None) + self.assertRaises(TypeError, self.validate, '1234', None) + self.assertRaises(TypeError, self.validate, self.schema.literal(ns.bsfs.URI), None) + # root_type must exist in the schema + self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Image), None) + self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity).get_child(ns.bsfs.Image), None) + # valid query returns true + self.assertTrue(self.validate(self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy), + ast.filter.Or( + ast.filter.Is('http://example.com/symbol#1234'), + ast.filter.All(ns.bse.comment, ast.filter.StartsWith('foo')), + ast.filter.And( + ast.filter.Has(ns.bse.comment, ast.filter.Or( + ast.filter.GreaterThan(5), + ast.filter.LessThan(1), + ) + ), + ast.filter.Not(ast.filter.Any(ns.bse.comment, + ast.filter.Not(ast.filter.Equals('hello world')))), + ))))) + # invalid paths raise consistency error + self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy), + ast.filter.Or( + ast.filter.All(ns.bse.comment, ast.filter.Equals('hello world')), + ast.filter.All(ns.bse.label, ast.filter.Equals('hello world')), # domain mismatch + ))) + + def test_routing(self): + self.assertRaises(errors.BackendError, self.validate._parse_filter_expression, ast.filter.FilterExpression(), self.schema.node(ns.bsfs.Node)) + self.assertRaises(errors.BackendError, self.validate._parse_predicate_expression, ast.filter.PredicateExpression()) + + def test_predicate(self): + # predicate must be in the schema + self.assertRaises(errors.ConsistencyError, self.validate._predicate, ast.filter.Predicate(ns.bse.invalid)) + # predicate must have a range + self.assertRaises(errors.BackendError, self.validate._predicate, ast.filter.Predicate(ns.bsfs.Predicate)) + # predicate returns domain and range + self.assertEqual(self.validate._predicate(ast.filter.Predicate(ns.bse.tag)), + (self.schema.node(ns.bsfs.Entity), self.schema.node(ns.bsfs.Tag))) + # reverse is applied + self.assertEqual(self.validate._predicate(ast.filter.Predicate(ns.bse.tag, reverse=True)), + (self.schema.node(ns.bsfs.Tag), self.schema.node(ns.bsfs.Entity))) + + def test_one_of(self): + # domains must both be nodes or literals + self.assertRaises(errors.ConsistencyError, self.validate._one_of, ast.filter.OneOf(ns.bse.tag, ast.filter.Predicate(ns.bse.label, reverse=True))) + # domains must be related + self.assertRaises(errors.ConsistencyError, self.validate._one_of, ast.filter.OneOf(ns.bse.tag, ns.bse.label)) + # ranges must both be nodes or literals + self.assertRaises(errors.ConsistencyError, self.validate._one_of, ast.filter.OneOf(ns.bse.tag, ns.bse.comment)) + # ranges must be related + self.assertRaises(errors.ConsistencyError, self.validate._one_of, ast.filter.OneOf(ns.bse.tag, ast.filter.Predicate(ns.bse.buddy, reverse=True))) + # one_of returns most specific domain + self.assertEqual(self.validate._one_of(ast.filter.OneOf(ns.bse.comment, ns.bse.label)), + (self.schema.node(ns.bsfs.Tag), self.schema.literal(ns.xsd.string))) + # one_of returns the most generic range + self.assertEqual(self.validate._one_of(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy)), + (self.schema.node(ns.bsfs.Entity), self.schema.node(ns.bsfs.Node))) + + def test_branch(self): + # type must be a node + self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.literal(ns.bsfs.Literal), None) + # type must be in the schema + self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), None) + # predicate is verified + self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bsfs.Invalid, ast.filter.Is('http://example.com/entity#1234'))) + # predicate must match the domain + self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Node), + ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234'))) + self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Tag), + ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234'))) + # child expression must be valid + self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.tag, ast.filter.Equals('hello world'))) + # branch accepts valid expressions + self.assertIsNone(self.validate._branch(self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/entity#1234')))) + self.assertIsNone(self.validate._branch(self.schema.node(ns.bsfs.Entity), + ast.filter.All(ns.bse.tag, ast.filter.Is('http://example.com/entity#1234')))) + + def test_agg(self): + # agg evaluates child expressions + self.assertRaises(errors.ConsistencyError, self.validate._agg, self.schema.node(ns.bsfs.Entity), + ast.filter.And(ast.filter.Is('http://example.com/entity#1234'), ast.filter.Equals('hello world'))) + self.assertRaises(errors.ConsistencyError, self.validate._agg, self.schema.literal(ns.xsd.string), + ast.filter.And(ast.filter.Is('http://example.com/entity#1234'), ast.filter.Equals('hello world'))) + self.assertRaises(errors.ConsistencyError, self.validate._agg, self.schema.node(ns.bsfs.Entity), + ast.filter.Or(ast.filter.Is('http://example.com/entity#1234'), ast.filter.Equals('hello world'))) + self.assertRaises(errors.ConsistencyError, self.validate._agg, self.schema.literal(ns.xsd.string), + ast.filter.Or(ast.filter.Is('http://example.com/entity#1234'), ast.filter.Equals('hello world'))) + # agg works on nodes + self.assertIsNone(self.validate._agg(self.schema.node(ns.bsfs.Entity), + ast.filter.And(ast.filter.Is('http://example.com/entity#1234'), ast.filter.Is('http://example.com/entity#4321')))) + self.assertIsNone(self.validate._agg(self.schema.node(ns.bsfs.Entity), + ast.filter.Or(ast.filter.Is('http://example.com/entity#1234'), ast.filter.Is('http://example.com/entity#4321')))) + # agg works on literals + self.assertIsNone(self.validate._agg(self.schema.literal(ns.xsd.string), + ast.filter.And(ast.filter.Equals('foobar'), ast.filter.Equals('hello world')))) + self.assertIsNone(self.validate._agg(self.schema.literal(ns.xsd.string), + ast.filter.Or(ast.filter.Equals('foobar'), ast.filter.Equals('hello world')))) + + def test_not(self): + # not evaluates child expressions + self.assertRaises(errors.ConsistencyError, self.validate._not, self.schema.node(ns.bsfs.Entity), + ast.filter.Not(ast.filter.Equals('hello world'))) + self.assertRaises(errors.ConsistencyError, self.validate._not, self.schema.literal(ns.xsd.string), + ast.filter.Not(ast.filter.Is('http://example.com/entity#1234'))) + # not works on nodes + self.assertIsNone(self.validate._not(self.schema.node(ns.bsfs.Entity), + ast.filter.Not(ast.filter.Is('http://example.com/entity#1234')))) + # not works on literals + self.assertIsNone(self.validate._not(self.schema.literal(ns.xsd.string), + ast.filter.Not(ast.filter.Equals('hello world')))) + + def test_has(self): + # type must be node + self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.literal(ns.bsfs.Literal), + ast.filter.Has(ns.bse.tag)) + # type must be in the schema + self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), + ast.filter.Has(ns.bse.tag)) + # has checks predicate + self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Entity), + ast.filter.Has(ns.bse.invalid)) + # predicate must match domain + self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Tag), + ast.filter.Has(ns.bse.tag)) + # has checks count expression + self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Entity), + ast.filter.Has(ns.bse.tag, ast.filter.Is('http://example.com/entity#1234'))) + # has accepts correct expressions + self.assertIsNone(self.validate._has(self.schema.node(ns.bsfs.Entity), ast.filter.Has(ns.bse.tag, ast.filter.GreaterThan(5)))) + + def test_is(self): + # type must be node + self.assertRaises(errors.ConsistencyError, self.validate._is, self.schema.literal(ns.bsfs.Literal), + ast.filter.Is('http://example.com/foo')) + # type must be in the schema + self.assertRaises(errors.ConsistencyError, self.validate._is, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), + ast.filter.Is('http://example.com/foo')) + # is accepts correct expressions + self.assertIsNone(self.validate._is(self.schema.node(ns.bsfs.Entity), ast.filter.Is('http://example.com/entity#1234'))) + + def test_value(self): + # type must be literal + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.node(ns.bsfs.Node), + ast.filter.Equals('hello world')) + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.node(ns.bsfs.Node), + ast.filter.Substring('hello world')) + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.node(ns.bsfs.Node), + ast.filter.StartsWith('hello world')) + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.node(ns.bsfs.Node), + ast.filter.EndsWith('hello world')) + # type must be in the schema + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + ast.filter.Equals('hello world')) + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + ast.filter.Substring('hello world')) + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + ast.filter.StartsWith('hello world')) + self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + ast.filter.EndsWith('hello world')) + # value accepts correct expressions + self.assertIsNone(self.validate._value(self.schema.literal(ns.xsd.string), ast.filter.Equals('hello world'))) + self.assertIsNone(self.validate._value(self.schema.literal(ns.xsd.string), ast.filter.Substring('hello world'))) + self.assertIsNone(self.validate._value(self.schema.literal(ns.xsd.string), ast.filter.StartsWith('hello world'))) + self.assertIsNone(self.validate._value(self.schema.literal(ns.xsd.string), ast.filter.EndsWith('hello world'))) + + def test_bounded(self): + # type must be literal + self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.node(ns.bsfs.Node), + ast.filter.GreaterThan(0)) + self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.node(ns.bsfs.Node), + ast.filter.LessThan(0)) + # type must be in the schema + self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + ast.filter.GreaterThan(0)) + self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid), + ast.filter.LessThan(0)) + # bounded accepts correct expressions + self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.LessThan(0))) + self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.GreaterThan(0))) + + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## diff --git a/test/triple_store/sparql/__init__.py b/test/triple_store/sparql/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/triple_store/sparql/__init__.py diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py new file mode 100644 index 0000000..bd19803 --- /dev/null +++ b/test/triple_store/sparql/test_parse_filter.py @@ -0,0 +1,727 @@ +""" + +Part of the bsfs test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import rdflib +import unittest + +# bsie imports +from bsfs import schema as _schema +from bsfs.namespace import ns +from bsfs.query import ast +from bsfs.utils import errors + +# objects to test +from bsfs.triple_store.sparql.parse_filter import Filter + + +## code ## + +class TestParseFilter(unittest.TestCase): + def setUp(self): + # schema + self.schema = _schema.Schema.from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + bsfs:Tag rdfs:subClassOf bsfs:Node . + + xsd:string rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Literal . + bsfs:URI rdfs:subClassOf bsfs:Literal . + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + + bse:filesize rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + + bse:buddy rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean . + + bse:representative rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Tag ; + rdfs:range bsfs:Image ; + bsfs:unique "false"^^xsd:boolean . + + bse:iso rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Image ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + + ''') + + # parser instance + self.parser = Filter(self.schema) + + # graph to test queries + self.graph = rdflib.Graph() + # schema hierarchies + self.graph.add((rdflib.URIRef('http://bsfs.ai/schema/Entity'), rdflib.RDFS.subClassOf, rdflib.URIRef('http://bsfs.ai/schema/Node'))) + self.graph.add((rdflib.URIRef('http://bsfs.ai/schema/Image'), rdflib.RDFS.subClassOf, rdflib.URIRef('http://bsfs.ai/schema/Entity'))) + self.graph.add((rdflib.URIRef('http://bsfs.ai/schema/Tag'), rdflib.RDFS.subClassOf, rdflib.URIRef('http://bsfs.ai/schema/Node'))) + # entities + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity'))) + self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity'))) + # tags + self.graph.add((rdflib.URIRef('http://example.com/tag#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag'))) + self.graph.add((rdflib.URIRef('http://example.com/tag#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag'))) + # images + self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Image'))) + self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Image'))) + # node comments + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('Me, Myself, and I', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('hello world', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('hello world', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('Me, Myself, and I', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/tag#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('Me, Myself, and I', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/tag#4321'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('4321', datatype=rdflib.XSD.string))) + # entity filesizes + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.filesize), rdflib.Literal(1234, datatype=rdflib.XSD.integer))) + self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.filesize), rdflib.Literal(4321, datatype=rdflib.XSD.integer))) + self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.filesize), rdflib.Literal(1234, datatype=rdflib.XSD.integer))) + self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.URIRef(ns.bse.filesize), rdflib.Literal(4321, datatype=rdflib.XSD.integer))) + # entity tags + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.tag), rdflib.URIRef('http://example.com/tag#1234'))) + self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.tag), rdflib.URIRef('http://example.com/tag#4321'))) + self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.tag), rdflib.URIRef('http://example.com/tag#1234'))) + # tag representatives + self.graph.add((rdflib.URIRef('http://example.com/tag#1234'), rdflib.URIRef(ns.bse.representative), rdflib.URIRef('http://example.com/image#1234'))) + self.graph.add((rdflib.URIRef('http://example.com/tag#4321'), rdflib.URIRef(ns.bse.representative), rdflib.URIRef('http://example.com/image#4321'))) + # entity buddies + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.buddy), rdflib.URIRef('http://example.com/image#1234'))) + self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.buddy), rdflib.URIRef('http://example.com/image#4321'))) + # image iso + self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(1234, datatype=rdflib.XSD.integer))) + self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(4321, datatype=rdflib.XSD.integer))) + + + def test_routing(self): + self.assertRaises(errors.BackendError, self.parser._parse_filter_expression, '1234', None, '') + self.assertRaises(errors.BackendError, self.parser._parse_predicate_expression, '1234', None) + + def test_call(self): + # NOTE: The individual ast components are considered in the respective tests. Here, we test __call__ specifics. + + # __call__ requires a valid root type + self.assertRaises(errors.BackendError, self.parser, self.schema.literal(ns.bsfs.Literal), None) + self.assertRaises(errors.ConsistencyError, self.parser, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), None) + # __call__ requires a parseable root + self.assertRaises(errors.BackendError, self.parser, self.schema.node(ns.bsfs.Entity), ast.filter.FilterExpression()) + # __call__ returns an executable query + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Is('http://example.com/entity#5678'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, {'http://example.com/entity#1234'}) + # root is optional + q = self.parser(self.schema.node(ns.bsfs.Entity)) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234', 'http://example.com/image#4321'}) + q = self.parser(self.schema.node(ns.bsfs.Tag)) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/tag#1234', 'http://example.com/tag#4321'}) + + + def test_is(self): + # _is requires a node + self.assertRaises(errors.BackendError, self.parser._is, self.schema.literal(ns.bsfs.Literal), ast.filter.Is('http://example.com/entity#1234'), '?ent') + # a single Is statement + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Is('http://example.com/entity#1234')) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234'}) + # an aggregate of Is statements + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Is('http://example.com/entity#4321'), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321'}) + # combined with other filters + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Or( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Is('http://example.com/entity#4321'), + ), + ast.filter.Any(ns.bse.comment, + ast.filter.Equals('Me, Myself, and I') + ), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234'}) + # as argument of Any/All + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + + + def test_equals(self): + # _equals requires a literal + self.assertRaises(errors.BackendError, self.parser._equals, self.schema.node(ns.bsfs.Entity), ast.filter.Equals('hello world'), '?ent') + # a single Equals statement + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321'}) + # a single Equals statement that includes subtypes + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # an Equals statement on an integer + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#4321'}) + + + def test_substring(self): + # _substring requires a literal + self.assertRaises(errors.BackendError, self.parser._substring, self.schema.node(ns.bsfs.Entity), ast.filter.Substring('hello world'), '?ent') + # a single Substring statement + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Substring('hello'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321'}) + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Substring('lo wo'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321'}) + # a single Substring statement that includes subtypes + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Substring('Myself'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # an Substring statement on an integer + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.Substring('32'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#4321'}) + + + def test_starts_with(self): + # _starts_with requires a literal + self.assertRaises(errors.BackendError, self.parser._starts_with, self.schema.node(ns.bsfs.Entity), ast.filter.StartsWith('hello world'), '?ent') + # a single StartsWith statement + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('hello'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321'}) + # a single StartsWith statement that includes subtypes + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('Me, Mys'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # an StartsWith statement on an integer + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.StartsWith(432))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#4321'}) + + + def test_ends_with(self): + # _ends_with requires a literal + self.assertRaises(errors.BackendError, self.parser._ends_with, self.schema.node(ns.bsfs.Entity), ast.filter.EndsWith('hello world'), '?ent') + # a single EndsWith statement + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.EndsWith('orld'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321'}) + # a single EndsWith statement that includes subtypes + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.EndsWith('and I'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # an EndsWith statement on an integer + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.EndsWith(321))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#4321'}) + + + def test_less_than(self): + # _less_than requires a literal + self.assertRaises(errors.BackendError, self.parser._less_than, self.schema.node(ns.bsfs.Entity), ast.filter.LessThan(2000), '?ent') + # a single LessThan statement + q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.LessThan(2000))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#1234'}) + # _less_than respects boundary + q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.LessThan(1234, strict=True))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.LessThan(1234, strict=False))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#1234'}) + # a single LessThan statement that includes subtypes + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.LessThan(2000))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # an LessThan statement on a string + # always negative; note that http://example.com/tag#4321 is also not returned although its comment is a pure number + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.LessThan(10_000))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + + + def test_greater_than(self): + # _greater_than requires a literal + self.assertRaises(errors.BackendError, self.parser._greater_than, self.schema.node(ns.bsfs.Entity), ast.filter.GreaterThan(2000), '?ent') + # a single GreaterThan statement + q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.GreaterThan(2000))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#4321'}) + # _greater_than respects boundary + q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.GreaterThan(4321, strict=True))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.GreaterThan(4321, strict=False))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#4321'}) + # a single GreaterThan statement that includes subtypes + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.GreaterThan(2000))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#4321'}) + # an GreaterThan statement on a string + # always positive + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.GreaterThan(0))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'}) + + + def test_and(self): + # And childs have to match the node type + self.assertRaises(errors.BackendError, self.parser, + self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.StartsWith('hello'), + ast.filter.EndsWith('world'), + )) + # no child produces an empty query + self.assertEqual(self.parser._and( + self.schema.node(ns.bsfs.Entity), + ast.filter.And(), '?ent'), '') + # And can mix different conditions + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234'}) + # all conditions have to match + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Is('http://example.com/entity#4321'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('foobar')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + # And can be nested + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.And( + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + ), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234'}) + + + def test_or(self): + # Or childs have to match the node type + self.assertRaises(errors.BackendError, self.parser, + self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.StartsWith('hello'), + ast.filter.EndsWith('world'), + )) + # no child produces an empty query + self.assertEqual(self.parser._and( + self.schema.node(ns.bsfs.Entity), + ast.filter.Or(), '?ent'), '') + # Or can mix different conditions + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234', 'http://example.com/entity#4321', 'http://example.com/image#4321'}) + # at least one condition has to match + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is('http://example.com/entity#5678'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(8765)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('foobar')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set()) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(8765)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('foobar')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234'}) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is('http://example.com/entity#5678'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('foobar')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#4321'}) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is('http://example.com/entity#5678'), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(8765)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # Or can be nested + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Or( + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + ), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234', 'http://example.com/entity#4321', 'http://example.com/image#4321'}) + + + + def test_any(self): + # _any requires a node + self.assertRaises(errors.BackendError, self.parser._any, + self.schema.literal(ns.bsfs.Literal), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)), '?ent') + # node type must match predicate's domain + self.assertRaises(errors.ConsistencyError, self.parser._any, + self.schema.node(ns.bsfs.Tag), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)), '?ent') + # predicate must be valid + self.assertRaises(errors.ConsistencyError, self.parser._any, + self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.invalid, ast.filter.Equals(1234)), '?ent') + # _any returns a valid query + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # _any can be nested + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.tag, + ast.filter.Any(ns.bse.representative, + ast.filter.Is('http://example.com/image#1234')))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + + + def test_all(self): + # All requires a Node + self.assertRaises(errors.BackendError, self.parser._all, self.schema.literal(ns.bsfs.Literal), None, '') + # All Nodes + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.All(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # All values + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.All(ns.bse.comment, ast.filter.Equals('hello world'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321'}) + # All on value within Or branch + # entity#1234 is selected because all of its comments are in ("hello world", "Me, Myself, and I") + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.All(ns.bse.comment, ast.filter.Or( + ast.filter.Equals('hello world'), + ast.filter.Equals('Me, Myself, and I')))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'}) + # All requires at least one predicate/value + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.All(ns.bse.comment, ast.filter.Equals('Me, Myself, and I'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#1234'}) + # All within a statement + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.All(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234')), # entity#1234, image#1234 + ast.filter.All(ns.bse.comment, ast.filter.Or( # entity#1234, entity#4321, image#1234 + ast.filter.Equals('hello world'), + ast.filter.Equals('Me, Myself, and I'), + )) + ) + ) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + # All with reversed Predicate + q = self.parser(self.schema.node(ns.bsfs.Tag), + ast.filter.All(ast.filter.Predicate(ns.bse.tag, reverse=True), ast.filter.Is('http://example.com/entity#4321'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/tag#4321'}) + # All with multiple predicates + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.All(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy), # entity#1234 (tag:tag#1234), entity#1234 (buddy:image#1234), image#1234(tag:tag#1234) + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')))) # entity#1234, image#1234, tag#1234 + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + + + + def test_not(self): + # Not applies on conditions + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Not(ast.filter.Is('http://example.com/entity#1234'))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#1234', 'http://example.com/entity#4321', 'http://example.com/image#4321'}) + # Not applies on conditions within branches + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.comment, ast.filter.Not(ast.filter.Equals('Me, Myself, and I')))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321'}) + # Not applies on branches + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Not(ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#4321'}) + # Double Not cancel each other + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Not(ast.filter.Not(ast.filter.Is('http://example.com/entity#1234')))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234'}) + # Not works within aggregation (and) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Not(ast.filter.Is('http://example.com/entity#1234')), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321'}) + # Not works within aggregation (or) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Or( + ast.filter.Not(ast.filter.Is('http://example.com/entity#1234')), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234', 'http://example.com/image#4321'}) + # Not works outside aggregation (and) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Not( + ast.filter.And( + ast.filter.Is('http://example.com/entity#1234'), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')), + ))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#1234', 'http://example.com/image#4321'}) + # Not works outside aggregation (or) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Not( + ast.filter.Or( + ast.filter.Is('http://example.com/entity#4321'), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')), + ))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#4321'}) + # Not mixed with branch, aggregation, id, and value + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.And( + ast.filter.Not( # image#1234, image#4321 + ast.filter.Or( # entity#4321, entity#1234 + ast.filter.Is('http://example.com/entity#4321'), + ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')), + ) + ), + ast.filter.Any(ns.bse.comment, ast.filter.Not(ast.filter.Equals('foobar'))), # entity#1234, entity#4321, image#1234 + )) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/image#1234'}) + + + def test_has(self): + # Has requires Node + self.assertRaises(errors.BackendError, self.parser._has, self.schema.literal(ns.bsfs.Literal), None, '') + # Has with GreaterThan constraint + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Has(ns.bse.comment, ast.filter.GreaterThan(0))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'}) + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Has(ns.bse.comment, ast.filter.GreaterThan(1))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234'}) + # Has with Equals constraint + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Has(ns.bse.comment, 1)) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#1234'}) + # Has with LessThan constraint + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Has(ns.bse.comment, ast.filter.LessThan(2))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321', 'http://example.com/image#1234', 'http://example.com/image#4321'}) + # Has with multiple constraints + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra1', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra2', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra3', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra4', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra5', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra1', datatype=rdflib.XSD.string))) + self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra2', datatype=rdflib.XSD.string))) + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Has(ns.bse.comment, + ast.filter.And(ast.filter.GreaterThan(1), ast.filter.LessThan(5)))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#4321'}) + # Has with OneOf predicate + q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Has(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy), + ast.filter.GreaterThan(1))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/entity#4321'}) + # Has with reversed predicate + q = self.parser(self.schema.node(ns.bsfs.Tag), ast.filter.Has(ast.filter.Predicate(ns.bse.tag, reverse=True), + ast.filter.GreaterThan(1))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/tag#1234'}) + + + def test_one_of(self): + # _one_of expects a node + self.assertRaises(errors.BackendError, self.parser._one_of, + self.schema.literal(ns.bsfs.Literal), + ast.filter.OneOf(ast.filter.Predicate(ns.bse.filesize))) + # invalid predicate for node type raises an error + self.assertRaises(errors.ConsistencyError, self.parser._one_of, + self.schema.node(ns.bsfs.Node), + ast.filter.OneOf(ast.filter.Predicate(ns.bse.filesize))) + self.assertRaises(errors.ConsistencyError, self.parser, + self.schema.node(ns.bsfs.Tag), + ast.filter.Any(ast.filter.OneOf(ast.filter.Predicate(ns.bse.filesize)), ast.filter.Equals(1234))) + self.assertRaises(errors.BackendError, self.parser._one_of, + self.schema.node(ns.bsfs.Node), + ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate))) + # invalid predicate combinations raise an error + self.assertRaises(errors.ConsistencyError, self.parser._one_of, + self.schema.node(ns.bsfs.Node), + ast.filter.OneOf( + ast.filter.Predicate(ns.bse.filesize), + ast.filter.Predicate(ns.bse.representative))) + # _one_of returns the URI and range + q = self.parser._one_of(self.schema.node(ns.bsfs.Image), + ast.filter.OneOf( + ast.filter.Predicate(ns.bse.iso), + ast.filter.Predicate(ns.bse.filesize))) + self.assertTrue(q[0] == f'<{ns.bse.iso}>|<{ns.bse.filesize}>' or q[0] == f'<{ns.bse.filesize}>|<{ns.bse.iso}>') + self.assertEqual(q[1], self.schema.literal(ns.xsd.integer)) + # OneOf can be nested + q = self.parser._one_of(self.schema.node(ns.bsfs.Image), + ast.filter.OneOf( + ast.filter.Predicate(ns.bse.iso), + ast.filter.OneOf( + ast.filter.Predicate(ns.bse.filesize)))) + self.assertTrue(q[0] == f'<{ns.bse.iso}>|<{ns.bse.filesize}>' or q[0] == f'<{ns.bse.filesize}>|<{ns.bse.iso}>') + self.assertEqual(q[1], self.schema.literal(ns.xsd.integer)) + # _one_of returns the most generic range + q = self.parser._one_of(self.schema.node(ns.bsfs.Entity), + ast.filter.OneOf( + ast.filter.Predicate(ns.bse.tag), + ast.filter.Predicate(ns.bse.buddy))) + self.assertTrue(q[0] == f'<{ns.bse.tag}>|<{ns.bse.buddy}>' or q[0] == f'<{ns.bse.buddy}>|<{ns.bse.tag}>') + self.assertEqual(q[1], self.schema.node(ns.bsfs.Node)) + # domains must match the given type + self.assertRaises(errors.ConsistencyError, self.parser, + self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy), + ast.filter.Any(ast.filter.OneOf(ns.bse.filesize), + ast.filter.Equals(1234)))) + # ranges must have the same type (Node/Literal) + self.assertRaises(errors.ConsistencyError, self.parser, + self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.filesize), + ast.filter.Equals(1234))) + # ranges must be related + self.assertRaises(errors.ConsistencyError, self.parser, + self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ast.filter.OneOf(ns.bse.comment, ns.bse.filesize), + ast.filter.Equals(1234))) + # integration: _one_of returns a valid sparql query + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy), + ast.filter.Any(ast.filter.OneOf(ns.bse.comment), + ast.filter.Equals('Me, Myself, and I')))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + + + def test_predicate(self): + # predicate cannot be the root predicate (ns.bsfs.Predicate) + self.assertRaises(errors.BackendError, self.parser._predicate, self.schema.node(ns.bsfs.Node), ast.filter.Predicate(ns.bsfs.Predicate)) + # _predicate expects a node + self.assertRaises(errors.BackendError, self.parser._predicate, + self.schema.literal(ns.bsfs.Literal), + ast.filter.Predicate(ns.bse.filesize)) + # invalid predicate for node type raises an error + self.assertRaises(errors.ConsistencyError, self.parser._predicate, + self.schema.node(ns.bsfs.Node), + ast.filter.Predicate(ns.bse.filesize)) + self.assertRaises(errors.ConsistencyError, self.parser, + self.schema.node(ns.bsfs.Tag), + ast.filter.Any(ast.filter.Predicate(ns.bse.filesize), ast.filter.Equals(1234))) + # _predicate returns the URI and range + self.assertEqual(self.parser._predicate(self.schema.node(ns.bsfs.Entity), ast.filter.Predicate(ns.bse.filesize)), + (f'<{ns.bse.filesize}>', self.schema.literal(ns.xsd.integer))) + self.assertEqual(self.parser._predicate(self.schema.node(ns.bsfs.Entity), ast.filter.Predicate(ns.bse.tag)), + (f'<{ns.bse.tag}>', self.schema.node(ns.bsfs.Tag))) + # _predicate respects reverse flag + self.assertEqual(self.parser._predicate(self.schema.node(ns.bsfs.Tag), ast.filter.Predicate(ns.bse.tag, reverse=True)), + ('^<' + ns.bse.tag + '>', self.schema.node(ns.bsfs.Entity))) + # integration: _predicate returns a valid sparql query + q = self.parser(self.schema.node(ns.bsfs.Entity), + ast.filter.Any(ns.bse.tag, + ast.filter.Any(ns.bse.representative, + ast.filter.Any(ns.bse.filesize, + ast.filter.Equals(1234))))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/entity#1234', 'http://example.com/image#1234'}) + q = self.parser(self.schema.node(ns.bsfs.Tag), + ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True), + ast.filter.Any(ns.bse.filesize, + ast.filter.LessThan(2000)))) + self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, + {'http://example.com/tag#1234'}) + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## diff --git a/test/triple_store/test_sparql.py b/test/triple_store/sparql/test_sparql.py index 8d98749..3d81de1 100644 --- a/test/triple_store/test_sparql.py +++ b/test/triple_store/sparql/test_sparql.py @@ -11,10 +11,11 @@ import unittest # bsie imports from bsfs import schema as _schema from bsfs.namespace import ns +from bsfs.query import ast from bsfs.utils import errors, URI # objects to test -from bsfs.triple_store.sparql import SparqlStore +from bsfs.triple_store.sparql.sparql import SparqlStore ## code ## @@ -59,6 +60,18 @@ class TestSparqlStore(unittest.TestCase): bsfs:unique "true"^^xsd:boolean . ''') + self.schema_triples = { + # schema hierarchy + (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.author), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + } def test_essentials(self): store = SparqlStore.Open() @@ -155,7 +168,7 @@ class TestSparqlStore(unittest.TestCase): store.set(curr.node(ns.bsfs.Entity), ent_ids, p_author, {URI('http://example.com/me')}) # check instances - instances = { + instances = self.schema_triples | { # node instances (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), @@ -228,7 +241,16 @@ class TestSparqlStore(unittest.TestCase): store.schema = curr self.assertEqual(store.schema, curr) # instances have not changed - self.assertSetEqual(set(store._graph), instances) + self.assertSetEqual(set(store._graph), instances | { + # schema hierarchy + (rdflib.URIRef(ns.bsfs.Collection), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.partOf), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef('http://bsfs.ai/schema/Tag#usedIn'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef('http://bsfs.ai/schema/Collection#tag'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef('http://bsfs.ai/schema/Tag#principal'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + }) # add some instances of the new classes p_partOf = curr.predicate(ns.bse.partOf) p_shared = curr.predicate(ns.bse.shared) @@ -248,6 +270,14 @@ class TestSparqlStore(unittest.TestCase): {URI('http://example.com/me/collection#1234')}) # new instances are now in the graph self.assertSetEqual(set(store._graph), instances | { + # same old schema hierarchy + (rdflib.URIRef(ns.bsfs.Collection), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.partOf), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef('http://bsfs.ai/schema/Tag#usedIn'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef('http://bsfs.ai/schema/Collection#tag'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef('http://bsfs.ai/schema/Tag#principal'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), # collections (rdflib.URIRef('http://example.com/me/collection#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Collection)), (rdflib.URIRef('http://example.com/me/collection#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Collection)), @@ -316,6 +346,16 @@ class TestSparqlStore(unittest.TestCase): self.assertEqual(store.schema, curr) # instances of old classes were removed self.assertSetEqual(set(store._graph), { + # schema hierarchy + (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)), + (rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)), + (rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), + (rdflib.URIRef('http://bsfs.ai/schema/Tag#principal'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)), # node instances (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), @@ -390,7 +430,7 @@ class TestSparqlStore(unittest.TestCase): ent_ids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')} tag_ids = {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')} # target instances - instances = { + instances = self.schema_triples | { # node instances (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), @@ -416,7 +456,7 @@ class TestSparqlStore(unittest.TestCase): # rollback undoes previous changes store.rollback() - self.assertSetEqual(set(store._graph), set()) + self.assertSetEqual(set(store._graph), self.schema_triples) # add some data once more store.create(ent_type, ent_ids) @@ -455,6 +495,39 @@ class TestSparqlStore(unittest.TestCase): (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)), }) + def test_get(self): + # store setup + store = SparqlStore.Open() + store.schema = self.schema + ent_type = self.schema.node(ns.bsfs.Entity) + tag_type = self.schema.node(ns.bsfs.Tag) + ent_ids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')} + tag_ids = {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')} + store.create(ent_type, ent_ids) + store.create(tag_type, tag_ids) + store.set(ent_type, ent_ids, self.schema.predicate(ns.bse.tag), tag_ids) + store.set(ent_type, {URI('http://example.com/me/entity#1234')}, self.schema.predicate(ns.bse.filesize), {1234}) + store.set(ent_type, {URI('http://example.com/me/entity#4321')}, self.schema.predicate(ns.bse.filesize), {4321}) + # node_type must be in the schema + self.assertRaises(errors.ConsistencyError, set, store.get(self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), ast.filter.IsIn(ent_ids))) + # query must be a filter expression + class Foo(): pass + self.assertRaises(TypeError, set, store.get(ent_type, 1234)) + self.assertRaises(TypeError, set, store.get(ent_type, '1234')) + self.assertRaises(TypeError, set, store.get(ent_type, Foo())) + # run some queries + self.assertSetEqual(set(store.get(tag_type, ast.filter.IsIn(tag_ids))), tag_ids) + self.assertSetEqual(set(store.get(ent_type, ast.filter.Any(ns.bse.tag, ast.filter.IsIn(tag_ids)))), ent_ids) + self.assertSetEqual(set(store.get(ent_type, ast.filter.IsIn(tag_ids))), set()) + # invalid queries raise error + self.assertRaises(errors.ConsistencyError, set, store.get(tag_type, ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)))) + self.assertRaises(errors.BackendError, set, store.get(ent_type, ast.filter.Equals('http://example.com/me/entity#1234'))) + # run some more complex query + q = store.get(tag_type, ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True), + ast.filter.Any(ns.bse.filesize, + ast.filter.LessThan(2000)))) + self.assertSetEqual(set(q), tag_ids) + def test_exists(self): # store setup @@ -507,14 +580,15 @@ class TestSparqlStore(unittest.TestCase): # can create some nodes ent_type = store.schema.node(ns.bsfs.Entity) store.create(ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}) - self.assertSetEqual(set(store._graph), { + self.assertSetEqual(set(store._graph), self.schema_triples | { + # instances (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), }) # existing nodes are skipped store.create(ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#5678')}) - self.assertSetEqual(set(store._graph), { + self.assertSetEqual(set(store._graph), self.schema_triples | { # previous triples (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), @@ -525,7 +599,7 @@ class TestSparqlStore(unittest.TestCase): # can create nodes of a different type tag_type = store.schema.node(ns.bsfs.Tag) store.create(tag_type, {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')}) - self.assertSetEqual(set(store._graph), { + self.assertSetEqual(set(store._graph), self.schema_triples | { # previous triples (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), @@ -538,7 +612,7 @@ class TestSparqlStore(unittest.TestCase): # creation does not change types of existing nodes tag_type = store.schema.node(ns.bsfs.Tag) store.create(tag_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}) - self.assertSetEqual(set(store._graph), { + self.assertSetEqual(set(store._graph), self.schema_triples | { # previous triples (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)), diff --git a/test/triple_store/test_base.py b/test/triple_store/test_base.py index a4b0559..a0c3260 100644 --- a/test/triple_store/test_base.py +++ b/test/triple_store/test_base.py @@ -35,6 +35,9 @@ class DummyBase(TripleStoreBase): def schema(self, schema): pass + def get(self, node_type, query): + pass + def exists(self, node_type, guids): pass diff --git a/test/utils/test_commons.py b/test/utils/test_commons.py index ce73788..3ad6dea 100644 --- a/test/utils/test_commons.py +++ b/test/utils/test_commons.py @@ -8,7 +8,7 @@ Author: Matthias Baumgartner, 2022 import unittest # objects to test -from bsfs.utils.commons import typename +from bsfs.utils.commons import typename, normalize_args ## code ## @@ -21,6 +21,21 @@ class TestCommons(unittest.TestCase): self.assertEqual(typename(123), 'int') self.assertEqual(typename(None), 'NoneType') + def test_normalize_args(self): + # one argument + self.assertEqual(normalize_args(1), (1, )) + # pass as arguments + self.assertEqual(normalize_args(1,2,3), (1,2,3)) + # pass as iterator + self.assertEqual(normalize_args(iter([1,2,3])), (1,2,3)) + # pass as generator + self.assertEqual(normalize_args((i for i in range(1, 4))), (1,2,3)) + self.assertEqual(normalize_args(i for i in range(1, 4)), (1,2,3)) # w/o brackets + # pass as iterable + self.assertEqual(normalize_args([1,2,3]), (1,2,3)) + # pass an iterable with a single item + self.assertEqual(normalize_args([1]), (1, )) + ## main ## |