From 929e7cb0056854061325ea4c7bf85405723dca3b Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Thu, 9 Apr 2026 10:13:56 +0200 Subject: [PATCH] support/testing/utils: add basic tests for utils/generate-cyclonedx Introduce unit-tests for the generate-cyclonedx script, covering basic script invocation, patch CVE extraction and virtual packages. Signed-off-by: Martin Willi Acked-By: Thomas Perale [Arnout: fix check-package errors] Signed-off-by: Arnout Vandecappelle --- .../tests/utils/test_generate_cyclonedx.py | 139 ++++++++++++++++++ .../0001-cve_upstream.patch | 14 ++ 2 files changed, 153 insertions(+) create mode 100644 support/testing/tests/utils/test_generate_cyclonedx.py create mode 100644 support/testing/tests/utils/test_generate_cyclonedx/0001-cve_upstream.patch diff --git a/support/testing/tests/utils/test_generate_cyclonedx.py b/support/testing/tests/utils/test_generate_cyclonedx.py new file mode 100644 index 0000000000..ddad92598b --- /dev/null +++ b/support/testing/tests/utils/test_generate_cyclonedx.py @@ -0,0 +1,139 @@ +"""Unit tests for utils/generate-cyclonedx.""" + +import json +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +import infra + +PATCH = "support/testing/tests/utils/test_generate_cyclonedx/0001-cve_upstream.patch" +SCHEMA_LICENSES = ["MIT", "Apache-2.0", "GPL-3.0-only"] + + +class TestGenerateCycloneDX(unittest.TestCase): + def setUp(self): + # Provide a fake SPDX schema so the script never hits the network. + self.schema_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.schema_dir.cleanup) + + cyclonedx_dir = Path(self.schema_dir.name) / "cyclonedx" + cyclonedx_dir.mkdir(parents=True) + schema_path = cyclonedx_dir / "spdx-1.6.schema.json" + schema_path.write_text(json.dumps({"enum": SCHEMA_LICENSES})) + + self.env = os.environ.copy() + self.env["BR2_DL_DIR"] = self.schema_dir.name + self.script = infra.basepath("utils/generate-cyclonedx") + self.cwd = infra.basepath() + + def _make_show_info(self) -> dict: + return { + "package-foo": { + "name": "foo", + "version": "1.2", + "type": "target", + "virtual": False, + "licenses": "MIT", + "cpe-id": "cpe:2.3:a:example:foo:1.2:*:*:*:*:*:*:*", + "patches": [PATCH], + "provides": ["package-virtual"], + "dependencies": ["skeleton-baz", "package-bar"], + "ignore_cves": ["CVE-2025-0001"], + "package_dir": "package/package-foo", + }, + "skeleton-baz": { + "name": "skeleton-baz", + "version": "0.1", + "type": "target", + "virtual": False, + "licenses": "Apache-2.0", + "dependencies": [], + "package_dir": "package/skeleton-baz", + }, + "package-bar": { + "name": "bar", + "version": "0.2", + "type": "target", + "virtual": False, + "licenses": "MIT", + "ignore_cves": ["CVE-2025-0002"], + "dependencies": ["package-virtual"], + "package_dir": "package/package-bar", + }, + "host-tool": { + "name": "host-tool", + "version": "0.3", + "type": "host", + "virtual": False, + "licenses": "GPL-3.0-only", + "dependencies": [], + "package_dir": "package/host-tool", + }, + "package-virtual": { + "name": "virtual-provider", + "virtual": True, + "type": "target", + "dependencies": ["package-foo"], + "package_dir": "package/package-virtual", + }, + } + + def _run_script(self, extra_args=(), show_info=None): + data = show_info if show_info is not None else self._make_show_info() + completed = subprocess.run( + [self.script, *extra_args], + cwd=self.cwd, + env=self.env, + input=json.dumps(data), + text=True, + capture_output=True, + check=True, + ) + return json.loads(completed.stdout) + + def _find_component(self, result: dict, name: str) -> dict: + for component in result["components"]: + if component["bom-ref"] == name: + return component + self.fail(f"component {name} missing") + + def test_default(self): + result = self._run_script() + + self.assertEqual(len(result["components"]), 4) + self.assertIn("vulnerabilities", result) + vulnerabilities = {v["id"]: v for v in result["vulnerabilities"]} + self.assertEqual(len(vulnerabilities), 2) + self.assertEqual(vulnerabilities["CVE-2025-0001"]["analysis"]["state"], "resolved_with_pedigree") + self.assertEqual(vulnerabilities["CVE-2025-0002"]["analysis"]["state"], "in_triage") + + foo = self._find_component(result, "package-foo") + patch = foo["pedigree"]["patches"][0] + self.assertIn("text", patch["diff"]) + self.assertIn("content", patch["diff"]["text"]) + + host = self._find_component(result, "host-tool") + self.assertEqual(host["properties"][0]["value"], "host") + + names = {c["bom-ref"] for c in result["components"]} + self.assertIn("skeleton-baz", names) + self.assertIn("package-bar", names) + self.assertNotIn("package-virtual", names) + + foo_deps = next(d for d in result["dependencies"] if d["ref"] == "package-foo") + self.assertEqual(foo_deps["dependsOn"], ["package-bar", "skeleton-baz"]) + + bar_deps = next(d for d in result["dependencies"] if d["ref"] == "package-bar") + self.assertEqual(bar_deps["dependsOn"], ["package-foo"]) + + def test_virtual(self): + result = self._run_script(["--virtual"]) + + names = {c["bom-ref"] for c in result["components"]} + self.assertEqual(names, {"package-foo", "skeleton-baz", "host-tool", "package-virtual", "package-bar"}) + + foo_deps = next(d for d in result["dependencies"] if d["ref"] == "package-foo") + self.assertEqual(foo_deps["dependsOn"], ["package-bar", "skeleton-baz"]) diff --git a/support/testing/tests/utils/test_generate_cyclonedx/0001-cve_upstream.patch b/support/testing/tests/utils/test_generate_cyclonedx/0001-cve_upstream.patch new file mode 100644 index 0000000000..75d8830203 --- /dev/null +++ b/support/testing/tests/utils/test_generate_cyclonedx/0001-cve_upstream.patch @@ -0,0 +1,14 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 + +CVE: CVE-2025-0001 +Upstream: https://patches.example/foo.patch + +Signed-off-by: the contributor + +diff --git a/foo.txt b/foo.txt +index 0000001..0000002 100644 +--- a/foo.txt ++++ b/foo.txt +@@ +-foo ++bar