mirror of https://github.com/razor-ai/soup.git
fix(bom): emit energy in SPDX output too + strengthen energy tests
PR #256 attached energy only to CycloneDX; #244's contract is "both outputs". Add _energy_annotations() so the SPDX model package carries energy as OTHER annotations (same soup:<field>=<value> naming). Strengthen the happy-path test to assert energy actually lands in the BOM (not just that a file is written) and add a --format both case asserting energy in BOTH cdx + spdx.
This commit is contained in:
parent
8254091e44
commit
79f01a52a8
|
|
@ -163,6 +163,24 @@ def _energy_properties(entry: BomEntry) -> list[dict]:
|
|||
return props
|
||||
|
||||
|
||||
def _energy_annotations(entry: BomEntry, annotation_date: str) -> list[dict]:
|
||||
"""SPDX has no native properties — surface energy as OTHER annotations.
|
||||
|
||||
Mirrors :func:`_energy_properties` (same ``soup:<field>=<value>`` naming) so
|
||||
energy data lands in the SPDX output as well as CycloneDX (the #244 contract
|
||||
is "both outputs").
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"annotator": "Tool: soup-cli",
|
||||
"annotationDate": annotation_date,
|
||||
"annotationType": "OTHER",
|
||||
"annotationComment": f"{prop['name']}={prop['value']}",
|
||||
}
|
||||
for prop in _energy_properties(entry)
|
||||
]
|
||||
|
||||
|
||||
def build_cyclonedx_bom(entry: BomEntry) -> dict:
|
||||
"""Render a CycloneDX 1.6 ML-BOM dict (in-memory)."""
|
||||
if not isinstance(entry, BomEntry):
|
||||
|
|
@ -264,7 +282,8 @@ def build_spdx_bom(entry: BomEntry) -> dict:
|
|||
"annotationDate": entry.created_at,
|
||||
"annotationType": "OTHER",
|
||||
"annotationComment": f"task={entry.task} base={entry.base_model}",
|
||||
}
|
||||
},
|
||||
*_energy_annotations(entry, entry.created_at),
|
||||
],
|
||||
"checksums": [{"algorithm": "SHA256", "checksumValue": entry.config_sha}],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1470,6 +1470,51 @@ class TestBomEnergyCli:
|
|||
)
|
||||
assert result.exit_code == 0, (result.output, repr(result.exception))
|
||||
assert out.is_file()
|
||||
# The feature's point: energy values must actually land in the BOM,
|
||||
# not just produce a file (a no-op attach_energy would pass otherwise).
|
||||
written = out.read_text()
|
||||
assert "soup:energy_kwh" in written
|
||||
assert "12.5" in written
|
||||
assert "codecarbon" in written
|
||||
|
||||
def test_emit_energy_both_formats(self, tmp_path, monkeypatch):
|
||||
"""--format both writes cdx + spdx, and energy lands in BOTH (#244)."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
energy_file = tmp_path / "energy.json"
|
||||
energy_file.write_text(json.dumps({
|
||||
"energy_kwh": 12.5,
|
||||
"co2_kg": 4.0,
|
||||
"pue": 1.2,
|
||||
"grid_intensity_g_per_kwh": 400.0,
|
||||
"source": "codecarbon",
|
||||
}))
|
||||
prefix = tmp_path / "bom"
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"bom", "emit",
|
||||
"--name", "adapter-v1",
|
||||
"--version", "0.1.0",
|
||||
"--base-model", "meta-llama/Llama-3.1-8B",
|
||||
"--base-sha", "a" * 64,
|
||||
"--config-sha", "b" * 64,
|
||||
"--task", "sft",
|
||||
"--license", "apache-2.0",
|
||||
"--format", "both",
|
||||
"--output", str(prefix),
|
||||
"--energy", str(energy_file),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, (result.output, repr(result.exception))
|
||||
cdx = tmp_path / "bom.cdx.json"
|
||||
spdx = tmp_path / "bom.spdx.json"
|
||||
assert cdx.is_file()
|
||||
assert spdx.is_file()
|
||||
for path in (cdx, spdx):
|
||||
body = path.read_text()
|
||||
assert "12.5" in body, f"energy_kwh missing from {path.name}"
|
||||
assert "codecarbon" in body, f"energy_source missing from {path.name}"
|
||||
|
||||
def test_emit_energy_malformed_json(self, tmp_path, monkeypatch):
|
||||
"""Malformed JSON in energy file produces exit code 2."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue