Skip to content

Commit 7543789

Browse files
authored
tests: snapshots and complete deep comparison, instead of pseudo-compare (#464)
part of #437 also fixed a bug: unused first level dependencies were not detected. now they are. --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent a68ae24 commit 7543789

File tree

239 files changed

+8322
-9984
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

239 files changed

+8322
-9984
lines changed

cyclonedx/model/bom.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,9 @@ def validate(self) -> bool:
552552
# 1. Make sure dependencies are all in this Bom.
553553
all_bom_refs = set(map(lambda c: c.bom_ref, self._get_all_components())) | set(
554554
map(lambda s: s.bom_ref, self.services))
555-
all_dependency_bom_refs = set().union(*(d.dependencies_as_bom_refs() for d in self.dependencies))
555+
all_dependency_bom_refs = set(chain((d.ref for d in self.dependencies),
556+
chain.from_iterable(
557+
d.dependencies_as_bom_refs() for d in self.dependencies)))
556558

557559
dependency_diff = all_dependency_bom_refs - all_bom_refs
558560
if len(dependency_diff) > 0:

cyclonedx/model/component.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from enum import Enum
2020
from os.path import exists
2121
from typing import Any, Iterable, Optional, Set, Union
22-
from uuid import uuid4
2322

2423
# See https://github.com/package-url/packageurl-python/issues/65
2524
import serializable
@@ -769,7 +768,7 @@ def __init__(self, *, name: str, type: ComponentType = ComponentType.LIBRARY,
769768
if isinstance(bom_ref, BomRef):
770769
self._bom_ref = bom_ref
771770
else:
772-
self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else str(uuid4()))
771+
self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else None)
773772
self.supplier = supplier
774773
self.author = author
775774
self.publisher = publisher
@@ -809,8 +808,6 @@ def __init__(self, *, name: str, type: ComponentType = ComponentType.LIBRARY,
809808
if not licenses:
810809
self.licenses = [LicenseChoice(expression=license_str)] # type: ignore
811810

812-
self.__dependencies: "SortedSet[BomRef]" = SortedSet()
813-
814811
@property
815812
@serializable.xml_attribute()
816813
def type(self) -> ComponentType:

cyclonedx/model/dependency.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,4 @@ class Dependable(ABC):
108108
@property
109109
@abstractmethod
110110
def bom_ref(self) -> BomRef:
111-
pass
111+
...

cyclonedx/model/service.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
# Copyright (c) OWASP Foundation. All Rights Reserved.
1515

1616
from typing import Any, Iterable, Optional, Union
17-
from uuid import uuid4
1817

1918
import serializable
2019
from sortedcontainers import SortedSet
@@ -66,7 +65,7 @@ def __init__(self, *, name: str, bom_ref: Optional[Union[str, BomRef]] = None,
6665
if isinstance(bom_ref, BomRef):
6766
self._bom_ref = bom_ref
6867
else:
69-
self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else str(uuid4()))
68+
self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else None)
7069
self.provider = provider
7170
self.group = group
7271
self.name = name

cyclonedx/model/vulnerability.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from decimal import Decimal
2222
from enum import Enum
2323
from typing import Any, Iterable, Optional, Tuple, Union
24-
from uuid import uuid4
2524

2625
import serializable
2726
from sortedcontainers import SortedSet
@@ -837,7 +836,7 @@ def __init__(self, *, bom_ref: Optional[Union[str, BomRef]] = None, id: Optional
837836
if isinstance(bom_ref, BomRef):
838837
self._bom_ref = bom_ref
839838
else:
840-
self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else str(uuid4()))
839+
self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else None)
841840
self.id = id
842841
self.source = source
843842
self.references = references or [] # type: ignore

cyclonedx/schema/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class OutputFormat(Enum):
3232
def __hash__(self) -> int:
3333
return hash(self.name)
3434

35+
def __eq__(self, other: Any) -> bool:
36+
return self is other
37+
3538

