Skip to content

Commit c5c3afc

Browse files
authored
Merge pull request #1040 from mprpic/spdx-schema-validation-in-tests
test(sbom): add SPDX 2.3 schema validation with spdx-tools
2 parents 3e7c564 + 6d6aae8 commit c5c3afc

3 files changed

Lines changed: 23 additions & 2 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> **Note**: This file is also available as `CLAUDE.md` (symlink) for Claude Code CLI users.
44
>
5-
> Read [CONTRIBUTING.md](CONTRIBUTING.md) for comprehensive coding standards, design patterns, and commit message format. This file provides agent-specific quick reference only.
5+
> **You MUST read [CONTRIBUTING.md](CONTRIBUTING.md) before writing code.** It contains coding standards, type annotation rules, design patterns, and commit message format. This file provides agent-specific quick reference only.
66
77
## Essential Rules (MUST FOLLOW)
88

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ test = [
7272
"coverage[toml]!=4.4,>=4.0",
7373
"pytest",
7474
"requests-mock",
75+
"spdx-tools",
7576
"twine>=6.1.0",
7677
"hatchling",
7778
"hatch-vcs",
@@ -204,7 +205,7 @@ exclude = [
204205

205206
[[tool.mypy.overrides]]
206207
# packages without typing annotations and stubs
207-
module = ["hatchling", "hatchling.build", "license_expression", "pyproject_hooks", "requests_mock", "resolver", "stevedore"]
208+
module = ["hatchling", "hatchling.build", "license_expression", "pyproject_hooks", "requests_mock", "resolver", "spdx_tools.*", "stevedore"]
208209
ignore_missing_imports = true
209210

210211
[tool.basedpyright]

tests/test_sbom.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import json
22
import pathlib
3+
import typing
34

45
from conftest import make_sbom_ctx
56
from packaging.requirements import Requirement
67
from packaging.version import Version
8+
from spdx_tools.spdx.parser.jsonlikedict.json_like_dict_parser import (
9+
JsonLikeDictParser,
10+
)
11+
from spdx_tools.spdx.validation.document_validator import validate_full_spdx_document
712

813
from fromager import sbom
914
from fromager.packagesettings import SbomSettings
1015

1116

17+
def _validate_spdx(doc: dict[str, typing.Any]) -> None:
18+
"""Validate an SBOM dict against the SPDX 2.3 spec using spdx-tools."""
19+
parsed = JsonLikeDictParser().parse(doc)
20+
errors = validate_full_spdx_document(parsed, spdx_version="SPDX-2.3")
21+
assert not errors, "\n".join(e.validation_message for e in errors)
22+
23+
1224
def test_generate_sbom_structure(tmp_path: pathlib.Path) -> None:
1325
"""Verify the generated SBOM has the required SPDX 2.3 fields."""
1426
ctx = make_sbom_ctx(tmp_path, sbom_settings=SbomSettings())
@@ -26,6 +38,7 @@ def test_generate_sbom_structure(tmp_path: pathlib.Path) -> None:
2638
assert "creationInfo" in doc
2739
assert doc["creationInfo"]["created"]
2840
assert any("fromager" in c for c in doc["creationInfo"]["creators"])
41+
_validate_spdx(doc)
2942

3043

3144
def test_generate_sbom_default_settings(tmp_path: pathlib.Path) -> None:
@@ -43,6 +56,7 @@ def test_generate_sbom_default_settings(tmp_path: pathlib.Path) -> None:
4356
assert doc["documentNamespace"] == (
4457
"https://spdx.org/spdxdocs/my-package-2.0.0.spdx.json"
4558
)
59+
_validate_spdx(doc)
4660

4761

4862
def test_generate_sbom_custom_settings(tmp_path: pathlib.Path) -> None:
@@ -67,6 +81,7 @@ def test_generate_sbom_custom_settings(tmp_path: pathlib.Path) -> None:
6781
creators = doc["creationInfo"]["creators"]
6882
assert "Organization: ExampleCo" in creators
6983
assert any("fromager" in c for c in creators)
84+
_validate_spdx(doc)
7085

7186

7287
def test_generate_sbom_purl_override(tmp_path: pathlib.Path) -> None:
@@ -86,6 +101,7 @@ def test_generate_sbom_purl_override(tmp_path: pathlib.Path) -> None:
86101
ext_refs = pkg["externalRefs"]
87102
assert len(ext_refs) == 1
88103
assert ext_refs[0]["referenceLocator"] == "pkg:generic/test-pkg@1.0.0"
104+
_validate_spdx(doc)
89105

90106

91107
def test_generate_sbom_default_purl(tmp_path: pathlib.Path) -> None:
@@ -99,6 +115,7 @@ def test_generate_sbom_default_purl(tmp_path: pathlib.Path) -> None:
99115

100116
pkg = doc["packages"][0]
101117
assert pkg["externalRefs"][0]["referenceLocator"] == "pkg:pypi/test@0.1.0"
118+
_validate_spdx(doc)
102119

103120

104121
def test_generate_sbom_canonicalizes_name(tmp_path: pathlib.Path) -> None:
@@ -114,6 +131,7 @@ def test_generate_sbom_canonicalizes_name(tmp_path: pathlib.Path) -> None:
114131
assert pkg["name"] == "my-package"
115132
assert doc["name"] == "my-package-1.0.0"
116133
assert pkg["externalRefs"][0]["referenceLocator"] == "pkg:pypi/my-package@1.0.0"
134+
_validate_spdx(doc)
117135

118136

119137
def test_generate_sbom_describes_relationship(tmp_path: pathlib.Path) -> None:
@@ -130,6 +148,7 @@ def test_generate_sbom_describes_relationship(tmp_path: pathlib.Path) -> None:
130148
assert rels[0]["spdxElementId"] == "SPDXRef-DOCUMENT"
131149
assert rels[0]["relationshipType"] == "DESCRIBES"
132150
assert rels[0]["relatedSpdxElement"] == "SPDXRef-wheel"
151+
_validate_spdx(doc)
133152

134153

135154
def test_write_sbom_creates_file(tmp_path: pathlib.Path) -> None:
@@ -150,6 +169,7 @@ def test_write_sbom_creates_file(tmp_path: pathlib.Path) -> None:
150169

151170
content = json.loads(result.read_text())
152171
assert content["spdxVersion"] == "SPDX-2.3"
172+
_validate_spdx(content)
153173

154174

155175
def test_write_sbom_preserves_existing_files(tmp_path: pathlib.Path) -> None:

0 commit comments

Comments
 (0)