Coverage for python/pyshacl_turbo/conformance.py: 100.0%
115 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 12:25 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 12:25 +0000
1"""DASH expected-result adapters. SPARQL execution remains in the Rust engine."""
3from io import StringIO
5from rdflib import BNode, Graph, Literal, Namespace
6from rdflib.compare import isomorphic
7from rdflib.namespace import OWL, RDF, RDFS, SH
8from rdflib.query import Result
10from . import _native
11from .rdf import input_graph, union
13DASH = Namespace("http://datashapes.org/dash#")
16def query_prefixes(graph, node):
17 prefixes = dict(graph.namespaces()) 1cmnMNpJabqrstuvwxyzABCDEFGH
18 pending, seen = list(graph.objects(node, SH.prefixes)), set() 1cmnMNpJabqrstuvwxyzABCDEFGH
19 if pending: 1cmnMNpJabqrstuvwxyzABCDEFGH
20 pending.extend(graph.subjects(RDF.type, OWL.Ontology)) 1MNp
21 pending.append(graph.identifier) 1MNp
22 while pending: 1cmnMNpJabqrstuvwxyzABCDEFGH
23 resource = pending.pop() 1MNp
24 if resource in seen: 1MNp
25 continue 1p
26 seen.add(resource) 1MNp
27 pending.extend(graph.objects(resource, OWL.imports)) 1MNp
28 for declaration in graph.objects(resource, SH.declare): 1MNp
29 prefix = graph.value(declaration, SH.prefix, any=False) 1MNp
30 namespace = graph.value(declaration, SH.namespace, any=False) 1MNp
31 if prefix is None or namespace is None: 1MNp
32 raise ValueError("Incomplete sh:declare") 1MN
33 prefixes[str(prefix)] = namespace 1p
34 return "".join(f"PREFIX {p}: <{ns}>\n" for p, ns in sorted(prefixes.items())) 1cmnpJabqrstuvwxyzABCDEFGH
37def extract_query_and_expected_result(test_case, graph, log=None):
38 text = graph.value(test_case, SH.select, any=False) 1cJWXUabqrstuvwxyzABCDEFGH
39 expected = graph.value(test_case, DASH.expectedResult, any=False) 1cJWXUabqrstuvwxyzABCDEFGH
40 if text is None or expected is None: 1cJWXUabqrstuvwxyzABCDEFGH
41 raise RuntimeError("QueryTestCase requires sh:select and dash:expectedResult") 1WX
42 result = Result.parse(StringIO(str(expected)), format="json") 1cJUabqrstuvwxyzABCDEFGH
43 if result.type != "SELECT": 1cJUabqrstuvwxyzABCDEFGH
44 raise RuntimeError("QueryTestCase expected result must be SELECT") 1U
45 return query_prefixes(graph, test_case) + str(text), result 1cJabqrstuvwxyzABCDEFGH
48def evaluate_query_testcase(query_graph, test_case, log=None):
49 query, expected = extract_query_and_expected_result(test_case, query_graph, log) 1cJabqrstuvwxyzABCDEFGH
50 snapshot = input_graph(query_graph) 1cJabqrstuvwxyzABCDEFGH
51 actual = Result.parse( 1cJabqrstuvwxyzABCDEFGH
52 StringIO(_native.query(snapshot.source, snapshot.format, query)), format="json"
53 )
54 if actual.type != "SELECT": 1JabqrstuvwxyzABCDEFGH
55 raise RuntimeError("QueryTestCase query must return SELECT results") 1J
56 matches = set(actual.vars) == set(expected.vars) and [row.asdict() for row in actual] == [ 1abqrstuvwxyzABCDEFGH
57 row.asdict() for row in expected
58 ]
59 return matches, None if matches else "SPARQL SELECT result differs from dash:expectedResult" 1abqrstuvwxyzABCDEFGH
62def report_subgraph(graph, root, messages, details):
63 result, seen, pending = Graph(), set(), [root] 1dKI
64 while pending: 1dKI
65 node = pending.pop() 1dKI
66 if node in seen: 1dKI
67 continue 1I
68 seen.add(node) 1dKI
69 for _, predicate, value in graph.triples((node, None, None)): 1dKI
70 if (
71 (predicate == RDF.type and value == RDFS.Resource)
72 or (predicate == SH.resultMessage and not messages)
73 or (predicate == SH.detail and not details)
74 ):
75 continue 1dKI
76 result.add((node, predicate, value)) 1dKI
77 if isinstance(value, BNode): 1dKI
78 pending.append(value) 1dKI
79 return result 1dKI
82def compare_reports(actual, expected, expected_root):
83 actual_root = actual.value(None, RDF.type, SH.ValidationReport, any=False) 1dKIYZ0
84 if (
85 actual_root is None
86 or actual.value(actual_root, SH.conforms) is None
87 or expected.value(expected_root, SH.conforms) is None
88 ):
89 raise RuntimeError("Expected and actual reports must declare sh:conforms") 1YZ0
90 expected_results = list(expected.objects(expected_root, SH.result)) 1dKI
91 messages = any(expected.value(node, SH.resultMessage) is not None for node in expected_results) 1dKI
92 details = any(expected.value(node, SH.detail) is not None for node in expected_results) 1dKI
93 return isomorphic( 1dKI
94 report_subgraph(actual, actual_root, messages, details),
95 report_subgraph(expected, expected_root, messages, details),
96 )
99def _literal_matches(expected, actual):
100 if not isinstance(actual, Literal): 1V1efghijkl
101 return False 11
102 try: 1Vefghijkl
103 return bool(expected.eq(actual)) 1Vefghijkl
104 except (TypeError, NotImplementedError): 1V
105 # Incomparable literal values are an expected-result mismatch.
106 return False 1V
109def check_dash_result(report, expected_graph, expanded_data):
110 expected = union(expected_graph) 1dcSPQTROmnoabefghijkl
111 data = None 1dcSPQTROmnoabefghijkl
112 checks = [] 1dcSPQTROmnoabefghijkl
113 for case in expected.subjects(RDF.type, DASH.GraphValidationTestCase): 1dcSPQTROmnoabefghijkl
114 root = expected.value(case, DASH.expectedResult, any=False) 1dT
115 if root is None: 1dT
116 raise RuntimeError("GraphValidationTestCase requires dash:expectedResult") 1T
117 checks.append(compare_reports(report, expected, root)) 1d
118 for case in expected.subjects(RDF.type, DASH.InferencingTestCase): 1dcSPQROmnoabefghijkl
119 if data is None: 1ROoefghijkl
120 from .rdf import load_from_source 1ROoefghijkl
122 data = union(load_from_source(expanded_data)) 1ROoefghijkl
123 results = list(expected.objects(case, DASH.expectedResult)) 1ROoefghijkl
124 if not results: 1ROoefghijkl
125 raise RuntimeError("InferencingTestCase requires dash:expectedResult") 1R
126 for result in results: 1Ooefghijkl
127 triple = tuple( 1Ooefghijkl
128 expected.value(result, p, any=False)
129 for p in (RDF.subject, RDF.predicate, RDF.object)
130 )
131 if None in triple: 1Ooefghijkl
132 raise RuntimeError("InferencingTestCase requires complete reified triples") 1O
133 subject, predicate, value = triple 1oefghijkl
134 if isinstance(value, Literal): 1oefghijkl
135 checks.append( 1efghijkl
136 any(
137 _literal_matches(value, actual)
138 for actual in data.objects(subject, predicate)
139 )
140 )
141 else:
142 checks.append(triple in data) 1o
143 for case in expected.subjects(RDF.type, DASH.QueryTestCase): 1dcSPQmnoabefghijkl
144 try: 1cab
145 matches, _ = evaluate_query_testcase(expected, case) 1cab
146 except (ValueError, RuntimeError, KeyError, TypeError): 1c
147 # DASH query cases intentionally test malformed queries/results.
148 # This is the expected-result checker, never a validator fallback.
149 matches = False 1c
150 checks.append(matches) 1cab
151 for case in expected.subjects(RDF.type, DASH.FunctionTestCase): 1dcSPQmnoabefghijkl
152 expression = expected.value(case, DASH.expression, any=False) 1PQmn
153 result = expected.value(case, DASH.expectedResult, any=False) 1PQmn
154 if expression is None or result is None: 1PQmn
155 raise RuntimeError("FunctionTestCase requires dash:expression and dash:expectedResult") 1PQ
156 query = query_prefixes(expected, case) + f"SELECT ({expression} AS ?result) WHERE {{}}" 1mn
157 snapshot = input_graph(expected) 1mn
158 actual = Result.parse( 1mn
159 StringIO(_native.query(snapshot.source, snapshot.format, query)), format="json"
160 )
161 checks.append([row.asdict().get("result") for row in actual] == [result]) 1mn
162 if not checks: 1dcSmnoabefghijkl
163 raise RuntimeError("Expected graph contains no DASH test cases") 1S
164 return all(checks) 1dcmnoabefghijkl