Skip to content

Commit 9a939ad

Browse files
committed
Adding tests for new functionality
I don't know if this is the best way, but the tests transit the the new functionality; both serialize and deserialize. And some linting cleanup
1 parent ea90034 commit 9a939ad

File tree

5 files changed

+151
-40
lines changed

5 files changed

+151
-40
lines changed

cyclonedx/model/bom.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from datetime import datetime
2020
from itertools import chain
21-
from typing import TYPE_CHECKING, Any, AnyStr, Dict, Generator, Iterable, Optional, Union
21+
from typing import TYPE_CHECKING, Generator, Iterable, Optional, Union
2222
from uuid import UUID, uuid4
2323
from warnings import warn
2424

@@ -27,7 +27,6 @@
2727

2828
from .._internal.time import get_now_utc as _get_now_utc
2929
from ..exception.model import LicenseExpressionAlongWithOthersException, UnknownComponentDependencyException
30-
from .tool import Tool, ToolsRepository, ToolsRepositoryHelper
3130
from ..schema.schema import (
3231
SchemaVersion1Dot0,
3332
SchemaVersion1Dot1,
@@ -45,6 +44,7 @@
4544
from .dependency import Dependable, Dependency
4645
from .license import License, LicenseExpression, LicenseRepository
4746
from .service import Service
47+
from .tool import Tool, ToolsRepository, ToolsRepositoryHelper
4848
from .vulnerability import Vulnerability
4949

5050
if TYPE_CHECKING: # pragma: no cover
@@ -60,7 +60,7 @@ class BomMetaData:
6060
See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.5/#type_metadata
6161
"""
6262

63-
def __init__(self, *, tools: Optional[Union[Iterable[Tool], Dict[AnyStr, Any]]] = None,
63+
def __init__(self, *, tools: Optional[Union[Iterable[Tool], ToolsRepository]] = None,
6464
authors: Optional[Iterable[OrganizationalContact]] = None, component: Optional[Component] = None,
6565
supplier: Optional[OrganizationalEntity] = None,
6666
licenses: Optional[Iterable[License]] = None,

cyclonedx/model/tool.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def __bool__(self) -> bool:
178178
return any([self._tools, self._components, self._services])
179179

180180
@property
181-
def components(self) -> Iterable[Component]:
181+
def components(self) -> SortedSet[Component]:
182182
"""
183183
Returns:
184184
A SortedSet of Components
@@ -196,7 +196,7 @@ def components(self, components: Iterable[Component]) -> None:
196196
self._components = SortedSet(components)
197197

198198
@property
199-
def services(self) -> Iterable[Service]:
199+
def services(self) -> SortedSet[Service]:
200200
"""
201201
Returns:
202202
A SortedSet of Services
@@ -240,9 +240,6 @@ def json_normalize(cls, o: ToolsRepository, *,
240240
if not any([o._tools, o.components, o.services]): # pylint: disable=protected-access
241241
return None
242242

243-
if o._tools: # pylint: disable=protected-access
244-
return [json_loads(Tool.as_json(t, view_=view)) for t in o] # type: ignore[attr-defined]
245-
246243
result = {}
247244

248245
if o.components:
@@ -253,7 +250,10 @@ def json_normalize(cls, o: ToolsRepository, *,
253250
result['services'] = [json_loads(Service.as_json(s, view_=view)) # type: ignore[attr-defined]
254251
for s in o.services]
255252

256-
return result
253+
if result:
254+
return result
255+
256+
return [json_loads(Tool.as_json(t, view_=view)) for t in o] # type: ignore[attr-defined]
257257

258258
@classmethod
259259
def json_denormalize(cls, o: Union[List[Dict[str, Any]], Dict[str, Any]],
@@ -291,31 +291,37 @@ def xml_normalize(cls, o: ToolsRepository, *,
291291

292292
elem = Element(element_name)
293293

294-
if o._tools: # pylint: disable=protected-access
295-
elem.extend(
296-
t.as_xml( # type: ignore[attr-defined]
297-
view_=view, as_string=False, element_name='tool', xmlns=xmlns)
298-
for t in o
299-
)
300-
301294
if o.components:
302-
c_elem = Element('components')
295+
c_elem = Element('{' + xmlns + '}' + 'components') # type: ignore[operator]
303296

304297
c_elem.extend(
305298
c.as_xml( # type: ignore[attr-defined]
306299
view_=view, as_string=False, element_name='component', xmlns=xmlns)
307300
for c in o.components
308301
)
309302

303+
elem.append(c_elem)
304+
310305
if o.services:
311-
s_elem = Element('services')
306+
s_elem = Element('{' + xmlns + '}' + 'services') # type: ignore[operator]
312307

313308
s_elem.extend(
314309
s.as_xml( # type: ignore[attr-defined]
315-
view_=view, as_string=False, element_name='services', xmlns=xmlns)
310+
view_=view, as_string=False, element_name='service', xmlns=xmlns)
316311
for s in o.services
317312
)
318313

314+
elem.append(s_elem)
315+
316+
if len(elem) > 0:
317+
return elem
318+
319+
elem.extend(
320+
t.as_xml( # type: ignore[attr-defined]
321+
view_=view, as_string=False, element_name='tool', xmlns=xmlns)
322+
for t in o
323+
)
324+
319325
return elem
320326

321327
@classmethod

cyclonedx/model/vulnerability.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
)
5454
from .tool import Tool, ToolsRepository, ToolsRepositoryHelper
5555

56+
5657
@serializable.serializable_class
5758
class BomTargetVersionRange:
5859
"""
@@ -917,7 +918,7 @@ def __init__(self, *,
917918
properties: Optional[Iterable[Property]] = None
918919
) -> None:
919920
if isinstance(bom_ref, BomRef):
920-
self._bom_ref = bom_ref
921+
self._bom_ref: BomRef = bom_ref
921922
else:
922923
self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else None)
923924
self.id = id

