Skip to content

Commit c120767

Browse files
Allow extra properties & missing properties in testcases (#421)
* Allow extra properties & missing properties in testcases * Add updated Documentation * Delete formating workflow
1 parent 672e629 commit c120767

5 files changed

Lines changed: 208 additions & 97 deletions

File tree

.github/workflows/format.yml

Lines changed: 0 additions & 41 deletions
This file was deleted.

docs/how-to/test_to_doc_links.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,17 @@ TestLink will extract test name, file, line, result and verification lists
2525
(`PartiallyVerifies`, `FullyVerifies`) and create external needs from tests
2626
and `testlink` attributes on requirements that reference the test.
2727

28+
.. hint::
29+
It is possible to have 'additional' properties on tests. They will not show up in the
30+
TestLink but also won't break the parsing process.
31+
32+
2833

2934
Limitations
3035
-----------
3136

3237
- Not compatible with Esbonio/Live_preview.
33-
- Tags and XML must match the expected format exactly for parsing to work.
38+
- To create a valid Testlink Tags and XML must match the expected format.
39+
- Partial properties will lead to no Testlink creation.
40+
If you want a test to be linked, please ensure all requirement properties are provided.
3441
- Tests must be executed by Bazel first so `test.xml` files exist.

src/extensions/score_source_code_linker/testlink.py

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -79,28 +79,28 @@ def DataForTestLink_JSON_Decoder(d: dict[str, Any]) -> DataForTestLink | dict[st
7979
# We will have everything as string here as that mirrors the xml file
8080
@dataclass
8181
class DataOfTestCase:
82-
name: str
83-
file: str
84-
line: str
85-
result: str # passed | falied | skipped | disabled
82+
name: str | None = None
83+
file: str | None = None
84+
line: str | None = None
85+
result: str | None = None # passed | falied | skipped | disabled
8686
# Intentionally not snakecase to make dict parsing simple
87-
TestType: str
88-
DerivationTechnique: str
89-
result_text: str = "" # Can be None on anything but failed
87+
TestType: str | None = None
88+
DerivationTechnique: str | None = None
89+
result_text: str | None = None # Can be None on anything but failed
9090
# Either or HAVE to be filled.
9191
PartiallyVerifies: str | None = None
9292
FullyVerifies: str | None = None
9393

9494
@classmethod
9595
def from_dict(cls, data: dict[str, Any]): # type-ignore
9696
return cls(
97-
name=data["name"],
98-
file=data["file"],
99-
line=data["line"],
100-
result=data["result"],
101-
TestType=data["TestType"],
102-
DerivationTechnique=data["DerivationTechnique"],
103-
result_text=data["result_text"],
97+
name=data.get("name"),
98+
file=data.get("file"),
99+
line=data.get("line"),
100+
result=data.get("result"),
101+
TestType=data.get("TestType"),
102+
DerivationTechnique=data.get("DerivationTechnique"),
103+
result_text=data.get("result_text"),
104104
PartiallyVerifies=data.get("PartiallyVerifies"),
105105
FullyVerifies=data.get("FullyVerifies"),
106106
)
@@ -122,24 +122,63 @@ def __post_init__(self):
122122
# Cleaning text
123123
if self.result_text:
124124
self.result_text = self.clean_text(self.result_text)
125-
# Self assertion to double check some mandatory options
126125
# For now this is disabled
127126

128-
# It's mandatory that the test either partially or fully verifies a requirement
129-
# if self.PartiallyVerifies is None and self.FullyVerifies is None:
130-
# raise ValueError(
131-
# f"TestCase: {self.id} Error. Either 'PartiallyVerifies' or "
132-
# "'FullyVerifies' must be provided."
133-
# )
134127
# Skipped tests should always have a reason associated with them
135128
# if "skipped" in self.result.keys() and not list(self.result.values())[0]:
136129
# raise ValueError(
137130
# f"TestCase: {self.id} Error. Test was skipped without provided "
138131
# "reason, reason is mandatory for skipped tests."
139132
# )
140133

134+
# Self assertion to double check some mandatory options
135+
def check_verifies_fields(self) -> bool:
136+
if self.PartiallyVerifies is None and self.FullyVerifies is None:
137+
# This might be a warning in the future, but for now we want be lenient.
138+
LOGGER.info(
139+
f"TestCase: {self.name} Error. Either 'PartiallyVerifies' or "
140+
"'FullyVerifies' must be provided."
141+
"This test case will be skipped and not linked.",
142+
type="score_source_code_linker",
143+
)
144+
return False
145+
# Either or is filled, this is fine
146+
return True
147+
148+
def is_valid(self) -> bool:
149+
if not self.check_verifies_fields():
150+
return False
151+
152+
# if (
153+
# # Result Text can be None if result is not failed.
154+
# self.name is not None
155+
# and self.file is not None
156+
# and self.line is not None
157+
# and self.result is not None
158+
# and self.TestType is not None
159+
# and self.DerivationTechnique is not None
160+
# ):
161+
fields = [
162+
x
163+
for x in self.__dataclass_fields__
164+
if x not in ["PartiallyVerifies", "FullyVerifies"]
165+
]
166+
for field in fields:
167+
if getattr(self, field) is None:
168+
# This might be a warning in the future, but for now we want be lenient.
169+
LOGGER.info(
170+
f"TestCase: {self.name} has a None value for the field: "
171+
f"{field}. This test case will be skipped and not linked.",
172+
type="score_source_code_linker",
173+
)
174+
return False
175+
# All properties are filled
176+
return True
177+
141178
def get_test_links(self) -> list[DataForTestLink]:
142179
"""Convert TestCaseNeed to list of TestLink objects."""
180+
if not self.is_valid():
181+
return []
143182

144183
def parse_attributes(verify_field: str | None, verify_type: str):
145184
"""Process a verification field and yield TestLink objects."""
@@ -151,11 +190,24 @@ def parse_attributes(verify_field: str | None, verify_type: str):
151190
type="score_source_code_linker",
152191
)
153192

193+
# LSP can not figure out that 'is_valid' up top
194+
# already gurantees non-None values here
195+
# So we assert our worldview here to ensure type safety.
196+
# Any of these being none should NOT happen at this point
197+
198+
assert self.name is not None
199+
assert self.file is not None
200+
assert self.line is not None
201+
assert self.result is not None
202+
assert self.result_text is not None
203+
assert self.TestType is not None
204+
assert self.DerivationTechnique is not None
205+
154206
for need in verify_field.split(","):
155207
yield DataForTestLink(
156-
name=self.name,
157-
file=Path(self.file),
158-
line=int(self.line),
208+
name=self.name, # type-ignore
209+
file=Path(self.file), # type-ignore
210+
line=int(self.line), # type-ignore
159211
need=need.strip(),
160212
verify_type=verify_type,
161213
result=self.result,

src/extensions/score_source_code_linker/tests/test_xml_parser.py

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,21 @@ def _write_test_xml(
6868

6969

7070
@pytest.fixture
71-
def tmp_xml_dirs(tmp_path: Path) -> Callable[..., tuple[Path, Path, Path]]:
72-
def _tmp_xml_dirs(test_folder: str = "bazel-testlogs") -> tuple[Path, Path, Path]:
71+
def tmp_xml_dirs(
72+
tmp_path: Path,
73+
) -> Callable[..., tuple[Path, Path, Path, Path, Path]]:
74+
def _tmp_xml_dirs(
75+
test_folder: str = "bazel-testlogs",
76+
) -> tuple[Path, Path, Path, Path, Path]:
7377
root = tmp_path / test_folder
74-
dir1, dir2 = root / "with_props", root / "no_props"
78+
dir1, dir2, dir3, dir4 = (
79+
root / "with_props",
80+
root / "no_props",
81+
root / "with_extra_props",
82+
root / "missing_props",
83+
)
7584

76-
for d in (dir1, dir2):
85+
for d in (dir1, dir2, dir3, dir4):
7786
d.mkdir(parents=True, exist_ok=True)
7887

7988
# File with properties
@@ -95,7 +104,42 @@ def _tmp_xml_dirs(test_folder: str = "bazel-testlogs") -> tuple[Path, Path, Path
95104
# File without properties
96105
_write_test_xml(dir2 / "test.xml", name="tc_no_props", file="path2", line=20)
97106

98-
return root, dir1, dir2
107+
# File with some properties that we don't care about
108+
_write_test_xml(
109+
dir3 / "test.xml",
110+
name="tc_with_extra_props",
111+
result="failed",
112+
file="path1",
113+
line=10,
114+
props={
115+
# Properties we do not parse should not throw an error
116+
"PartiallyVerifies": "REQ1",
117+
"FullyVerifies": "",
118+
"TestType": "type",
119+
"DerivationTechnique": "tech",
120+
"Description": "desc",
121+
"ASIL": "B",
122+
"important": "yes",
123+
},
124+
)
125+
126+
# File with some properties missing
127+
_write_test_xml(
128+
dir4 / "test.xml",
129+
name="tc_with_missing_props",
130+
result="failed",
131+
file="path1",
132+
line=10,
133+
props={
134+
# derivation_technique and test_type are missing
135+
# This should not make a 'valid' testlink
136+
"PartiallyVerifies": "REQ1",
137+
"FullyVerifies": "",
138+
"Description": "desc",
139+
},
140+
)
141+
142+
return root, dir1, dir2, dir3, dir4
99143

100144
return _tmp_xml_dirs
101145

@@ -105,48 +149,62 @@ def _tmp_xml_dirs(test_folder: str = "bazel-testlogs") -> tuple[Path, Path, Path
105149
test_type="requirements-based",
106150
derivation_technique="requirements-analysis",
107151
)
108-
def test_find_xml_files(tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]]):
152+
def test_find_xml_files(
153+
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path, Path, Path]],
154+
):
109155
"""Ensure xml files are found as expected if bazel-testlogs is used"""
110156
root: Path
111157
dir1: Path
112158
dir2: Path
113-
root, dir1, dir2 = tmp_xml_dirs()
159+
root, dir1, dir2, dir3, dir4 = tmp_xml_dirs()
114160
found = xml_parser.find_xml_files(root)
115-
expected: set[Path] = {dir1 / "test.xml", dir2 / "test.xml"}
161+
expected: set[Path] = {
162+
dir1 / "test.xml",
163+
dir2 / "test.xml",
164+
dir3 / "test.xml",
165+
dir4 / "test.xml",
166+
}
116167
assert set(found) == expected
117168

