Coverage for lobster/tools/cpptest/cpptest.py: 98%
156 statements
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-27 13:02 +0000
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-27 13:02 +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
21from argparse import Namespace
22import os.path
23from copy import copy
24from dataclasses import dataclass, field
25from typing import List, Optional, Sequence, Union
26from enum import Enum
27import yaml
28from lobster.common.errors import LOBSTER_Error
29from lobster.common.exceptions import LOBSTER_Exception
30from lobster.common.items import Tracing_Tag, Activity
31from lobster.common.location import File_Reference
32from lobster.common.io import lobster_write
33from lobster.common.file_tag_generator import FileTagGenerator
34from lobster.tools.cpptest.constants import Constants
35from lobster.tools.cpptest.requirements_parser import \
36 ParserForRequirements
37from lobster.common.meta_data_tool_base import MetaDataToolBase
39OUTPUT_FILE = "output_file"
40CODEBEAMER_URL = "codebeamer_url"
41KIND = "kind"
42FILES = "files"
43REQUIREMENTS = 'requirements'
45NAMESPACE_CPP = "cpp"
46FRAMEWORK_CPP_TEST = "cpptest"
47KIND_FUNCTION = "Function"
48CB_PREFIX = "CB-#"
49MISSING = "Missing"
50ORPHAN_TESTS = "OrphanTests"
52TOOL_NAME = "lobster-cpptest"
55class KindTypes(str, Enum):
56 REQ = "req"
57 ACT = "act"
58 IMP = "imp"
61@dataclass
62class Config:
63 codebeamer_url: str
64 kind: KindTypes = KindTypes.REQ
65 files: List[str] = field(default_factory=lambda: ["."])
66 output_file: str = "report.lobster"
69SUPPORTED_KINDS = [kind_type.value for kind_type in KindTypes]
72def parse_config_file(file_name: str) -> Config:
73 """
74 Parse the configuration dictionary from the given YAML config file.
76 The configuration dictionary for cpptest must contain `codebeamer_url`.
77 It may also contain `kind`, `files`, and `output_file` keys.
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):
96 raise FileNotFoundError(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
105 CODEBEAMER_URL not in config_dict):
106 raise KeyError(f'Please follow the right config file structure! '
107 f'Missing attribute {CODEBEAMER_URL}')
109 codebeamer_url = config_dict.get(CODEBEAMER_URL)
110 kind = config_dict.get(KIND, KindTypes.REQ.value)
111 files = config_dict.get(FILES, ["."])
112 output_file = config_dict.get(OUTPUT_FILE, "report.lobster")
114 if not isinstance(output_file, str):
115 raise ValueError(f'Please follow the right config file structure! '
116 f'{OUTPUT_FILE} must be a string but got '
117 f'{type(output_file).__name__}.')
119 if not isinstance(kind, str):
120 raise ValueError(f'Please follow the right config file structure! '
121 f'{KIND} must be a string but got '
122 f'{type(kind).__name__}.')
124 if kind not in SUPPORTED_KINDS:
125 raise ValueError(f'Please follow the right config file structure! '
126 f'{KIND} must be one of '
127 f'{",".join(SUPPORTED_KINDS)} but got {kind}.')
129 return Config(
130 codebeamer_url=codebeamer_url,
131 kind=kind,
132 files=files if isinstance(files, list) else [files],
133 output_file=output_file
134 )
137def get_test_file_list(file_dir_list: list, extension_list: list) -> list:
138 """
139 Gets the list of test files.
141 Given file names are added to the test file list without
142 validating against the extension list.
143 From given directory names only file names will be added
144 to the test file list if their extension matches against
145 the extension list.
147 Parameters
148 ----------
149 file_dir_list : list
150 A list containing file names and/or directory names
151 to parse for file names.
152 extension_list : list
153 The list of file name extensions.
155 Returns
156 -------
157 list
158 The list of test files
160 Raises
161 ------
162 Exception
163 If the config dict does not contain the required keys
164 or contains not supported values.
165 """
166 test_file_list = []
168 for file_dir_entry in file_dir_list:
169 if os.path.isfile(file_dir_entry):
170 test_file_list.append(file_dir_entry)
171 elif os.path.isdir(file_dir_entry):
172 for path, _, files in os.walk(file_dir_entry):
173 for filename in files:
174 _, ext = os.path.splitext(filename)
175 if ext in extension_list:
176 test_file_list.append(os.path.join(path, filename))
177 else:
178 raise FileNotFoundError(f'"{file_dir_entry}" is not a file or directory.')
180 if len(test_file_list) == 0:
181 raise ValueError(f'"{file_dir_list}" does not contain any test file.')
183 return test_file_list
186def collect_test_cases_from_test_files(test_file_list: list,
187 codebeamer_url: str) -> list:
188 """
189 Collects the list of test cases from the given test files.
191 Parameters
192 ----------
193 test_file_list : list
194 The list of test files.
195 codebeamer_url: str
197 Returns
198 -------
199 list
200 The list of test cases.
201 """
202 parser = ParserForRequirements()
203 test_case_list = parser.collect_test_cases_for_test_files(
204 test_files=test_file_list,
205 codebeamer_url=codebeamer_url
206 )
207 return test_case_list
210def create_lobster_items_output_dict_from_test_cases(
211 test_case_list: list,
212 config: Config) -> dict:
213 """
214 Creates the lobster items dictionary for the given test cases grouped by
215 configured output.
217 Parameters
218 ----------
219 test_case_list : list
220 The list of test cases.
221 config : Config
222 The configuration setting.
224 Returns
225 -------
226 dict
227 The lobster items dictionary for the given test cases
228 grouped by configured output.
229 """
231 output_file = config.output_file
232 lobster_items_output_dict = {ORPHAN_TESTS: {}, output_file: {}}
234 file_tag_generator = FileTagGenerator()
235 for test_case in test_case_list:
236 function_name: str = test_case.test_name
237 file_name = os.path.abspath(test_case.file_name)
238 line_nr = int(test_case.docu_start_line)
239 function_uid = f"{file_tag_generator.get_tag(file_name)}" \
240 f":{function_name}:{line_nr}"
241 tag = Tracing_Tag(NAMESPACE_CPP, function_uid)
242 loc = File_Reference(file_name, line_nr)
243 key = tag.key()
245 activity = \
246 Activity(
247 tag=tag,
248 location=loc,
249 framework=FRAMEWORK_CPP_TEST,
250 kind=KIND_FUNCTION
251 )
253 contains_no_tracing_target = True
255 tracing_target_list = []
256 tracing_target_kind = config.kind
257 for test_case_marker_value in getattr(
258 test_case,
259 REQUIREMENTS
260 ):
261 if MISSING not in test_case_marker_value: 261 ↛ 257line 261 didn't jump to line 257 because the condition on line 261 was always true
262 test_case_marker_value = (
263 test_case_marker_value.replace(CB_PREFIX, ""))
264 tracing_target = Tracing_Tag(
265 tracing_target_kind,
266 test_case_marker_value
267 )
268 tracing_target_list.append(tracing_target)
270 if len(tracing_target_list) >= 1:
271 contains_no_tracing_target = False
272 lobster_item = copy(activity)
273 for tracing_target in tracing_target_list:
274 lobster_item.add_tracing_target(tracing_target)
276 lobster_items_output_dict[output_file][key] = (
277 lobster_item)
279 if contains_no_tracing_target:
280 lobster_items_output_dict.get(ORPHAN_TESTS)[key] = (
281 activity)
283 return lobster_items_output_dict
286def write_lobster_items_output_dict(lobster_items_output_dict: dict):
287 """
288 Write the lobster items to the output.
289 If the output file name is empty everything is written to stdout.
291 Parameters
292 ----------
293 lobster_items_output_dict : dict
294 The lobster items dictionary grouped by output.
295 """
296 lobster_generator = Constants.LOBSTER_GENERATOR
297 orphan_test_items = lobster_items_output_dict.get(ORPHAN_TESTS, {})
298 for output_file_name, lobster_items in lobster_items_output_dict.items():
299 if output_file_name == ORPHAN_TESTS:
300 continue
302 lobster_items_dict: dict = copy(lobster_items)
303 lobster_items_dict.update(orphan_test_items)
304 item_count = len(lobster_items_dict)
306 if output_file_name: 306 ↛ 298line 306 didn't jump to line 298 because the condition on line 306 was always true
307 with open(output_file_name, "w", encoding="UTF-8") as output_file:
308 lobster_write(
309 output_file,
310 Activity,
311 lobster_generator,
312 lobster_items_dict.values()
313 )
314 print(f'Written {item_count} lobster items to '
315 f'"{output_file_name}".')
318def lobster_cpptest(config: Config):
319 """
320 The main function to parse requirements from comments
321 for the given list of files and/or directories and write the
322 created lobster dictionary to the configured outputs.
324 Parameters
325 ----------
326 config : Config
327 The configuration setting
328 """
329 test_file_list = \
330 get_test_file_list(
331 file_dir_list=config.files,
332 extension_list=[".cpp", ".cc", ".c", ".h"]
333 )
335 test_case_list = \
336 collect_test_cases_from_test_files(
337 test_file_list=test_file_list,
338 codebeamer_url=config.codebeamer_url
339 )
341 lobster_items_output_dict: dict = \
342 create_lobster_items_output_dict_from_test_cases(
343 test_case_list=test_case_list,
344 config=config
345 )
347 write_lobster_items_output_dict(
348 lobster_items_output_dict=lobster_items_output_dict
349 )
352class CppTestTool(MetaDataToolBase):
353 def __init__(self):
354 super().__init__(
355 name="cpptest",
356 description="Extract C++ tracing tags from comments in tests for LOBSTER",
357 official=True,
358 )
359 self._argument_parser.add_argument(
360 "--config",
361 help=("Path to YAML file with arguments, "
362 "by default (cpptest-config.yaml)"),
363 default="cpptest-config.yaml",
364 )
366 def _run_impl(self, options: Namespace) -> int:
367 try:
368 self._execute(options)
369 return 0
370 except FileNotFoundError as file_not_found_error:
371 self._print_error(file_not_found_error)
372 except ValueError as value_error:
373 self._print_error(value_error)
374 except KeyError as key_error:
375 self._print_error(key_error)
376 except LOBSTER_Error as lobster_error:
377 self._print_error(lobster_error)
378 return 1
380 @staticmethod
381 def _print_error(error: Union[Exception, str]):
382 print(f"{TOOL_NAME}: {error}", file=sys.stderr)
384 @staticmethod
385 def _execute(options: Namespace) -> None:
386 config = parse_config_file(options.config)
388 lobster_cpptest(
389 config=config
390 )
393def cpptest_items_to_lobster_file(config: Config) -> None:
394 """Loads items from cpptests and serializes them in the LOBSTER interchange
395 format to the given file.
396 """
397 # This is an API function.
398 lobster_cpptest(config=config)
401def main(args: Optional[Sequence[str]] = None) -> int:
402 return CppTestTool().run(args)