11import json
22import pathlib
3+ import typing
34
45from conftest import make_sbom_ctx
56from packaging .requirements import Requirement
67from 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
813from fromager import sbom
914from 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+
1224def 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
3144def 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
4862def 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
7287def 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
91107def 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
104121def 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
119137def 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
135154def 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
155175def test_write_sbom_preserves_existing_files (tmp_path : pathlib .Path ) -> None :
0 commit comments