11# Copyright (c) 2024-2026 CRS4
2+ # Copyright (c) 2025-2026 eScience Lab, The University of Manchester
23#
34# Licensed under the Apache License, Version 2.0 (the "License");
45# you may not use this file except in compliance with the License.
@@ -49,7 +50,6 @@ def first(c: Collection[T]) -> T:
4950
5051
5152def load_graph_and_preserve_relative_ids (json_data , base = "http://example.org/" ):
52-
5353 rel_ids = set ()
5454
5555 def collect_ids (obj ):
@@ -78,54 +78,90 @@ def replace_uri_in_graph(graph, old_uri_str, new_uri_str):
7878 new = rdflib .URIRef (new_uri_str )
7979 triples_to_add = []
8080 triples_to_remove = []
81+
8182 for s , p , o in graph .triples ((None , None , None )):
8283 s2 = new if (isinstance (s , rdflib .URIRef ) and str (s ) == old_uri_str ) else s
8384 o2 = new if (isinstance (o , rdflib .URIRef ) and str (o ) == old_uri_str ) else o
85+
8486 if (s2 , p , o2 ) != (s , p , o ):
8587 triples_to_remove .append ((s , p , o ))
8688 triples_to_add .append ((s2 , p , o2 ))
87- for t in triples_to_remove :
88- graph .remove (t )
89- for t in triples_to_add :
90- graph .add (t )
89+
90+ for triple in triples_to_remove :
91+ graph .remove (triple )
92+
93+ for triple in triples_to_add :
94+ graph .add (triple )
9195
9296 for expanded , rel in mapping .items ():
9397 replace_uri_in_graph (g , expanded , rel )
9498
9599 return g
96100
97101
102+ def _uses_https_schema (graph : rdflib .Graph ) -> bool :
103+ for s , p , o in graph .triples ((None , None , None )):
104+ for term in (s , p , o ):
105+ if isinstance (term , rdflib .URIRef ) and str (term ).startswith ("https://schema.org/" ):
106+ return True
107+
108+ return False
109+
110+
98111def _prepare_temp_rocrate (
99112 rocrate_path : Path ,
100113 rocrate_entity_patch : dict | None ,
101114 rocrate_entity_mod_sparql : str | None ,
102115) -> Path :
103116 # `mkdtemp` returns a stable path the test owns; `TemporaryDirectory().name`
104117 # was deleted on GC before copytree ran. `dirs_exist_ok=True` lets us copy
105- # into the ( already-created) mkdtemp directory.
118+ # into the already-created directory.
106119 temp_rocrate_path = Path (tempfile .mkdtemp ())
107- shutil .copytree (rocrate_path , temp_rocrate_path , dirs_exist_ok = True )
108- with (temp_rocrate_path / "ro-crate-metadata.json" ).open (encoding = "utf-8" ) as f :
120+ shutil .copytree (
121+ rocrate_path ,
122+ temp_rocrate_path ,
123+ dirs_exist_ok = True ,
124+ )
125+
126+ metadata_path = temp_rocrate_path / "ro-crate-metadata.json"
127+
128+ with metadata_path .open (encoding = "utf-8" ) as f :
109129 rocrate = json .load (f )
130+
110131 if rocrate_entity_patch is not None :
111132 for key , value in rocrate_entity_patch .items ():
112133 for entity in rocrate ["@graph" ]:
113134 if entity ["@id" ] == key :
114135 entity .update (value )
115136 break
116- with (temp_rocrate_path / "ro-crate-metadata.json" ).open ("w" , encoding = "utf-8" ) as f :
137+
138+ with metadata_path .open ("w" , encoding = "utf-8" ) as f :
117139 json .dump (rocrate , f )
140+
118141 if rocrate_entity_mod_sparql is not None :
119142 rocrate_graph = load_graph_and_preserve_relative_ids (rocrate )
143+
144+ if _uses_https_schema (rocrate_graph ):
145+ rocrate_entity_mod_sparql = rocrate_entity_mod_sparql .replace (
146+ "http://schema.org/" ,
147+ "https://schema.org/" ,
148+ )
149+
120150 rocrate_graph .update (rocrate_entity_mod_sparql )
121- context_uri = "https://w3id.org/ro/crate/1.1/context"
151+
152+ context = rocrate .get (
153+ "@context" ,
154+ "https://w3id.org/ro/crate/1.2/context" ,
155+ )
156+
122157 rocrate_graph .serialize (
123- Path ( temp_rocrate_path , "ro-crate-metadata.json" ) ,
158+ metadata_path ,
124159 format = "json-ld" ,
125- context = context_uri ,
160+ context = context ,
126161 indent = 2 ,
127162 use_native_types = True ,
128163 )
164+
129165 return temp_rocrate_path
130166
131167
@@ -151,30 +187,36 @@ def do_entity_test( # pylint: disable=too-many-locals
151187 """
152188 Shared function to test a RO-Crate entity.
153189
154- Additional keyword arguments (kwargs) are passed along to initialise ValidationSettings.
190+ Additional keyword arguments (kwargs) are passed along to initialise
191+ ValidationSettings.
155192 """
156193 assert not (rocrate_entity_patch and rocrate_entity_mod_sparql ), (
157194 "Cannot use rocrate_entity_patch and rocrate_entity_mod_sparql together"
158195 )
159196
160- # declare variables
161197 failed_requirements = None
162198 detected_issues = None
163199
164200 if not isinstance (rocrate_path , Path ) and not rocrate_path .startswith ("http" ):
165201 rocrate_path = Path (rocrate_path )
166202
167203 temp_rocrate_path = None
204+
168205 if (
169206 any ([rocrate_entity_patch , rocrate_entity_mod_sparql ])
170207 and isinstance (rocrate_path , Path )
171208 and rocrate_path .is_dir ()
172209 ):
173- temp_rocrate_path = _prepare_temp_rocrate (rocrate_path , rocrate_entity_patch , rocrate_entity_mod_sparql )
210+ temp_rocrate_path = _prepare_temp_rocrate (
211+ rocrate_path ,
212+ rocrate_entity_patch ,
213+ rocrate_entity_mod_sparql ,
214+ )
174215 rocrate_path = temp_rocrate_path
175216
176217 if expected_triggered_requirements is None :
177218 expected_triggered_requirements = []
219+
178220 if expected_triggered_issues is None :
179221 expected_triggered_issues = []
180222
@@ -183,8 +225,8 @@ def do_entity_test( # pylint: disable=too-many-locals
183225 logger .debug ("Requirement severity: %s" , requirement_severity )
184226 logger .debug ("Checks to skip: %s" , skip_checks )
185227
186- # validate RO-Crate
187228 relative_root_path = Path (rocrate_relative_root_path ) if rocrate_relative_root_path is not None else None
229+
188230 result : models .ValidationResult = services .validate (
189231 models .ValidationSettings (
190232 rocrate_uri = models .URI (rocrate_path ),
@@ -198,50 +240,82 @@ def do_entity_test( # pylint: disable=too-many-locals
198240 ** kwargs ,
199241 )
200242 )
201- logger .debug ("Expected validation result: %s" , expected_validation_result )
243+
244+ logger .debug (
245+ "Expected validation result: %s" ,
246+ expected_validation_result ,
247+ )
202248
203249 assert result .context is not None , "Validation context should not be None"
250+
204251 logger .debug (
205252 "Expected requirement severity to be %s, got %s" ,
206253 requirement_severity ,
207254 result .context .requirement_severity ,
208255 )
256+
209257 assert result .passed () == expected_validation_result , (
210258 f"RO-Crate should be { 'valid' if expected_validation_result else 'invalid' } "
211259 )
212260
213- # check requirement
214- failed_requirements = [_ .name for _ in result .failed_requirements ]
261+ failed_requirements = [requirement .name for requirement in result .failed_requirements ]
215262
216- # check that the expected requirements are triggered
217263 for expected_triggered_requirement in expected_triggered_requirements :
218264 if expected_triggered_requirement not in failed_requirements :
219265 raise AssertionError (
220- f"The expected requirement "
221- f'"{ expected_triggered_requirement } " was not found in the failed requirements'
266+ "The expected requirement "
267+ f'"{ expected_triggered_requirement } " was not found in the '
268+ "failed requirements"
222269 )
223270
224- # check requirement issues
225271 detected_issues = [
226272 issue .message for issue in result .get_issues (requirement_severity ) if issue .message is not None
227273 ]
274+
228275 logger .debug ("Detected issues: %s" , detected_issues )
229276 logger .debug ("Expected issues: %s" , expected_triggered_issues )
277+
230278 for expected_issue in expected_triggered_issues :
231- if not any (expected_issue in issue for issue in detected_issues ): # support partial match
279+ if not any (expected_issue in issue for issue in detected_issues ):
232280 raise AssertionError (f'The expected issue "{ expected_issue } " was not found in the detected issues' )
281+
233282 except Exception :
234283 if logger .isEnabledFor (logging .DEBUG ):
235- logger .exception ("Failed to validate RO-Crate @ path: %s" , rocrate_path )
236- logger .debug ("Requirement severity: %s" , requirement_severity )
237- logger .debug ("Expected validation result: %s" , expected_validation_result )
238- logger .debug ("Expected triggered requirements: %s" , expected_triggered_requirements )
239- logger .debug ("Expected triggered issues: %s" , expected_triggered_issues )
240- logger .debug ("Failed requirements: %s" , failed_requirements )
241- logger .debug ("Detected issues: %s" , detected_issues )
284+ logger .exception (
285+ "Failed to validate RO-Crate @ path: %s" ,
286+ rocrate_path ,
287+ )
288+ logger .debug (
289+ "Requirement severity: %s" ,
290+ requirement_severity ,
291+ )
292+ logger .debug (
293+ "Expected validation result: %s" ,
294+ expected_validation_result ,
295+ )
296+ logger .debug (
297+ "Expected triggered requirements: %s" ,
298+ expected_triggered_requirements ,
299+ )
300+ logger .debug (
301+ "Expected triggered issues: %s" ,
302+ expected_triggered_issues ,
303+ )
304+ logger .debug (
305+ "Failed requirements: %s" ,
306+ failed_requirements ,
307+ )
308+ logger .debug (
309+ "Detected issues: %s" ,
310+ detected_issues ,
311+ )
312+
242313 raise
314+
243315 finally :
244- # cleanup
245316 if temp_rocrate_path is not None :
246- logger .debug ("Cleaning up temporary RO-Crate @ path: %s" , temp_rocrate_path )
317+ logger .debug (
318+ "Cleaning up temporary RO-Crate @ path: %s" ,
319+ temp_rocrate_path ,
320+ )
247321 shutil .rmtree (temp_rocrate_path )
0 commit comments