Skip to content

Commit 1acd74d

Browse files
committed
test(checks): ✅ cover deactivated flag on RequirementCheck
1 parent 71540b1 commit 1acd74d

4 files changed

Lines changed: 249 additions & 1 deletion

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) 2024-2026 CRS4
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
@prefix dct: <http://purl.org/dc/terms/> .
16+
@prefix prof: <http://www.w3.org/ns/dx/prof/> .
17+
@prefix role: <http://www.w3.org/ns/dx/prof/role/> .
18+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
19+
20+
<https://w3id.org/c4>
21+
a prof:Profile ;
22+
rdfs:label "Profile C4" ;
23+
rdfs:comment """Comment for Profile C4 (deactivates Profile C's ShapeC by IRI, no override-by-name)."""@en ;
24+
dct:publisher <https://publisherC4> ;
25+
prof:isProfileOf <https://w3id.org/c> ;
26+
prof:isTransitiveProfileOf <https://w3id.org/a>, <https://w3id.org/c> ;
27+
prof:hasToken "c-deactivated-direct" ;
28+
.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) 2024-2026 CRS4
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
@prefix dct: <http://purl.org/dc/terms/> .
16+
@prefix prof: <http://www.w3.org/ns/dx/prof/> .
17+
@prefix role: <http://www.w3.org/ns/dx/prof/role/> .
18+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
19+
20+
<https://w3id.org/c3>
21+
a prof:Profile ;
22+
rdfs:label "Profile C3" ;
23+
rdfs:comment """Comment for Profile C3 (deactivates the inherited check from Profile C)."""@en ;
24+
dct:publisher <https://publisherC3> ;
25+
prof:isProfileOf <https://w3id.org/c> ;
26+
prof:isTransitiveProfileOf <https://w3id.org/a>, <https://w3id.org/c> ;
27+
prof:hasToken "c-deactivated" ;
28+
.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright (c) 2024-2026 CRS4
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
@prefix ro: <./> .
16+
@prefix dct: <http://purl.org/dc/terms/> .
17+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
18+
@prefix schema_org: <http://schema.org/> .
19+
@prefix sh: <http://www.w3.org/ns/shacl#> .
20+
@prefix xml1: <http://www.w3.org/2001/XMLSchema#> .
21+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
22+
23+
24+
# Same NodeShape + PropertyShape names as the inherited Shape C in profile c,
25+
# so the existing override-by-name mechanism wires this shape to the parent's.
26+
# The PropertyShape is marked sh:deactivated true: pyshacl skips it during
27+
# validation, and the override surfaces via RequirementCheck.deactivated.
28+
ro:ShapeC
29+
a sh:NodeShape ;
30+
sh:name "The Shape C" ;
31+
sh:description "Deactivates the inherited Shape C check from profile c." ;
32+
sh:targetNode ro:ro-crate-metadata.json ;
33+
sh:property [
34+
a sh:PropertyShape ;
35+
sh:name "Check Metadata File Descriptor entity existence" ;
36+
sh:description "Check disabled by extension profile" ;
37+
sh:path rdf:type ;
38+
sh:minCount 1 ;
39+
sh:deactivated true ;
40+
sh:message "Disabled" ;
41+
] .

tests/unit/requirements/test_profiles.py

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
import os
1717

1818
import pytest
19+
from rdflib import Literal, Namespace
1920

20-
from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER
21+
from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER, SHACL_NS
2122
from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileSpecificationError
2223
from rocrate_validator.models import Profile, ValidationContext, ValidationSettings, Validator
2324
from rocrate_validator.requirements.shacl.checks import SHACLCheck
25+
from rocrate_validator.requirements.shacl.models import ShapesRegistry
2426
from tests.ro_crates import InvalidFileDescriptorEntity, ValidROC
2527

