diff --git a/swh/model/model.py b/swh/model/model.py --- a/swh/model/model.py +++ b/swh/model/model.py @@ -194,6 +194,10 @@ deduplication.""" raise NotImplementedError(f"unique_key for {self}") + def check(self) -> None: + """Performs internal consistency checks, and raises an error if one fails.""" + attr.validate(self) + def _compute_hash_from_manifest(manifest: bytes) -> Sha1Git: return hashlib.new("sha1", manifest).digest() @@ -225,6 +229,12 @@ def unique_key(self) -> KeyType: return self.id + def check(self) -> None: + super().check() # type: ignore + + if self.id != self.compute_hash(): + raise ValueError("'id' does not match recomputed hash.") + @attr.s(frozen=True, slots=True) class Person(BaseModel): diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py --- a/swh/model/tests/test_model.py +++ b/swh/model/tests/test_model.py @@ -17,6 +17,7 @@ from swh.model.collections import ImmutableDict from swh.model.from_disk import DentryPerms +import swh.model.git_objects from swh.model.hashutil import MultiHash, hash_to_bytes import swh.model.hypothesis_strategies as strategies import swh.model.model @@ -748,6 +749,15 @@ # Directory +@given(strategies.directories()) +def test_directory_check(directory): + directory.check() + + directory2 = attr.evolve(directory, id=b"\x00" * 20) + with pytest.raises(ValueError, match="does not match recomputed hash"): + directory2.check() + + def test_directory_entry_name_validation(): with pytest.raises(ValueError, match="valid directory entry name."): DirectoryEntry(name=b"foo/", type="dir", target=b"\x00" * 20, perms=0), @@ -769,9 +779,30 @@ Directory(entries=entries) +# Release + + +@given(strategies.releases()) +def test_release_check(release): + release.check() + + release2 = attr.evolve(release, id=b"\x00" * 20) + with pytest.raises(ValueError, match="does not match recomputed hash"): + release2.check() + + # Revision +@given(strategies.revisions()) +def test_revision_check(revision): + revision.check() + + revision2 = attr.evolve(revision, id=b"\x00" * 20) + with pytest.raises(ValueError, match="does not match recomputed hash"): + revision2.check() + + def test_revision_extra_headers_no_headers(): rev_dict = revision_example.copy() rev_dict.pop("id")