3639
_SV = TypeVar('_SV', bound='SchemaVersion')
3740

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ keywords = [
6161
# ATTENTION: keep `deps.lowest.r` file in sync
6262
python = "^3.8"
6363
packageurl-python = ">= 0.11"
64-
py-serializable = "^0.14.0"
64+
py-serializable = "^0.14.1"
6565
sortedcontainers = "^2.4.0"
6666
license-expression = "^30"
6767
jsonschema = { version = "^4.18", extras=['format'], optional=true }

tests/__init__.py

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,137 @@
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

1818
from os import getenv, path
19+
from os.path import join
20+
from typing import TYPE_CHECKING, Any, Generator, Iterable, List, Optional, TypeVar, Union
21+
from unittest import TestCase
22+
from uuid import UUID
1923

20-
TESTDATA_DIRECTORY = path.join(path.dirname(__file__), '_data')
24+
from sortedcontainers import SortedSet
2125

22-
RECREATE_SNAPSHOTS = bool(getenv('CDX_TEST_RECREATE_SNAPSHOTS'))
26+
from cyclonedx.schema import OutputFormat, SchemaVersion
27+
28+
if TYPE_CHECKING:
29+
from cyclonedx.model.bom import Bom
30+
from cyclonedx.model.dependency import Dependency
31+
32+
_T = TypeVar('_T')
33+
34+
_TESTDATA_DIRECTORY = path.join(path.dirname(__file__), '_data')
35+
36+
SCHEMA_TESTDATA_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'schemaTestData')
37+
OWN_DATA_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'own')
38+
SNAPSHOTS_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'snapshots')
39+
40+
RECREATE_SNAPSHOTS = '1' == getenv('CDX_TEST_RECREATE_SNAPSHOTS')
2341
if RECREATE_SNAPSHOTS:
2442
print('!!! WILL RECREATE ALL SNAPSHOTS !!!')
43+
44+
45+
class SnapshotMixin:
46+
47+
@staticmethod
48+
def getSnapshotFile(snapshot_name: str) -> str:
49+
return join(SNAPSHOTS_DIRECTORY, f'{snapshot_name}.bin')
50+
51+
@classmethod
52+
def writeSnapshot(cls, snapshot_name: str, data: str) -> None:
53+
with open(cls.getSnapshotFile(snapshot_name), 'w') as s:
54+
s.write(data)
55+
56+
@classmethod
57+
def readSnapshot(cls, snapshot_name: str) -> str:
58+
with open(cls.getSnapshotFile(snapshot_name), 'r') as s:
59+
return s.read()
60+
61+
def assertEqualSnapshot(self: Union[TestCase, 'SnapshotMixin'], actual: str, snapshot_name: str) -> None:
62+
if RECREATE_SNAPSHOTS:
63+
self.writeSnapshot(snapshot_name, actual)
64+
_omd = self.maxDiff
65+
_omd = self.maxDiff
66+
self.maxDiff = None
67+
try:
68+
self.assertEqual(actual, self.readSnapshot(snapshot_name))
69+
finally:
70+
self.maxDiff = _omd
71+
72+
73+
class DeepCompareMixin:
74+
def assertDeepEqual(self: Union[TestCase, 'DeepCompareMixin'], first: Any, second: Any,
75+
msg: Optional[str] = None) -> None:
76+
"""costly compare, but very verbose"""
77+
_omd = self.maxDiff
78+
self.maxDiff = None
79+
try:
80+
self.maxDiff = None
81+
dd1 = self.__deepDict(first)
82+
dd2 = self.__deepDict(second)
83+
self.assertDictEqual(dd1, dd2, msg)
84+
finally:
85+
self.maxDiff = _omd
86+
87+
def __deepDict(self, o: Any) -> Any:
88+
if isinstance(o, tuple):
89+
return tuple(self.__deepDict(i) for i in o)
90+
if isinstance(o, list):
91+
return list(self.__deepDict(i) for i in o)
92+
if isinstance(o, dict):
93+
return {k: self.__deepDict(v) for k, v in o.items()}
94+
if isinstance(o, (set, SortedSet)):
95+
# this method returns dict. `dict` is not hashable, so use `tuple` instead.
96+
return tuple(self.__deepDict(i) for i in sorted(o, key=hash)) + ('%conv:%set',)
97+
if hasattr(o, '__dict__'):
98+
d = {a: self.__deepDict(v) for a, v in o.__dict__.items() if '__' not in a}
99+
d['%conv'] = str(type(o))
100+
return d
101+
return o
102+
103+
def assertBomDeepEqual(self: Union[TestCase, 'DeepCompareMixin'], expected: 'Bom', actual: 'Bom',
104+
msg: Optional[str] = None, *,
105+
fuzzy_deps: bool = True) -> None:
106+
# deps might have been upgraded on serialization, so they might differ
107+
edeps = expected.dependencies
108+
adeps = actual.dependencies
109+
if fuzzy_deps:
110+
expected.dependencies = []
111+
actual.dependencies = []
112+
try:
113+
self.assertDeepEqual(expected, actual, msg)
114+
if fuzzy_deps:
115+
self._assertDependenciesFuzzyEqual(edeps, adeps)
116+
finally:
117+
expected.dependencies = edeps
118+
actual.dependencies = adeps
119+
120+
def _assertDependenciesFuzzyEqual(self: TestCase, a: Iterable['Dependency'], b: Iterable['Dependency']) -> None:
121+
delta = set(a) ^ set(b)
122+
for d in delta:
123+
# only actual relevant dependencies shall be taken into account.
124+
self.assertEqual(0, len(d.dependencies), f'unexpected dependencies for {d.ref}')
125+
126+
127+
def reorder(items: List[_T], indexes: List[int]) -> List[_T]:
128+
"""
129+
Return list of items in the order indicated by indexes.
130+
"""
131+
reordered_items = []
132+
for i in range(len(items)):
133+
reordered_items.append(items[indexes[i]])
134+
return reordered_items
135+
136+
137+
def uuid_generator(offset: int = 0, version: int = 4) -> Generator[UUID, None, None]:
138+
v = offset
139+
while True:
140+
v += 1
141+
yield UUID(int=v, version=version)
142+
143+
144+
_SNAME_EXT = {
145+
OutputFormat.JSON: 'json',
146+
OutputFormat.XML: 'xml',
147+
}
148+
149+
150+
def mksname(purpose: Union[Any], sv: SchemaVersion, f: OutputFormat) -> str:
151+
purpose = purpose if isinstance(purpose, str) else purpose.__name__
152+
return f'{purpose}-{sv.to_version()}.{_SNAME_EXT[f]}'

tests/_data/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# encoding: utf-8
2+
3+
# This file is part of CycloneDX Python Lib
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# SPDX-License-Identifier: Apache-2.0
18+
# Copyright (c) OWASP Foundation. All Rights Reserved.

0 commit comments

Comments
 (0)