# SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2024 Intel Corporation import argparse import json import re import ijson import xlsxwriter import yaml class ComponentStats: def __init__( self, testsuites: int, runnable: int, build_only: int, sim_only: int, hw_only: int, mixed: int, ): self.testsuites = testsuites self.runnable = runnable self.build_only = build_only self.sim_only = sim_only self.hw_only = hw_only self.mixed = mixed class Json_report: json_object = {"components": []} simulators = [ 'unit_testing', 'native', 'qemu', 'mps2/an385', ] report_json = {} def __init__(self): args = parse_args() self.parse_testplan(args.testplan) self.maintainers_file = self.get_maintainers_file(args.maintainers) self.report_json = self.generate_json_report(args.coverage) if args.format == "json": self.save_json_report(args.output, self.report_json) elif args.format == "xlsx": self.generate_xlsx_report(self.report_json, args.output) elif args.format == "all": self.save_json_report(args.output, self.report_json) self.generate_xlsx_report(self.report_json, args.output) else: print("Format incorrect") def get_maintainers_file(self, maintainers): maintainers_file = "" with open(maintainers) as file: maintainers_file = yaml.safe_load(file) file.close() return maintainers_file def _parse_testcase(self, testsuite, testcase): if testcase['status'] == 'None': testcase_name = testsuite['name'] component_name = testcase_name[: testcase_name.find('.')] component = {"name": component_name, "sub_components": [], "files": []} features = self.json_object['components'] known_component_flag = False for item in features: if component_name == item['name']: component = item known_component_flag = True break sub_component_name = testcase_name[testcase_name.find('.') :] sub_component_name = sub_component_name[1:] if idx := sub_component_name.find(".") > 0: sub_component_name = sub_component_name[:idx] if known_component_flag is False: sub_component = {"name": sub_component_name, "test_suites": []} test_suite = { "name": testsuite['name'], "path": testsuite['path'], "platforms": [], "runnable": testsuite['runnable'], "status": "", "test_cases": [], } test_case = {"name": testcase_name} if any(platform in testsuite['platform'] for platform in self.simulators): if test_suite['status'] == "": test_suite['status'] = 'sim_only' if test_suite['status'] == 'hw_only': test_suite['status'] = 'mixed' else: if test_suite['status'] == "": test_suite['status'] = 'hw_only' if test_suite['status'] == 'sim_only': test_suite['status'] = 'mixed' test_suite['test_cases'].append(test_case) test_suite['platforms'].append(testsuite['platform']) sub_component["test_suites"].append(test_suite) component['sub_components'].append(sub_component) self.json_object['components'].append(component) else: sub_component = {} sub_components = component['sub_components'] known_sub_component_flag = False for i_sub_component in sub_components: if sub_component_name == i_sub_component['name']: sub_component = i_sub_component known_sub_component_flag = True break if known_sub_component_flag is False: sub_component = {"name": sub_component_name, "test_suites": []} test_suite = { "name": testsuite['name'], "path": testsuite['path'], "platforms": [], "runnable": testsuite['runnable'], "status": "", "test_cases": [], } test_case = {"name": testcase_name} if any(platform in testsuite['platform'] for platform in self.simulators): if test_suite['status'] == "": test_suite['status'] = 'sim_only' if test_suite['status'] == 'hw_only': test_suite['status'] = 'mixed' else: if test_suite['status'] == "": test_suite['status'] = 'hw_only' if test_suite['status'] == 'sim_only': test_suite['status'] = 'mixed' test_suite['test_cases'].append(test_case) test_suite['platforms'].append(testsuite['platform']) sub_component["test_suites"].append(test_suite) component['sub_components'].append(sub_component) else: test_suite = {} test_suites = sub_component['test_suites'] known_testsuite_flag = False for i_testsuite in test_suites: if testsuite['name'] == i_testsuite['name']: test_suite = i_testsuite known_testsuite_flag = True break if known_testsuite_flag is False: test_suite = { "name": testsuite['name'], "path": testsuite['path'], "platforms": [], "runnable": testsuite['runnable'], "status": "", "test_cases": [], } test_case = {"name": testcase_name} if any(platform in testsuite['platform'] for platform in self.simulators): if test_suite['status'] == "": test_suite['status'] = 'sim_only' if test_suite['status'] == 'hw_only': test_suite['status'] = 'mixed' else: if test_suite['status'] == "": test_suite['status'] = 'hw_only' if test_suite['status'] == 'sim_only': test_suite['status'] = 'mixed' test_suite['test_cases'].append(test_case) test_suite['platforms'].append(testsuite['platform']) sub_component["test_suites"].append(test_suite) else: if any(platform in testsuite['platform'] for platform in self.simulators): if test_suite['status'] == "": test_suite['status'] = 'sim_only' if test_suite['status'] == 'hw_only': test_suite['status'] = 'mixed' else: if test_suite['status'] == "": test_suite['status'] = 'hw_only' if test_suite['status'] == 'sim_only': test_suite['status'] = 'mixed' test_case = {} test_cases = test_suite['test_cases'] known_testcase_flag = False for i_testcase in test_cases: if testcase_name == i_testcase['name']: test_case = i_testcase known_testcase_flag = True break if known_testcase_flag is False: test_case = {"name": testcase_name} test_suite['test_cases'].append(test_case) def parse_testplan(self, testplan_path): with open(testplan_path) as file: parser = ijson.items(file, 'testsuites') for element in parser: for testsuite in element: for testcase in testsuite['testcases']: self._parse_testcase(testsuite, testcase) def get_files_from_maintainers_file(self, component_name): files_path = [] for item in self.maintainers_file: _found_flag = False try: tests = self.maintainers_file[item].get('tests', []) for i_test in tests: if component_name in i_test: _found_flag = True if _found_flag is True: for path in self.maintainers_file[item]['files']: path = path.replace('*', '.*') files_path.append(path) except TypeError: print("ERROR: Fail while parsing MAINTAINERS file at %s", component_name) return files_path def _generate_component_report(self, element, component) -> dict: json_component = {} json_component["name"] = component["name"] json_component["sub_components"] = component["sub_components"] json_component["Comment"] = "" files_path = self.get_files_from_maintainers_file(component["name"]) if len(files_path) == 0: json_component["files"] = [] json_component["Comment"] = "Missed in maintainers.yml file." return json_component json_files = [] for i_file in files_path: for covered_file in element: x = re.search(('.*' + i_file + '.*'), covered_file['file']) if not x: continue file_name = covered_file['file'][covered_file['file'].rfind('/') + 1 :] file_path = covered_file['file'] file_coverage, file_lines, file_hit = self._calculate_coverage_of_file(covered_file) json_file = { "Name": file_name, "Path": file_path, "Lines": file_lines, "Hit": file_hit, "Coverage": file_coverage, "Covered_Functions": [], "Uncovered_Functions": [], } for i_fun in covered_file['functions']: if i_fun['execution_count'] != 0: json_covered_funciton = {"Name": i_fun['name']} json_file['Covered_Functions'].append(json_covered_funciton) for i_fun in covered_file['functions']: if i_fun['execution_count'] == 0: json_uncovered_funciton = {"Name": i_fun['name']} json_file['Uncovered_Functions'].append(json_uncovered_funciton) comp_exists = [x for x in json_files if x['Path'] == json_file['Path']] if not comp_exists: json_files.append(json_file) json_component['files'] = json_files return json_component def generate_json_report(self, coverage): output_json = {"components": []} with open(coverage) as file: parser = ijson.items(file, 'files') for element in parser: for i_json_component in self.json_object['components']: json_component = self._generate_component_report(element, i_json_component) output_json['components'].append(json_component) return output_json def _calculate_coverage_of_file(self, file): tracked_lines = len(file['lines']) covered_lines = 0 for line in file['lines']: if line['count'] != 0: covered_lines += 1 return ((covered_lines / tracked_lines) * 100), tracked_lines, covered_lines def save_json_report(self, output_path, json_object): json_object = json.dumps(json_object, indent=4) with open(output_path + '.json', "w") as outfile: outfile.write(json_object) def _find_char(self, path, str, n): sep = path.split(str, n) if len(sep) <= n: return -1 return len(path) - len(sep[-1]) - len(str) def _component_calculate_stats(self, json_component) -> ComponentStats: testsuites_count = 0 runnable_count = 0 build_only_count = 0 sim_only_count = 0 hw_only_count = 0 mixed_count = 0 for i_sub_component in json_component['sub_components']: for i_testsuit in i_sub_component['test_suites']: testsuites_count += 1 if i_testsuit['runnable'] is True: runnable_count += 1 else: build_only_count += 1 if i_testsuit['status'] == "hw_only": hw_only_count += 1 elif i_testsuit['status'] == "sim_only": sim_only_count += 1 else: mixed_count += 1 return ComponentStats( testsuites_count, runnable_count, build_only_count, sim_only_count, hw_only_count, mixed_count, ) def _xlsx_generate_summary_page(self, workbook, json_report): # formats header_format = workbook.add_format( { "bold": True, "fg_color": "#538DD5", "font_color": "white", } ) cell_format = workbook.add_format( { 'valign': 'vcenter', } ) # generate summary page worksheet = workbook.add_worksheet('Summary') row = 0 col = 0 worksheet.write(row, col, "Components", header_format) worksheet.write(row, col + 1, "TestSuites", header_format) worksheet.write(row, col + 2, "Runnable", header_format) worksheet.write(row, col + 3, "Build only", header_format) worksheet.write(row, col + 4, "Simulators only", header_format) worksheet.write(row, col + 5, "Hardware only", header_format) worksheet.write(row, col + 6, "Mixed", header_format) worksheet.write(row, col + 7, "Coverage [%]", header_format) worksheet.write(row, col + 8, "Total Functions", header_format) worksheet.write(row, col + 9, "Uncovered Functions", header_format) worksheet.write(row, col + 10, "Comment", header_format) row = 1 col = 0 for item in json_report['components']: worksheet.write(row, col, item['name'], cell_format) stats = self._component_calculate_stats(item) worksheet.write(row, col + 1, stats.testsuites, cell_format) worksheet.write(row, col + 2, stats.runnable, cell_format) worksheet.write(row, col + 3, stats.build_only, cell_format) worksheet.write(row, col + 4, stats.sim_only, cell_format) worksheet.write(row, col + 5, stats.hw_only, cell_format) worksheet.write(row, col + 6, stats.mixed, cell_format) lines = 0 hit = 0 coverage = 0.0 total_funs = 0 uncovered_funs = 0 for i_file in item['files']: lines += i_file['Lines'] hit += i_file['Hit'] total_funs += len(i_file['Covered_Functions']) + len(i_file['Uncovered_Functions']) uncovered_funs += len(i_file['Uncovered_Functions']) if lines != 0: coverage = (hit / lines) * 100 worksheet.write_number( row, col + 7, coverage, workbook.add_format({'num_format': '#,##0.00'}) ) worksheet.write_number(row, col + 8, total_funs) worksheet.write_number(row, col + 9, uncovered_funs) worksheet.write(row, col + 10, item["Comment"], cell_format) row += 1 col = 0 worksheet.conditional_format( 1, col + 7, row, col + 7, { 'type': 'data_bar', 'min_value': 0, 'max_value': 100, 'bar_color': '#3fd927', 'bar_solid': True, }, ) worksheet.autofit() worksheet.set_default_row(15) def generate_xlsx_report(self, json_report, output): self.report_book = xlsxwriter.Workbook(output + ".xlsx") header_format = self.report_book.add_format( {"bold": True, "fg_color": "#538DD5", "font_color": "white"} ) # Create a format to use in the merged range. merge_format = self.report_book.add_format( { "bold": 1, "align": "center", "valign": "vcenter", "fg_color": "#538DD5", "font_color": "white", } ) cell_format = self.report_book.add_format({'valign': 'vcenter'}) self._xlsx_generate_summary_page(self.report_book, self.report_json) row = 0 col = 0 for item in json_report['components']: worksheet = self.report_book.add_worksheet(item['name']) row = 0 col = 0 worksheet.write(row, col, "File Name", header_format) worksheet.write(row, col + 1, "File Path", header_format) worksheet.write(row, col + 2, "Coverage [%]", header_format) worksheet.write(row, col + 3, "Lines", header_format) worksheet.write(row, col + 4, "Hits", header_format) worksheet.write(row, col + 5, "Diff", header_format) row += 1 col = 0 for i_file in item['files']: worksheet.write( row, col, i_file['Path'][i_file['Path'].rfind('/') + 1 :], cell_format ) worksheet.write( row, col + 1, i_file["Path"][(self._find_char(i_file["Path"], '/', 3) + 1) :], cell_format, ) worksheet.write_number( row, col + 2, i_file["Coverage"], self.report_book.add_format({'num_format': '#,##0.00'}), ) worksheet.write(row, col + 3, i_file["Lines"], cell_format) worksheet.write(row, col + 4, i_file["Hit"], cell_format) worksheet.write(row, col + 5, i_file["Lines"] - i_file["Hit"], cell_format) row += 1 col = 0 row += 1 col = 0 worksheet.conditional_format( 1, col + 2, row, col + 2, { 'type': 'data_bar', 'min_value': 0, 'max_value': 100, 'bar_color': '#3fd927', 'bar_solid': True, }, ) worksheet.merge_range(row, col, row, col + 2, "Uncovered Functions", merge_format) row += 1 worksheet.write(row, col, 'Function Name', header_format) worksheet.write(row, col + 1, 'Implementation File', header_format) worksheet.write(row, col + 2, 'Comment', header_format) row += 1 col = 0 for i_file in item['files']: for i_uncov_fun in i_file['Uncovered_Functions']: worksheet.write(row, col, i_uncov_fun["Name"], cell_format) worksheet.write( row, col + 1, i_file["Path"][self._find_char(i_file["Path"], '/', 3) + 1 :], cell_format, ) row += 1 col = 0 row += 1 col = 0 worksheet.write(row, col, "Components", header_format) worksheet.write(row, col + 1, "Sub-Components", header_format) worksheet.write(row, col + 2, "TestSuites", header_format) worksheet.write(row, col + 3, "Runnable", header_format) worksheet.write(row, col + 4, "Build only", header_format) worksheet.write(row, col + 5, "Simulation only", header_format) worksheet.write(row, col + 6, "Hardware only", header_format) worksheet.write(row, col + 7, "Mixed", header_format) row += 1 col = 0 worksheet.write(row, col, item['name'], cell_format) for i_sub_component in item['sub_components']: testsuites_count = 0 runnable_count = 0 build_only_count = 0 sim_only_count = 0 hw_only_count = 0 mixed_count = 0 worksheet.write(row, col + 1, i_sub_component['name'], cell_format) for i_testsuit in i_sub_component['test_suites']: testsuites_count += 1 if i_testsuit['runnable'] is True: runnable_count += 1 else: build_only_count += 1 if i_testsuit['status'] == "hw_only": hw_only_count += 1 elif i_testsuit['status'] == "sim_only": sim_only_count += 1 else: mixed_count += 1 worksheet.write(row, col + 2, testsuites_count, cell_format) worksheet.write(row, col + 3, runnable_count, cell_format) worksheet.write(row, col + 4, build_only_count, cell_format) worksheet.write(row, col + 5, sim_only_count, cell_format) worksheet.write(row, col + 6, hw_only_count, cell_format) worksheet.write(row, col + 7, mixed_count, cell_format) row += 1 col = 0 worksheet.autofit() worksheet.set_default_row(15) self.report_book.close() def parse_args(): parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument( '-m', '--maintainers', help='Path to maintainers.yml [Required]', required=True ) parser.add_argument('-t', '--testplan', help='Path to testplan [Required]', required=True) parser.add_argument( '-c', '--coverage', help='Path to components file [Required]', required=True ) parser.add_argument('-o', '--output', help='Report name [Required]', required=True) parser.add_argument( '-f', '--format', help='Output format (json, xlsx, all) [Required]', required=True ) args = parser.parse_args() return args if __name__ == '__main__': Json_report()