#!/usr/bin/env python3 # # Copyright (C) Advanced Micro Devices. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of # the Software, and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import argparse import datetime import os import pathlib import sys import textwrap version_number = "1.0.0" build_date = f"{datetime.datetime.now():%b %d %Y}" verbose_choices = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "ALWAYS"] not_supported_error_codes = [ ("2", "AMDSMI_STATUS_NOT_SUPPORTED"), ("3", "AMDSMI_STATUS_NOT_YET_IMPLEMENTED"), ("49", "AMDSMI_STATUS_NO_HSMP_MSG_SUP"), ] open_parenthesis = "(" close_parenthesis = ")" colon = ":" header = "any C(any, func, unit) py(any, func, unit)" def Print(cond, msg, ending=None, sysout=sys.stdout): if isinstance(cond, str): # Check verbose level if cond in verbose_choices: cond_num = verbose_choices.index(cond) if cond_num < args.verbose_num: return elif not cond: # Check cond return if ending == None: print(msg, file=sysout) else: print(msg, end=ending, file=sysout) return def Find_Root_Path(): last_path = pathlib.Path("/") root_path = pathlib.Path.cwd() while True: git_path = root_path / ".git" if git_path.exists(): return root_path / "projects/amdsmi" if root_path == last_path: break root_path = root_path.parent return None def Check_Inputs(amdsmi, file_names): root_path = Find_Root_Path() # Check for header file amdsmi_path = pathlib.Path(amdsmi) if not amdsmi_path.exists(): if root_path: amdsmi_path = root_path / amdsmi_path if not amdsmi_path.exists(): amdsmi_path = None else: amdsmi_path = None # Check for any log files file_paths = {} missing_file_paths = {} for key, file_name in file_names.items(): file_path = args.log_dir / pathlib.Path(file_name) if file_path.exists(): file_paths[key] = file_path else: if root_path: file_path = root_path / args.log_dir / file_name if file_path.exists(): file_paths[key] = file_path else: missing_file_paths[key] = file_path else: missing_file_paths[key] = file_path return (amdsmi_path, file_paths, missing_file_paths) def ReadAmdsmiHeader(file_content): api_map = {} end_block_pos = 0 while True: start_block_pos = file_content.find("@ingroup", end_block_pos) if start_block_pos == -1: break end_block_pos = file_content.find(f"{close_parenthesis};", start_block_pos) if end_block_pos == -1: Print( "ERROR", f"Could not find func definition end, {start_block_pos}", sysout=sys.stderr ) break end_pos = file_content.rfind(open_parenthesis, start_block_pos, end_block_pos) start_pos = file_content.rfind("amdsmi_", start_block_pos, end_pos) func_name = file_content[start_pos:end_pos].strip() api_map[func_name] = 0 sorted_map = {} sorted_keys = sorted(api_map.keys()) for key in sorted_keys: sorted_map[key] = api_map[key] return sorted_map def Find_Name(line, start, name_start, name_end, cof): if start in line: pos_start = line.find(start) pos_start = line.find(name_start, pos_start) if pos_start == -1: Print("DEBUG", f'start: Bad "{start}" definition, {line}') return None pos_end = line.find(name_end, pos_start) if pos_end == -1: if not cof: Print("DEBUG", f'end: Bad "{start}" definition, {line}') return None pos_end = len(line) data = line[pos_start:pos_end].strip() return data return None def ReadTestingInput(file_contents): # api_map[file_name][func_name] = number_times_called api_map = {} api_support_map = {} fc = {} lines = [] # Create content that only contains API name and status # This eliminates any statements between API name and status for file_name in file_contents: for line in file_contents[file_name]: if "### amdsmi_" in line or "AMDSMI API" in line: lines.append(line) fc[file_name] = lines for file_name in fc: len_file_contents = len(file_contents[file_name]) api_map[file_name] = {} for index, line in enumerate(file_contents[file_name]): # Finding API name func_name = Find_Name(line, "### amdsmi_", "amdsmi_", open_parenthesis, False) if func_name == None: continue if func_name in api_map[file_name]: api_map[file_name][func_name] += 1 else: api_map[file_name][func_name] = 1 # Finding APIs that are Not Supported # An API must have at least one SUCCESS to be supported if index + 1 < len_file_contents: line = file_contents[file_name][index + 1] else: line = "" status = Find_Name(line, "AMDSMI API Returned", "AMDSMI_", colon, True) if status: if func_name in api_support_map: if api_support_map[func_name] > 0: api_support_map[func_name] += 1 # API is already supported elif not any(status in value for value in not_supported_error_codes): if status == "AMDSMI_STATUS_SUCCESS": api_support_map[func_name] = 1 # API is now supported else: supported = not any(status in value for value in not_supported_error_codes) if supported and status == "AMDSMI_STATUS_SUCCESS": api_support_map[func_name] = 1 # API is supported else: api_support_map[func_name] = 0 # API is not supported else: api_support_map[func_name] = 1 # API is supported if there is no status by default return (api_map, api_support_map) def Main(amdsmi, file_names): amdsmi_path, file_paths, missing_file_paths = Check_Inputs(amdsmi, file_names) if not amdsmi_path: Print("WARNING", f"Missing header, {amdsmi}", sysout=sys.stderr) for key, file_path in missing_file_paths.items(): Print("WARNING", f"Missing file, {key}={file_path}", sysout=sys.stderr) if amdsmi_path: Print("INFO", f"Using header, {amdsmi_path}") for key, file_path in file_paths.items(): Print("INFO", f"Using file, {key}={file_path}") if not amdsmi_path and not len(file_paths): Print("ERROR", "No header or log files found, exiting script", sysout=sys.stderr) return 1 # Header file is stored as a string amdsmi_content = amdsmi_path.read_text() # Each log file is stored as a [string] file_contents = {} for key, file_name in file_paths.items(): content = file_name.read_text() file_contents[key] = content.split("\n") # Read in header input amdsmi_map = {} if amdsmi_content: amdsmi_map = ReadAmdsmiHeader(amdsmi_content) num_api = len(amdsmi_map) if not num_api: num_api = 1 # set so code does not divide by zero Print("DEBUG", "No header APIs found") Print("DEBUG", f"amdsmi APIs = {num_api}") for func_name in amdsmi_map: Print("DEBUG", f"\tfunc: {func_name}") # Read in testing inputs api_map, api_support_map = ReadTestingInput(file_contents) found = False for file_name in api_map: if len(api_map[file_name]): found = True if not found: Print("ERROR", "No testing APIs found", sysout=sys.stderr) return 1 for file_name in api_map: Print("DEBUG", f"Tested {file_name} funcs = {len(api_map[file_name])}") for func_name in api_map[file_name]: Print("DEBUG", f"\t{func_name}()") if not args.output_dir.exists(): args.output_dir.mkdir(parents=True) # Initialize for func_name in amdsmi_map: amdsmi_map[func_name] = { "tested": 0, "c_unit_test": 0, "c_integration": 0, "py_unit_test": 0, "py_integration": 0, } # api_map[file_name][func_name] = number_times_called for file_name in api_map: for func_name in api_map[file_name]: if func_name not in amdsmi_map: Print( "ERROR", f"func {func_name}() in {file_name} tested but not in amdsmi.h", sysout=sys.stderr, ) continue amdsmi_map[func_name]["tested"] += 1 amdsmi_map[func_name][file_name] = 1 api_summary = [] msg = f"API, Tested, c_unit_test, c_integration, py_unit_test, py_integration" api_summary.append(msg) for func_name, tests_map in amdsmi_map.items(): msg = f"{func_name}, {tests_map['tested']}, {tests_map['c_unit_test']}, {tests_map['c_integration']}, {tests_map['py_unit_test']}, {tests_map['py_integration']}" api_summary.append(msg) api_summary.append("") api_summary = "\n".join(api_summary) api_summary_csv = pathlib.Path(args.output_dir / "_api_summary.csv") api_summary_csv.write_text(api_summary) # Get information about each test component c_any_total = 0 c_unit_test_total = 0 c_integration_total = 0 py_any_total = 0 py_unit_test_total = 0 py_integration_total = 0 any_total = 0 unit_test_total = 0 integration_total = 0 api_missing = [] api_tested = [] for func_name, values in amdsmi_map.items(): c_any = 0 py_any = 0 if values["tested"]: if values["c_unit_test"] or values["c_integration"]: c_any = 1 c_any_total += 1 if values["c_unit_test"]: c_unit_test_total += 1 if values["c_integration"]: c_integration_total += 1 if values["py_unit_test"] or values["py_integration"]: py_any = 1 py_any_total += 1 if values["py_unit_test"]: py_unit_test_total += 1 if values["py_integration"]: py_integration_total += 1 if values["c_integration"] or values["py_integration"]: integration_total += 1 if values["c_unit_test"] or values["py_unit_test"]: unit_test_total += 1 if ( values["c_unit_test"] or values["c_integration"] or values["py_unit_test"] or values["py_integration"] ): any_total += 1 if values["tested"] == 0: api_missing.append(func_name) Print("DEBUG", f"Missing:", ending="") else: api_tested.append(func_name) Print("DEBUG", f" Tested:", ending="") Print( "DEBUG", f" {func_name}: C(any={c_any} unit={values['c_unit_test']} int={values['c_integration']}) py(any={py_any} unit={values['py_unit_test']} int={values['py_integration']})", ) # Create summary report api_summary_support = [] if len(api_support_map): sorted_map = {} sorted_keys = sorted(api_support_map.keys()) for key in sorted_keys: sorted_map[key] = api_support_map[key] num_supported = 0 num_not_supported = 0 for func_name, supported in sorted_map.items(): if supported: num_supported += 1 else: num_not_supported += 1 api_summary_support.append(f"APIs Supported: {num_supported}") for func_name, supported in sorted_map.items(): if supported: api_summary_support.append(f"\t{func_name}()") api_summary_support.append(f"\nAPIs Not Supported: {num_not_supported}") for func_name, supported in sorted_map.items(): if not supported: api_summary_support.append(f"\t{func_name}()") api_summary_support.append(f"\nAPIs Missing: {len(api_missing)}") for func_name in api_missing: api_summary_support.append(f"\t{func_name}()") api_summary_support.append(f"\nAPI Summary") api_summary_support.append(f"Not Supported: {num_not_supported:>3d}") api_summary_support.append(f" Supported: {num_supported:>3d}") api_summary_support.append(f" Missing: {len(api_missing):>3d}") api_summary_support.append(f" Total: {len(sorted_map) + len(api_missing):>3d}") api_summary_support = "\n".join(api_summary_support) api_summary_support_txt = pathlib.Path(args.output_dir / "_api_summary_support.txt") api_summary_support_txt.write_text(api_summary_support) # Create summary table api_summary_table = [] c_any_total_percent = (c_any_total / num_api) * 100 c_unit_test_total_percent = (c_unit_test_total / num_api) * 100 c_integration_total_percent = (c_integration_total / num_api) * 100 py_any_total_percent = (py_any_total / num_api) * 100 py_unit_test_total_percent = (py_unit_test_total / num_api) * 100 py_integration_total_percent = (py_integration_total / num_api) * 100 any_total_percent = (any_total / num_api) * 100 unit_test_total_percent = (unit_test_total / num_api) * 100 integration_total_percent = (integration_total / num_api) * 100 msg = f"{'API':^5s} {'Test':>5s}({'%':^5s}) {'Unit':>5s}({'%':^5s}) {'Func':>5s}({'%':^5s})" api_summary_table.append(msg) msg = f"{'C':^5s} {c_any_total:>5d}({c_any_total_percent:>5.1f}) {c_unit_test_total:>5d}({c_unit_test_total_percent:>5.1f}) {c_integration_total:>5d}({c_integration_total_percent:>5.1f})" api_summary_table.append(msg) msg = f"{'Py':^5s} {py_any_total:>5d}({py_any_total_percent:>5.1f}) {py_unit_test_total:>5d}({py_unit_test_total_percent:>5.1f}) {py_integration_total:>5d}({py_integration_total_percent:>5.1f})" api_summary_table.append(msg) msg = f"{'Total':^5s} {any_total:>5d}({any_total_percent:>5.1f}) {unit_test_total:>5d}({unit_test_total_percent:>5.1f}) {integration_total:>5d}({integration_total_percent:>5.1f})" api_summary_table.append(msg) api_summary_table.append(f"Num APIs: {num_api}") api_summary_table.append("") api_summary_table = "\n".join(api_summary_table) api_summary_table_txt = pathlib.Path(args.output_dir / "_api_summary_table.txt") api_summary_table_txt.write_text(api_summary_table) return 0 def Parse_Command_Line(cmds=None): class CalledAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, self.dest, values) setattr(namespace, f"{self.dest}_called", True) msg_description = "Create API coverage report for unit_test.py and integration_test.py tests" msg_epilog = "Example:\n\t%(prog)s --c_unit_test --py_integration_test " parser = argparse.ArgumentParser( description=msg_description, formatter_class=argparse.RawTextHelpFormatter, epilog=textwrap.dedent(msg_epilog), ) parser_header = parser.add_argument_group("Information") parser.add_argument( "--version", action="version", version=version_number, help="Show version and exit" ) parser.add_argument("--build", action="version", version=build_date, help="Show build and exit") parser.add_argument( "--verbose", choices=verbose_choices, type=str, default="WARNING", help="Level of information to output, default=%(default)s", ) parser_header = parser.add_argument_group("Header File") parser_header.add_argument( "--amdsmi", default="include/amd_smi/amdsmi.h", help="Path to header file, default=%(default)s", ) parser_logs = parser.add_argument_group("Log Files") parser_logs.add_argument( "--log_dir", default="build", help="Path to where logs exist, default=%(default)s" ) parser_logs.add_argument( "--c_unit_test", default="_c_unit_test.log", help="Filename for C unit_test output, default=%(default)s", ) parser_logs.add_argument( "--c_integration", default="_amdsmitst.log", help="Filename for C integration_test output, default=%(default)s", ) parser_logs.add_argument( "--py_unit_test", default="_unit_test.log", help="Filename for Python unit_test output, default=%(default)s", ) parser_logs.add_argument( "--py_integration", default="_integration_test.log", help="Filename for Python integration_test output, default=%(default)s", ) parser_output = parser.add_argument_group("Output File") parser_output.add_argument( "--output_dir", default="build", action=CalledAction, help="Path to output dir, will create if does not exist, default=%(default)s", ) if cmds: args = parser.parse_args(cmds.split()) else: args = parser.parse_args() args.verbose_num = verbose_choices.index(args.verbose) if getattr(args, "output_dir_called", False): args.output_dir = pathlib.Path(args.output_dir) else: root_path = Find_Root_Path() args.output_dir = root_path / args.output_dir args.file_names = {} args.file_names["c_unit_test"] = args.c_unit_test args.file_names["c_integration"] = args.c_integration args.file_names["py_unit_test"] = args.py_unit_test args.file_names["py_integration"] = args.py_integration return args if __name__ == "__main__": rc = 1 args = Parse_Command_Line() rc = Main(args.amdsmi, args.file_names) sys.exit(rc)