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

1"""DASH expected-result adapters. SPARQL execution remains in the Rust engine.""" 

2 

3from io import StringIO 

4 

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 

9 

10from . import _native 

11from .rdf import input_graph, union 

12 

13DASH = Namespace("http://datashapes.org/dash#") 

14 

15 

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

35 

36 

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

46 

47 

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

60 

61 

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

80 

81 

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 ) 

97 

98 

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

107 

108 

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

121 

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