Skip to content

Commit ebceafa

Browse files
s1ns3nz0degenaro
andauthored
fix: ssp-generate KeyError on missing profile-param-value-origin (#2222)
* fix: ssp-generate KeyError on missing profile-param-value-origin dict.pop() without a default value raises KeyError when profile-param-value-origin key is absent from the parameter dict in _construct_set_parameters_dict. Add None as default to handle missing key gracefully. Signed-off-by: s1ns3nz0 <s1ns3nz0@gmail.com> * test: add deterministic unit tests for all KeyError paths in _construct_set_parameters_dict (#2221) Three code paths where PROFILE_PARAM_VALUE_ORIGIN is never written into new_dict but pop() is called — all now covered: Path 1: prof_param_value_origin == '' + AGGREGATES in profile param → test_construct_set_parameters_dict_aggregates_no_origin_unit Path 2: prof_param_value_origin != '' + purpose=SSP + AGGREGATES in control → test_construct_set_parameters_dict_aggregates_with_prof_origin_ssp Path 3: prof_param_value_origin != '' + purpose=PROFILE + AGGREGATES in both → test_construct_set_parameters_dict_aggregates_with_prof_origin_profile_purpose Also added test_ssp_generate_missing_profile_param_value_origin_regression which uses profile_aggregation fixture (exercises AGGREGATES branch via SSPGenerate._run) as a higher-level regression guard. All tests fail deterministically on unpatched code. Signed-off-by: s1ns3nz0 <s1ns3nz0@gmail.com> * style: apply code-format and mdformat fixes for lint complianc Signed-off-by: s1ns3nz0 <s1ns3nz0@gmail.com> * chore: remove accidentally tracked venv.trestle-contrib files Signed-off-by: s1ns3nz0 <s1ns3nz0@gmail.com> * refactor: move imports to module level and add smoke test clarification - Move Parameter1, Property, CatalogWriter imports to module level (ContextPurpose, ControlContext were already at module level) - Add NOTE to test_ssp_generate_missing_profile_param_value_origin clarifying it does not exercise the AGGREGATES branch and is kept as a smoke test only Signed-off-by: s1ns3nz0 <s1ns3nz0@gmail.com> * fix: replace fragile __new__ bypass with Mock(spec=CatalogInterface) in unit tests Signed-off-by: s1ns3nz0 <s1ns3nz0@gmail.com> --------- Signed-off-by: s1ns3nz0 <s1ns3nz0@gmail.com> Co-authored-by: Lou DeGenaro <lou.degenaro@gmail.com>
1 parent e22e35b commit ebceafa

2 files changed

Lines changed: 214 additions & 1 deletion

File tree

tests/trestle/core/commands/author/ssp_test.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,15 @@
3131
from trestle.common import const, file_utils, list_utils
3232
from trestle.common.model_utils import ModelUtils
3333
from trestle.core.commands.author.ssp import SSPAssemble, SSPFilter, SSPGenerate
34+
from trestle.core.catalog.catalog_writer import CatalogWriter
35+
from trestle.core.catalog.catalog_interface import CatalogInterface
3436
from trestle.core.control_context import ContextPurpose, ControlContext
37+
from trestle.oscal.common import Parameter1, Property
3538
from trestle.core.control_reader import ControlReader
3639
from trestle.core.markdown.markdown_api import MarkdownAPI
3740
from trestle.core.models.file_content_type import FileContentType
3841
from trestle.core.profile_resolver import ProfileResolver
42+
from unittest.mock import Mock
3943

4044

4145
prof_name = 'comp_prof'
@@ -1456,6 +1460,32 @@ def test_ssp_generate_aggregates_no_param_value_orig(tmp_trestle_dir: pathlib.Pa
14561460
assert const.PARAM_VALUE_ORIGIN not in si_7_prm_1.keys()
14571461

14581462

1463+
def test_ssp_generate_missing_profile_param_value_origin(tmp_trestle_dir: pathlib.Path) -> None:
1464+
"""Test ssp-generate succeeds when profile set-parameter has no profile-param-value-origin.
1465+
1466+
Regression test for KeyError raised in catalog_writer._construct_set_parameters_dict
1467+
when profile-param-value-origin key is absent from parameter dict.
1468+
1469+
NOTE: This test uses the standard profile fixture which does NOT exercise
1470+
the AGGREGATES branch — it would pass even on unpatched code. Kept as a
1471+
smoke test only. See test_ssp_generate_missing_profile_param_value_origin_regression
1472+
for the true regression guard that fails deterministically on unpatched code.
1473+
"""
1474+
args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name)
1475+
args.compdefs = None
1476+
ssp_cmd = SSPGenerate()
1477+
# This should not raise KeyError: 'profile-param-value-origin'
1478+
assert ssp_cmd._run(args) == 0
1479+
md_dir = tmp_trestle_dir / ssp_name
1480+
ac_1 = md_dir / 'ac' / 'ac-1.md'
1481+
assert ac_1.exists()
1482+
md_api = MarkdownAPI()
1483+
header, _ = md_api.processor.process_markdown(ac_1)
1484+
# Verify ssp-generate completed without KeyError on missing profile-param-value-origin
1485+
# The header should be populated correctly
1486+
assert 'x-trestle-set-params' in header
1487+
1488+
14591489
def test_ssp_generate_includes_all_imp_reqs(tmp_trestle_dir: pathlib.Path) -> None:
14601490
"""Test component prose is included for all implemented-requirements regardless of rules."""
14611491

@@ -1543,3 +1573,186 @@ def test_ssp_generate_includes_all_imp_reqs(tmp_trestle_dir: pathlib.Path) -> No
15431573
'OSCO' in ac3_content_3
15441574
and 'Ensure that the Container Network Interface file ownership is set to root:root' in ac3_content_3
15451575
)
1576+
1577+
1578+
def test_ssp_generate_missing_profile_param_value_origin_regression(tmp_trestle_dir: pathlib.Path) -> None:
1579+
"""Strengthened regression test for #2221.
1580+
1581+
Verifies that _construct_set_parameters_dict does NOT raise KeyError when:
1582+
- profile set-parameter has AGGREGATES prop (so PROFILE_PARAM_VALUE_ORIGIN
1583+
is intentionally never written into new_dict), AND
1584+
- control parameter also carries AGGREGATES (so pop is called on new_dict)
1585+
1586+
This test uses profile_aggregation fixture which exercises that exact code path.
1587+
The previous test (test_ssp_generate_missing_profile_param_value_origin) used
1588+
a standard profile and did NOT exercise the AGGREGATES branch — meaning it
1589+
would pass even against the unpatched code. This test would fail on unpatched code.
1590+
"""
1591+
args, _ = setup_for_ssp(tmp_trestle_dir, 'profile_aggregation', ssp_name)
1592+
args.compdefs = None
1593+
ssp_cmd = SSPGenerate()
1594+
1595+
# On unpatched code this raises:
1596+
# KeyError: 'profile-param-value-origin'
1597+
# inside _construct_set_parameters_dict at the line:
1598+
# new_dict.pop(const.PROFILE_PARAM_VALUE_ORIGIN) # no default
1599+
assert ssp_cmd._run(args) == 0
1600+
1601+
md_dir = tmp_trestle_dir / ssp_name
1602+
si_7 = md_dir / 'si-7.md'
1603+
assert si_7.exists()
1604+
1605+
md_api = MarkdownAPI()
1606+
header, _ = md_api.processor.process_markdown(si_7)
1607+
1608+
# si-7_prm_1 exercises the AGGREGATES path.
1609+
# After the fix, profile-param-value-origin must be absent (popped cleanly).
1610+
si_7_prm_1 = header['x-trestle-set-params']['si-7_prm_1']
1611+
assert const.PROFILE_PARAM_VALUE_ORIGIN not in si_7_prm_1, (
1612+
'profile-param-value-origin should be absent for AGGREGATES params'
1613+
)
1614+
1615+
1616+
def test_construct_set_parameters_dict_aggregates_no_origin_unit(tmp_path: pathlib.Path) -> None:
1617+
"""Direct unit test for _construct_set_parameters_dict regression (#2221).
1618+
1619+
Constructs the exact scenario that caused KeyError:
1620+
- profile set-parameter has 'aggregates' prop and NO param-value-origin
1621+
→ PROFILE_PARAM_VALUE_ORIGIN is never written into new_dict
1622+
- control parameter also has 'aggregates' prop
1623+
→ pop(PROFILE_PARAM_VALUE_ORIGIN) is called on new_dict
1624+
→ KeyError on unpatched code, clean exit on patched code
1625+
1626+
This test does NOT rely on fixture data and will fail
1627+
deterministically against the unpatched code.
1628+
"""
1629+
1630+
param_id = 'ac-1_prm_1'
1631+
1632+
# profile set-parameter: AGGREGATES prop, no param-value-origin
1633+
# → prof_param_value_origin == '' → enters else branch
1634+
# → AGGREGATES in param.props → PROFILE_PARAM_VALUE_ORIGIN skipped
1635+
profile_param = Parameter1(id=param_id, props=[Property(name=const.AGGREGATES, value=const.AGGREGATES)])
1636+
profile_set_param_dict = {param_id: profile_param}
1637+
1638+
# control parameter: AGGREGATES prop + values
1639+
# → AGGREGATES in orig_param.props → pop(PROFILE_PARAM_VALUE_ORIGIN) called
1640+
# → KeyError on unpatched code (no default), clean on patched (None default)
1641+
control_param = Parameter1(
1642+
id=param_id, props=[Property(name=const.AGGREGATES, value=const.AGGREGATES)], values=['some-value']
1643+
)
1644+
control_param_dict = {param_id: control_param}
1645+
1646+
context = ControlContext.generate(
1647+
purpose=ContextPurpose.SSP, to_markdown=True, trestle_root=tmp_path, md_root=tmp_path
1648+
)
1649+
1650+
writer = CatalogWriter(Mock(spec=CatalogInterface))
1651+
1652+
try:
1653+
result = writer._construct_set_parameters_dict(profile_set_param_dict, control_param_dict, context)
1654+
except KeyError as e:
1655+
raise AssertionError(
1656+
f'KeyError raised for key {e} — regression: unpatched code path hit. '
1657+
f'Ensure dict.pop(const.PROFILE_PARAM_VALUE_ORIGIN, None) is applied.'
1658+
) from e
1659+
1660+
param_result = result.get(param_id, {})
1661+
assert const.PROFILE_PARAM_VALUE_ORIGIN not in param_result, (
1662+
'profile-param-value-origin must be absent for params with aggregates prop'
1663+
)
1664+
1665+
1666+
def test_construct_set_parameters_dict_aggregates_with_prof_origin_ssp(tmp_path: pathlib.Path) -> None:
1667+
"""Regression test: KeyError when prof_param_value_origin is set + purpose=SSP + AGGREGATES.
1668+
1669+
Unpatched code path:
1670+
- prof_param_value_origin != '' → enters first if branch
1671+
- purpose == SSP (not PROFILE) → inner if skipped
1672+
- PROFILE_PARAM_VALUE_ORIGIN never added to new_dict
1673+
- AGGREGATES in control param → pop → KeyError on unpatched code
1674+
"""
1675+
1676+
param_id = 'ac-1_prm_1'
1677+
1678+
# profile param: has param-value-origin (not empty) + AGGREGATES prop
1679+
# → prof_param_value_origin != '' → first branch
1680+
# → purpose=SSP → inner if (purpose==PROFILE) skipped
1681+
# → PROFILE_PARAM_VALUE_ORIGIN not added
1682+
profile_param = Parameter1(
1683+
id=param_id,
1684+
props=[
1685+
Property(name=const.AGGREGATES, value=const.AGGREGATES),
1686+
Property(name='param-value-origin', value='OCISO'),
1687+
],
1688+
)
1689+
profile_set_param_dict = {param_id: profile_param}
1690+
1691+
# control param: AGGREGATES prop → pop(PROFILE_PARAM_VALUE_ORIGIN) called
1692+
control_param = Parameter1(
1693+
id=param_id, props=[Property(name=const.AGGREGATES, value=const.AGGREGATES)], values=['some-value']
1694+
)
1695+
control_param_dict = {param_id: control_param}
1696+
1697+
context = ControlContext.generate(
1698+
purpose=ContextPurpose.SSP, to_markdown=True, trestle_root=tmp_path, md_root=tmp_path
1699+
)
1700+
1701+
writer = CatalogWriter(Mock(spec=CatalogInterface))
1702+
1703+
try:
1704+
result = writer._construct_set_parameters_dict(profile_set_param_dict, control_param_dict, context)
1705+
except KeyError as e:
1706+
raise AssertionError(
1707+
f'KeyError raised for {e}: prof_param_value_origin set + SSP purpose + AGGREGATES path not handled.'
1708+
) from e
1709+
1710+
param_result = result.get(param_id, {})
1711+
assert const.PROFILE_PARAM_VALUE_ORIGIN not in param_result
1712+
1713+
1714+
def test_construct_set_parameters_dict_aggregates_with_prof_origin_profile_purpose(tmp_path: pathlib.Path) -> None:
1715+
"""Regression test: KeyError when prof_param_value_origin set + purpose=PROFILE + AGGREGATES in both.
1716+
1717+
Unpatched code path:
1718+
- prof_param_value_origin != '' → enters first if branch
1719+
- purpose == PROFILE → inner if entered
1720+
- AGGREGATES in profile param.props → skip adding PROFILE_PARAM_VALUE_ORIGIN
1721+
- AGGREGATES in control param → pop → KeyError on unpatched code
1722+
"""
1723+
1724+
param_id = 'ac-1_prm_1'
1725+
1726+
# profile param: has param-value-origin + AGGREGATES
1727+
# → prof_param_value_origin != '' + purpose==PROFILE + AGGREGATES in props
1728+
# → PROFILE_PARAM_VALUE_ORIGIN not added
1729+
profile_param = Parameter1(
1730+
id=param_id,
1731+
props=[
1732+
Property(name=const.AGGREGATES, value=const.AGGREGATES),
1733+
Property(name='param-value-origin', value='OCISO'),
1734+
],
1735+
)
1736+
profile_set_param_dict = {param_id: profile_param}
1737+
1738+
# control param: AGGREGATES → pop called
1739+
control_param = Parameter1(
1740+
id=param_id, props=[Property(name=const.AGGREGATES, value=const.AGGREGATES)], values=['some-value']
1741+
)
1742+
control_param_dict = {param_id: control_param}
1743+
1744+
context = ControlContext.generate(
1745+
purpose=ContextPurpose.PROFILE, to_markdown=True, trestle_root=tmp_path, md_root=tmp_path
1746+
)
1747+
1748+
writer = CatalogWriter(Mock(spec=CatalogInterface))
1749+
1750+
try:
1751+
result = writer._construct_set_parameters_dict(profile_set_param_dict, control_param_dict, context)
1752+
except KeyError as e:
1753+
raise AssertionError(
1754+
f'KeyError raised for {e}: prof_param_value_origin set + PROFILE purpose + AGGREGATES path not handled.'
1755+
) from e
1756+
1757+
param_result = result.get(param_id, {})
1758+
assert const.PROFILE_PARAM_VALUE_ORIGIN not in param_result

trestle/core/catalog/catalog_writer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def _construct_set_parameters_dict(
183183
if new_dict[const.GUIDELINES] is None:
184184
new_dict.pop(const.GUIDELINES)
185185
if const.AGGREGATES in [prop.name for prop in as_list(orig_param.props)]:
186-
new_dict.pop(const.PROFILE_PARAM_VALUE_ORIGIN)
186+
new_dict.pop(const.PROFILE_PARAM_VALUE_ORIGIN, None)
187187
else:
188188
# if the profile doesnt change this param at all, show it in the header with values
189189
tmp_dict = ModelUtils.parameter_to_dict(param_dict, True)

0 commit comments

Comments
 (0)