1# SPDX-License-Identifier: Apache-2.0 2# Copyright (c) 2024 Intel Corporation 3 4import argparse 5import json 6import re 7 8import ijson 9import xlsxwriter 10import yaml 11 12 13class ComponentStats: 14 def __init__( 15 self, 16 testsuites: int, 17 runnable: int, 18 build_only: int, 19 sim_only: int, 20 hw_only: int, 21 mixed: int, 22 ): 23 self.testsuites = testsuites 24 self.runnable = runnable 25 self.build_only = build_only 26 self.sim_only = sim_only 27 self.hw_only = hw_only 28 self.mixed = mixed 29 30 31class Json_report: 32 json_object = {"components": []} 33 34 simulators = [ 35 'unit_testing', 36 'native', 37 'qemu', 38 'mps2/an385', 39 ] 40 41 report_json = {} 42 43 def __init__(self): 44 args = parse_args() 45 self.parse_testplan(args.testplan) 46 self.maintainers_file = self.get_maintainers_file(args.maintainers) 47 self.report_json = self.generate_json_report(args.coverage) 48 49 if args.format == "json": 50 self.save_json_report(args.output, self.report_json) 51 elif args.format == "xlsx": 52 self.generate_xlsx_report(self.report_json, args.output) 53 elif args.format == "all": 54 self.save_json_report(args.output, self.report_json) 55 self.generate_xlsx_report(self.report_json, args.output) 56 else: 57 print("Format incorrect") 58 59 def get_maintainers_file(self, maintainers): 60 maintainers_file = "" 61 with open(maintainers) as file: 62 maintainers_file = yaml.safe_load(file) 63 file.close() 64 return maintainers_file 65 66 def _parse_testcase(self, testsuite, testcase): 67 if testcase['status'] == 'None': 68 testcase_name = testsuite['name'] 69 component_name = testcase_name[: testcase_name.find('.')] 70 component = {"name": component_name, "sub_components": [], "files": []} 71 features = self.json_object['components'] 72 known_component_flag = False 73 for item in features: 74 if component_name == item['name']: 75 component = item 76 known_component_flag = True 77 break 78 sub_component_name = testcase_name[testcase_name.find('.') :] 79 sub_component_name = sub_component_name[1:] 80 if idx := sub_component_name.find(".") > 0: 81 sub_component_name = sub_component_name[:idx] 82 if known_component_flag is False: 83 sub_component = {"name": sub_component_name, "test_suites": []} 84 test_suite = { 85 "name": testsuite['name'], 86 "path": testsuite['path'], 87 "platforms": [], 88 "runnable": testsuite['runnable'], 89 "status": "", 90 "test_cases": [], 91 } 92 test_case = {"name": testcase_name} 93 if any(platform in testsuite['platform'] for platform in self.simulators): 94 if test_suite['status'] == "": 95 test_suite['status'] = 'sim_only' 96 97 if test_suite['status'] == 'hw_only': 98 test_suite['status'] = 'mixed' 99 else: 100 if test_suite['status'] == "": 101 test_suite['status'] = 'hw_only' 102 103 if test_suite['status'] == 'sim_only': 104 test_suite['status'] = 'mixed' 105 test_suite['test_cases'].append(test_case) 106 test_suite['platforms'].append(testsuite['platform']) 107 sub_component["test_suites"].append(test_suite) 108 component['sub_components'].append(sub_component) 109 self.json_object['components'].append(component) 110 else: 111 sub_component = {} 112 sub_components = component['sub_components'] 113 known_sub_component_flag = False 114 for i_sub_component in sub_components: 115 if sub_component_name == i_sub_component['name']: 116 sub_component = i_sub_component 117 known_sub_component_flag = True 118 break 119 if known_sub_component_flag is False: 120 sub_component = {"name": sub_component_name, "test_suites": []} 121 test_suite = { 122 "name": testsuite['name'], 123 "path": testsuite['path'], 124 "platforms": [], 125 "runnable": testsuite['runnable'], 126 "status": "", 127 "test_cases": [], 128 } 129 test_case = {"name": testcase_name} 130 if any(platform in testsuite['platform'] for platform in self.simulators): 131 if test_suite['status'] == "": 132 test_suite['status'] = 'sim_only' 133 134 if test_suite['status'] == 'hw_only': 135 test_suite['status'] = 'mixed' 136 else: 137 if test_suite['status'] == "": 138 test_suite['status'] = 'hw_only' 139 140 if test_suite['status'] == 'sim_only': 141 test_suite['status'] = 'mixed' 142 test_suite['test_cases'].append(test_case) 143 test_suite['platforms'].append(testsuite['platform']) 144 sub_component["test_suites"].append(test_suite) 145 component['sub_components'].append(sub_component) 146 else: 147 test_suite = {} 148 test_suites = sub_component['test_suites'] 149 known_testsuite_flag = False 150 for i_testsuite in test_suites: 151 if testsuite['name'] == i_testsuite['name']: 152 test_suite = i_testsuite 153 known_testsuite_flag = True 154 break 155 if known_testsuite_flag is False: 156 test_suite = { 157 "name": testsuite['name'], 158 "path": testsuite['path'], 159 "platforms": [], 160 "runnable": testsuite['runnable'], 161 "status": "", 162 "test_cases": [], 163 } 164 test_case = {"name": testcase_name} 165 if any(platform in testsuite['platform'] for platform in self.simulators): 166 if test_suite['status'] == "": 167 test_suite['status'] = 'sim_only' 168 169 if test_suite['status'] == 'hw_only': 170 test_suite['status'] = 'mixed' 171 else: 172 if test_suite['status'] == "": 173 test_suite['status'] = 'hw_only' 174 175 if test_suite['status'] == 'sim_only': 176 test_suite['status'] = 'mixed' 177 test_suite['test_cases'].append(test_case) 178 test_suite['platforms'].append(testsuite['platform']) 179 sub_component["test_suites"].append(test_suite) 180 else: 181 if any(platform in testsuite['platform'] for platform in self.simulators): 182 if test_suite['status'] == "": 183 test_suite['status'] = 'sim_only' 184 185 if test_suite['status'] == 'hw_only': 186 test_suite['status'] = 'mixed' 187 else: 188 if test_suite['status'] == "": 189 test_suite['status'] = 'hw_only' 190 191 if test_suite['status'] == 'sim_only': 192 test_suite['status'] = 'mixed' 193 test_case = {} 194 test_cases = test_suite['test_cases'] 195 known_testcase_flag = False 196 for i_testcase in test_cases: 197 if testcase_name == i_testcase['name']: 198 test_case = i_testcase 199 known_testcase_flag = True 200 break 201 if known_testcase_flag is False: 202 test_case = {"name": testcase_name} 203 test_suite['test_cases'].append(test_case) 204 205 def parse_testplan(self, testplan_path): 206 with open(testplan_path) as file: 207 parser = ijson.items(file, 'testsuites') 208 for element in parser: 209 for testsuite in element: 210 for testcase in testsuite['testcases']: 211 self._parse_testcase(testsuite, testcase) 212 213 def get_files_from_maintainers_file(self, component_name): 214 files_path = [] 215 for item in self.maintainers_file: 216 _found_flag = False 217 try: 218 tests = self.maintainers_file[item].get('tests', []) 219 for i_test in tests: 220 if component_name in i_test: 221 _found_flag = True 222 223 if _found_flag is True: 224 for path in self.maintainers_file[item]['files']: 225 path = path.replace('*', '.*') 226 files_path.append(path) 227 except TypeError: 228 print("ERROR: Fail while parsing MAINTAINERS file at %s", component_name) 229 return files_path 230 231 def _generate_component_report(self, element, component) -> dict: 232 json_component = {} 233 json_component["name"] = component["name"] 234 json_component["sub_components"] = component["sub_components"] 235 json_component["Comment"] = "" 236 files_path = self.get_files_from_maintainers_file(component["name"]) 237 238 if len(files_path) == 0: 239 json_component["files"] = [] 240 json_component["Comment"] = "Missed in maintainers.yml file." 241 return json_component 242 243 json_files = [] 244 for i_file in files_path: 245 for covered_file in element: 246 x = re.search(('.*' + i_file + '.*'), covered_file['file']) 247 if not x: 248 continue 249 250 file_name = covered_file['file'][covered_file['file'].rfind('/') + 1 :] 251 file_path = covered_file['file'] 252 file_coverage, file_lines, file_hit = self._calculate_coverage_of_file(covered_file) 253 json_file = { 254 "Name": file_name, 255 "Path": file_path, 256 "Lines": file_lines, 257 "Hit": file_hit, 258 "Coverage": file_coverage, 259 "Covered_Functions": [], 260 "Uncovered_Functions": [], 261 } 262 for i_fun in covered_file['functions']: 263 if i_fun['execution_count'] != 0: 264 json_covered_funciton = {"Name": i_fun['name']} 265 json_file['Covered_Functions'].append(json_covered_funciton) 266 for i_fun in covered_file['functions']: 267 if i_fun['execution_count'] == 0: 268 json_uncovered_funciton = {"Name": i_fun['name']} 269 json_file['Uncovered_Functions'].append(json_uncovered_funciton) 270 comp_exists = [x for x in json_files if x['Path'] == json_file['Path']] 271 if not comp_exists: 272 json_files.append(json_file) 273 json_component['files'] = json_files 274 return json_component 275 276 def generate_json_report(self, coverage): 277 output_json = {"components": []} 278 279 with open(coverage) as file: 280 parser = ijson.items(file, 'files') 281 for element in parser: 282 for i_json_component in self.json_object['components']: 283 json_component = self._generate_component_report(element, i_json_component) 284 output_json['components'].append(json_component) 285 286 return output_json 287 288 def _calculate_coverage_of_file(self, file): 289 tracked_lines = len(file['lines']) 290 covered_lines = 0 291 for line in file['lines']: 292 if line['count'] != 0: 293 covered_lines += 1 294 return ((covered_lines / tracked_lines) * 100), tracked_lines, covered_lines 295 296 def save_json_report(self, output_path, json_object): 297 json_object = json.dumps(json_object, indent=4) 298 with open(output_path + '.json', "w") as outfile: 299 outfile.write(json_object) 300 301 def _find_char(self, path, str, n): 302 sep = path.split(str, n) 303 if len(sep) <= n: 304 return -1 305 return len(path) - len(sep[-1]) - len(str) 306 307 def _component_calculate_stats(self, json_component) -> ComponentStats: 308 testsuites_count = 0 309 runnable_count = 0 310 build_only_count = 0 311 sim_only_count = 0 312 hw_only_count = 0 313 mixed_count = 0 314 for i_sub_component in json_component['sub_components']: 315 for i_testsuit in i_sub_component['test_suites']: 316 testsuites_count += 1 317 if i_testsuit['runnable'] is True: 318 runnable_count += 1 319 else: 320 build_only_count += 1 321 322 if i_testsuit['status'] == "hw_only": 323 hw_only_count += 1 324 elif i_testsuit['status'] == "sim_only": 325 sim_only_count += 1 326 else: 327 mixed_count += 1 328 return ComponentStats( 329 testsuites_count, 330 runnable_count, 331 build_only_count, 332 sim_only_count, 333 hw_only_count, 334 mixed_count, 335 ) 336 337 def _xlsx_generate_summary_page(self, workbook, json_report): 338 # formats 339 header_format = workbook.add_format( 340 { 341 "bold": True, 342 "fg_color": "#538DD5", 343 "font_color": "white", 344 } 345 ) 346 cell_format = workbook.add_format( 347 { 348 'valign': 'vcenter', 349 } 350 ) 351 352 # generate summary page 353 worksheet = workbook.add_worksheet('Summary') 354 row = 0 355 col = 0 356 worksheet.write(row, col, "Components", header_format) 357 worksheet.write(row, col + 1, "TestSuites", header_format) 358 worksheet.write(row, col + 2, "Runnable", header_format) 359 worksheet.write(row, col + 3, "Build only", header_format) 360 worksheet.write(row, col + 4, "Simulators only", header_format) 361 worksheet.write(row, col + 5, "Hardware only", header_format) 362 worksheet.write(row, col + 6, "Mixed", header_format) 363 worksheet.write(row, col + 7, "Coverage [%]", header_format) 364 worksheet.write(row, col + 8, "Total Functions", header_format) 365 worksheet.write(row, col + 9, "Uncovered Functions", header_format) 366 worksheet.write(row, col + 10, "Comment", header_format) 367 row = 1 368 col = 0 369 for item in json_report['components']: 370 worksheet.write(row, col, item['name'], cell_format) 371 stats = self._component_calculate_stats(item) 372 worksheet.write(row, col + 1, stats.testsuites, cell_format) 373 worksheet.write(row, col + 2, stats.runnable, cell_format) 374 worksheet.write(row, col + 3, stats.build_only, cell_format) 375 worksheet.write(row, col + 4, stats.sim_only, cell_format) 376 worksheet.write(row, col + 5, stats.hw_only, cell_format) 377 worksheet.write(row, col + 6, stats.mixed, cell_format) 378 lines = 0 379 hit = 0 380 coverage = 0.0 381 total_funs = 0 382 uncovered_funs = 0 383 for i_file in item['files']: 384 lines += i_file['Lines'] 385 hit += i_file['Hit'] 386 total_funs += len(i_file['Covered_Functions']) + len(i_file['Uncovered_Functions']) 387 uncovered_funs += len(i_file['Uncovered_Functions']) 388 389 if lines != 0: 390 coverage = (hit / lines) * 100 391 392 worksheet.write_number( 393 row, col + 7, coverage, workbook.add_format({'num_format': '#,##0.00'}) 394 ) 395 worksheet.write_number(row, col + 8, total_funs) 396 worksheet.write_number(row, col + 9, uncovered_funs) 397 worksheet.write(row, col + 10, item["Comment"], cell_format) 398 row += 1 399 col = 0 400 worksheet.conditional_format( 401 1, 402 col + 7, 403 row, 404 col + 7, 405 { 406 'type': 'data_bar', 407 'min_value': 0, 408 'max_value': 100, 409 'bar_color': '#3fd927', 410 'bar_solid': True, 411 }, 412 ) 413 worksheet.autofit() 414 worksheet.set_default_row(15) 415 416 def generate_xlsx_report(self, json_report, output): 417 self.report_book = xlsxwriter.Workbook(output + ".xlsx") 418 header_format = self.report_book.add_format( 419 {"bold": True, "fg_color": "#538DD5", "font_color": "white"} 420 ) 421 422 # Create a format to use in the merged range. 423 merge_format = self.report_book.add_format( 424 { 425 "bold": 1, 426 "align": "center", 427 "valign": "vcenter", 428 "fg_color": "#538DD5", 429 "font_color": "white", 430 } 431 ) 432 cell_format = self.report_book.add_format({'valign': 'vcenter'}) 433 434 self._xlsx_generate_summary_page(self.report_book, self.report_json) 435 row = 0 436 col = 0 437 for item in json_report['components']: 438 worksheet = self.report_book.add_worksheet(item['name']) 439 row = 0 440 col = 0 441 worksheet.write(row, col, "File Name", header_format) 442 worksheet.write(row, col + 1, "File Path", header_format) 443 worksheet.write(row, col + 2, "Coverage [%]", header_format) 444 worksheet.write(row, col + 3, "Lines", header_format) 445 worksheet.write(row, col + 4, "Hits", header_format) 446 worksheet.write(row, col + 5, "Diff", header_format) 447 row += 1 448 col = 0 449 for i_file in item['files']: 450 worksheet.write( 451 row, col, i_file['Path'][i_file['Path'].rfind('/') + 1 :], cell_format 452 ) 453 worksheet.write( 454 row, 455 col + 1, 456 i_file["Path"][(self._find_char(i_file["Path"], '/', 3) + 1) :], 457 cell_format, 458 ) 459 worksheet.write_number( 460 row, 461 col + 2, 462 i_file["Coverage"], 463 self.report_book.add_format({'num_format': '#,##0.00'}), 464 ) 465 worksheet.write(row, col + 3, i_file["Lines"], cell_format) 466 worksheet.write(row, col + 4, i_file["Hit"], cell_format) 467 worksheet.write(row, col + 5, i_file["Lines"] - i_file["Hit"], cell_format) 468 row += 1 469 col = 0 470 row += 1 471 col = 0 472 worksheet.conditional_format( 473 1, 474 col + 2, 475 row, 476 col + 2, 477 { 478 'type': 'data_bar', 479 'min_value': 0, 480 'max_value': 100, 481 'bar_color': '#3fd927', 482 'bar_solid': True, 483 }, 484 ) 485 worksheet.merge_range(row, col, row, col + 2, "Uncovered Functions", merge_format) 486 row += 1 487 worksheet.write(row, col, 'Function Name', header_format) 488 worksheet.write(row, col + 1, 'Implementation File', header_format) 489 worksheet.write(row, col + 2, 'Comment', header_format) 490 row += 1 491 col = 0 492 for i_file in item['files']: 493 for i_uncov_fun in i_file['Uncovered_Functions']: 494 worksheet.write(row, col, i_uncov_fun["Name"], cell_format) 495 worksheet.write( 496 row, 497 col + 1, 498 i_file["Path"][self._find_char(i_file["Path"], '/', 3) + 1 :], 499 cell_format, 500 ) 501 row += 1 502 col = 0 503 row += 1 504 col = 0 505 worksheet.write(row, col, "Components", header_format) 506 worksheet.write(row, col + 1, "Sub-Components", header_format) 507 worksheet.write(row, col + 2, "TestSuites", header_format) 508 worksheet.write(row, col + 3, "Runnable", header_format) 509 worksheet.write(row, col + 4, "Build only", header_format) 510 worksheet.write(row, col + 5, "Simulation only", header_format) 511 worksheet.write(row, col + 6, "Hardware only", header_format) 512 worksheet.write(row, col + 7, "Mixed", header_format) 513 row += 1 514 col = 0 515 worksheet.write(row, col, item['name'], cell_format) 516 for i_sub_component in item['sub_components']: 517 testsuites_count = 0 518 runnable_count = 0 519 build_only_count = 0 520 sim_only_count = 0 521 hw_only_count = 0 522 mixed_count = 0 523 worksheet.write(row, col + 1, i_sub_component['name'], cell_format) 524 for i_testsuit in i_sub_component['test_suites']: 525 testsuites_count += 1 526 if i_testsuit['runnable'] is True: 527 runnable_count += 1 528 else: 529 build_only_count += 1 530 531 if i_testsuit['status'] == "hw_only": 532 hw_only_count += 1 533 elif i_testsuit['status'] == "sim_only": 534 sim_only_count += 1 535 else: 536 mixed_count += 1 537 worksheet.write(row, col + 2, testsuites_count, cell_format) 538 worksheet.write(row, col + 3, runnable_count, cell_format) 539 worksheet.write(row, col + 4, build_only_count, cell_format) 540 worksheet.write(row, col + 5, sim_only_count, cell_format) 541 worksheet.write(row, col + 6, hw_only_count, cell_format) 542 worksheet.write(row, col + 7, mixed_count, cell_format) 543 row += 1 544 col = 0 545 546 worksheet.autofit() 547 worksheet.set_default_row(15) 548 self.report_book.close() 549 550 551def parse_args(): 552 parser = argparse.ArgumentParser(allow_abbrev=False) 553 parser.add_argument( 554 '-m', '--maintainers', help='Path to maintainers.yml [Required]', required=True 555 ) 556 parser.add_argument('-t', '--testplan', help='Path to testplan [Required]', required=True) 557 parser.add_argument( 558 '-c', '--coverage', help='Path to components file [Required]', required=True 559 ) 560 parser.add_argument('-o', '--output', help='Report name [Required]', required=True) 561 parser.add_argument( 562 '-f', '--format', help='Output format (json, xlsx, all) [Required]', required=True 563 ) 564 565 args = parser.parse_args() 566 return args 567 568 569if __name__ == '__main__': 570 Json_report() 571