Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Note**: This file is also available as `CLAUDE.md` (symlink) for Claude Code CLI users.
>
> Read [CONTRIBUTING.md](CONTRIBUTING.md) for comprehensive coding standards, design patterns, and commit message format. This file provides agent-specific quick reference only.
> **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.

## Essential Rules (MUST FOLLOW)

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ test = [
"coverage[toml]!=4.4,>=4.0",
"pytest",
"requests-mock",
"spdx-tools",
"twine>=6.1.0",
"hatchling",
"hatch-vcs",
Expand Down Expand Up @@ -204,7 +205,7 @@ exclude = [

[[tool.mypy.overrides]]
# packages without typing annotations and stubs
module = ["hatchling", "hatchling.build", "license_expression", "pyproject_hooks", "requests_mock", "resolver", "stevedore"]
module = ["hatchling", "hatchling.build", "license_expression", "pyproject_hooks", "requests_mock", "resolver", "spdx_tools.*", "stevedore"]
ignore_missing_imports = true

[tool.basedpyright]
Expand Down
20 changes: 20 additions & 0 deletions tests/test_sbom.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import json
import pathlib
import typing

from conftest import make_sbom_ctx
from packaging.requirements import Requirement
from packaging.version import Version
from spdx_tools.spdx.parser.jsonlikedict.json_like_dict_parser import (
JsonLikeDictParser,
)
from spdx_tools.spdx.validation.document_validator import validate_full_spdx_document

from fromager import sbom
from fromager.packagesettings import SbomSettings


def _validate_spdx(doc: dict[str, typing.Any]) -> None:
"""Validate an SBOM dict against the SPDX 2.3 spec using spdx-tools."""
parsed = JsonLikeDictParser().parse(doc)
errors = validate_full_spdx_document(parsed, spdx_version="SPDX-2.3")
assert not errors, "\n".join(e.validation_message for e in errors)


def test_generate_sbom_structure(tmp_path: pathlib.Path) -> None:
"""Verify the generated SBOM has the required SPDX 2.3 fields."""
ctx = make_sbom_ctx(tmp_path, sbom_settings=SbomSettings())
Expand All @@ -26,6 +38,7 @@ def test_generate_sbom_structure(tmp_path: pathlib.Path) -> None:
assert "creationInfo" in doc
assert doc["creationInfo"]["created"]
assert any("fromager" in c for c in doc["creationInfo"]["creators"])
_validate_spdx(doc)


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


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


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


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

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


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


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


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

content = json.loads(result.read_text())
assert content["spdxVersion"] == "SPDX-2.3"
_validate_spdx(content)


def test_write_sbom_preserves_existing_files(tmp_path: pathlib.Path) -> None:
Expand Down
Loading