Coverage for lobster/tools/cpptest/cpptest.py: 91%
142 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 14:55 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 14:55 +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/>.
20import sys
21import argparse
22import os.path
23from copy import copy
24from enum import Enum
25import yaml
26from lobster.exceptions import LOBSTER_Exception
27from lobster.items import Tracing_Tag, Activity
28from lobster.location import File_Reference
29from lobster.io import lobster_write
30from lobster.file_tag_generator import FileTagGenerator
31from lobster.tools.cpptest.parser.constants import Constants
32from lobster.tools.cpptest.parser.requirements_parser import \
33 ParserForRequirements
34from lobster.version import get_version
36OUTPUT = "output"
37CODEBEAMER_URL = "codebeamer_url"
38MARKERS = "markers"
39KIND = "kind"
41NAMESPACE_CPP = "cpp"
42FRAMEWORK_CPP_TEST = "cpptest"
43KIND_FUNCTION = "Function"
44CB_PREFIX = "CB-#"
45MISSING = "Missing"
46ORPHAN_TESTS = "OrphanTests"
49class RequirementTypes(Enum):
50 REQS = '@requirement'
51 REQ_BY = '@requiredby'
52 DEFECT = '@defect'
55SUPPORTED_REQUIREMENTS = [
56 RequirementTypes.REQS.value,
57 RequirementTypes.REQ_BY.value,
58 RequirementTypes.DEFECT.value
59]
61map_test_type_to_key_name = {
62 RequirementTypes.REQS.value: 'requirements',
63 RequirementTypes.REQ_BY.value: 'required_by',
64 RequirementTypes.DEFECT.value: 'defect_tracking_ids',
65}
68def parse_config_file(file_name: str) -> dict:
69 """
70 Parse the configuration dictionary from the given YAML config file.
72 The configuration dictionary for cpptest must contain the `output` and
73 `codebeamer_url` keys.
74 Each output configuration dictionary contains a file name as a key and
75 a value dictionary containing the keys `markers` and `kind`.
76 The supported values for the `markers` list are specified in
77 SUPPORTED_REQUIREMENTS.
79 Parameters
80 ----------
81 file_name : str
82 The file name of the cpptest YAML config file.
84 Returns
85 -------
86 dict
87 The dictionary containing the configuration for cpptest.
89 Raises
90 ------
91 Exception
92 If the config dictionary does not contain the required keys
93 or is improperly formatted.
94 """
95 if not os.path.isfile(file_name): 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 raise ValueError(f'{file_name} is not an existing file!')
98 with open(file_name, "r", encoding='utf-8') as file:
99 try:
100 config_dict = yaml.safe_load(file)
101 except yaml.scanner.ScannerError as ex:
102 raise LOBSTER_Exception(message="Invalid config file") from ex
104 if (not config_dict or OUTPUT not in config_dict or 104 ↛ 106line 104 didn't jump to line 106 because the condition on line 104 was never true
105 CODEBEAMER_URL not in config_dict):
106 raise ValueError(f'Please follow the right config file structure! '
107 f'Missing attribute "{OUTPUT}" and '
108 f'"{CODEBEAMER_URL}"')
110 output_config_dict = config_dict.get(OUTPUT)
112 supported_markers = ', '.join(SUPPORTED_REQUIREMENTS)
113 for output_file, output_file_config_dict in output_config_dict.items():
114 if MARKERS not in output_file_config_dict: 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 raise ValueError(f'Please follow the right config file structure! '
116 f'Missing attribute "{MARKERS}" for output file '
117 f'"{output_file}"')
118 if KIND not in output_file_config_dict: 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true
119 raise ValueError(f'Please follow the right config file structure! '
120 f'Missing attribute "{KIND}" for output file '
121 f'"{output_file}"')
123 for output_file_marker in output_file_config_dict.get(MARKERS, []):
124 if output_file_marker not in SUPPORTED_REQUIREMENTS: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 raise ValueError(f'"{output_file_marker}" is not a supported '
126 f'"{MARKERS}" value '
127 f'for output file "{output_file}". '
128 f'Supported values are: '
129 f'"{supported_markers}"')
131 return config_dict
134def get_test_file_list(file_dir_list: list, extension_list: list) -> list:
135 """
136 Gets the list of test files.
138 Given file names are added to the test file list without
139 validating against the extension list.
140 From given directory names only file names will be added
141 to the test file list if their extension matches against
142 the extension list.
144 Parameters
145 ----------
146 file_dir_list : list
147 A list containing file names and/or directory names
148 to parse for file names.
149 extension_list : list
150 The list of file name extensions.
152 Returns
153 -------
154 list
155 The list of test files
157 Raises
158 ------
159 Exception
160 If the config dict does not contain the required keys
161 or contains not supported values.
162 """
163 test_file_list = []
165 for file_dir_entry in file_dir_list:
166 if os.path.isfile(file_dir_entry):
167 test_file_list.append(file_dir_entry)
168 elif os.path.isdir(file_dir_entry):
169 for path, _, files in os.walk(file_dir_entry):
170 for filename in files:
171 _, ext = os.path.splitext(filename)
172 if ext in extension_list:
173 test_file_list.append(os.path.join(path, filename))
174 else:
175 raise ValueError(f'"{file_dir_entry}" is not a file or directory.')
177 if len(test_file_list) == 0:
178 raise ValueError(f'"{file_dir_list}" does not contain any test file.')
180 return test_file_list
183def collect_test_cases_from_test_files(test_file_list: list,
184 codebeamer_url: str) -> list:
185 """
186 Collects the list of test cases from the given test files.
188 Parameters
189 ----------
190 test_file_list : list
191 The list of test files.
192 codebeamer_url: str
194 Returns
195 -------
196 list
197 The list of test cases.
198 """
199 parser = ParserForRequirements()
200 test_case_list = parser.collect_test_cases_for_test_files(
201 test_files=test_file_list,
202 codebeamer_url = codebeamer_url
203 )
204 return test_case_list
207def create_lobster_items_output_dict_from_test_cases(
208 test_case_list: list,
209 config_dict: dict) -> dict:
210 """
211 Creates the lobster items dictionary for the given test cases grouped by
212 configured output.
214 Parameters
215 ----------
216 test_case_list : list
217 The list of test cases.
218 config_dict : dict
219 The configuration dictionary.
221 Returns
222 -------
223 dict
224 The lobster items dictionary for the given test cases
225 grouped by configured output.
226 """
227 lobster_items_output_dict = {ORPHAN_TESTS: {}}
229 output_config: dict = config_dict.get(OUTPUT)
230 marker_output_config_dict = {}
231 for output_file_name, output_config_dict in output_config.items():
232 lobster_items_output_dict[output_file_name] = {}
233 marker_list = output_config_dict.get(MARKERS)
234 if isinstance(marker_list, list) and len(marker_list) >= 1:
235 marker_output_config_dict[output_file_name] = output_config_dict
237 file_tag_generator = FileTagGenerator()
238 for test_case in test_case_list:
239 function_name: str = test_case.test_name
240 file_name = os.path.abspath(test_case.file_name)
241 line_nr = int(test_case.docu_start_line)
242 function_uid = "%s:%s:%u" % (file_tag_generator.get_tag(file_name),
243 function_name,
244 line_nr)
245 tag = Tracing_Tag(NAMESPACE_CPP, function_uid)
246 loc = File_Reference(file_name, line_nr)
247 key = tag.key()
249 activity = \
250 Activity(
251 tag=tag,
252 location=loc,
253 framework=FRAMEWORK_CPP_TEST,
254 kind=KIND_FUNCTION
255 )
257 contains_no_tracing_target = True
258 for output_file_name, output_config_dict in (
259 marker_output_config_dict.items()):
260 tracing_target_list = []
261 tracing_target_kind = output_config_dict.get(KIND)
262 for marker in output_config_dict.get(MARKERS):
263 if marker not in map_test_type_to_key_name: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true
264 continue
266 for test_case_marker_value in getattr(
267 test_case,
268 map_test_type_to_key_name.get(marker)
269 ):
270 if MISSING not in test_case_marker_value: 270 ↛ 266line 270 didn't jump to line 266 because the condition on line 270 was always true
271 test_case_marker_value = (
272 test_case_marker_value.replace(CB_PREFIX, ""))
273 tracing_target = Tracing_Tag(
274 tracing_target_kind,
275 test_case_marker_value
276 )
277 tracing_target_list.append(tracing_target)
279 if len(tracing_target_list) >= 1:
280 contains_no_tracing_target = False
281 lobster_item = copy(activity)
282 for tracing_target in tracing_target_list:
283 lobster_item.add_tracing_target(tracing_target)
285 lobster_items_output_dict.get(output_file_name)[key] = (
286 lobster_item)
288 if contains_no_tracing_target:
289 lobster_items_output_dict.get(ORPHAN_TESTS)[key] = (
290 activity)
292 return lobster_items_output_dict
295def write_lobster_items_output_dict(lobster_items_output_dict: dict):
296 """
297 Write the lobster items to the output.
298 If the output file name is empty everything is written to stdout.
300 Parameters
301 ----------
302 lobster_items_output_dict : dict
303 The lobster items dictionary grouped by output.
304 """
305 lobster_generator = Constants.LOBSTER_GENERATOR
306 orphan_test_items = lobster_items_output_dict.get(ORPHAN_TESTS, {})
307 for output_file_name, lobster_items in lobster_items_output_dict.items():
308 if output_file_name == ORPHAN_TESTS:
309 continue
311 lobster_items_dict: dict = copy(lobster_items)
312 lobster_items_dict.update(orphan_test_items)
313 item_count = len(lobster_items_dict)
315 if output_file_name: 315 ↛ 327line 315 didn't jump to line 327 because the condition on line 315 was always true
316 with open(output_file_name, "w", encoding="UTF-8") as output_file:
317 lobster_write(
318 output_file,
319 Activity,
320 lobster_generator,
321 lobster_items_dict.values()
322 )
323 print(f'Written {item_count} lobster items to '
324 f'"{output_file_name}".')
326 else:
327 lobster_write(
328 sys.stdout,
329 Activity,
330 lobster_generator,
331 lobster_items_dict.values()
332 )
333 print(f'Written {item_count} lobster items to stdout.')
336def lobster_cpptest(file_dir_list: list, config_dict: dict):
337 """
338 The main function to parse requirements from comments
339 for the given list of files and/or directories and write the
340 created lobster dictionary to the configured outputs.
342 Parameters
343 ----------
344 file_dir_list : list
345 The list of files and/or directories to be parsed
346 config_dict : dict
347 The configuration dictionary
348 """
349 test_file_list = \
350 get_test_file_list(
351 file_dir_list=file_dir_list,
352 extension_list=[".cpp", ".cc", ".c", ".h"]
353 )
355 test_case_list = \
356 collect_test_cases_from_test_files(
357 test_file_list=test_file_list,
358 codebeamer_url=config_dict.get(CODEBEAMER_URL, '')
359 )
361 lobster_items_output_dict: dict = \
362 create_lobster_items_output_dict_from_test_cases(
363 test_case_list=test_case_list,
364 config_dict=config_dict
365 )
367 write_lobster_items_output_dict(
368 lobster_items_output_dict=lobster_items_output_dict
369 )
372ap = argparse.ArgumentParser()
375@get_version(ap)
376def main():
377 """
378 Main function to parse arguments, read configuration
379 and launch lobster_cpptest.
380 """
381 # lobster-trace: cpptest_req.Dummy_Requirement
382 ap.add_argument("--config",
383 help=("Path to YAML file with arguments, "
384 "by default (cpptest-config.yaml)"),
385 default="cpptest-config.yaml")
387 options = ap.parse_args()
389 try:
390 config_dict = parse_config_file(options.config)
392 options.files = config_dict.get("files", ["."])
393 config_dict.pop("files", None)
395 lobster_cpptest(
396 file_dir_list=options.files,
397 config_dict=config_dict
398 )
400 except ValueError as exception:
401 ap.error(exception)
404if __name__ == "__main__":
405 sys.exit(main())