tests/test_model.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
Note,
4141
NoteText,
4242
Property,
43-
Tool,
4443
XsUri,
4544
)
4645
from cyclonedx.model.contact import OrganizationalContact
@@ -546,22 +545,3 @@ def test_sort(self) -> None:
546545
sorted_props = sorted(props)
547546
expected_props = reorder(props, expected_order)
548547
self.assertListEqual(sorted_props, expected_props)
549-
550-
551-
class TestModelTool(TestCase):
552-
553-
def test_sort(self) -> None:
554-
# expected sort order: (vendor, name, version)
555-
expected_order = [0, 1, 2, 3, 4, 5, 6]
556-
tools = [
557-
Tool(vendor='a', name='a', version='1.0.0'),
558-
Tool(vendor='a', name='a', version='2.0.0'),
559-
Tool(vendor='a', name='b', version='1.0.0'),
560-
Tool(vendor='a', name='b'),
561-
Tool(vendor='b', name='a'),
562-
Tool(vendor='b', name='b', version='1.0.0'),
563-
Tool(name='b'),
564-
]
565-
sorted_tools = sorted(tools)
566-
expected_tools = reorder(tools, expected_order)
567-
self.assertListEqual(sorted_tools, expected_tools)

tests/test_model_tool.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import io
2+
from json import loads as json_loads
3+
from unittest import TestCase
4+
5+
from cyclonedx.model.bom import Bom
6+
from cyclonedx.model.component import Component, ComponentType
7+
from cyclonedx.model.service import Service
8+
from cyclonedx.model.tool import Tool
9+
from cyclonedx.output.json import JsonV1Dot5
10+
from cyclonedx.output.xml import XmlV1Dot5
11+
from tests import reorder
12+
13+
14+
class TestModelTool(TestCase):
15+
16+
def test_sort(self) -> None:
17+
# expected sort order: (vendor, name, version)
18+
expected_order = [0, 1, 2, 3, 4, 5, 6]
19+
tools = [
20+
Tool(vendor='a', name='a', version='1.0.0'),
21+
Tool(vendor='a', name='a', version='2.0.0'),
22+
Tool(vendor='a', name='b', version='1.0.0'),
23+
Tool(vendor='a', name='b'),
24+
Tool(vendor='b', name='a'),
25+
Tool(vendor='b', name='b', version='1.0.0'),
26+
Tool(name='b'),
27+
]
28+
sorted_tools = sorted(tools)
29+
expected_tools = reorder(tools, expected_order)
30+
self.assertListEqual(sorted_tools, expected_tools)
31+
32+
def test_tool_with_component_and_service_load_json(self) -> None:
33+
# test_file = join(OWN_DATA_DIRECTORY, 'json', '1.5', 'tools_with_components_and_services.json')
34+
bom_json = """
35+
{
36+
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
37+
"bomFormat": "CycloneDX",
38+
"specVersion": "1.5",
39+
"serialNumber": "urn:uuid:60bd7113-edac-4277-a518-88d84aef2399",
40+
"version": 1337,
41+
"metadata": {
42+
"tools": {
43+
"components": [
44+
{
45+
"type": "application",
46+
"author": "anchore",
47+
"name": "syft",
48+
"version": "1.4.1"
49+
}
50+
],
51+
"services": [
52+
{
53+
"name": "testing-service"
54+
}
55+
]
56+
},
57+
"component": {
58+
"type": "file",
59+
"name": "Testing tools with components and services"
60+
}
61+
}
62+
}
63+
"""
64+
bom = Bom.from_json(json_loads(bom_json)) # type: ignore[attr-defined]
65+
self.assertEqual(bom.metadata.tools.components[0].type, 'application')
66+
self.assertEqual(bom.metadata.tools.components[0].name, 'syft')
67+
self.assertEqual(bom.metadata.tools.services[0].name, 'testing-service')
68+
69+
def test_tool_with_component_and_service_render_json(self) -> None:
70+
bom = Bom()
71+
bom.metadata.tools.components.add(Component(type=ComponentType.APPLICATION, author='adobe',
72+
name='test-component', version='1.2.3'))
73+
bom.metadata.tools.services.add(Service(name='test-service'))
74+
out = json_loads(JsonV1Dot5(bom).output_as_string())
75+
self.assertEqual(out['metadata']['tools']['components'][0]['name'], 'test-component')
76+
self.assertEqual(out['metadata']['tools']['services'][0]['name'], 'test-service')
77+
78+
def test_tool_with_component_and_service_load_xml(self) -> None:
79+
bom_xml = io.StringIO("""<?xml version="1.0" ?>
80+
<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="urn:uuid:f9dbe3d0-53ee-485d-96ae-1d4dce7deea2" version="1">
81+
<metadata>
82+
<timestamp>2024-06-20T22:49:51.530453+00:00</timestamp>
83+
<tools>
84+
<components>
85+
<component type="application" bom-ref="None">
86+
<author>adobe</author>
87+
<name>test-component</name>
88+
<version>1.2.3</version>
89+
</component>
90+
</components>
91+
<services>
92+
<service bom-ref="None">
93+
<name>test-service</name>
94+
</service>
95+
</services>
96+
</tools>
97+
</metadata>
98+
</bom>
99+
""") # noqa: E501
100+
bom = Bom.from_xml(bom_xml) # type: ignore[attr-defined]
101+
self.assertEqual(bom.metadata.tools.components[0].type, 'application')
102+
self.assertEqual(bom.metadata.tools.components[0].name, 'test-component')
103+
self.assertEqual(bom.metadata.tools.services[0].name, 'test-service')
104+
105+
def test_tool_with_componet_and_service_render_xml(self) -> None:
106+
bom = Bom()
107+
bom.metadata.tools.components.add(Component(type=ComponentType.APPLICATION, author='adobe',
108+
name='test-component', version='1.2.3'))
109+
bom.metadata.tools.services.add(Service(name='test-service'))
110+
out = XmlV1Dot5(bom).output_as_string(indent=2)
111+
self.assertIn(""" <tools>
112+
<components>
113+
<component type="application" bom-ref="None">
114+
<author>adobe</author>
115+
<name>test-component</name>
116+
<version>1.2.3</version>
117+
</component>
118+
</components>
119+
<services>
120+
<service bom-ref="None">
121+
<name>test-service</name>
122+
</service>
123+
</services>
124+
</tools>""", out)

0 commit comments

Comments
 (0)