From 44f8ac090ed866ef359e5fafcd0eb597f6c41407 Mon Sep 17 00:00:00 2001 From: Maximilian Gruber Date: Thu, 24 Jul 2025 15:57:51 +0200 Subject: [PATCH 1/2] provide functionality to test if valid units pass and invalid units fail certain constraints --- test/run_test.py | 80 ++++++++++++++++++++++++++++++++++++++++++--- test/shacl_utils.py | 43 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 test/shacl_utils.py diff --git a/test/run_test.py b/test/run_test.py index 1ceb319..e4ff242 100644 --- a/test/run_test.py +++ b/test/run_test.py @@ -1,11 +1,24 @@ import rdflib from pathlib import Path +from shacl_utils import SHACLutils + +common_knowledge_bases = { + "si": {"path": "TTL/si.ttl", "format": "ttl"}, + "prefixes": {"path": "TTL/prefixes.ttl", "format": "ttl"}, + "units": {"path": "TTL/units.ttl", "format": "ttl"}, +} + +constraint_shapes = { + "test": {"path": "test/test_constraints.ttl", "format": "ttl"}, +} + + def test_syntax(): error_status = False this_dir = Path(__file__).parent - ttl_file_names = (this_dir.parent / "TTL" ).glob("*.ttl") + ttl_file_names = (this_dir.parent / "TTL").glob("*.ttl") try: g = rdflib.Graph() @@ -18,8 +31,8 @@ def test_syntax(): return g, error_status -def test_semantics(g): +def test_semantics(g): error_status = False # some not very elaborate test @@ -28,17 +41,74 @@ def test_semantics(g): return error_status + +def test_valid_individuals(): + uh = SHACLutils() + + invalid_individuals = { + "ex": {"path": "test/valid_individuals.ttl", "format": "ttl"}, + } + + # SHACL validation + data = invalid_individuals + onto = common_knowledge_bases + shapes = constraint_shapes + + data_graph = uh.load_knowledge_bases(data | onto) + shapes_graph = uh.load_knowledge_bases(shapes) + + conforms, results_graph = uh.validate_against_constraints(data_graph, shapes_graph) + + error_status = not conforms # if data conforms to shape, this means no error + return error_status + + +def test_invalid_individuals(): + uh = SHACLutils() + + invalid_individuals = { + "ex": {"path": "test/invalid_individuals.ttl", "format": "ttl"}, + } + + # SHACL validation + data = invalid_individuals + onto = common_knowledge_bases + shapes = constraint_shapes + + data_graph = uh.load_knowledge_bases(data | onto) + shapes_graph = uh.load_knowledge_bases(shapes) + + conforms, results_graph = uh.validate_against_constraints(data_graph, shapes_graph) + + for s, p, o in results_graph.triples((None, rdflib.SH["result"], None)): + focusNode = results_graph.value(o, rdflib.SH["focusNode"]) + message = results_graph.value(o, rdflib.SH["resultMessage"]) + + print("Focus node: ", focusNode) + print("Result message: ", message) + print("") + + error_status = conforms # if data conforms to shape, this means error + return error_status + + def main(): g, syntax_error = test_syntax() - if syntax_error: raise ImportError("The syntax of the generated files is not ok.") semantic_error = test_semantics(g) if semantic_error: - raise ValueError("A requirement regarding the content of the output files is not met.") + raise ValueError( + "A requirement regarding the content of the output files is not met." + ) + + shacl_error_valid_ind = test_valid_individuals() + + shacl_error_invalid_ind = test_invalid_individuals() + + print(syntax_error, semantic_error, shacl_error_valid_ind, shacl_error_invalid_ind) - print(syntax_error, semantic_error) if __name__ == "__main__": main() diff --git a/test/shacl_utils.py b/test/shacl_utils.py new file mode 100644 index 0000000..01b2f11 --- /dev/null +++ b/test/shacl_utils.py @@ -0,0 +1,43 @@ +import owlrl +from pyshacl import validate +import rdflib +from rdflib import OWL + + +class SHACLutils: + def load_knowledge_bases(self, knowledge_bases): + # load them into rdflib-graph + g_rdf = rdflib.Graph() + for kb, kb_val in knowledge_bases.items(): + g_rdf.parse(kb_val["path"], format=kb_val["format"]) + + return g_rdf + + def run_reasoner(self, g_rdf): + # infer implicit triples by reasoning + owlrl.DeductiveClosure(owlrl.RDFS_OWLRL_Semantics).expand(g_rdf) + + return g_rdf + + def remove_sameAs(self, g_rdf): + # remove owl:sameAs relations, if they only cover identity + for subj, pred, obj in g_rdf.triples((None, OWL.sameAs, None)): + # if pred.startswith(rdflib.RDFS): + if subj == obj: + g_rdf.remove((subj, pred, obj)) + + return g_rdf + + def show_all_triples(self, g_rdf): + for subj, pred, obj in g_rdf.triples((None, None, None)): + print(subj, pred, obj) + + def validate_against_constraints(self, g_data, g_shapes, verbose=False): + r = validate(g_data, shacl_graph=g_shapes, inference="both", advanced=False) + + conforms, results_graph, results_text = r + + if verbose: + print(results_text) + + return conforms, results_graph From cfe5ccfac32cca515b7a76080011e581bc404eb9 Mon Sep 17 00:00:00 2001 From: Maximilian Gruber Date: Thu, 24 Jul 2025 15:58:31 +0200 Subject: [PATCH 2/2] add shacl shapes and examples to test for invalid PrefixedUnit --- test/invalid_individuals.ttl | 13 +++++++++++++ test/test_constraints.ttl | 37 ++++++++++++++++++++++++++++++++++++ test/valid_individuals.ttl | 12 ++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 test/invalid_individuals.ttl create mode 100644 test/test_constraints.ttl create mode 100644 test/valid_individuals.ttl diff --git a/test/invalid_individuals.ttl b/test/invalid_individuals.ttl new file mode 100644 index 0000000..83eca25 --- /dev/null +++ b/test/invalid_individuals.ttl @@ -0,0 +1,13 @@ +@prefix si: . +@prefix prefixes: . +@prefix units: . +@prefix ex: . + + +ex:invalid_unit_1 a si:PrefixedUnit ; + si:hasPrefix prefixes:milli ; + si:hasBaseUnit units:kilogram . + +ex:invalid_unit_2 a si:PrefixedUnit ; + si:hasPrefix prefixes:milli ; + si:hasBaseUnit units:neper . diff --git a/test/test_constraints.ttl b/test/test_constraints.ttl new file mode 100644 index 0000000..c611152 --- /dev/null +++ b/test/test_constraints.ttl @@ -0,0 +1,37 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix xsd: . + +@prefix si: . +@prefix prefixes: . +@prefix units: . +@prefix test: . + + +# base units shall be units, but no prefixed units +test:range_of_hasBaseUnit a sh:NodeShape ; + sh:targetClass si:PrefixedUnit ; + sh:property [ + sh:path si:hasBaseUnit ; + sh:maxCount 1 ; + sh:class si:MeasurementUnit ; + sh:not [ + a sh:NodeShape; + sh:class si:PrefixedUnit; + ] ; + sh:nodeKind sh:IRI ; + ] . + + +# only use base units, that are allowed with prefixes (without a prefix restriction) +test:no_prefixRestriction a sh:NodeShape ; + sh:targetClass si:PrefixedUnit ; + sh:property [ + sh:path ( si:hasBaseUnit si:prefixRestriction ); + sh:maxCount 1 ; + sh:not [ + a sh:NodeShape; + sh:hasValue true; + ]; + ] . diff --git a/test/valid_individuals.ttl b/test/valid_individuals.ttl new file mode 100644 index 0000000..d1b29a8 --- /dev/null +++ b/test/valid_individuals.ttl @@ -0,0 +1,12 @@ +@prefix si: . +@prefix prefixes: . +@prefix units: . +@prefix ex: . + +ex:valid_unit_1 a si:PrefixedUnit ; + si:hasPrefix prefixes:milli ; + si:hasBaseUnit units:gram . + +ex:valid_unit_2 a si:PrefixedUnit ; + si:hasPrefix prefixes:mega ; + si:hasBaseUnit units:second .