118169

119-
def test_find_xml_folder(tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]]):
170+
def test_find_xml_folder(
171+
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path, Path, Path]],
172+
):
120173
"""Ensure xml files are found as expected if bazel-testlogs is used"""
121174
root: Path
122-
root, _, _ = tmp_xml_dirs()
175+
root, _, _, _, _ = tmp_xml_dirs()
123176
found = xml_parser.find_test_folder(base_path=root.parent)
124177
assert found is not None
125178
assert found == root
126179

127180

128181
def test_find_xml_folder_test_reports(
129-
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]],
182+
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path, Path, Path]],
130183
):
131184
# root is the 'tests-report' folder inside tmp_path
132-
root, _, _ = tmp_xml_dirs(test_folder="tests-report")
185+
root, _, _, _, _ = tmp_xml_dirs(test_folder="tests-report")
133186
# We pass the PARENT of 'tests-report' as the workspace root
134187
found = xml_parser.find_test_folder(base_path=root.parent)
135188
assert found is not None
136189
assert found == root
137190

138191

139192
def test_find_xml_files_test_reports(
140-
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]],
193+
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path, Path, Path]],
141194
):
142195
"""Ensure xml files are found as expected if tests-report is used"""
143196
root: Path
144197
dir1: Path
145198
dir2: Path
146-
root, dir1, dir2 = tmp_xml_dirs(test_folder="tests-report")
199+
root, dir1, dir2, dir3, dir4 = tmp_xml_dirs(test_folder="tests-report")
147200
found = xml_parser.find_xml_files(dir=root)
148201
assert found is not None
149-
expected: set[Path] = {root / dir1 / "test.xml", root / dir2 / "test.xml"}
202+
expected: set[Path] = {
203+
root / dir1 / "test.xml",
204+
root / dir2 / "test.xml",
205+
root / dir3 / "test.xml",
206+
root / dir4 / "test.xml",
207+
}
150208
assert set(found) == expected
151209