2628
# set up logging
@@ -468,3 +470,152 @@ def check_profile(profile, check, inherited_profiles, overridden_by, override):
468470
# Check the profile 'x'
469471
elif profile.token == "x":
470472
check_profile(profile, check, ["a", "b", "d"], [], ["d"])
473+
474+
475+
def test_python_check_decorator_sets_deactivated_flag():
476+
"""The @check decorator must propagate the `deactivated` flag onto the
477+
decorated function so that PyRequirement.__init_checks__ can read it."""
478+
from rocrate_validator.requirements.python import check
479+
480+
@check(name="off", deactivated=True)
481+
def disabled(self, ctx): # noqa: ANN001
482+
return False
483+
484+
@check(name="on")
485+
def enabled(self, ctx): # noqa: ANN001
486+
return True
487+
488+
assert disabled.deactivated is True
489+
assert enabled.deactivated is False
490+
491+
492+
def test_shacl_shape_with_deactivated_marks_check_skipped(fake_profiles_path: str):
493+
"""A child profile that overrides an inherited NodeShape by `sh:name` and
494+
sets `sh:deactivated true` should produce a check whose `deactivated`
495+
property is True; the parent's check should be marked as `overridden`."""
496+
settings = {
497+
"profiles_path": fake_profiles_path,
498+
"profile_identifier": "c-deactivated",
499+
"rocrate_uri": ValidROC().wrroc_paper,
500+
"enable_profile_inheritance": True,
501+
"allow_requirement_check_override": True,
502+
}
503+
504+
settings = ValidationSettings(**settings)
505+
validator = Validator(settings)
506+
context = ValidationContext(validator, validator.validation_settings)
507+
508+
profiles = context.profiles
509+
profile_tokens = sorted(p.token for p in profiles)
510+
# Inheritance chain: a <- c <- c-deactivated
511+
assert profile_tokens == ["a", "c", "c-deactivated"]
512+
513+
target = next(p for p in profiles if p.token == "c-deactivated")
514+
parent_c = next(p for p in profiles if p.token == "c")
515+
516+
# The PropertyShape carries `sh:deactivated true`; the matching check is
517+
# the second one (the first is the hidden NodeShape root check).
518+
target_property_check = target.requirements[0].get_checks()[1]
519+
parent_property_check = parent_c.requirements[0].get_checks()[1]
520+
521+
assert (
522+
target_property_check.deactivated is True
523+
), "The deactivated property should reflect sh:deactivated true on the PropertyShape"
524+
525+
# The parent property check is overridden by the child's (same sh:name).
526+
overridden_by_tokens = [c.requirement.profile.token for c in parent_property_check.overridden_by]
527+
assert "c-deactivated" in overridden_by_tokens, "The parent check should be reported as overridden by c-deactivated"
528+
assert parent_property_check.overridden is True
529+
530+
# Default state for a non-deactivated check.
531+
assert parent_property_check.deactivated is False
532+
533+
534+
def test_shacl_check_deactivated_via_cross_profile_triple(fake_profiles_path: str):
535+
"""A child profile that adds `<parentShapeIRI> sh:deactivated true` to its
536+
own shapes graph (without redeclaring the shape) should cause the parent's
537+
check to report `deactivated=True`. Verifies the cross-profile lookup in
538+
SHACLCheck.deactivated and the pre-load pass in Validator."""
539+
540+
settings = ValidationSettings(
541+
**{
542+
"profiles_path": fake_profiles_path,
543+
"profile_identifier": "c-deactivated-direct",
544+
"rocrate_uri": ValidROC().wrroc_paper,
545+
"enable_profile_inheritance": True,
546+
"allow_requirement_check_override": True,
547+
}
548+
)
549+
validator = Validator(settings)
550+
context = ValidationContext(validator, validator.validation_settings)
551+
552+
profiles = context.profiles
553+
profile_tokens = sorted(p.token for p in profiles)
554+
assert profile_tokens == ["a", "c", "c-deactivated-direct"]
555+
556+
target = next(p for p in profiles if p.token == "c-deactivated-direct")
557+
parent_c = next(p for p in profiles if p.token == "c")
558+
559+
# Trigger lazy loading of every profile's shape graph (the Validator
560+
# would do this in __do_validate__; we replay it here for the unit test).
561+
for p in profiles:
562+
_ = p.requirements
563+
564+
parent_shape_check = next(c for c in parent_c.requirements[0].get_checks() if isinstance(c, SHACLCheck))
565+
assert parent_shape_check.deactivated is False, "Sanity check: the parent shape should not be deactivated yet"
566+
567+
# Simulate what a child-profile shape file would contribute: a single
568+
# `<parentShapeIRI> sh:deactivated true` triple in its own shapes graph.
569+
target_registry = ShapesRegistry.get_instance(target)
570+
sh = Namespace(SHACL_NS)
571+
target_registry._shapes_graph.add((parent_shape_check.shape.node, sh.deactivated, Literal(True)))
572+
573+
assert (
574+
parent_shape_check.deactivated is True
575+
), "The parent's check must read sh:deactivated true from the child's shapes graph"
576+
577+
578+
def test_shacl_check_deactivation_scoped_to_descendants(fake_profiles_path: str):
579+
"""A `sh:deactivated true` triple declared by a profile that does NOT
580+
inherit from the shape's owning profile must be ignored. Otherwise
581+
unrelated profiles loaded in the same process could spuriously deactivate
582+
one another's checks."""
583+
584+
settings = ValidationSettings(
585+
**{
586+
"profiles_path": fake_profiles_path,
587+
"profile_identifier": "c",
588+
"rocrate_uri": ValidROC().wrroc_paper,
589+
"enable_profile_inheritance": True,
590+
"allow_requirement_check_override": True,
591+
}
592+
)
593+
validator = Validator(settings)
594+
context = ValidationContext(validator, validator.validation_settings)
595+
596+
# Force population of the global profile registry by listing all profiles
597+
# under the fake_profiles_path, then resolve the specific ones we need.
598+
all_profiles = Profile.load_profiles(profiles_path=fake_profiles_path)
599+
parent_c = next(p for p in all_profiles if p.token == "c")
600+
# Profile "b" is a descendant of "a" but NOT of "c" — unrelated.
601+
profile_b = next(p for p in all_profiles if p.token == "b")
602+
assert parent_c not in profile_b.inherited_profiles
603+
assert profile_b not in Profile.get_descendants(parent_c)
604+
605+
# Trigger lazy loading.
606+
for p in all_profiles:
607+
_ = p.requirements
608+
_ = context.profiles # warm context too
609+
610+
parent_shape_check = next(c for c in parent_c.requirements[0].get_checks() if isinstance(c, SHACLCheck))
611+
assert parent_shape_check.deactivated is False
612+
613+
# Inject a deactivation triple into an unrelated profile's registry.
614+
sh = Namespace(SHACL_NS)
615+
ShapesRegistry.get_instance(profile_b)._shapes_graph.add(
616+
(parent_shape_check.shape.node, sh.deactivated, Literal(True))
617+
)
618+
619+
assert (
620+
parent_shape_check.deactivated is False
621+
), "An unrelated profile's deactivation triple must not affect the check"

0 commit comments

Comments
 (0)