Coverage for lobster/tools/cpptest/cpptest.py: 99%

141 statements  

« 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/>. 

19 

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 

34 

35OUTPUT = "output" 

36CODEBEAMER_URL = "codebeamer_url" 

37MARKERS = "markers" 

38KIND = "kind" 

39 

40NAMESPACE_CPP = "cpp" 

41FRAMEWORK_CPP_TEST = "cpptest" 

42KIND_FUNCTION = "Function" 

43CB_PREFIX = "CB-#" 

44MISSING = "Missing" 

45ORPHAN_TESTS = "OrphanTests" 

46 

47 

48class RequirementTypes(Enum): 

49 REQS = '@requirement' 

50 REQ_BY = '@requiredby' 

51 DEFECT = '@defect' 

52 

53 

54SUPPORTED_REQUIREMENTS = [ 

55 RequirementTypes.REQS.value, 

56 RequirementTypes.REQ_BY.value, 

57 RequirementTypes.DEFECT.value 

58] 

59 

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} 

65 

66 

67def parse_config_file(file_name: str) -> dict: 

68 """ 

69 Parse the configuration dictionary from the given YAML config file. 

70 

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. 

77 

78 Parameters 

79 ---------- 

80 file_name : str 

81 The file name of the cpptest YAML config file. 

82 

83 Returns 

84 ------- 

85 dict 

86 The dictionary containing the configuration for cpptest. 

87 

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): 

95 raise ValueError(f'{file_name} is not an existing file!') 

96 

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 

102 

103 if (not config_dict or OUTPUT not in config_dict or 

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}"') 

108 

109 output_config_dict = config_dict.get(OUTPUT) 

110 

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: 

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: 

118 raise ValueError(f'Please follow the right config file structure! ' 

119 f'Missing attribute "{KIND}" for output file ' 

120 f'"{output_file}"') 

121 

122 for output_file_marker in output_file_config_dict.get(MARKERS, []): 

123 if output_file_marker not in SUPPORTED_REQUIREMENTS: 

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}"') 

129 

130 return config_dict 

131 

132 

133def get_test_file_list(file_dir_list: list, extension_list: list) -> list: 

134 """ 

135 Gets the list of test files. 

136 

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. 

142 

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. 

150 

151 Returns 

152 ------- 

153 list 

154 The list of test files 

155 

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 = [] 

163 

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.') 

175 

176 if len(test_file_list) == 0: 

177 raise ValueError(f'"{file_dir_list}" does not contain any test file.') 

178 

179 return test_file_list 

180 

181 

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. 

186 

187 Parameters 

188 ---------- 

189 test_file_list : list 

190 The list of test files. 

191 codebeamer_url: str 

192 

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 

204 

205 

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. 

212 

213 Parameters 

214 ---------- 

215 test_case_list : list 

216 The list of test cases. 

217 config_dict : dict 

218 The configuration dictionary. 

219 

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: {}} 

227 

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: 

234 marker_output_config_dict[output_file_name] = output_config_dict 

235 

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() 

246 

247 activity = \ 

248 Activity( 

249 tag=tag, 

250 location=loc, 

251 framework=FRAMEWORK_CPP_TEST, 

252 kind=KIND_FUNCTION 

253 ) 

254 

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) 

273 

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) 

279 

280 lobster_items_output_dict.get(output_file_name)[key] = ( 

281 lobster_item) 

282 

283 if contains_no_tracing_target: 

284 lobster_items_output_dict.get(ORPHAN_TESTS)[key] = ( 

285 activity) 

286 

287 return lobster_items_output_dict 

288 

289 

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. 

294 

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 

305 

306 lobster_items_dict: dict = copy(lobster_items) 

307 lobster_items_dict.update(orphan_test_items) 

308 item_count = len(lobster_items_dict) 

309 

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}".') 

320 

321 

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. 

327 

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 ) 

340 

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 ) 

346 

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 ) 

352 

353 write_lobster_items_output_dict( 

354 lobster_items_output_dict=lobster_items_output_dict 

355 ) 

356 

357 

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 ) 

371 

372 def _run_impl(self, options: Namespace) -> int: 

373 options = self._argument_parser.parse_args() 

374 

375 try: 

376 config_dict = parse_config_file(options.config) 

377 

378 options.files = config_dict.get("files", ["."]) 

379 config_dict.pop("files", None) 

380 

381 lobster_cpptest( 

382 file_dir_list=options.files, 

383 config_dict=config_dict 

384 ) 

385 

386 except ValueError as exception: 

387 self._argument_parser.error(str(exception)) 

388 

389 return 0 

390 

391 

392def main() -> int: 

393 return CppTestTool().run()