Skip to content

Commit 89f556e

Browse files
authored
Refactor io.py to simplify unit tests (#335)
- Split function into smaller functions - Add unit test - Add type hints
1 parent 1083b4e commit 89f556e

3 files changed

Lines changed: 135 additions & 11 deletions

File tree

lobster/io.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
22
#
33
# LOBSTER - Lightweight Open BMW Software Traceability Evidence Report
4-
# Copyright (C) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
4+
# Copyright (C) 2023, 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
55
#
66
# This program is free software: you can redistribute it and/or modify
77
# it under the terms of the GNU Affero General Public License as
@@ -21,6 +21,7 @@
2121
import io
2222
from collections.abc import Iterable
2323
import json
24+
from typing import Dict, Sequence, Union
2425

2526
from lobster.errors import Message_Handler
2627
from lobster.location import File_Reference
@@ -52,7 +53,13 @@ def lobster_write(fd, kind, generator, items):
5253
fd.write("\n")
5354

5455

55-
def lobster_read(mh, filename, level, items, source_info=None):
56+
def lobster_read(
57+
mh,
58+
filename,
59+
level,
60+
items: Dict[str, Union[Activity, Implementation, Requirement]],
61+
source_info=None,
62+
):
5663
assert isinstance(mh, Message_Handler)
5764
assert isinstance(filename, str)
5865
assert isinstance(level, str)
@@ -132,11 +139,24 @@ def lobster_read(mh, filename, level, items, source_info=None):
132139
else:
133140
items[item.tag.key()] = item
134141

142+
signal_duplicate_items(mh, items, duplicate_items)
143+
144+
145+
def signal_duplicate_items(
146+
mh: Message_Handler,
147+
items,
148+
duplicate_items: Sequence[Union[Activity, Implementation, Requirement]],
149+
):
150+
"""
151+
Report errors for duplicate items to the message handler.
152+
If there are any duplicate items, the last one is considered fatal.
153+
"""
135154
if duplicate_items:
136155
for counter, item in enumerate(duplicate_items, start=1):
137156
mh.error(
138-
item.location,
139-
f"duplicate definition of {item.tag.key()}, "
140-
f"previously defined at {items[item.tag.key()].location.to_string()}",
157+
location=item.location,
158+
message=f"duplicate definition of {item.tag.key()}, "
159+
f"previously defined at "
160+
f"{items[item.tag.key()].location.to_string()}",
141161
fatal=(counter == len(duplicate_items)),
142162
)

lobster/items.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@
2020
from enum import Enum, auto
2121
from abc import ABCMeta
2222
from hashlib import sha1
23+
from typing import Optional
2324

2425
from lobster.location import Location
2526

2627

2728
class Tracing_Tag:
28-
def __init__(self, namespace, tag, version=None):
29+
def __init__(
30+
self,
31+
namespace: str,
32+
tag: str,
33+
version: Optional[str] = None,
34+
):
2935
assert isinstance(namespace, str) and " " not in namespace
3036
assert isinstance(tag, str)
3137
assert version is None or isinstance(version, (str, int))
@@ -84,7 +90,7 @@ class Tracing_Status(Enum):
8490

8591

8692
class Item(metaclass=ABCMeta):
87-
def __init__(self, tag, location):
93+
def __init__(self, tag: Tracing_Tag, location: Location):
8894
assert isinstance(tag, Tracing_Tag)
8995
assert isinstance(location, Location)
9096

@@ -220,8 +226,16 @@ def to_json(self):
220226

221227

222228
class Requirement(Item):
223-
def __init__(self, tag, location, framework, kind, name,
224-
text=None, status=None):
229+
def __init__(
230+
self,
231+
tag: Tracing_Tag,
232+
location: Location,
233+
framework: str,
234+
kind: str,
235+
name: str,
236+
text: Optional[str] = None,
237+
status: Optional[str] = None,
238+
):
225239
super().__init__(tag, location)
226240
assert isinstance(framework, str)
227241
assert isinstance(kind, str)
@@ -270,7 +284,14 @@ def from_json(cls, level, data, schema_version):
270284

271285

272286
class Implementation(Item):
273-
def __init__(self, tag, location, language, kind, name):
287+
def __init__(
288+
self,
289+
tag: Tracing_Tag,
290+
location: Location,
291+
language: str,
292+
kind: str,
293+
name: str,
294+
):
274295
super().__init__(tag, location)
275296
assert isinstance(language, str)
276297
assert isinstance(kind, str)
@@ -303,7 +324,14 @@ def from_json(cls, level, data, schema_version):
303324

304325

305326
class Activity(Item):
306-
def __init__(self, tag, location, framework, kind, status=None):
327+
def __init__(
328+
self,
329+
tag: Tracing_Tag,
330+
location: Location,
331+
framework: str,
332+
kind: str,
333+
status: Optional[str] = None,
334+
):
307335
super().__init__(tag, location)
308336
assert isinstance(framework, str)
309337
assert isinstance(kind, str)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from itertools import islice
2+
from typing import List
3+
from unittest import TestCase
4+
from unittest.mock import Mock
5+
from lobster.errors import Message_Handler
6+
from lobster.items import Tracing_Tag, Requirement
7+
from lobster.io import signal_duplicate_items
8+
from lobster.location import File_Reference
9+
10+
class SignalDuplicateItemsTest(TestCase):
11+
def setUp(self) -> None:
12+
self._mh = Mock(spec=Message_Handler)
13+
self._items = {}
14+
for pizza in ["Capricciosa", "Margherita", "Quattro Stagioni", "Pepperoni"]:
15+
req = Requirement(
16+
Tracing_Tag("Pizza", pizza, "tomatoe"),
17+
File_Reference("filename"),
18+
"woodstove",
19+
"delicious",
20+
"name",
21+
)
22+
self._items[req.tag.key()] = req
23+
24+
def _create_duplicates(self, source: List[Requirement], count) -> List[Requirement]:
25+
"""
26+
Create a list of duplicate items based on the existing items.
27+
They use the same tracing tag, but a different location.
28+
The number of duplicates is determined by the count parameter.
29+
"""
30+
if len(source) < count:
31+
self.fail("Count exceeds the number of available items.")
32+
33+
duplicates = []
34+
for item in source[:count]:
35+
duplicate = Requirement(
36+
Tracing_Tag(item.tag.namespace, item.tag.tag, item.tag.version),
37+
File_Reference(f"{item.location.filename}-duplicated"),
38+
item.framework,
39+
item.kind,
40+
item.name,
41+
)
42+
duplicates.append(duplicate)
43+
return duplicates
44+
45+
def test_no_duplicates(self):
46+
signal_duplicate_items(self._mh, self._items, [])
47+
self.assertEqual(self._mh.error.call_count, 0)
48+
49+
def test_few_duplicates(self):
50+
num_duplicates = 2
51+
duplicate_items = self._create_duplicates(
52+
list(self._items.values()),
53+
num_duplicates,
54+
)
55+
56+
signal_duplicate_items(self._mh, self._items, duplicate_items)
57+
58+
self.assertEqual(self._mh.error.call_count, num_duplicates)
59+
60+
for i, call in enumerate(self._mh.error.call_args_list[:-1]):
61+
self.assertFalse(
62+
call.kwargs.get("fatal", False),
63+
f"Call #{i + 1} did not use fatal=False",
64+
)
65+
duplicated_key = duplicate_items[i].tag.key()
66+
self.assertEqual(
67+
call.kwargs.get("message"),
68+
f"duplicate definition of {duplicated_key}, "
69+
f"previously defined at {self._items[duplicated_key].location.to_string()}",
70+
)
71+
self.assertIs(duplicate_items[i].location, call.kwargs.get("location"))
72+
73+
self.assertTrue(
74+
self._mh.error.call_args_list[-1].kwargs.get("fatal"),
75+
"Last call did not use fatal=True",
76+
)

0 commit comments

Comments
 (0)