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

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 

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 

35 

36OUTPUT = "output" 

37CODEBEAMER_URL = "codebeamer_url" 

38MARKERS = "markers" 

39KIND = "kind" 

40 

41NAMESPACE_CPP = "cpp" 

42FRAMEWORK_CPP_TEST = "cpptest" 

43KIND_FUNCTION = "Function" 

44CB_PREFIX = "CB-#" 

45MISSING = "Missing" 

46ORPHAN_TESTS = "OrphanTests" 

47 

48 

49class RequirementTypes(Enum): 

50 REQS = '@requirement' 

51 REQ_BY = '@requiredby' 

52 DEFECT = '@defect' 

53 

54 

55SUPPORTED_REQUIREMENTS = [ 

56 RequirementTypes.REQS.value, 

57 RequirementTypes.REQ_BY.value, 

58 RequirementTypes.DEFECT.value 

59] 

60 

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} 

66 

67 

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

69 """ 

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

71 

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. 

78 

79 Parameters 

80 ---------- 

81 file_name : str 

82 The file name of the cpptest YAML config file. 

83 

84 Returns 

85 ------- 

86 dict 

87 The dictionary containing the configuration for cpptest. 

88 

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

97 

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 

103 

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

109 

110 output_config_dict = config_dict.get(OUTPUT) 

111 

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

122 

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

130 

131 return config_dict 

132 

133 

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

135 """ 

136 Gets the list of test files. 

137 

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. 

143 

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. 

151 

152 Returns 

153 ------- 

154 list 

155 The list of test files 

156 

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

164 

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

176 

177 if len(test_file_list) == 0: 

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

179 

180 return test_file_list 

181 

182 

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. 

187 

188 Parameters 

189 ---------- 

190 test_file_list : list 

191 The list of test files. 

192 codebeamer_url: str 

193 

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 

205 

206 

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. 

213 

214 Parameters 

215 ---------- 

216 test_case_list : list 

217 The list of test cases. 

218 config_dict : dict 

219 The configuration dictionary. 

220 

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

228 

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 

236 

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

248 

249 activity = \ 

250 Activity( 

251 tag=tag, 

252 location=loc, 

253 framework=FRAMEWORK_CPP_TEST, 

254 kind=KIND_FUNCTION 

255 ) 

256 

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 

265 

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) 

278 

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) 

284 

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

286 lobster_item) 

287 

288 if contains_no_tracing_target: 

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

290 activity) 

291 

292 return lobster_items_output_dict 

293 

294 

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. 

299 

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 

310 

311 lobster_items_dict: dict = copy(lobster_items) 

312 lobster_items_dict.update(orphan_test_items) 

313 item_count = len(lobster_items_dict) 

314 

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

325 

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

334 

335 

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. 

341 

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 ) 

354 

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 ) 

360 

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 ) 

366 

367 write_lobster_items_output_dict( 

368 lobster_items_output_dict=lobster_items_output_dict 

369 ) 

370 

371 

372ap = argparse.ArgumentParser() 

373 

374 

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

386 

387 options = ap.parse_args() 

388 

389 try: 

390 config_dict = parse_config_file(options.config) 

391 

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

393 config_dict.pop("files", None) 

394 

395 lobster_cpptest( 

396 file_dir_list=options.files, 

397 config_dict=config_dict 

398 ) 

399 

400 except ValueError as exception: 

401 ap.error(exception) 

402 

403 

404if __name__ == "__main__": 

405 sys.exit(main())