# @file GuidCheck.py # # Copyright (c) Microsoft Corporation. # SPDX-License-Identifier: BSD-2-Clause-Patent ## import logging from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin from edk2toollib.uefi.edk2.guid_list import GuidList from edk2toolext.environment.var_dict import VarDict class GuidCheck(ICiBuildPlugin): """ A CiBuildPlugin that scans the code tree and looks for duplicate guids from the package being tested. Configuration options: "GuidCheck": { "IgnoreGuidName": [], # provide in format guidname=guidvalue or just guidname "IgnoreGuidValue": [], "IgnoreFoldersAndFiles": [], "IgnoreDuplicates": [] # Provide in format guidname=guidname=guidname... } """ def GetTestName(self, packagename: str, environment: VarDict) -> tuple: """ Provide the testcase name and classname for use in reporting Args: packagename: string containing name of package to build environment: The VarDict for the test to run in Returns: a tuple containing the testcase name and the classname (testcasename, classname) testclassname: a descriptive string for the testcase can include whitespace classname: should be patterned .. """ return ("Confirm GUIDs are unique in " + packagename, packagename + ".GuidCheck") def _FindConflictingGuidValues(self, guidlist: list) -> list: """ Find all duplicate guids by guid value and report them as errors """ # Sort the list by guid guidsorted = sorted( guidlist, key=lambda x: x.guid.upper(), reverse=True) previous = None # Store previous entry for comparison error = None errors = [] for index in range(len(guidsorted)): i = guidsorted[index] if(previous is not None): if i.guid == previous.guid: # Error if(error is None): # Catch errors with more than 1 conflict error = ErrorEntry("guid") error.entries.append(previous) errors.append(error) error.entries.append(i) else: # no match. clear error error = None previous = i return errors def _FindConflictingGuidNames(self, guidlist: list) -> list: """ Find all duplicate guids by name and if they are not all from inf files report them as errors. It is ok to have BASE_NAME duplication. Is this useful? It would catch two same named guids in dec file that resolve to different values. """ # Sort the list by guid namesorted = sorted(guidlist, key=lambda x: x.name.upper()) previous = None # Store previous entry for comparison error = None errors = [] for index in range(len(namesorted)): i = namesorted[index] if(previous is not None): # If name matches if i.name == previous.name: if(error is None): # Catch errors with more than 1 conflict error = ErrorEntry("name") error.entries.append(previous) errors.append(error) error.entries.append(i) else: # no match. clear error error = None previous = i # Loop thru and remove any errors where all files are infs as it is ok if # they have the same inf base name. for e in errors[:]: if len( [en for en in e.entries if not en.absfilepath.lower().endswith(".inf")]) == 0: errors.remove(e) return errors ## # External function of plugin. This function is used to perform the task of the MuBuild Plugin # # - package is the edk2 path to package. This means workspace/packagepath relative. # - edk2path object configured with workspace and packages path # - PkgConfig Object (dict) for the pkg # - EnvConfig Object # - Plugin Manager Instance # - Plugin Helper Obj Instance # - Junit Logger # - output_stream the StringIO output stream from this plugin via logging def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None): Errors = [] abs_pkg_path = Edk2pathObj.GetAbsolutePathOnThisSytemFromEdk2RelativePath( packagename) if abs_pkg_path is None: tc.SetSkipped() tc.LogStdError("No package {0}".format(packagename)) return -1 All_Ignores = ["/Build", "/Conf"] # Parse the config for other ignores if "IgnoreFoldersAndFiles" in pkgconfig: All_Ignores.extend(pkgconfig["IgnoreFoldersAndFiles"]) # Parse the workspace for all GUIDs gs = GuidList.guidlist_from_filesystem( Edk2pathObj.WorkspacePath, ignore_lines=All_Ignores) # Remove ignored guidvalue if "IgnoreGuidValue" in pkgconfig: for a in pkgconfig["IgnoreGuidValue"]: try: tc.LogStdOut("Ignoring Guid {0}".format(a.upper())) for b in gs[:]: if b.guid == a.upper(): gs.remove(b) except: tc.LogStdError("GuidCheck.IgnoreGuid -> {0} not found. Invalid ignore guid".format(a.upper())) logging.info("GuidCheck.IgnoreGuid -> {0} not found. Invalid ignore guid".format(a.upper())) # Remove ignored guidname if "IgnoreGuidName" in pkgconfig: for a in pkgconfig["IgnoreGuidName"]: entry = a.split("=") if(len(entry) > 2): tc.LogStdError("GuidCheck.IgnoreGuidName -> {0} Invalid Format.".format(a)) logging.info("GuidCheck.IgnoreGuidName -> {0} Invalid Format.".format(a)) continue try: tc.LogStdOut("Ignoring Guid {0}".format(a)) for b in gs[:]: if b.name == entry[0]: if(len(entry) == 1): gs.remove(b) elif(len(entry) == 2 and b.guid.upper() == entry[1].upper()): gs.remove(b) else: c.LogStdError("GuidCheck.IgnoreGuidName -> {0} incomplete match. Invalid ignore guid".format(a)) except: tc.LogStdError("GuidCheck.IgnoreGuidName -> {0} not found. Invalid ignore name".format(a)) logging.info("GuidCheck.IgnoreGuidName -> {0} not found. Invalid ignore name".format(a)) # Find conflicting Guid Values Errors.extend(self._FindConflictingGuidValues(gs)) # Check if there are expected duplicates and remove it from the error list if "IgnoreDuplicates" in pkgconfig: for a in pkgconfig["IgnoreDuplicates"]: names = a.split("=") if len(names) < 2: tc.LogStdError("GuidCheck.IgnoreDuplicates -> {0} invalid format".format(a)) logging.info("GuidCheck.IgnoreDuplicates -> {0} invalid format".format(a)) continue for b in Errors[:]: if b.type != "guid": continue ## Make a list of the names that are not in the names list. If there ## are any in the list then this error should not be ignored. t = [x for x in b.entries if x.name not in names] if(len(t) == len(b.entries)): ## did not apply to any entry continue elif(len(t) == 0): ## full match - ignore duplicate tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0}".format(a)) Errors.remove(b) elif(len(t) < len(b.entries)): ## partial match tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0} incomplete match".format(a)) logging.info("GuidCheck.IgnoreDuplicates -> {0} incomplete match".format(a)) else: tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0} unknown error.".format(a)) logging.info("GuidCheck.IgnoreDuplicates -> {0} unknown error".format(a)) # Find conflicting Guid Names Errors.extend(self._FindConflictingGuidNames(gs)) # Log errors for anything within the package under test for er in Errors[:]: InMyPackage = False for a in er.entries: if abs_pkg_path in a.absfilepath: InMyPackage = True break if(not InMyPackage): Errors.remove(er) else: logging.error(str(er)) tc.LogStdError(str(er)) # add result to test case overall_status = len(Errors) if overall_status != 0: tc.SetFailed("GuidCheck {0} Failed. Errors {1}".format( packagename, overall_status), "CHECK_FAILED") else: tc.SetSuccess() return overall_status class ErrorEntry(): """ Custom/private class for reporting errors in the GuidList """ def __init__(self, errortype): self.type = errortype # 'guid' or 'name' depending on error type self.entries = [] # GuidListEntry that are in error condition def __str__(self): a = f"Error Duplicate {self.type}: " if(self.type == "guid"): a += f" {self.entries[0].guid}" elif(self.type == "name"): a += f" {self.entries[0].name}" a += f" ({len(self.entries)})\n" for e in self.entries: a += "\t" + str(e) + "\n" return a