Skip to content

Commit df4fa67

Browse files
saquibsaifeeclaude
andcommitted
test: update tests to use ModelValidator directly
Replace bom.validate() calls with ModelValidator().validate(bom) across test_model_bom.py and test_real_world_examples.py. Add regression tests for the two bug fixes (nested component license check, top-level dependency ref). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Saquib Saifee <saquibsaifee2@gmail.com>
1 parent d3c6b92 commit df4fa67

3 files changed

Lines changed: 70 additions & 33 deletions

File tree

tests/test_model_bom.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from cyclonedx.model.lifecycle import LifecyclePhase, NamedLifecycle, PredefinedLifecycle
3333
from cyclonedx.model.tool import Tool
3434
from cyclonedx.output.json import JsonV1Dot7
35+
from cyclonedx.validation.model import ModelValidationErrorSeverity, ModelValidator
3536
from tests import reorder
3637
from tests._data.models import (
3738
get_bom_component_licenses_invalid,
@@ -254,7 +255,9 @@ def test_bom_nested_components_issue_275(self) -> None:
254255
bom = get_bom_for_issue_275_components()
255256
self.assertIsInstance(bom.metadata.component, Component)
256257
self.assertEqual(2, len(bom.components))
257-
bom.validate()
258+
errors = [e for e in ModelValidator().validate(bom)
259+
if e.severity is ModelValidationErrorSeverity.ERROR]
260+
self.assertFalse(errors)
258261

259262
@named_data(
260263
['metadata_licenses', get_bom_metadata_licenses_invalid],
@@ -266,8 +269,8 @@ def test_bom_nested_components_issue_275(self) -> None:
266269
)
267270
def test_validate_with_invalid_license_constellation_throws(self, get_bom: Callable[[], Bom]) -> None:
268271
bom = get_bom()
269-
with self.assertRaises(LicenseExpressionAlongWithOthersException):
270-
bom.validate()
272+
error_types = [type(e.data) for e in ModelValidator().validate(bom)]
273+
self.assertIn(LicenseExpressionAlongWithOthersException, error_types)
271274

272275
# def test_bom_nested_services_issue_275(self) -> None:
273276
# """regression test for issue #275

tests/test_real_world_examples.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from unittest.mock import patch
2424

2525
from cyclonedx.model.bom import Bom
26+
from cyclonedx.validation.model import ModelValidationErrorSeverity, ModelValidator
2627
from tests import OWN_DATA_DIRECTORY
2728

2829

@@ -44,15 +45,19 @@ def test_regression_issue677(self, *_: Any, **__: Any) -> None:
4445
json = json_loads(input_json.read())
4546
bom = Bom.from_json(json)
4647
self.assertEqual(4, len(bom.components))
47-
bom.validate()
48+
errors = [e for e in ModelValidator().validate(bom)
49+
if e.severity is ModelValidationErrorSeverity.ERROR]
50+
self.assertFalse(errors)
4851

4952
def test_regression_issue753(self, *_: Any, **__: Any) -> None:
5053
# tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/753
5154
with open(join(OWN_DATA_DIRECTORY, 'json', '1.5', 'issue753.json')) as input_json:
5255
json = json_loads(input_json.read())
5356
bom = Bom.from_json(json)
5457
self.assertEqual(2, len(bom.components))
55-
bom.validate()
58+
errors = [e for e in ModelValidator().validate(bom)
59+
if e.severity is ModelValidationErrorSeverity.ERROR]
60+
self.assertFalse(errors)
5661

5762
def test_regression_issue_850(self, *_: Any, **__: Any) -> None:
5863
# tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/850

tests/test_validation_model.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,55 +22,84 @@
2222
from cyclonedx.model.component import Component
2323
from cyclonedx.model.dependency import Dependency
2424
from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression
25-
from cyclonedx.validation.model import ModelValidator
25+
from cyclonedx.validation.model import ModelValidationErrorSeverity, ModelValidator
2626

2727

2828
class TestModelValidator(TestCase):
29-
def test_validate_multiple_errors(self) -> None:
29+
30+
def test_validate_clean_bom(self) -> None:
31+
bom = Bom()
32+
bom.metadata.component = Component(name='root', version='1.0', bom_ref='root')
33+
errors = list(ModelValidator().validate(bom))
34+
self.assertEqual(0, len(errors))
35+
36+
def test_validate_multiple_errors_have_error_severity(self) -> None:
3037
bom = Bom()
31-
# Error 1: Component with multiple licenses including expression
3238
comp = Component(name='test', version='1.0', bom_ref='test-comp')
3339
comp.licenses.update([
3440
DisjunctiveLicense(id='MIT'),
35-
LicenseExpression(value='Apache-2.0 OR MIT')
41+
LicenseExpression(value='Apache-2.0 OR MIT'),
3642
])
3743
bom.components.add(comp)
38-
39-
# Error 2: Unknown dependency reference
4044
bom.dependencies.add(Dependency('test-comp', dependencies=[Dependency('non-existent-ref')]))
4145

42-
validator = ModelValidator()
43-
errors = list(validator.validate(bom))
46+
errors = list(ModelValidator().validate(bom))
4447

45-
self.assertEqual(len(errors), 2)
48+
self.assertEqual(2, len(errors))
4649
error_types = [type(e.data) for e in errors]
4750
self.assertIn(UnknownComponentDependencyException, error_types)
4851
self.assertIn(LicenseExpressionAlongWithOthersException, error_types)
52+
for error in errors:
53+
self.assertEqual(ModelValidationErrorSeverity.ERROR, error.severity)
4954

50-
def test_validate_clean_bom(self) -> None:
55+
def test_validate_unknown_toplevel_dependency_ref_detected(self) -> None:
56+
"""Regression: top-level d.ref values must also be validated against known BOM components."""
5157
bom = Bom()
52-
bom.metadata.component = Component(name='root', version='1.0', bom_ref='root')
53-
validator = ModelValidator()
54-
errors = list(validator.validate(bom))
55-
self.assertEqual(len(errors), 0)
58+
comp = Component(name='real', version='1.0', bom_ref='real-comp')
59+
bom.components.add(comp)
60+
# 'ghost-ref' is not in the BOM at all
61+
bom.dependencies.add(Dependency('ghost-ref'))
5662

57-
def test_bom_validate_deprecated_behavior(self) -> None:
63+
errors = list(ModelValidator().validate(bom))
64+
65+
error_types = [type(e.data) for e in errors]
66+
self.assertIn(UnknownComponentDependencyException, error_types)
67+
68+
def test_validate_incomplete_dependency_graph_yields_warning(self) -> None:
69+
"""Check #2 must yield a WARNING-severity error, not a Python UserWarning."""
70+
import warnings as _warnings
5871
bom = Bom()
5972
bom.metadata.component = Component(name='root', version='1.0', bom_ref='root')
73+
bom.components.add(Component(name='dep', version='1.0', bom_ref='dep'))
74+
75+
with _warnings.catch_warnings():
76+
_warnings.simplefilter('error') # turn any Python warning into an error
77+
errors = list(ModelValidator().validate(bom)) # must not raise
6078

61-
# Verify side effect: register_dependency is called by Bom.validate
62-
self.assertEqual(len(bom.dependencies), 0)
63-
with self.assertWarns(DeprecationWarning):
64-
bom.validate()
65-
self.assertEqual(len(bom.dependencies), 1)
66-
self.assertEqual(next(iter(bom.dependencies)).ref.value, 'root')
79+
warning_errors = [e for e in errors if e.severity == ModelValidationErrorSeverity.WARNING]
80+
self.assertEqual(1, len(warning_errors))
81+
self.assertIsInstance(warning_errors[0].data, UserWarning)
6782

68-
def test_model_validator_no_side_effects(self) -> None:
83+
def test_validate_nested_root_component_license_invalid(self) -> None:
84+
"""Regression: nested components under metadata.component must be license-checked."""
6985
bom = Bom()
70-
bom.metadata.component = Component(name='root', version='1.0', bom_ref='root')
86+
root = Component(name='root', version='1.0', bom_ref='root')
87+
nested = Component(name='nested', version='1.0', bom_ref='nested')
88+
nested.licenses.update([
89+
DisjunctiveLicense(id='MIT'),
90+
LicenseExpression(value='Apache-2.0 OR MIT'),
91+
])
92+
root.components.add(nested)
93+
bom.metadata.component = root
94+
95+
errors = list(ModelValidator().validate(bom))
7196

72-
# Verify NO side effect: ModelValidator should not call register_dependency
73-
self.assertEqual(len(bom.dependencies), 0)
74-
validator = ModelValidator()
75-
list(validator.validate(bom))
76-
self.assertEqual(len(bom.dependencies), 0)
97+
error_types = [type(e.data) for e in errors]
98+
self.assertIn(LicenseExpressionAlongWithOthersException, error_types)
99+
100+
def test_validate_no_side_effects(self) -> None:
101+
bom = Bom()
102+
bom.metadata.component = Component(name='root', version='1.0', bom_ref='root')
103+
self.assertEqual(0, len(bom.dependencies))
104+
list(ModelValidator().validate(bom))
105+
self.assertEqual(0, len(bom.dependencies))

0 commit comments

Comments
 (0)