From 276588485a976b1e1436477856738d04abbd95ef Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:05:35 +0200 Subject: [PATCH] feat: refuse a profile check with no value --- scripts/validate_schemas.py | 37 +++++++++++++++++++++ tests/test_checks_have_values.py | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/test_checks_have_values.py diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index 752290a0..1784e0fe 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -57,6 +57,43 @@ def _validate_yaml_directory( continue out.extend(_errors(validator, data, str(path.relative_to(ROOT)))) out.extend(_unreachable_citations(path, data)) + if schema_name == "emulator.schema.json": + out.extend(_checks_without_values(path, data)) + return out + + +_CHECK_FIELDS = { + "crc32": ("crc32",), + "md5": ("md5",), + "sha1": ("sha1",), + "sha256": ("sha256",), + "size": ("size", "min_size", "max_size"), +} + + +def _checks_without_values(path: Path, document: object) -> list[str]: + """A check an entry declares with nothing to compare against. + + validation: [crc32] with no crc32 runs nothing: the file passes whatever + it holds, and the report reads as checked. Fourteen profiles carried + one, the code they describe either refusing or warning on a value the + entry never wrote down. + """ + out: list[str] = [] + files = document.get("files") if isinstance(document, dict) else None + for index, entry in enumerate(files or []): + if not isinstance(entry, dict): + continue + declared = entry.get("validation") or [] + if isinstance(declared, dict): + declared = [check for checks in declared.values() for check in checks or []] + for check in declared: + fields = _CHECK_FIELDS.get(check) + if fields and not any(entry.get(field) not in (None, "", []) for field in fields): + out.append( + f"{path.relative_to(ROOT)}: files[{index}] ({entry.get('name')}) " + f"declares a {check} check with no {'/'.join(fields)} value" + ) return out diff --git a/tests/test_checks_have_values.py b/tests/test_checks_have_values.py new file mode 100644 index 00000000..b7069301 --- /dev/null +++ b/tests/test_checks_have_values.py @@ -0,0 +1,55 @@ +"""A check a profile declares has a value to compare against. + +validation: [crc32] with no crc32 ran nothing, and fourteen profiles carried +one. validate_schemas now refuses it. +""" + +from __future__ import annotations + +import subprocess +import sys +import unittest +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + + +class ChecksHaveValues(unittest.TestCase): + def setUp(self): + try: + import validate_schemas + except ImportError as exc: + self.skipTest(f"validate_schemas needs jsonschema: {exc}") + self.check = validate_schemas._checks_without_values + + def test_a_valueless_check_is_named(self): + path = REPO_ROOT / "emulators" / "x.yml" + document = {"files": [ + {"name": "a.bin", "validation": ["crc32"]}, + {"name": "b.bin", "validation": ["size"], "min_size": 4}, + {"name": "c.bin", "validation": {"core": ["size"], "upstream": ["sha1"]}, + "size": 4}, + ]} + found = self.check(path, document) + self.assertEqual(len(found), 2) + self.assertIn("a.bin", found[0]) + self.assertIn("c.bin", found[1]) + + def test_every_tracked_profile(self): + tracked = subprocess.run( + ["git", "ls-files", "emulators/*.yml"], cwd=REPO_ROOT, + capture_output=True, text=True, check=True, + ).stdout.split() + found = [] + for name in tracked: + path = REPO_ROOT / name + with path.open(encoding="utf-8") as fh: + found.extend(self.check(path, yaml.safe_load(fh))) + self.assertEqual(found, []) + + +if __name__ == "__main__": + unittest.main()