|
16 | 16 | import os |
17 | 17 |
|
18 | 18 | import pytest |
| 19 | +from rdflib import Literal, Namespace |
19 | 20 |
|
20 | | -from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER |
| 21 | +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER, SHACL_NS |
21 | 22 | from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileSpecificationError |
22 | 23 | from rocrate_validator.models import Profile, ValidationContext, ValidationSettings, Validator |
23 | 24 | from rocrate_validator.requirements.shacl.checks import SHACLCheck |
| 25 | +from rocrate_validator.requirements.shacl.models import ShapesRegistry |
24 | 26 | from tests.ro_crates import InvalidFileDescriptorEntity, ValidROC |
25 | 27 |
|
26 | 28 | # set up logging |
@@ -468,3 +470,152 @@ def check_profile(profile, check, inherited_profiles, overridden_by, override): |
468 | 470 | # Check the profile 'x' |
469 | 471 | elif profile.token == "x": |
470 | 472 | 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