Coverage for lobster/tools/cpptest/cpptest.py: 86%
141 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-06 09:51 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-06 09:51 +0000
1#!/usr/bin/env python3
2#
3# lobster_cpptest - Extract C++ tracing tags from comments in tests for LOBSTER
4# Copyright (C) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14# Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public
17# License along with this program. If not, see
18# <https://www.gnu.org/licenses/>.
20from argparse import Namespace
21import os.path
22from copy import copy
23from enum import Enum
24import yaml
25from lobster.exceptions import LOBSTER_Exception
26from lobster.items import Tracing_Tag, Activity
27from lobster.location import File_Reference
28from lobster.io import lobster_write
29from lobster.file_tag_generator import FileTagGenerator
30from lobster.tools.cpptest.parser.constants import Constants
31from lobster.tools.cpptest.parser.requirements_parser import \
32 ParserForRequirements
33from lobster.meta_data_tool_base import MetaDataToolBase
35OUTPUT = "output"
36CODEBEAMER_URL = "codebeamer_url"
37MARKERS = "markers"
38KIND = "kind"
40NAMESPACE_CPP = "cpp"
41FRAMEWORK_CPP_TEST = "cpptest"
42KIND_FUNCTION = "Function"
43CB_PREFIX = "CB-#"
44MISSING = "Missing"
45ORPHAN_TESTS = "OrphanTests"
48class RequirementTypes(Enum):
49 REQS = '@requirement'
50 REQ_BY = '@requiredby'
51 DEFECT = '@defect'
54SUPPORTED_REQUIREMENTS = [
55 RequirementTypes.REQS.value,
56 RequirementTypes.REQ_BY.value,
57 RequirementTypes.DEFECT.value
58]
60map_test_type_to_key_name = {
61 RequirementTypes.REQS.value: 'requirements',
62 RequirementTypes.REQ_BY.value: 'required_by',
63 RequirementTypes.DEFECT.value: 'defect_tracking_ids',
64}
67def parse_config_file(file_name: str) -> dict:
68 """
69 Parse the configuration dictionary from the given YAML config file.
71 The configuration dictionary for cpptest must contain the `output` and
72 `codebeamer_url` keys.
73 Each output configuration dictionary contains a file name as a key and
74 a value dictionary containing the keys `markers` and `kind`.
75 The supported values for the `markers` list are specified in
76 SUPPORTED_REQUIREMENTS.
78 Parameters
79 ----------
80 file_name : str
81 The file name of the cpptest YAML config file.
83 Returns
84 -------
85 dict
86 The dictionary containing the configuration for cpptest.
88 Raises
89 ------
90 Exception
91 If the config dictionary does not contain the required keys
92 or is improperly formatted.
93 """
94 if not os.path.isfile(file_name): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 raise ValueError(f'{file_name} is not an existing file!')
97 with open(file_name, "r", encoding='utf-8') as file:
98 try:
99 config_dict = yaml.safe_load(file)
100 except yaml.scanner.ScannerError as ex:
101 raise LOBSTER_Exception(message="Invalid config file") from ex
103 if (not config_dict or OUTPUT not in config_dict or 103 ↛ 105line 103 didn't jump to line 105 because the condition on line 103 was never true
104 CODEBEAMER_URL not in config_dict):
105 raise ValueError(f'Please follow the right config file structure! '
106 f'Missing attribute "{OUTPUT}" and '
107 f'"{CODEBEAMER_URL}"')
109 output_config_dict = config_dict.get(OUTPUT)
111 supported_markers = ', '.join(SUPPORTED_REQUIREMENTS)
112 for output_file, output_file_config_dict in output_config_dict.items():
113 if MARKERS not in output_file_config_dict: 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true
114 raise ValueError(f'Please follow the right config file structure! '
115 f'Missing attribute "{MARKERS}" for output file '
116 f'"{output_file}"')
117 if KIND not in output_file_config_dict: 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true
118 raise ValueError(f'Please follow the right config file structure! '
119 f'Missing attribute "{KIND}" for output file '
120 f'"{output_file}"')
122 for output_file_marker in output_file_config_dict.get(MARKERS, []):
123 if output_file_marker not in SUPPORTED_REQUIREMENTS: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 raise ValueError(f'"{output_file_marker}" is not a supported '
125 f'"{MARKERS}" value '
126 f'for output file "{output_file}". '
127 f'Supported values are: '
128 f'"{supported_markers}"')
130 return config_dict
133def get_test_file_list(file_dir_list: list, extension_list: list) -> list:
134 """
135 Gets the list of test files.
137 Given file names are added to the test file list without
138 validating against the extension list.
139 From given directory names only file names will be added
140 to the test file list if their extension matches against
141 the extension list.
143 Parameters
144 ----------
145 file_dir_list : list
146 A list containing file names and/or directory names
147 to parse for file names.
148 extension_list : list
149 The list of file name extensions.
151 Returns
152 -------
153 list
154 The list of test files
156 Raises
157 ------
158 Exception
159 If the config dict does not contain the required keys
160 or contains not supported values.
161 """
162 test_file_list = []
164 for file_dir_entry in file_dir_list:
165 if os.path.isfile(file_dir_entry):
166 test_file_list.append(file_dir_entry)
167 elif os.path.isdir(file_dir_entry):
168 for path, _, files in os.walk(file_dir_entry):
169 for filename in files:
170 _, ext = os.path.splitext(filename)
171 if ext in extension_list:
172 test_file_list.append(os.path.join(path, filename))
173 else:
174 raise ValueError(f'"{file_dir_entry}" is not a file or directory.')
176 if len(test_file_list) == 0:
177 raise ValueError(f'"{file_dir_list}" does not contain any test file.')
179 return test_file_list
182def collect_test_cases_from_test_files(test_file_list: list,
183 codebeamer_url: str) -> list:
184 """
185 Collects the list of test cases from the given test files.
187 Parameters
188 ----------
189 test_file_list : list
190 The list of test files.
191 codebeamer_url: str
193 Returns
194 -------
195 list
196 The list of test cases.
197 """
198 parser = ParserForRequirements()
199 test_case_list = parser.collect_test_cases_for_test_files(
200 test_files=test_file_list,
201 codebeamer_url = codebeamer_url
202 )
203 return test_case_list
206def create_lobster_items_output_dict_from_test_cases(
207 test_case_list: list,
208 config_dict: dict) -> dict:
209 """
210 Creates the lobster items dictionary for the given test cases grouped by
211 configured output.
213 Parameters
214 ----------
215 test_case_list : list
216 The list of test cases.
217 config_dict : dict
218 The configuration dictionary.
220 Returns
221 -------
222 dict
223 The lobster items dictionary for the given test cases
224 grouped by configured output.
225 """
226 lobster_items_output_dict = {ORPHAN_TESTS: {}}
228 output_config: dict = config_dict.get(OUTPUT)
229 marker_output_config_dict = {}
230 for output_file_name, output_config_dict in output_config.items():
231 lobster_items_output_dict[output_file_name] = {}
232 marker_list = output_config_dict.get(MARKERS)
233 if isinstance(marker_list, list) and len(marker_list) >= 1: 233 ↛ 230line 233 didn't jump to line 230 because the condition on line 233 was always true
234 marker_output_config_dict[output_file_name] = output_config_dict
236 file_tag_generator = FileTagGenerator()
237 for test_case in test_case_list:
238 function_name: str = test_case.test_name
239 file_name = os.path.abspath(test_case.file_name)
240 line_nr = int(test_case.docu_start_line)
241 function_uid = f"{file_tag_generator.get_tag(file_name)}" \
242 f":{function_name}:{line_nr}"
243 tag = Tracing_Tag(NAMESPACE_CPP, function_uid)
244 loc = File_Reference(file_name, line_nr)
245 key = tag.key()
247 activity = \
248 Activity(
249 tag=tag,
250 location=loc,
251 framework=FRAMEWORK_CPP_TEST,
252 kind=KIND_FUNCTION
253 )
255 contains_no_tracing_target = True
256 for output_file_name, output_config_dict in (
257 marker_output_config_dict.items()):
258 tracing_target_list = []
259 tracing_target_kind = output_config_dict.get(KIND)
260 for marker in output_config_dict.get(MARKERS):
261 for test_case_marker_value in getattr(
262 test_case,
263 map_test_type_to_key_name.get(marker)
264 ):
265 if MISSING not in test_case_marker_value: 265 ↛ 261line 265 didn't jump to line 261 because the condition on line 265 was always true
266 test_case_marker_value = (
267 test_case_marker_value.replace(CB_PREFIX, ""))
268 tracing_target = Tracing_Tag(
269 tracing_target_kind,
270 test_case_marker_value
271 )
272 tracing_target_list.append(tracing_target)
274 if len(tracing_target_list) >= 1:
275 contains_no_tracing_target = False
276 lobster_item = copy(activity)
277 for tracing_target in tracing_target_list:
278 lobster_item.add_tracing_target(tracing_target)
280 lobster_items_output_dict.get(output_file_name)[key] = (
281 lobster_item)
283 if contains_no_tracing_target:
284 lobster_items_output_dict.get(ORPHAN_TESTS)[key] = (
285 activity)
287 return lobster_items_output_dict
290def write_lobster_items_output_dict(lobster_items_output_dict: dict):
291 """
292 Write the lobster items to the output.
293 If the output file name is empty everything is written to stdout.
295 Parameters
296 ----------
297 lobster_items_output_dict : dict
298 The lobster items dictionary grouped by output.
299 """
300 lobster_generator = Constants.LOBSTER_GENERATOR
301 orphan_test_items = lobster_items_output_dict.get(ORPHAN_TESTS, {})
302 for output_file_name, lobster_items in lobster_items_output_dict.items():
303 if output_file_name == ORPHAN_TESTS:
304 continue
306 lobster_items_dict: dict = copy(lobster_items)
307 lobster_items_dict.update(orphan_test_items)
308 item_count = len(lobster_items_dict)
310 if output_file_name: 310 ↛ 302line 310 didn't jump to line 302 because the condition on line 310 was always true
311 with open(output_file_name, "w", encoding="UTF-8") as output_file:
312 lobster_write(
313 output_file,
314 Activity,
315 lobster_generator,
316 lobster_items_dict.values()
317 )
318 print(f'Written {item_count} lobster items to '
319 f'"{output_file_name}".')
322def lobster_cpptest(file_dir_list: list, config_dict: dict):
323 """
324 The main function to parse requirements from comments
325 for the given list of files and/or directories and write the
326 created lobster dictionary to the configured outputs.
328 Parameters
329 ----------
330 file_dir_list : list
331 The list of files and/or directories to be parsed
332 config_dict : dict
333 The configuration dictionary
334 """
335 test_file_list = \
336 get_test_file_list(
337 file_dir_list=file_dir_list,
338 extension_list=[".cpp", ".cc", ".c", ".h"]
339 )
341 test_case_list = \
342 collect_test_cases_from_test_files(
343 test_file_list=test_file_list,
344 codebeamer_url=config_dict.get(CODEBEAMER_URL, '')
345 )
347 lobster_items_output_dict: dict = \
348 create_lobster_items_output_dict_from_test_cases(
349 test_case_list=test_case_list,
350 config_dict=config_dict
351 )
353 write_lobster_items_output_dict(
354 lobster_items_output_dict=lobster_items_output_dict
355 )
358class CppTestTool(MetaDataToolBase):
359 def __init__(self):
360 super().__init__(
361 name="cpptest",
362 description="Extract C++ tracing tags from comments in tests for LOBSTER",
363 official=True,
364 )
365 self._argument_parser.add_argument(
366 "--config",
367 help=("Path to YAML file with arguments, "
368 "by default (cpptest-config.yaml)"),
369 default="cpptest-config.yaml",
370 )
372 def _run_impl(self, options: Namespace) -> int:
373 options = self._argument_parser.parse_args()
375 try:
376 config_dict = parse_config_file(options.config)
378 options.files = config_dict.get("files", ["."])
379 config_dict.pop("files", None)
381 lobster_cpptest(
382 file_dir_list=options.files,
383 config_dict=config_dict
384 )
386 except ValueError as exception:
387 self._argument_parser.error(str(exception))
389 return 0
392def main() -> int:
393 return CppTestTool().run()