152210

@@ -204,23 +262,42 @@ def test_parse_properties():
204262
test_type="requirements-based",
205263
derivation_technique="requirements-analysis",
206264
)
207-
def test_read_test_xml_file(tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]]):
265+
def test_read_test_xml_file(
266+
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path, Path, Path]],
267+
):
208268
"""Ensure a whole pre-defined xml file is parsed correctly"""
209269
_: Path
210270
dir1: Path
211271
dir2: Path
212-
_, dir1, dir2 = tmp_xml_dirs()
213-
214-
needs1, no_props1 = xml_parser.read_test_xml_file(dir1 / "test.xml")
272+
_, dir1, dir2, dir3, dir4 = tmp_xml_dirs()
273+
needs1, no_props1, missing_props1 = xml_parser.read_test_xml_file(dir1 / "test.xml")
274+
# Should parse the properties and create a 'valid' testlink
215275
assert isinstance(needs1, list) and len(needs1) == 1
216276
tcneed = needs1[0]
217277
assert isinstance(tcneed, DataOfTestCase)
218278
assert tcneed.result == "failed"
219279
assert no_props1 == []
280+
assert missing_props1 == []
220281

221-
needs2, no_props2 = xml_parser.read_test_xml_file(dir2 / "test.xml")
282+
# No properties at all => Should not be a 'valid' testlink
283+
needs2, no_props2, missing_props2 = xml_parser.read_test_xml_file(dir2 / "test.xml")
222284
assert needs2 == []
223285
assert no_props2 == ["tc_no_props"]
286+
assert missing_props2 == []
287+
288+
# Extra Properties => Should not cause an error
289+
needs3, no_props3, missing_props3 = xml_parser.read_test_xml_file(dir3 / "test.xml")
290+
assert isinstance(needs1, list) and len(needs1) == 1
291+
tcneed3 = needs3[0]
292+
assert isinstance(tcneed3, DataOfTestCase)
293+
assert no_props3 == []
294+
assert missing_props3 == []
295+
296+
# Missing some properties => Should not be a 'valid' testlink
297+
needs4, no_props4, missing_props4 = xml_parser.read_test_xml_file(dir4 / "test.xml")
298+
assert needs4 == []
299+
assert no_props4 == []
300+
assert missing_props4 == ["tc_with_missing_props"]
224301

225302

226303
@add_test_properties(

0 commit comments

Comments
 (0)