[edk2-devel] [PATCH v1 7/7] .pytool/Plugin: Add DebugMacroCheck

Michael Kubacki mikuback at linux.microsoft.com
Thu Sep 7 21:43:03 UTC 2023


There's not package level control at the moment from a given package CI 
YAML file. We've treated this as fundamental for "core" packages (those 
often built from a single CISettings.py file) and on-by-default but with 
a disable option via the env var for individual platform build files 
(like the disable I applied for OvmfPkg in Patch 6 of the series).

If you'd like a disable option available in CI YAML files, it can easily 
be consumed in DebugMacroCheckBuildPlugin.py and I'll add it in v2. Let 
me know.

Thanks,
Michael

On 9/6/2023 9:35 PM, Michael D Kinney wrote:
> Reviewed-by: Michael D Kinney <michael.d.kinney at intel.com>
> 
> Really like the inclusion of unit tests.  If any issues are
> found, can update unit test to cover that case.
> 
> One quick question.  I see a global disable through env var.
> I also see it says that is it runs on packages that use the
> compiler plugin.  Is there a way to disable this plugin at
> the package scope? Many plugins support a "skip" setting today.
> 
> Thanks,
> 
> Mike
> 
>> -----Original Message-----
>> From: mikuback at linux.microsoft.com <mikuback at linux.microsoft.com>
>> Sent: Monday, August 14, 2023 1:49 PM
>> To: devel at edk2.groups.io
>> Cc: Sean Brogan <sean.brogan at microsoft.com>; Kinney, Michael D
>> <michael.d.kinney at intel.com>; Gao, Liming <gaoliming at byosoft.com.cn>
>> Subject: [PATCH v1 7/7] .pytool/Plugin: Add DebugMacroCheck
>>
>> From: Michael Kubacki <michael.kubacki at microsoft.com>
>>
>> Adds a plugin that finds debug macro formatting issues. These errors
>> often creep into debug prints in error conditions not frequently
>> executed and make debug more difficult when they are encountered.
>>
>> The code can be as a standalone script which is useful to find
>> problems in a large codebase that has not been checked before or as
>> a CI plugin that notifies a developer of an error right away.
>>
>> The script was already used to find numerous issues in edk2 in the
>> past so there's not many code fixes in this change. More details
>> are available in the readme file:
>>
>> .pytool\Plugin\DebugMacroCheck\Readme.md
>>
>> Cc: Sean Brogan <sean.brogan at microsoft.com>
>> Cc: Michael D Kinney <michael.d.kinney at intel.com>
>> Cc: Liming Gao <gaoliming at byosoft.com.cn>
>> Signed-off-by: Michael Kubacki <michael.kubacki at microsoft.com>
>> ---
>>   .pytool/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheckBuildPlugin.py
>> | 127 +++
>>   .pytool/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheck_plug_in.yaml
>> |  11 +
>>   .pytool/Plugin/DebugMacroCheck/DebugMacroCheck.py
>> | 859 ++++++++++++++++++++
>>   .pytool/Plugin/DebugMacroCheck/Readme.md
>> | 253 ++++++
>>   .pytool/Plugin/DebugMacroCheck/tests/DebugMacroDataSet.py
>> | 674 +++++++++++++++
>>   .pytool/Plugin/DebugMacroCheck/tests/MacroTest.py
>> | 131 +++
>>   .pytool/Plugin/DebugMacroCheck/tests/__init__.py
>> |   0
>>   .pytool/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py
>> | 201 +++++
>>   8 files changed, 2256 insertions(+)
>>
>> diff --git
>> a/.pytool/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheckBuildPlugin.p
>> y
>> b/.pytool/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheckBuildPlugin.p
>> y
>> new file mode 100644
>> index 000000000000..b1544666025e
>> --- /dev/null
>> +++
>> b/.pytool/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheckBuildPlugin.p
>> y
>> @@ -0,0 +1,127 @@
>> +# @file DebugMacroCheckBuildPlugin.py
>> +#
>> +# A build plugin that checks if DEBUG macros are formatted properly.
>> +#
>> +# In particular, that print format specifiers are defined
>> +# with the expected number of arguments in the variable
>> +# argument list.
>> +#
>> +# Copyright (c) Microsoft Corporation. All rights reserved.
>> +# SPDX-License-Identifier: BSD-2-Clause-Patent
>> +##
>> +
>> +import logging
>> +import os
>> +import pathlib
>> +import sys
>> +import yaml
>> +
>> +# Import the build plugin
>> +plugin_file = pathlib.Path(__file__)
>> +sys.path.append(str(plugin_file.parent.parent))
>> +
>> +# flake8 (E402): Ignore flake8 module level import not at top of file
>> +import DebugMacroCheck                          # noqa: E402
>> +
>> +from edk2toolext import edk2_logging                               #
>> noqa: E402
>> +from edk2toolext.environment.plugintypes.uefi_build_plugin import \
>> +    IUefiBuildPlugin                                               #
>> noqa: E402
>> +from edk2toolext.environment.uefi_build import UefiBuilder         #
>> noqa: E402
>> +from edk2toollib.uefi.edk2.path_utilities import Edk2Path          #
>> noqa: E402
>> +from pathlib import Path                                           #
>> noqa: E402
>> +
>> +
>> +class DebugMacroCheckBuildPlugin(IUefiBuildPlugin):
>> +
>> +    def do_pre_build(self, builder: UefiBuilder) -> int:
>> +        """Debug Macro Check pre-build functionality.
>> +
>> +        The plugin is invoked in pre-build since it can operate
>> independently
>> +        of build tools and to notify the user of any errors earlier in
>> the
>> +        build process to reduce feedback time.
>> +
>> +        Args:
>> +            builder (UefiBuilder): A UEFI builder object for this build.
>> +
>> +        Returns:
>> +            int: The number of debug macro errors found. Zero indicates
>> the
>> +            check either did not run or no errors were found.
>> +        """
>> +
>> +        # Check if disabled in the environment
>> +        env_disable = builder.env.GetValue("DISABLE_DEBUG_MACRO_CHECK")
>> +        if env_disable:
>> +            return 0
>> +
>> +        # Only run on targets with compilation
>> +        build_target = builder.env.GetValue("TARGET").lower()
>> +        if "no-target" in build_target:
>> +            return 0
>> +
>> +        pp = builder.pp.split(os.pathsep)
>> +        edk2 = Edk2Path(builder.ws, pp)
>> +        package = edk2.GetContainingPackage(
>> +                            builder.mws.join(builder.ws,
>> +                                             builder.env.GetValue(
>> +                                                "ACTIVE_PLATFORM")))
>> +        package_path = Path(
>> +
>> edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
>> +                                package))
>> +
>> +        # Every debug macro is printed at DEBUG logging level.
>> +        # Ensure the level is above DEBUG while executing the macro
>> check
>> +        # plugin to avoid flooding the log handler.
>> +        handler_level_context = []
>> +        for h in logging.getLogger().handlers:
>> +            if h.level < logging.INFO:
>> +                handler_level_context.append((h, h.level))
>> +                h.setLevel(logging.INFO)
>> +
>> +        edk2_logging.log_progress("Checking DEBUG Macros")
>> +
>> +        # There are two ways to specify macro substitution data for this
>> +        # plugin. If multiple options are present, data is appended from
>> +        # each option.
>> +        #
>> +        # 1. Specify the substitution data in the package CI YAML file.
>> +        # 2. Specify a standalone substitution data YAML file.
>> +        ##
>> +        sub_data = {}
>> +
>> +        # 1. Allow substitution data to be specified in a
>> "DebugMacroCheck" of
>> +        # the package CI YAML file. This is used to provide a familiar
>> per-
>> +        # package customization flow for a package maintainer.
>> +        package_config_file = Path(
>> +                                os.path.join(
>> +                                    package_path, package + ".ci.yaml"))
>> +        if package_config_file.is_file():
>> +            with open(package_config_file, 'r') as cf:
>> +                package_config_file_data = yaml.safe_load(cf)
>> +                if "DebugMacroCheck" in package_config_file_data and \
>> +                   "StringSubstitutions" in \
>> +                   package_config_file_data["DebugMacroCheck"]:
>> +                    logging.info(f"Loading substitution data in "
>> +                                 f"{str(package_config_file)}")
>> +                    sub_data |=
>> package_config_file_data["DebugMacroCheck"]["StringSubstitutions"] # noqa
>> +
>> +        # 2. Allow a substitution file to be specified as an environment
>> +        # variable. This is used to provide flexibility in how to
>> specify a
>> +        # substitution file. The value can be set anywhere prior to this
>> plugin
>> +        # getting called such as pre-existing build script.
>> +        sub_file = builder.env.GetValue("DEBUG_MACRO_CHECK_SUB_FILE")
>> +        if sub_file:
>> +            logging.info(f"Loading substitution file {sub_file}")
>> +            with open(sub_file, 'r') as sf:
>> +                sub_data |= yaml.safe_load(sf)
>> +
>> +        try:
>> +            error_count = DebugMacroCheck.check_macros_in_directory(
>> +                                            package_path,
>> +                                            ignore_git_submodules=False,
>> +                                            show_progress_bar=False,
>> +                                            **sub_data)
>> +        finally:
>> +            for h, l in handler_level_context:
>> +                h.setLevel(l)
>> +
>> +        return error_count
>> diff --git
>> a/.pytool/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheck_plug_in.yaml
>> b/.pytool/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheck_plug_in.yaml
>> new file mode 100644
>> index 000000000000..50f97cbd3935
>> --- /dev/null
>> +++
>> b/.pytool/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheck_plug_in.yaml
>> @@ -0,0 +1,11 @@
>> +## @file
>> +# Build plugin used to check that debug macros are formatted properly.
>> +#
>> +# Copyright (c) Microsoft Corporation. All rights reserved.
>> +# SPDX-License-Identifier: BSD-2-Clause-Patent
>> +##
>> +{
>> +  "scope": "global",
>> +  "name": "Debug Macro Check Plugin",
>> +  "module": "DebugMacroCheckBuildPlugin"
>> +}
>> diff --git a/.pytool/Plugin/DebugMacroCheck/DebugMacroCheck.py
>> b/.pytool/Plugin/DebugMacroCheck/DebugMacroCheck.py
>> new file mode 100644
>> index 000000000000..ffabcdf91b60
>> --- /dev/null
>> +++ b/.pytool/Plugin/DebugMacroCheck/DebugMacroCheck.py
>> @@ -0,0 +1,859 @@
>> +# @file DebugMacroCheck.py
>> +#
>> +# A script that checks if DEBUG macros are formatted properly.
>> +#
>> +# In particular, that print format specifiers are defined
>> +# with the expected number of arguments in the variable
>> +# argument list.
>> +#
>> +# Copyright (c) Microsoft Corporation. All rights reserved.
>> +# SPDX-License-Identifier: BSD-2-Clause-Patent
>> +##
>> +
>> +from argparse import RawTextHelpFormatter
>> +import logging
>> +import os
>> +import re
>> +import regex
>> +import sys
>> +import shutil
>> +import timeit
>> +import yaml
>> +
>> +from edk2toollib.utility_functions import RunCmd
>> +from io import StringIO
>> +from pathlib import Path, PurePath
>> +from typing import Dict, Iterable, List, Optional, Tuple
>> +
>> +
>> +PROGRAM_NAME = "Debug Macro Checker"
>> +
>> +
>> +class GitHelpers:
>> +    """
>> +    Collection of Git helpers.
>> +
>> +    Will be moved to a more generic module and imported in the future.
>> +    """
>> +
>> +    @staticmethod
>> +    def get_git_ignored_paths(directory_path: PurePath) -> List[Path]:
>> +        """Returns ignored files in this git repository.
>> +
>> +        Args:
>> +            directory_path (PurePath): Path to the git directory.
>> +
>> +        Returns:
>> +            List[Path]: List of file absolute paths to all files ignored
>> +                        in this git repository. If git is not found, an
>> empty
>> +                        list will be returned.
>> +        """
>> +        if not shutil.which("git"):
>> +            logging.warn(
>> +                "Git is not found on this system. Git submodule paths
>> will "
>> +                "not be considered.")
>> +            return []
>> +
>> +        out_stream_buffer = StringIO()
>> +        exit_code = RunCmd("git", "ls-files --other",
>> +                           workingdir=str(directory_path),
>> +                           outstream=out_stream_buffer,
>> +                           logging_level=logging.NOTSET)
>> +        if exit_code != 0:
>> +            return []
>> +
>> +        rel_paths = out_stream_buffer.getvalue().strip().splitlines()
>> +        abs_paths = []
>> +        for path in rel_paths:
>> +            abs_paths.append(Path(directory_path, path))
>> +        return abs_paths
>> +
>> +    @staticmethod
>> +    def get_git_submodule_paths(directory_path: PurePath) -> List[Path]:
>> +        """Returns submodules in the given workspace directory.
>> +
>> +        Args:
>> +            directory_path (PurePath): Path to the git directory.
>> +
>> +        Returns:
>> +            List[Path]: List of directory absolute paths to the root of
>> +            each submodule found from this folder. If submodules are not
>> +            found, an empty list will be returned.
>> +        """
>> +        if not shutil.which("git"):
>> +            return []
>> +
>> +        if os.path.isfile(directory_path.joinpath(".gitmodules")):
>> +            out_stream_buffer = StringIO()
>> +            exit_code = RunCmd(
>> +                "git", "config --file .gitmodules --get-regexp path",
>> +                workingdir=str(directory_path),
>> +                outstream=out_stream_buffer,
>> +                logging_level=logging.NOTSET)
>> +            if exit_code != 0:
>> +                return []
>> +
>> +            submodule_paths = []
>> +            for line in
>> out_stream_buffer.getvalue().strip().splitlines():
>> +                submodule_paths.append(
>> +                    Path(directory_path, line.split()[1]))
>> +
>> +            return submodule_paths
>> +        else:
>> +            return []
>> +
>> +
>> +class QuietFilter(logging.Filter):
>> +    """A logging filter that temporarily suppresses message output."""
>> +
>> +    def __init__(self, quiet: bool = False):
>> +        """Class constructor method.
>> +
>> +        Args:
>> +            quiet (bool, optional): Indicates if messages are currently
>> being
>> +            printed (False) or not (True). Defaults to False.
>> +        """
>> +
>> +        self._quiet = quiet
>> +
>> +    def filter(self, record: logging.LogRecord) -> bool:
>> +        """Quiet filter method.
>> +
>> +        Args:
>> +            record (logging.LogRecord): A log record object that the
>> filter is
>> +            applied to.
>> +
>> +        Returns:
>> +            bool: True if messages are being suppressed. Otherwise,
>> False.
>> +        """
>> +        return not self._quiet
>> +
>> +
>> +class ProgressFilter(logging.Filter):
>> +    """A logging filter that suppresses 'Progress' messages."""
>> +
>> +    def filter(self, record: logging.LogRecord) -> bool:
>> +        """Progress filter method.
>> +
>> +        Args:
>> +            record (logging.LogRecord): A log record object that the
>> filter is
>> +            applied to.
>> +
>> +        Returns:
>> +            bool: True if the message is not a 'Progress' message.
>> Otherwise,
>> +            False.
>> +        """
>> +        return not record.getMessage().startswith("\rProgress")
>> +
>> +
>> +class CacheDuringProgressFilter(logging.Filter):
>> +    """A logging filter that suppresses messages during progress
>> operations."""
>> +
>> +    _message_cache = []
>> +
>> +    @property
>> +    def message_cache(self) -> List[logging.LogRecord]:
>> +        """Contains a cache of messages accumulated during time of
>> operation.
>> +
>> +        Returns:
>> +            List[logging.LogRecord]: List of log records stored while
>> the
>> +            filter was active.
>> +        """
>> +        return self._message_cache
>> +
>> +    def filter(self, record: logging.LogRecord):
>> +        """Cache progress filter that suppresses messages during
>> progress
>> +           display output.
>> +
>> +        Args:
>> +            record (logging.LogRecord): A log record to cache.
>> +        """
>> +        self._message_cache.append(record)
>> +
>> +
>> +def check_debug_macros(macros: Iterable[Dict[str, str]],
>> +                       file_dbg_path: str,
>> +                       **macro_subs: str
>> +                       ) -> Tuple[int, int, int]:
>> +    """Checks if debug macros contain formatting errors.
>> +
>> +    Args:
>> +        macros (Iterable[Dict[str, str]]): : A groupdict of macro
>> matches.
>> +        This is an iterable of dictionaries with group names from the
>> regex
>> +        match as the key and the matched string as the value for the
>> key.
>> +
>> +        file_dbg_path (str): The file path (or other custom string) to
>> display
>> +        in debug messages.
>> +
>> +        macro_subs (Dict[str,str]): Variable-length keyword and
>> replacement
>> +        value string pairs to substitute during debug macro checks.
>> +
>> +    Returns:
>> +        Tuple[int, int, int]: A tuple of the number of formatting
>> errors,
>> +        number of print specifiers, and number of arguments for the
>> macros
>> +        given.
>> +    """
>> +
>> +    macro_subs = {k.lower(): v for k, v in macro_subs.items()}
>> +
>> +    arg_cnt, failure_cnt, print_spec_cnt = 0, 0, 0
>> +    for macro in macros:
>> +        # Special Specifier Handling
>> +        processed_dbg_str = macro['dbg_str'].strip().lower()
>> +
>> +        logging.debug(f"Inspecting macro: {macro}")
>> +
>> +        # Make any macro substitutions so further processing is applied
>> +        # to the substituted value.
>> +        for k in macro_subs.keys():
>> +            processed_dbg_str = processed_dbg_str.replace(k,
>> macro_subs[k])
>> +
>> +        logging.debug("Debug macro string after replacements: "
>> +                      f"{processed_dbg_str}")
>> +
>> +        # These are very rarely used in debug strings. They are somewhat
>> +        # more common in HII code to control text displayed on the
>> +        # console. Due to the rarity and likelihood usage is a mistake,
>> +        # a warning is shown if found.
>> +        specifier_display_replacements = ['%n', '%h', '%e', '%b', '%v']
>> +        for s in specifier_display_replacements:
>> +            if s in processed_dbg_str:
>> +                logging.warning(f"File: {file_dbg_path}")
>> +                logging.warning(f"  {s} found in string and ignored:")
>> +                logging.warning(f"  \"{processed_dbg_str}\"")
>> +                processed_dbg_str = processed_dbg_str.replace(s, '')
>> +
>> +        # These are miscellaneous print specifiers that do not require
>> +        # special parsing and simply need to be replaced since they do
>> +        # have a corresponding argument associated with them.
>> +        specifier_other_replacements = ['%%', '\r', '\n']
>> +        for s in specifier_other_replacements:
>> +            if s in processed_dbg_str:
>> +                processed_dbg_str = processed_dbg_str.replace(s, '')
>> +
>> +        processed_dbg_str = re.sub(
>> +            r'%[.\-+ ,Ll0-9]*\*[.\-+ ,Ll0-9]*[a-zA-Z]', '%_%_',
>> +            processed_dbg_str)
>> +        logging.debug(f"Final macro before print specifier scan: "
>> +                      f"{processed_dbg_str}")
>> +
>> +        print_spec_cnt = processed_dbg_str.count('%')
>> +
>> +        # Need to take into account parentheses between args in function
>> +        # calls that might be in the args list. Use regex module for
>> +        # this one since the recursive pattern match helps simplify
>> +        # only matching commas outside nested call groups.
>> +        if macro['dbg_args'] is None:
>> +            processed_arg_str = ""
>> +        else:
>> +            processed_arg_str = macro['dbg_args'].strip()
>> +
>> +        argument_other_replacements = ['\r', '\n']
>> +        for r in argument_other_replacements:
>> +            if s in processed_arg_str:
>> +                processed_arg_str = processed_arg_str.replace(s, '')
>> +        processed_arg_str = re.sub(r'  +', ' ', processed_arg_str)
>> +
>> +        # Handle special case of commas in arg strings - remove them for
>> +        # final count to pick up correct number of argument separating
>> +        # commas.
>> +        processed_arg_str = re.sub(
>> +                                r'([\"\'])(?:|\\.|[^\\])*?(\1)',
>> +                                '',
>> +                                processed_arg_str)
>> +
>> +        arg_matches = regex.findall(
>> +            r'(?:\((?:[^)(]+|(?R))*+\))|(,)',
>> +            processed_arg_str,
>> +            regex.MULTILINE)
>> +
>> +        arg_cnt = 0
>> +        if processed_arg_str != '':
>> +            arg_cnt = arg_matches.count(',')
>> +
>> +        if print_spec_cnt != arg_cnt:
>> +            logging.error(f"File: {file_dbg_path}")
>> +            logging.error(f"  Message         = {macro['dbg_str']}")
>> +            logging.error(f"  Arguments       =
>> \"{processed_arg_str}\"")
>> +            logging.error(f"  Specifier Count = {print_spec_cnt}")
>> +            logging.error(f"  Argument Count  = {arg_cnt}")
>> +
>> +            failure_cnt += 1
>> +
>> +    return failure_cnt, print_spec_cnt, arg_cnt
>> +
>> +
>> +def get_debug_macros(file_contents: str) -> List[Dict[str, str]]:
>> +    """Extract debug macros from the given file contents.
>> +
>> +    Args:
>> +        file_contents (str): A string of source file contents that may
>> +        contain debug macros.
>> +
>> +    Returns:
>> +        List[Dict[str, str]]: A groupdict of debug macro regex matches
>> +        within the file contents provided.
>> +    """
>> +
>> +    # This is the main regular expression that is responsible for
>> identifying
>> +    # DEBUG macros within source files and grouping the macro message
>> string
>> +    # and macro arguments strings so they can be further processed.
>> +    r = regex.compile(
>> +
>> r'(?>(?P<prologue>DEBUG\s*\(\s*\((?:.*?,))(?:\s*))(?P<dbg_str>.*?(?:\"'
>> +
>> r'(?:[^\"\\]|\\.)*\".*?)*)(?:(?(?=,)(?<dbg_args>.*?(?=(?:\s*\)){2}\s*;'
>> +        r'))))(?:\s*\)){2,};?',
>> +        regex.MULTILINE | regex.DOTALL)
>> +    return [m.groupdict() for m in r.finditer(file_contents)]
>> +
>> +
>> +def check_macros_in_string(src_str: str,
>> +                           file_dbg_path: str,
>> +                           **macro_subs: str) -> Tuple[int, int, int]:
>> +    """Checks for debug macro formatting errors in a string.
>> +
>> +    Args:
>> +        src_str (str): Contents of the string with debug macros.
>> +
>> +        file_dbg_path (str): The file path (or other custom string) to
>> display
>> +        in debug messages.
>> +
>> +        macro_subs (Dict[str,str]): Variable-length keyword and
>> replacement
>> +        value string pairs to substitute during debug macro checks.
>> +
>> +    Returns:
>> +        Tuple[int, int, int]: A tuple of the number of formatting
>> errors,
>> +        number of print specifiers, and number of arguments for the
>> macros
>> +        in the string given.
>> +    """
>> +    return check_debug_macros(
>> +                get_debug_macros(src_str), file_dbg_path, **macro_subs)
>> +
>> +
>> +def check_macros_in_file(file: PurePath,
>> +                         file_dbg_path: str,
>> +                         show_utf8_decode_warning: bool = False,
>> +                         **macro_subs: str) -> Tuple[int, int, int]:
>> +    """Checks for debug macro formatting errors in a file.
>> +
>> +    Args:
>> +        file (PurePath): The file path to check.
>> +
>> +        file_dbg_path (str): The file path (or other custom string) to
>> display
>> +        in debug messages.
>> +
>> +        show_utf8_decode_warning (bool, optional): Indicates whether to
>> show
>> +        warnings if UTF-8 files fail to decode. Defaults to False.
>> +
>> +        macro_subs (Dict[str,str]): Variable-length keyword and
>> replacement
>> +        value string pairs to substitute during debug macro checks.
>> +
>> +    Returns:
>> +        Tuple[int, int, int]: A tuple of the number of formatting
>> errors,
>> +        number of print specifiers, and number of arguments for the
>> macros
>> +        in the file given.
>> +    """
>> +    try:
>> +        return check_macros_in_string(
>> +                    file.read_text(encoding='utf-8'), file_dbg_path,
>> +                    **macro_subs)
>> +    except UnicodeDecodeError as e:
>> +        if show_utf8_decode_warning:
>> +            logging.warning(
>> +                f"{file_dbg_path} UTF-8 decode error.\n"
>> +                "         Debug macro code check skipped!\n"
>> +                f"         -> {str(e)}")
>> +    return 0, 0, 0
>> +
>> +
>> +def check_macros_in_directory(directory: PurePath,
>> +                              file_extensions: Iterable[str] = ('.c',),
>> +                              ignore_git_ignore_files: Optional[bool] =
>> True,
>> +                              ignore_git_submodules: Optional[bool] =
>> True,
>> +                              show_progress_bar: Optional[bool] = True,
>> +                              show_utf8_decode_warning: bool = False,
>> +                              **macro_subs: str
>> +                              ) -> int:
>> +    """Checks files with the given extension in the given directory for
>> debug
>> +       macro formatting errors.
>> +
>> +    Args:
>> +        directory (PurePath): The path to the directory to check.
>> +        file_extensions (Iterable[str], optional): An iterable of
>> strings
>> +        representing file extensions to check. Defaults to ('.c',).
>> +
>> +        ignore_git_ignore_files (Optional[bool], optional): Indicates
>> whether
>> +        files ignored by git should be ignored for the debug macro
>> check.
>> +        Defaults to True.
>> +
>> +        ignore_git_submodules (Optional[bool], optional): Indicates
>> whether
>> +        files located in git submodules should not be checked. Defaults
>> to
>> +        True.
>> +
>> +        show_progress_bar (Optional[bool], optional): Indicates whether
>> to
>> +        show a progress bar to show progress status while checking
>> macros.
>> +        This is more useful on a very large directories. Defaults to
>> True.
>> +
>> +        show_utf8_decode_warning (bool, optional): Indicates whether to
>> show
>> +        warnings if UTF-8 files fail to decode. Defaults to False.
>> +
>> +        macro_subs (Dict[str,str]): Variable-length keyword and
>> replacement
>> +        value string pairs to substitute during debug macro checks.
>> +
>> +    Returns:
>> +        int: Count of debug macro errors in the directory.
>> +    """
>> +    def _get_file_list(root_directory: PurePath,
>> +                       extensions: Iterable[str]) -> List[Path]:
>> +        """Returns a list of files recursively located within the path.
>> +
>> +        Args:
>> +            root_directory (PurePath): A directory Path object to the
>> root
>> +            folder.
>> +
>> +            extensions (Iterable[str]): An iterable of strings that
>> +            represent file extensions to recursively search for within
>> +            root_directory.
>> +
>> +        Returns:
>> +            List[Path]: List of file Path objects to files found in the
>> +            given directory with the given extensions.
>> +        """
>> +        def _show_file_discovered_message(file_count: int,
>> +                                          elapsed_time: float) -> None:
>> +            print(f"\rDiscovered {file_count:,} files in",
>> +                  f"{current_start_delta:-.0f}s"
>> +                  f"{'.' * min(int(current_start_delta), 40)}",
>> end="\r")
>> +
>> +        start_time = timeit.default_timer()
>> +        previous_indicator_time = start_time
>> +
>> +        files = []
>> +        for file in root_directory.rglob('*'):
>> +            if file.suffix in extensions:
>> +                files.append(Path(file))
>> +
>> +            # Give an indicator progress is being made
>> +            # This has a negligible impact on overall performance
>> +            # with print emission limited to half second intervals.
>> +            current_time = timeit.default_timer()
>> +            current_start_delta = current_time - start_time
>> +
>> +            if current_time - previous_indicator_time >= 0.5:
>> +                # Since this rewrites the line, it can be considered a
>> form
>> +                # of progress bar
>> +                if show_progress_bar:
>> +                    _show_file_discovered_message(len(files),
>> +                                                  current_start_delta)
>> +                previous_indicator_time = current_time
>> +
>> +        if show_progress_bar:
>> +            _show_file_discovered_message(len(files),
>> current_start_delta)
>> +            print()
>> +
>> +        return files
>> +
>> +    logging.info(f"Checking Debug Macros in directory: "
>> +                 f"{directory.resolve()}\n")
>> +
>> +    logging.info("Gathering the overall file list. This might take a"
>> +                 "while.\n")
>> +
>> +    start_time = timeit.default_timer()
>> +    file_list = set(_get_file_list(directory, file_extensions))
>> +    end_time = timeit.default_timer() - start_time
>> +
>> +    logging.debug(f"[PERF] File search found {len(file_list):,} files in
>> "
>> +                  f"{end_time:.2f} seconds.")
>> +
>> +    if ignore_git_ignore_files:
>> +        logging.info("Getting git ignore files...")
>> +        start_time = timeit.default_timer()
>> +        ignored_file_paths = GitHelpers.get_git_ignored_paths(directory)
>> +        end_time = timeit.default_timer() - start_time
>> +
>> +        logging.debug(f"[PERF] File ignore gathering took {end_time:.2f}
>> "
>> +                      f"seconds.")
>> +
>> +        logging.info("Ignoring git ignore files...")
>> +        logging.debug(f"File list count before git ignore
>> {len(file_list):,}")
>> +        start_time = timeit.default_timer()
>> +        file_list = file_list.difference(ignored_file_paths)
>> +        end_time = timeit.default_timer() - start_time
>> +        logging.info(f"  {len(ignored_file_paths):,} files are ignored
>> by git")
>> +        logging.info(f"  {len(file_list):,} files after removing "
>> +                     f"ignored files")
>> +
>> +        logging.debug(f"[PERF] File ignore calculation took
>> {end_time:.2f} "
>> +                      f"seconds.")
>> +
>> +    if ignore_git_submodules:
>> +        logging.info("Ignoring git submodules...")
>> +        submodule_paths = GitHelpers.get_git_submodule_paths(directory)
>> +        if submodule_paths:
>> +            logging.debug(f"File list count before git submodule
>> exclusion "
>> +                          f"{len(file_list):,}")
>> +            start_time = timeit.default_timer()
>> +            file_list = [f for f in file_list
>> +                         if not f.is_relative_to(*submodule_paths)]
>> +            end_time = timeit.default_timer() - start_time
>> +
>> +            for path in enumerate(submodule_paths):
>> +                logging.debug("  {0}. {1}".format(*path))
>> +
>> +            logging.info(f"  {len(submodule_paths):,} submodules found")
>> +            logging.info(f"  {len(file_list):,} files will be examined
>> after "
>> +                         f"excluding files in submodules")
>> +
>> +            logging.debug(f"[PERF] Submodule exclusion calculation took
>> "
>> +                          f"{end_time:.2f} seconds.")
>> +        else:
>> +            logging.warning("No submodules found")
>> +
>> +    logging.info(f"\nStarting macro check on {len(file_list):,} files.")
>> +
>> +    cache_progress_filter = CacheDuringProgressFilter()
>> +    handler = next((h for h in logging.getLogger().handlers if
>> h.get_name() ==
>> +                   'stdout_logger_handler'), None)
>> +
>> +    if handler is not None:
>> +        handler.addFilter(cache_progress_filter)
>> +
>> +    start_time = timeit.default_timer()
>> +
>> +    failure_cnt, file_cnt = 0, 0
>> +    for file_cnt, file in enumerate(file_list):
>> +        file_rel_path = str(file.relative_to(directory))
>> +        failure_cnt += check_macros_in_file(
>> +                            file, file_rel_path,
>> show_utf8_decode_warning,
>> +                            **macro_subs)[0]
>> +        if show_progress_bar:
>> +            _show_progress(file_cnt, len(file_list),
>> +                           f" {failure_cnt} errors" if failure_cnt > 0
>> else "")
>> +
>> +    if show_progress_bar:
>> +        _show_progress(len(file_list), len(file_list),
>> +                       f" {failure_cnt} errors" if failure_cnt > 0 else
>> "")
>> +        print("\n", flush=True)
>> +
>> +    end_time = timeit.default_timer() - start_time
>> +
>> +    if handler is not None:
>> +        handler.removeFilter(cache_progress_filter)
>> +
>> +        for record in cache_progress_filter.message_cache:
>> +            handler.emit(record)
>> +
>> +    logging.debug(f"[PERF] The macro check operation took {end_time:.2f}
>> "
>> +                  f"seconds.")
>> +
>> +    _log_failure_count(failure_cnt, file_cnt)
>> +
>> +    return failure_cnt
>> +
>> +
>> +def _log_failure_count(failure_count: int, file_count: int) -> None:
>> +    """Logs the failure count.
>> +
>> +    Args:
>> +        failure_count (int): Count of failures to log.
>> +
>> +        file_count (int): Count of files with failures.
>> +    """
>> +    if failure_count > 0:
>> +        logging.error("\n")
>> +        logging.error(f"{failure_count:,} debug macro errors in "
>> +                      f"{file_count:,} files")
>> +
>> +
>> +def _show_progress(step: int, total: int, suffix: str = '') -> None:
>> +    """Print progress of tick to total.
>> +
>> +    Args:
>> +        step (int): The current step count.
>> +
>> +        total (int): The total step count.
>> +
>> +        suffix (str): String to print at the end of the progress bar.
>> +    """
>> +    global _progress_start_time
>> +
>> +    if step == 0:
>> +        _progress_start_time = timeit.default_timer()
>> +
>> +    terminal_col = shutil.get_terminal_size().columns
>> +    var_consume_len = (len("Progress|\u2588| 000.0% Complete 000s") +
>> +                       len(suffix))
>> +    avail_len = terminal_col - var_consume_len
>> +
>> +    percent = f"{100 * (step / float(total)):3.1f}"
>> +    filled = int(avail_len * step // total)
>> +    bar = '\u2588' * filled + '-' * (avail_len - filled)
>> +    step_time = timeit.default_timer() - _progress_start_time
>> +
>> +    print(f'\rProgress|{bar}| {percent}% Complete {step_time:-3.0f}s'
>> +          f'{suffix}', end='\r')
>> +
>> +
>> +def _module_invocation_check_macros_in_directory_wrapper() -> int:
>> +    """Provides an command-line argument wrapper for checking debug
>> macros.
>> +
>> +    Returns:
>> +        int: The system exit code value.
>> +    """
>> +    import argparse
>> +    import builtins
>> +
>> +    def _check_dir_path(dir_path: str) -> bool:
>> +        """Returns the absolute path if the path is a directory."
>> +
>> +        Args:
>> +            dir_path (str): A directory file system path.
>> +
>> +        Raises:
>> +            NotADirectoryError: The directory path given is not a
>> directory.
>> +
>> +        Returns:
>> +            bool: True if the path is a directory else False.
>> +        """
>> +        abs_dir_path = os.path.abspath(dir_path)
>> +        if os.path.isdir(dir_path):
>> +            return abs_dir_path
>> +        else:
>> +            raise NotADirectoryError(abs_dir_path)
>> +
>> +    def _check_file_path(file_path: str) -> bool:
>> +        """Returns the absolute path if the path is a file."
>> +
>> +        Args:
>> +            file_path (str): A file path.
>> +
>> +        Raises:
>> +            FileExistsError: The path is not a valid file.
>> +
>> +        Returns:
>> +            bool: True if the path is a valid file else False.
>> +        """
>> +        abs_file_path = os.path.abspath(file_path)
>> +        if os.path.isfile(file_path):
>> +            return abs_file_path
>> +        else:
>> +            raise FileExistsError(file_path)
>> +
>> +    def _quiet_print(*args, **kwargs):
>> +        """Replaces print when quiet is requested to prevent printing
>> messages.
>> +        """
>> +        pass
>> +
>> +    root_logger = logging.getLogger()
>> +    root_logger.setLevel(logging.DEBUG)
>> +
>> +    stdout_logger_handler = logging.StreamHandler(sys.stdout)
>> +    stdout_logger_handler.set_name('stdout_logger_handler')
>> +    stdout_logger_handler.setLevel(logging.INFO)
>> +    stdout_logger_handler.setFormatter(logging.Formatter('%(message)s'))
>> +    root_logger.addHandler(stdout_logger_handler)
>> +
>> +    parser = argparse.ArgumentParser(
>> +                        prog=PROGRAM_NAME,
>> +                        description=(
>> +                            "Checks for debug macro formatting "
>> +                            "errors within files recursively located
>> within "
>> +                            "a given directory."),
>> +                        formatter_class=RawTextHelpFormatter)
>> +
>> +    io_req_group = parser.add_mutually_exclusive_group(required=True)
>> +    io_opt_group = parser.add_argument_group(
>> +                            "Optional input and output")
>> +    git_group = parser.add_argument_group("Optional git control")
>> +
>> +    io_req_group.add_argument('-w', '--workspace-directory',
>> +                              type=_check_dir_path,
>> +                              help="Directory of source files to
>> check.\n\n")
>> +
>> +    io_req_group.add_argument('-i', '--input-file', nargs='?',
>> +                              type=_check_file_path,
>> +                              help="File path for an input file to
>> check.\n\n"
>> +                                   "Note that some other options do not
>> apply "
>> +                                   "if a single file is specified such
>> as "
>> +                                   "the\ngit options and file
>> extensions.\n\n")
>> +
>> +    io_opt_group.add_argument('-l', '--log-file',
>> +                              nargs='?',
>> +                              default=None,
>> +                              const='debug_macro_check.log',
>> +                              help="File path for log output.\n"
>> +                                   "(default: if the flag is given with
>> no "
>> +                                   "file path then a file called\n"
>> +                                   "debug_macro_check.log is created and
>> used "
>> +                                   "in the current directory)\n\n")
>> +
>> +    io_opt_group.add_argument('-s', '--substitution-file',
>> +                              type=_check_file_path,
>> +                              help="A substitution YAML file specifies
>> string "
>> +                                   "substitutions to perform within the
>> debug "
>> +                                   "macro.\n\nThis is intended to be a
>> simple "
>> +                                   "mechanism to expand the rare cases
>> of pre-"
>> +                                   "processor\nmacros without directly "
>> +                                   "involving the pre-processor. The
>> file "
>> +                                   "consists of one or more\nstring
>> value "
>> +                                   "pairs where the key is the
>> identifier to "
>> +                                   "replace and the value is the
>> value\nto "
>> +                                   "replace it with.\n\nThis can also be
>> used "
>> +                                   "as a method to ignore results by "
>> +                                   "replacing the problematic
>> string\nwith a "
>> +                                   "different string.\n\n")
>> +
>> +    io_opt_group.add_argument('-v', '--verbose-log-file',
>> +                              action='count',
>> +                              default=0,
>> +                              help="Set file logging verbosity level.\n"
>> +                                   " - None:    Info & > level
>> messages\n"
>> +                                   " - '-v':    + Debug level
>> messages\n"
>> +                                   " - '-vv':   + File name and
>> function\n"
>> +                                   " - '-vvv':  + Line number\n"
>> +                                   " - '-vvvv': + Timestamp\n"
>> +                                   "(default: verbose logging is not
>> enabled)"
>> +                                   "\n\n")
>> +
>> +    io_opt_group.add_argument('-n', '--no-progress-bar',
>> action='store_true',
>> +                              help="Disables progress bars.\n"
>> +                                   "(default: progress bars are used in
>> some"
>> +                                   "places to show progress)\n\n")
>> +
>> +    io_opt_group.add_argument('-q', '--quiet', action='store_true',
>> +                              help="Disables console output.\n"
>> +                                   "(default: console output is
>> enabled)\n\n")
>> +
>> +    io_opt_group.add_argument('-u', '--utf8w', action='store_true',
>> +                              help="Shows warnings for file UTF-8 decode
>> "
>> +                                   "errors.\n"
>> +                                   "(default: UTF-8 decode errors are
>> not "
>> +                                   "shown)\n\n")
>> +
>> +    git_group.add_argument('-df', '--do-not-ignore-git-ignore-files',
>> +                           action='store_true',
>> +                           help="Do not ignore git ignored files.\n"
>> +                                "(default: files in git ignore files are
>> "
>> +                                "ignored)\n\n")
>> +
>> +    git_group.add_argument('-ds', '--do-not-ignore-git_submodules',
>> +                           action='store_true',
>> +                           help="Do not ignore files in git
>> submodules.\n"
>> +                                "(default: files in git submodules are "
>> +                                "ignored)\n\n")
>> +
>> +    parser.add_argument('-e', '--extensions', nargs='*', default=['.c'],
>> +                        help="List of file extensions to include.\n"
>> +                             "(default: %(default)s)")
>> +
>> +    args = parser.parse_args()
>> +
>> +    if args.quiet:
>> +        # Don't print in the few places that directly print
>> +        builtins.print = _quiet_print
>> +    stdout_logger_handler.addFilter(QuietFilter(args.quiet))
>> +
>> +    if args.log_file:
>> +        file_logger_handler =
>> logging.FileHandler(filename=args.log_file,
>> +                                                  mode='w',
>> encoding='utf-8')
>> +
>> +        # In an ideal world, everyone would update to the latest Python
>> +        # minor version (3.10) after a few weeks/months. Since that's
>> not the
>> +        # case, resist from using structural pattern matching in Python
>> 3.10.
>> +        # https://peps.python.org/pep-0636/
>> +
>> +        if args.verbose_log_file == 0:
>> +            file_logger_handler.setLevel(logging.INFO)
>> +            file_logger_formatter = logging.Formatter(
>> +                '%(levelname)-8s %(message)s')
>> +        elif args.verbose_log_file == 1:
>> +            file_logger_handler.setLevel(logging.DEBUG)
>> +            file_logger_formatter = logging.Formatter(
>> +                '%(levelname)-8s %(message)s')
>> +        elif args.verbose_log_file == 2:
>> +            file_logger_handler.setLevel(logging.DEBUG)
>> +            file_logger_formatter = logging.Formatter(
>> +                '[%(filename)s - %(funcName)20s() ] %(levelname)-8s '
>> +                '%(message)s')
>> +        elif args.verbose_log_file == 3:
>> +            file_logger_handler.setLevel(logging.DEBUG)
>> +            file_logger_formatter = logging.Formatter(
>> +                '[%(filename)s:%(lineno)s - %(funcName)20s() ] '
>> +                '%(levelname)-8s %(message)s')
>> +        elif args.verbose_log_file == 4:
>> +            file_logger_handler.setLevel(logging.DEBUG)
>> +            file_logger_formatter = logging.Formatter(
>> +                '%(asctime)s [%(filename)s:%(lineno)s - %(funcName)20s()
>> ]'
>> +                ' %(levelname)-8s %(message)s')
>> +        else:
>> +            file_logger_handler.setLevel(logging.DEBUG)
>> +            file_logger_formatter = logging.Formatter(
>> +                '%(asctime)s [%(filename)s:%(lineno)s - %(funcName)20s()
>> ]'
>> +                ' %(levelname)-8s %(message)s')
>> +
>> +        file_logger_handler.addFilter(ProgressFilter())
>> +        file_logger_handler.setFormatter(file_logger_formatter)
>> +        root_logger.addHandler(file_logger_handler)
>> +
>> +    logging.info(PROGRAM_NAME + "\n")
>> +
>> +    substitution_data = {}
>> +    if args.substitution_file:
>> +        logging.info(f"Loading substitution file
>> {args.substitution_file}")
>> +        with open(args.substitution_file, 'r') as sf:
>> +            substitution_data = yaml.safe_load(sf)
>> +
>> +    if args.workspace_directory:
>> +        return check_macros_in_directory(
>> +                    Path(args.workspace_directory),
>> +                    args.extensions,
>> +                    not args.do_not_ignore_git_ignore_files,
>> +                    not args.do_not_ignore_git_submodules,
>> +                    not args.no_progress_bar,
>> +                    args.utf8w,
>> +                    **substitution_data)
>> +    else:
>> +        curr_dir = Path(__file__).parent
>> +        input_file = Path(args.input_file)
>> +
>> +        rel_path = str(input_file)
>> +        if input_file.is_relative_to(curr_dir):
>> +            rel_path = str(input_file.relative_to(curr_dir))
>> +
>> +        logging.info(f"Checking Debug Macros in File: "
>> +                     f"{input_file.resolve()}\n")
>> +
>> +        start_time = timeit.default_timer()
>> +        failure_cnt = check_macros_in_file(
>> +                        input_file,
>> +                        rel_path,
>> +                        args.utf8w,
>> +                        **substitution_data)[0]
>> +        end_time = timeit.default_timer() - start_time
>> +
>> +        logging.debug(f"[PERF] The file macro check operation took "
>> +                      f"{end_time:.2f} seconds.")
>> +
>> +        _log_failure_count(failure_cnt, 1)
>> +
>> +        return failure_cnt
>> +
>> +
>> +if __name__ == '__main__':
>> +    # The exit status value is the number of macro formatting errors
>> found.
>> +    # Therefore, if no macro formatting errors are found, 0 is returned.
>> +    # Some systems require the return value to be in the range 0-127, so
>> +    # a lower maximum of 100 is enforced to allow a wide range of
>> potential
>> +    # values with a reasonably large maximum.
>> +    try:
>> +
>> sys.exit(max(_module_invocation_check_macros_in_directory_wrapper(),
>> +                 100))
>> +    except KeyboardInterrupt:
>> +        logging.warning("Exiting due to keyboard interrupt.")
>> +        # Actual formatting errors are only allowed to reach 100.
>> +        # 101 signals a keyboard interrupt.
>> +        sys.exit(101)
>> +    except FileExistsError as e:
>> +        # 102 signals a file not found error.
>> +        logging.critical(f"Input file {e.args[0]} does not exist.")
>> +        sys.exit(102)
>> diff --git a/.pytool/Plugin/DebugMacroCheck/Readme.md
>> b/.pytool/Plugin/DebugMacroCheck/Readme.md
>> new file mode 100644
>> index 000000000000..33f1ad9790ed
>> --- /dev/null
>> +++ b/.pytool/Plugin/DebugMacroCheck/Readme.md
>> @@ -0,0 +1,253 @@
>> +# Debug Macro Check
>> +
>> +This Python application scans all files in a build package for debug
>> macro formatting issues. It is intended to be a
>> +fundamental build-time check that is part of a normal developer build
>> process to catch errors right away.
>> +
>> +As a build plugin, it is capable of finding these errors early in the
>> development process after code is initially
>> +written to ensure that all code tested is free of debug macro formatting
>> errors. These errors often creep into debug
>> +prints in error conditions that are not frequently executed making debug
>> even more difficult and confusing when they
>> +are encountered. In other cases, debug macros with these errors in the
>> main code path can lead to unexpected behavior
>> +when executed. As a standalone script, it can be easily run manually or
>> integrated into other CI processes.
>> +
>> +The plugin is part of a set of debug macro check scripts meant to be
>> relatively portable so they can be applied to
>> +additional code bases with minimal effort.
>> +
>> +## 1. BuildPlugin/DebugMacroCheckBuildPlugin.py
>> +
>> +This is the build plugin. It is discovered within the Stuart Self-
>> Describing Environment (SDE) due to the accompanying
>> +file `DebugMacroCheck_plugin_in.yaml`.
>> +
>> +Since macro errors are considered a coding bug that should be found and
>> fixed during the build phase of the developer
>> +process (before debug and testing), this plugin is run in pre-build. It
>> will run within the scope of the package
>> +being compiled. For a platform build, this means it will run against the
>> package being built. In a CI build, it will
>> +run in pre-build for each package as each package is built.
>> +
>> +The build plugin has the following attributes:
>> +
>> +  1. Registered at `global` scope. This means it will always run.
>> +
>> +  2. Called only on compilable build targets (i.e. does nothing on `"NO-
>> TARGET"`).
>> +
>> +  3. Runs as a pre-build step. This means it gives results right away to
>> ensure compilation follows on a clean slate.
>> +     This also means it runs in platform build and CI. It is run in CI
>> as a pre-build step when the `CompilerPlugin`
>> +     compiles code. This ensures even if the plugin was not run locally,
>> all code submissions have been checked.
>> +
>> +  4. Reports any errors in the build log and fails the build upon error
>> making it easy to discover problems.
>> +
>> +  5. Supports two methods of configuration via "substitution strings":
>> +
>> +     1. By setting a build variable called `DEBUG_MACRO_CHECK_SUB_FILE`
>> with the name of a substitution YAML file to
>> +        use.
>> +
>> +        **Example:**
>> +
>> +        ```python
>> +        shell_environment.GetBuildVars().SetValue(
>> +
>> "DEBUG_MACRO_CHECK_SUB_FILE",
>> +
>> os.path.join(self.GetWorkspaceRoot(), "DebugMacroCheckSub.yaml"),
>> +                                            "Set in CISettings.py")
>> +        ```
>> +
>> +        **Substitution File Content Example:**
>> +
>> +        ```yaml
>> +        ---
>> +        # OvmfPkg/CpuHotplugSmm/ApicId.h
>> +        # Reason: Substitute with macro value
>> +        FMT_APIC_ID: 0x%08x
>> +
>> +        # DynamicTablesPkg/Include/ConfigurationManagerObject.h
>> +        # Reason: Substitute with macro value
>> +        FMT_CM_OBJECT_ID: 0x%lx
>> +
>> +        # OvmfPkg/IntelTdx/TdTcg2Dxe/TdTcg2Dxe.c
>> +        # Reason: Acknowledging use of two format specifiers in string
>> with one argument
>> +        #         Replace ternary operator in debug string with single
>> specifier
>> +        'Index == COLUME_SIZE/2 ? " | %02x" : " %02x"': "%d"
>> +
>> +        #
>> DynamicTablesPkg/Library/Common/TableHelperLib/ConfigurationManagerObject
>> Parser.c
>> +        # ShellPkg/Library/UefiShellAcpiViewCommandLib/AcpiParser.c
>> +        # Reason: Acknowledge that string *should* expand to one
>> specifier
>> +        #         Replace variable with expected number of specifiers
>> (1)
>> +        Parser[Index].Format: "%d"
>> +        ```
>> +
>> +     2. By entering the string substitutions directory into a dictionary
>> called `StringSubstitutions` in a
>> +        `DebugMacroCheck` section of the package CI YAML file.
>> +
>> +        **Example:**
>> +
>> +        ```yaml
>> +        "DebugMacroCheck": {
>> +          "StringSubstitutions": {
>> +            "SUB_A": "%Lx"
>> +          }
>> +        }
>> +        ```
>> +
>> +### Debug Macro Check Build Plugin: Simple Disable
>> +
>> +The build plugin can simply be disabled by setting an environment
>> variable named `"DISABLE_DEBUG_MACRO_CHECK"`. The
>> +plugin is disabled on existence of the variable. The contents of the
>> variable are not inspected at this time.
>> +
>> +## 2. DebugMacroCheck.py
>> +
>> +This is the main Python module containing the implementation logic. The
>> build plugin simply wraps around it.
>> +
>> +When first running debug macro check against a new, large code base, it
>> is recommended to first run this standalone
>> +script and address all of the issues and then enable the build plugin.
>> +
>> +The module supports a number of configuration parameters to ease debug
>> of errors and to provide flexibility for
>> +different build environments.
>> +
>> +### EDK 2 PyTool Library Dependency
>> +
>> +This script has minimal library dependencies. However, it has one
>> dependency you might not be familiar with on the
>> +Tianocore EDK 2 PyTool Library (edk2toollib):
>> +
>> +```py
>> +from edk2toollib.utility_functions import RunCmd
>> +```
>> +
>> +You simply need to install the following pip module to use this library:
>> `edk2-pytool-library`
>> +(e.g. `pip install edk2-pytool-library`)
>> +
>> +More information is available here:
>> +
>> +- PyPI page: [edk2-pytool-library](https://pypi.org/project/edk2-pytool-
>> library/)
>> +- GitHub repo: [tianocore/edk2-pytool-
>> library](https://github.com/tianocore/edk2-pytool-library)
>> +
>> +If you strongly prefer not including this additional dependency, the
>> functionality imported here is relatively
>> +simple to substitute with the Python
>> [`subprocess`](https://docs.python.org/3/library/subprocess.html) built-
>> in
>> +module.
>> +
>> +### Examples
>> +
>> +Simple run against current directory:
>> +
>> +`> python DebugMacroCheck.py -w .`
>> +
>> +Simple run against a single file:
>> +
>> +`> python DebugMacroCheck.py -i filename.c`
>> +
>> +Run against a directory with output placed into a file called
>> "debug_macro_check.log":
>> +
>> +`> python DebugMacroCheck.py -w . -l`
>> +
>> +Run against a directory with output placed into a file called
>> "custom.log" and debug log messages enabled:
>> +
>> +`> python DebugMacroCheck.py -w . -l custom.log -v`
>> +
>> +Run against a directory with output placed into a file called
>> "custom.log", with debug log messages enabled including
>> +python script function and line number, use a substitution file called
>> "file_sub.yaml", do not show the progress bar,
>> +and run against .c and .h files:
>> +
>> +`> python DebugMacroCheck.py -w . -l custom.log -vv -s file_sub.yaml -n
>> -e .c .h`
>> +
>> +> **Note**: It is normally not recommended to run against .h files as
>> they and many other non-.c files normally do
>> +  not have full `DEBUG` macro prints.
>> +
>> +```plaintext
>> +usage: Debug Macro Checker [-h] (-w WORKSPACE_DIRECTORY | -i
>> [INPUT_FILE]) [-l [LOG_FILE]] [-s SUBSTITUTION_FILE] [-v] [-n] [-q] [-u]
>> +                           [-df] [-ds] [-e [EXTENSIONS ...]]
>> +
>> +Checks for debug macro formatting errors within files recursively
>> located within a given directory.
>> +
>> +options:
>> +  -h, --help            show this help message and exit
>> +  -w WORKSPACE_DIRECTORY, --workspace-directory WORKSPACE_DIRECTORY
>> +                        Directory of source files to check.
>> +
>> +  -i [INPUT_FILE], --input-file [INPUT_FILE]
>> +                        File path for an input file to check.
>> +
>> +                        Note that some other options do not apply if a
>> single file is specified such as the
>> +                        git options and file extensions.
>> +
>> +  -e [EXTENSIONS ...], --extensions [EXTENSIONS ...]
>> +                        List of file extensions to include.
>> +                        (default: ['.c'])
>> +
>> +Optional input and output:
>> +  -l [LOG_FILE], --log-file [LOG_FILE]
>> +                        File path for log output.
>> +                        (default: if the flag is given with no file path
>> then a file called
>> +                        debug_macro_check.log is created and used in the
>> current directory)
>> +
>> +  -s SUBSTITUTION_FILE, --substitution-file SUBSTITUTION_FILE
>> +                        A substitution YAML file specifies string
>> substitutions to perform within the debug macro.
>> +
>> +                        This is intended to be a simple mechanism to
>> expand the rare cases of pre-processor
>> +                        macros without directly involving the pre-
>> processor. The file consists of one or more
>> +                        string value pairs where the key is the
>> identifier to replace and the value is the value
>> +                        to replace it with.
>> +
>> +                        This can also be used as a method to ignore
>> results by replacing the problematic string
>> +                        with a different string.
>> +
>> +  -v, --verbose-log-file
>> +                        Set file logging verbosity level.
>> +                         - None:    Info & > level messages
>> +                         - '-v':    + Debug level messages
>> +                         - '-vv':   + File name and function
>> +                         - '-vvv':  + Line number
>> +                         - '-vvvv': + Timestamp
>> +                        (default: verbose logging is not enabled)
>> +
>> +  -n, --no-progress-bar
>> +                        Disables progress bars.
>> +                        (default: progress bars are used in some places
>> to show progress)
>> +
>> +  -q, --quiet           Disables console output.
>> +                        (default: console output is enabled)
>> +
>> +  -u, --utf8w           Shows warnings for file UTF-8 decode errors.
>> +                        (default: UTF-8 decode errors are not shown)
>> +
>> +
>> +Optional git control:
>> +  -df, --do-not-ignore-git-ignore-files
>> +                        Do not ignore git ignored files.
>> +                        (default: files in git ignore files are ignored)
>> +
>> +  -ds, --do-not-ignore-git_submodules
>> +                        Do not ignore files in git submodules.
>> +                        (default: files in git submodules are ignored)
>> +```
>> +
>> +## String Substitutions
>> +
>> +`DebugMacroCheck` currently runs separate from the compiler toolchain.
>> This has the advantage that it is very portable
>> +and can run early in the build process, but it also means pre-processor
>> macro expansion does not happen when it is
>> +invoked.
>> +
>> +In practice, it has been very rare that this is an issue for how most
>> debug macros are written. In case it is, a
>> +substitution file can be used to inform `DebugMacroCheck` about the
>> string substitution the pre-processor would
>> +perform.
>> +
>> +This pattern should be taken as a warning. It is just as difficult for
>> humans to keep debug macro specifiers and
>> +arguments balanced as it is for `DebugMacroCheck` pre-processor macro
>> substitution is used. By separating the string
>> +from the actual arguments provided, it is more likely for developers to
>> make mistakes matching print specifiers in
>> +the string to the arguments. If usage is reasonable, a string
>> substitution can be used as needed.
>> +
>> +### Ignoring Errors
>> +
>> +Since substitution files perform a straight textual substitution in
>> macros discovered, it can be used to replace
>> +problematic text with text that passes allowing errors to be ignored.
>> +
>> +## Python Version Required (3.10)
>> +
>> +This script is written to take advantage of new Python language features
>> in Python 3.10. If you are not using Python
>> +3.10 or later, you can:
>> +
>> +  1. Upgrade to Python 3.10 or greater
>> +  2. Run this script in a [virtual
>> environment](https://docs.python.org/3/tutorial/venv.html) with Python
>> 3.10
>> +     or greater
>> +  3. Customize the script for compatibility with your Python version
>> +
>> +These are listed in order of recommendation. **(1)** is the simplest
>> option and will upgrade your environment to a
>> +newer, safer, and better Python experience. **(2)** is the simplest
>> approach to isolate dependencies to what is needed
>> +to run this script without impacting the rest of your system
>> environment. **(3)** creates a one-off fork of the script
>> +that, by nature, has a limited lifespan and will make accepting future
>> updates difficult but can be done with relatively
>> +minimal effort back to recent Python 3 releases.
>> diff --git a/.pytool/Plugin/DebugMacroCheck/tests/DebugMacroDataSet.py
>> b/.pytool/Plugin/DebugMacroCheck/tests/DebugMacroDataSet.py
>> new file mode 100644
>> index 000000000000..98629bb23333
>> --- /dev/null
>> +++ b/.pytool/Plugin/DebugMacroCheck/tests/DebugMacroDataSet.py
>> @@ -0,0 +1,674 @@
>> +# @file DebugMacroDataSet.py
>> +#
>> +# Contains a debug macro test data set for verifying debug macros are
>> +# recognized and parsed properly.
>> +#
>> +# This data is automatically converted into test cases. Just add the new
>> +# data object here and run the tests.
>> +#
>> +# Copyright (c) Microsoft Corporation. All rights reserved.
>> +# SPDX-License-Identifier: BSD-2-Clause-Patent
>> +##
>> +
>> +from .MacroTest import (NoSpecifierNoArgumentMacroTest,
>> +                        EqualSpecifierEqualArgumentMacroTest,
>> +                        MoreSpecifiersThanArgumentsMacroTest,
>> +                        LessSpecifiersThanArgumentsMacroTest,
>> +                        IgnoredSpecifiersMacroTest,
>> +                        SpecialParsingMacroTest,
>> +                        CodeSnippetMacroTest)
>> +
>> +
>> +# Ignore flake8 linter errors for lines that are too long (E501)
>> +# flake8: noqa: E501
>> +
>> +# Data Set of DEBUG macros and expected results.
>> +# macro: A string representing a DEBUG macro.
>> +# result: A tuple with the following value representations.
>> +#         [0]: Count of total formatting errors
>> +#         [1]: Count of print specifiers found
>> +#         [2]: Count of macro arguments found
>> +DEBUG_MACROS = [
>> +
>> #####################################################################
>> +    # Section: No Print Specifiers No Arguments
>> +
>> #####################################################################
>> +    NoSpecifierNoArgumentMacroTest(
>> +        r'',
>> +        (0, 0, 0)
>> +    ),
>> +    NoSpecifierNoArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "\\"));',
>> +        (0, 0, 0)
>> +    ),
>> +    NoSpecifierNoArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_EVENT, ""));',
>> +        (0, 0, 0)
>> +    ),
>> +    NoSpecifierNoArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_EVENT, "\n"));',
>> +        (0, 0, 0)
>> +    ),
>> +    NoSpecifierNoArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_EVENT, "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"));',
>> +        (0, 0, 0)
>> +    ),
>> +    NoSpecifierNoArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_EVENT, "GCD:Initial GCD Memory Space Map\n"));',
>> +        (0, 0, 0)
>> +    ),
>> +    NoSpecifierNoArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_GCD, "GCD:Initial GCD Memory Space Map\n"));',
>> +        (0, 0, 0)
>> +    ),
>> +    NoSpecifierNoArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "   Retuning TimerCnt Disabled\n"));',
>> +        (0, 0, 0)
>> +    ),
>> +
>> +
>> #####################################################################
>> +    # Section: Equal Print Specifiers to Arguments
>> +
>> #####################################################################
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "%d", Number));',
>> +        (0, 1, 1)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_BLKIO, "NorFlashBlockIoReset(MediaId=0x%x)\n",
>> This->Media->MediaId));',
>> +        (0, 1, 1)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "   Retuning TimerCnt %dseconds\n", 2 *
>> (Capability->TimerCount - 1)));',
>> +        (0, 1, 1)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "UsbEnumerateNewDev: failed to reset port
>> %d - %r\n", Port, Status));',
>> +        (0, 2, 2)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "UsbEnumerateNewDev: failed to reset port
>> %d - %r\n", Port, Status));',
>> +        (0, 2, 2)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "Find GPT Partition [0x%lx",
>> PartitionEntryBuffer[Index].StartingLBA));',
>> +        (0, 1, 1)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "Failed to locate
>> gEdkiiBootLogo2ProtocolGuid Status = %r.  No Progress bar support. \n",
>> Status));',
>> +        (0, 1, 1)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_LOAD, " (%s)", Image->ExitData));',
>> +        (0, 1, 1)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_DISPATCH, "%a%r%s%lx%p%c%g", Ascii, Status,
>> Unicode, Hex, Pointer, Character, Guid));',
>> +        (0, 7, 7)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "LoadCapsuleOnDisk - LoadRecoveryCapsule
>> (%d) - %r\n", CapsuleInstance, Status));',
>> +        (0, 2, 2)
>> +    ),
>> +    EqualSpecifierEqualArgumentMacroTest(
>> +        r'DEBUG ((DEBUG_DISPATCH,
>> "%a%r%s%lx%p%c%g%a%r%s%lx%p%c%g%a%r%s%lx%p%c%g%a%r%s%lx%p%c%g", Ascii,
>> Status, Unicode, Hex, Pointer, Character, Guid, Ascii, Status, Unicode,
>> Hex, Pointer, Character, Guid, Ascii, Status, Unicode, Hex, Pointer,
>> Character, Guid, Ascii, Status, Unicode, Hex, Pointer, Character,
>> Guid));',
>> +        (0, 28, 28)
>> +    ),
>> +
>> +
>> #####################################################################
>> +    # Section: More Print Specifiers Than Arguments
>> +
>> #####################################################################
>> +    MoreSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_BLKIO, "NorFlashBlockIoReadBlocks(MediaId=0x%x,
>> Lba=%ld, BufferSize=0x%x bytes (%d kB), BufferPtr @ 0x%08x)\n", MediaId,
>> Lba, BufferSizeInBytes, Buffer));',
>> +        (1, 5, 4)
>> +    ),
>> +    MoreSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "%a: Request=%s\n", __func__));',
>> +        (1, 2, 1)
>> +    ),
>> +    MoreSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "%a: Invalid request format %d for %d\n",
>> CertFormat, CertRequest));',
>> +        (1, 3, 2)
>> +    ),
>> +
>> +
>> #####################################################################
>> +    # Section: Less Print Specifiers Than Arguments
>> +
>> #####################################################################
>> +    LessSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "Find GPT Partition [0x%lx",
>> PartitionEntryBuffer[Index].StartingLBA, BlockDevPtr->LastBlock));',
>> +        (1, 1, 2)
>> +    ),
>> +    LessSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "   Retuning TimerCnt Disabled\n", 2 *
>> (Capability->TimerCount - 1)));',
>> +        (1, 0, 1)
>> +    ),
>> +    LessSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "Failed to locate
>> gEdkiiBootLogo2ProtocolGuid.  No Progress bar support. \n", Status));',
>> +        (1, 0, 1)
>> +    ),
>> +    LessSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "UsbEnumeratePort: Critical Over
>> Current\n", Port));',
>> +        (1, 0, 1)
>> +    ),
>> +    LessSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "[TPM2] Submit PP Request failure! Sync
>> PPRQ/PPRM with PP variable.\n", Status));',
>> +        (1, 0, 1)
>> +    ),
>> +    LessSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, ": Failed to update debug log index file:
>> %r !\n", __func__, Status));',
>> +        (1, 1, 2)
>> +    ),
>> +    LessSpecifiersThanArgumentsMacroTest(
>> +        r'DEBUG ((DEBUG_ERROR, "%a - Failed to extract nonce from policy
>> blob with return status %r\n", __func__,
>> gPolicyBlobFieldName[MFCI_POLICY_TARGET_NONCE], Status));',
>> +        (1, 2, 3)
>> +    ),
>> +
>> +
>> #####################################################################
>> +    # Section: Macros with Ignored Specifiers
>> +
>> #####################################################################
>> +    IgnoredSpecifiersMacroTest(
>> +        r'DEBUG ((DEBUG_INIT, "%HEmuOpenBlock: opened %a%N\n", Private-
>>> Filename));',
>> +        (0, 1, 1)
>> +    ),
>> +    IgnoredSpecifiersMacroTest(
>> +        r'DEBUG ((DEBUG_LOAD, " (%hs)", Image->ExitData));',
>> +        (0, 1, 1)
>> +    ),
>> +    IgnoredSpecifiersMacroTest(
>> +        r'DEBUG ((DEBUG_LOAD, "%H%s%N: Unknown flag - ''%H%s%N''\r\n",
>> String1, String2));',
>> +        (0, 2, 2)
>> +    ),
>> +
>> +
>> #####################################################################
>> +    # Section: Macros with Special Parsing Scenarios
>> +
>> #####################################################################
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, " File Name: %a\n", "Document.txt"))',
>> +        (0, 1, 1),
>> +        "Malformatted Macro - Missing Semicolon"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG (DEBUG_INFO, " File Name: %a\n", "Document.txt");',
>> +        (0, 0, 0),
>> +        "Malformatted Macro - Missing Two Parentheses"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "%a\n", "Removable Slot"));',
>> +        (0, 1, 1),
>> +        "Single String Argument in Quotes"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "   SDR50 Tuning      %a\n", Capability-
>>> TuningSDR50 ? "TRUE" : "FALSE"));',
>> +        (0, 1, 1),
>> +        "Ternary Operator Present"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_INFO, "   SDR50 Tuning      %a\n", Capability-
>>> TuningSDR50 ? "TRUE" : "FALSE"));',
>> +        (0, 1, 1),
>> +        "Ternary Operator Present"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((DEBUG_ERROR, "\\"));
>> +        DEBUG ((DEBUG_ERROR, "\\"));
>> +        DEBUG ((DEBUG_ERROR, "\\"));
>> +        DEBUG ((DEBUG_ERROR, "\\"));
>> +        ''',
>> +        (0, 0, 0),
>> +        "Multiple Macros with an Escaped Character"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_INFO,
>> +          "UsbEnumerateNewDev: device uses translator (%d, %d)\n",
>> +          Child->Translator.TranslatorHubAddress,
>> +          Child->Translator.TranslatorPortNumber
>> +          ));
>> +        ''',
>> +        (0, 2, 2),
>> +        "Multi-line Macro"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_INFO,
>> +          "UsbEnumeratePort: port %d state - %02x, change - %02x on
>> %p\n",
>> +          Port,
>> +          PortState.PortStatus,
>> +          PortState.PortChangeStatus,
>> +          HubIf
>> +          ));
>> +        ''',
>> +        (0, 4, 4),
>> +        "Multi-line Macro"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_ERROR,
>> +          "%a:%a: failed to allocate reserved pages: "
>> +          "BufferSize=%Lu LoadFile=\"%s\" FilePath=\"%s\"\n",
>> +          gEfiCallerBaseName,
>> +          __func__,
>> +          (UINT64)BufferSize,
>> +          LoadFileText,
>> +          FileText
>> +          ));
>> +        ''',
>> +        (0, 5, 5),
>> +        "Multi-line Macro with Compiler String Concatenation"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_ERROR,
>> +          "ERROR: GTDT: GT Block Frame Info Structures %d and %d have
>> the same " \
>> +          "frame number: 0x%x.\n",
>> +          Index1,
>> +          Index2,
>> +          FrameNumber1
>> +          ));
>> +        ''',
>> +        (0, 3, 3),
>> +        "Multi-line Macro with Backslash String Concatenation"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_ERROR,
>> +          "ERROR: PPTT: Too many private resources. Count = %d. " \
>> +          "Maximum supported Processor Node size exceeded. " \
>> +          "Token = %p. Status = %r\n",
>> +          ProcInfoNode->NoOfPrivateResources,
>> +          ProcInfoNode->ParentToken,
>> +          Status
>> +          ));
>> +        ''',
>> +        (0, 3, 3),
>> +        "Multi-line Macro with Backslash String Concatenation"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_VERBOSE,
>> +          "% 20a % 20a % 20a % 20a\n",
>> +          "PhysicalStart(0x)",
>> +          "PhysicalSize(0x)",
>> +          "CpuStart(0x)",
>> +          "RegionState(0x)"
>> +          ));
>> +        ''',
>> +        (0, 4, 4),
>> +        "Multi-line Macro with Quoted String Arguments"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_ERROR,
>> +          "XenPvBlk: "
>> +          "%a error %d on %a at sector %Lx, num bytes %Lx\n",
>> +          Response->operation == BLKIF_OP_READ ? "read" : "write",
>> +          Status,
>> +          IoData->Dev->NodeName,
>> +          (UINT64)IoData->Sector,
>> +          (UINT64)IoData->Size
>> +          ));
>> +        ''',
>> +        (0, 5, 5),
>> +        "Multi-line Macro with Ternary Operator and Quoted String
>> Arguments"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_ERROR,
>> +          "%a: Label=\"%s\" OldParentNodeId=%Lu OldName=\"%a\" "
>> +          "NewParentNodeId=%Lu NewName=\"%a\" Errno=%d\n",
>> +          __func__,
>> +          VirtioFs->Label,
>> +          OldParentNodeId,
>> +          OldName,
>> +          NewParentNodeId,
>> +          NewName,
>> +          CommonResp.Error
>> +          ));
>> +        ''',
>> +        (0, 7, 7),
>> +        "Multi-line Macro with Escaped Quotes and String Concatenation"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((DEBUG_WARN, "Failed to retrieve Variable:\"MebxData\",
>> Status = %r\n", Status));
>> +        ''',
>> +        (0, 1, 1),
>> +        "Escaped Parentheses in Debug Message"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG((DEBUG_INFO, "%0d %s", XbB_ddr4[1][bankBit][xorBit],
>> xorBit == (XaB_NUM_OF_BITS-1) ? "]": ", "));
>> +        ''',
>> +        (0, 2, 2),
>> +        "Parentheses in Ternary Operator Expression"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_INFO | DEBUG_EVENT | DEBUG_WARN, "   %u\n",
>> &Structure->Block.Value));',
>> +        (0, 1, 1),
>> +        "Multiple Print Specifier Levels Present"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString()));',
>> +        (0, 1, 1),
>> +        "Function Call Argument No Params"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString(&Param1)));',
>> +        (0, 1, 1),
>> +        "Function Call Argument 1 Param"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString(&Param1,
>> Param2)));',
>> +        (0, 1, 1),
>> +        "Function Call Argument Multiple Params"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString(&Param1,
>> ReturnParam())));',
>> +        (0, 1, 1),
>> +        "Function Call Argument 2-Level Depth No 2nd-Level Param"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString(&Param1,
>> ReturnParam(*Param))));',
>> +        (0, 1, 1),
>> +        "Function Call Argument 2-Level Depth 1 2nd-Level Param"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString(&Param1,
>> ReturnParam(*Param, &ParamNext))));',
>> +        (0, 1, 1),
>> +        "Function Call Argument 2-Level Depth Multiple 2nd-Level Param"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString(&Param1,
>> ReturnParam(*Param, GetParam(1, 2, 3)))));',
>> +        (0, 1, 1),
>> +        "Function Call Argument 3-Level Depth Multiple Params"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString(&Param1,
>> ReturnParam(*Param, GetParam(1, 2, 3), NextParam))));',
>> +        (0, 1, 1),
>> +        "Function Call Argument 3-Level Depth Multiple Params with Param
>> After Function Call"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s-%a\n", ReturnString(&Param1),
>> ReturnString2(&ParamN)));',
>> +        (0, 2, 2),
>> +        "Multiple Function Call Arguments"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s\n", ReturnString(&Param1),
>> ReturnString2(&ParamN)));',
>> +        (1, 1, 2),
>> +        "Multiple Function Call Arguments with Imbalance"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "   %s%s\n", (ReturnString(&Param1)),
>> (ReturnString2(&ParamN))));',
>> +        (0, 2, 2),
>> +        "Multiple Function Call Arguments Surrounded with Parentheses"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, " %s\n",
>> ((((ReturnString(&Param1)))))));',
>> +        (0, 1, 1),
>> +        "Multiple Function Call Arguments Surrounded with Many
>> Parentheses"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, ""%B%08X%N: %-48a %V*%a*%N"", HexNumber,
>> ReturnString(Array[Index]), &AsciiString[0]));',
>> +        (0, 3, 3),
>> +        "Complex String Print Specifier 1"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "0x%-8x:%H%s%N % -64s(%73-.73s){%g}<%H% -
>> 70s%N>\n.   Size: 0x%-16x (%-,d) bytes.\n\n", HexNumber, GetUnicodeString
>> (), &UnicodeString[4], UnicodeString2, &Guid, AnotherUnicodeString,
>> Struct.SomeSize, CommaDecimalValue));',
>> +        (0, 8, 8),
>> +        "Multiple Complex Print Specifiers 1"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'DEBUG ((DEBUG_WARN, "0x%-8x:%H%s%N % -64s(%73-.73s){%g}<%H% -
>> 70s%N%r>\n.   Size: 0x%-16x (%-,d) bytes.\n\n", HexNumber,
>> GetUnicodeString (), &UnicodeString[4], UnicodeString2, &Guid,
>> AnotherUnicodeString, Struct.SomeSize, CommaDecimalValue));',
>> +        (1, 9, 8),
>> +        "Multiple Complex Print Specifiers Imbalance 1"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_ERROR,
>> +          ("%a: Label=\"%s\" CanonicalPathname=\"%a\" FileName=\"%s\" "
>> +           "OpenMode=0x%Lx Attributes=0x%Lx: nonsensical request to
>> possibly "
>> +           "create a file marked read-only, for read-write access\n"),
>> +          __func__,
>> +          VirtioFs->Label,
>> +          VirtioFsFile->CanonicalPathname,
>> +          FileName,
>> +          OpenMode,
>> +          Attributes
>> +          ));
>> +        ''',
>> +        (0, 6, 6),
>> +        "Multi-Line with Parentheses Around Debug String Compiler String
>> Concat"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG (
>> +          (DEBUG_INFO,
>> +          " %02x: %04x %02x/%02x/%02x %02x/%02x %04x %04x %04x:%04x\n",
>> +          (UINTN)Index,
>> +          (UINTN)LocalBbsTable[Index].BootPriority,
>> +          (UINTN)LocalBbsTable[Index].Bus,
>> +          (UINTN)LocalBbsTable[Index].Device,
>> +          (UINTN)LocalBbsTable[Index].Function,
>> +          (UINTN)LocalBbsTable[Index].Class,
>> +          (UINTN)LocalBbsTable[Index].SubClass,
>> +          (UINTN)LocalBbsTable[Index].DeviceType,
>> +          (UINTN)*(UINT16 *)&LocalBbsTable[Index].StatusFlags,
>> +          (UINTN)LocalBbsTable[Index].BootHandlerSegment,
>> +          (UINTN)LocalBbsTable[Index].BootHandlerOffset,
>> +          (UINTN)((LocalBbsTable[Index].MfgStringSegment << 4) +
>> LocalBbsTable[Index].MfgStringOffset),
>> +          (UINTN)((LocalBbsTable[Index].DescStringSegment << 4) +
>> LocalBbsTable[Index].DescStringOffset))
>> +          );
>> +        ''',
>> +        (1, 11, 13),
>> +        "Multi-line Macro with Many Arguments And Multi-Line
>> Parentheses"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_WARN,
>> +          "0x%-8x:%H%s%N % -64s(%73-.73s){%g}<%H% -70s%N>\n.   Size:
>> 0x%-16x (%-,d) bytes.\n\n",
>> +          HexNumber,
>> +          GetUnicodeString (InnerFunctionCall(Arg1, &Arg2)),
>> +          &UnicodeString[4],
>> +          UnicodeString2,
>> +          &Guid,
>> +          AnotherUnicodeString,
>> +          Struct.SomeSize,
>> +          CommaDecimalValue
>> +          ));
>> +        ''',
>> +        (0, 8, 8),
>> +        "Multi-line Macro with Multiple Complex Print Specifiers 1 and
>> 2-Depth Function Calls"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG (
>> +          (DEBUG_NET,
>> +          "TcpFastRecover: enter fast retransmission for TCB %p, recover
>> point is %d\n",
>> +          Tcb,
>> +          Tcb->Recover)
>> +          );
>> +        ''',
>> +        (0, 2, 2),
>> +        "Multi-line Macro with Parentheses Separated"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_VERBOSE,
>> +          "%a: APIC ID " FMT_APIC_ID " was hot-plugged "
>> +                                     "before; ignoring it\n",
>> +          __func__,
>> +          NewApicId
>> +          ));
>> +        ''',
>> +        (1, 1, 2),
>> +        "Multi-line Imbalanced Macro with Indented String Concatenation"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_VERBOSE,
>> +          "%a: APIC ID was hot-plugged - %a",
>> +          __func__,
>> +          "String with , inside"
>> +          ));
>> +        ''',
>> +        (0, 2, 2),
>> +        "Multi-line with Quoted String Argument Containing Comma"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_VERBOSE,
>> +          "%a: APIC ID was hot-plugged - %a",
>> +          __func__,
>> +          "St,ring, with , ins,ide"
>> +          ));
>> +        ''',
>> +        (0, 2, 2),
>> +        "Multi-line with Quoted String Argument Containing Multiple
>> Commas"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((DEBUG_VERBOSE, "%a: APIC ID was hot-plugged, \"%a\"",
>> __func__, "S\"t,\"ring, with , ins,i\"de"));
>> +        ''',
>> +        (0, 2, 2),
>> +        "Quoted String Argument with Escaped Quotes and Multiple Commas"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_ERROR,
>> +          "%a: AddProcessor(" FMT_APIC_ID "): %r\n",
>> +          __func__,
>> +          Status
>> +          ));
>> +        ''',
>> +        (0, 2, 2),
>> +        "Quoted Parenthesized String Inside Debug Message String"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((
>> +          DEBUG_INFO,
>> +          "%a: hot-added APIC ID " FMT_APIC_ID ", SMBASE 0x%Lx, "
>> +
>> "EFI_SMM_CPU_SERVICE_PROTOCOL assigned number %Lu\n",
>> +          __func__,
>> +          (UINT64)mCpuHotPlugData->SmBase[NewSlot],
>> +          (UINT64)NewProcessorNumberByProtocol
>> +          ));
>> +        ''',
>> +        (0, 3, 3),
>> +        "Quoted String with Concatenation Inside Debug Message String"
>> +    ),
>> +    SpecialParsingMacroTest(
>> +        r'''
>> +        DEBUG ((DEBUG_INFO, Index == COLUMN_SIZE/2 ? "0" : " %02x",
>> (UINTN)Data[Index]));
>> +        ''',
>> +        (0, 1, 1),
>> +        "Ternary Operating in Debug Message String"
>> +    ),
>> +
>> +
>> #####################################################################
>> +    # Section: Code Snippet Tests
>> +
>> #####################################################################
>> +    CodeSnippetMacroTest(
>> +        r'''
>> +        /**
>> +        Print the BBS Table.
>> +
>> +        @param LocalBbsTable   The BBS table.
>> +        @param BbsCount        The count of entry in BBS table.
>> +        **/
>> +        VOID
>> +        LegacyBmPrintBbsTable (
>> +        IN BBS_TABLE  *LocalBbsTable,
>> +        IN UINT16     BbsCount
>> +        )
>> +        {
>> +        UINT16  Index;
>> +
>> +        DEBUG ((DEBUG_INFO, "\n"));
>> +        DEBUG ((DEBUG_INFO, " NO  Prio bb/dd/ff cl/sc Type Stat
>> segm:offs\n"));
>> +        DEBUG ((DEBUG_INFO,
>> "=============================================\n"));
>> +        for (Index = 0; Index < BbsCount; Index++) {
>> +            if (!LegacyBmValidBbsEntry (&LocalBbsTable[Index])) {
>> +            continue;
>> +            }
>> +
>> +            DEBUG (
>> +              (DEBUG_INFO,
>> +              " %02x: %04x %02x/%02x/%02x %02x/%02x %04x %04x
>> %04x:%04x\n",
>> +              (UINTN)Index,
>> +              (UINTN)LocalBbsTable[Index].BootPriority,
>> +              (UINTN)LocalBbsTable[Index].Bus,
>> +              (UINTN)LocalBbsTable[Index].Device,
>> +              (UINTN)LocalBbsTable[Index].Function,
>> +              (UINTN)LocalBbsTable[Index].Class,
>> +              (UINTN)LocalBbsTable[Index].SubClass,
>> +              (UINTN)LocalBbsTable[Index].DeviceType,
>> +              (UINTN)*(UINT16 *)&LocalBbsTable[Index].StatusFlags,
>> +              (UINTN)LocalBbsTable[Index].BootHandlerSegment,
>> +              (UINTN)LocalBbsTable[Index].BootHandlerOffset,
>> +              (UINTN)((LocalBbsTable[Index].MfgStringSegment << 4) +
>> LocalBbsTable[Index].MfgStringOffset),
>> +              (UINTN)((LocalBbsTable[Index].DescStringSegment << 4) +
>> LocalBbsTable[Index].DescStringOffset))
>> +              );
>> +        }
>> +
>> +        DEBUG ((DEBUG_INFO, "\n"));
>> +        ''',
>> +        (1, 0, 0),
>> +        "Code Section with An Imbalanced Macro"
>> +    ),
>> +    CodeSnippetMacroTest(
>> +        r'''
>> +        if (*Buffer == AML_ROOT_CHAR) {
>> +            //
>> +            // RootChar
>> +            //
>> +            Buffer++;
>> +            DEBUG ((DEBUG_ERROR, "\\"));
>> +        } else if (*Buffer == AML_PARENT_PREFIX_CHAR) {
>> +            //
>> +            // ParentPrefixChar
>> +            //
>> +            do {
>> +            Buffer++;
>> +            DEBUG ((DEBUG_ERROR, "^"));
>> +            } while (*Buffer == AML_PARENT_PREFIX_CHAR);
>> +        }
>> +        DEBUG ((DEBUG_WARN, "Failed to retrieve Variable:\"MebxData\",
>> Status = %r\n", Status));
>> +        ''',
>> +        (0, 1, 1),
>> +        "Code Section with Escaped Backslash and Escaped Quotes"
>> +    ),
>> +    CodeSnippetMacroTest(
>> +        r'''
>> +        if (EFI_ERROR (Status)) {
>> +          UINTN  Offset;
>> +          UINTN  Start;
>> +
>> +          DEBUG ((
>> +            DEBUG_INFO,
>> +            "Variable FV header is not valid. It will be
>> reinitialized.\n"
>> +            ));
>> +
>> +          //
>> +          // Get FvbInfo to provide in FwhInstance.
>> +          //
>> +          Status = GetFvbInfo (Length, &GoodFwVolHeader);
>> +          ASSERT (!EFI_ERROR (Status));
>> +        }
>> +        ''',
>> +        (0, 0, 0),
>> +        "Code Section with Multi-Line Macro with No Arguments"
>> +    )
>> +]
>> diff --git a/.pytool/Plugin/DebugMacroCheck/tests/MacroTest.py
>> b/.pytool/Plugin/DebugMacroCheck/tests/MacroTest.py
>> new file mode 100644
>> index 000000000000..3b966d31ffaa
>> --- /dev/null
>> +++ b/.pytool/Plugin/DebugMacroCheck/tests/MacroTest.py
>> @@ -0,0 +1,131 @@
>> +# @file MacroTest.py
>> +#
>> +# Contains the data classes that are used to compose debug macro tests.
>> +#
>> +# All data classes inherit from a single abstract base class that
>> expects
>> +# the subclass to define the category of test it represents.
>> +#
>> +# Copyright (c) Microsoft Corporation. All rights reserved.
>> +# SPDX-License-Identifier: BSD-2-Clause-Patent
>> +##
>> +
>> +from dataclasses import dataclass, field
>> +from os import linesep
>> +from typing import Tuple
>> +
>> +import abc
>> +
>> +
>> + at dataclass(frozen=True)
>> +class MacroTest(abc.ABC):
>> +    """Abstract base class for an individual macro test case."""
>> +
>> +    macro: str
>> +    result: Tuple[int, int, int]
>> +    description: str = field(default='')
>> +
>> +    @property
>> +    @abc.abstractmethod
>> +    def category(self) -> str:
>> +        """Returns the test class category identifier.
>> +
>> +        Example: 'equal_specifier_equal_argument_macro_test'
>> +
>> +        This string is used to bind test objects against this class.
>> +
>> +        Returns:
>> +            str: Test category identifier string.
>> +        """
>> +        pass
>> +
>> +    @property
>> +    def category_description(self) -> str:
>> +        """Returns the test class category description.
>> +
>> +        Example: 'Test case with equal count of print specifiers to
>> arguments.'
>> +
>> +        This string is a human readable description of the test
>> category.
>> +
>> +        Returns:
>> +            str: String describing the test category.
>> +        """
>> +        return self.__doc__
>> +
>> +    def __str__(self):
>> +        """Returns a macro test case description string."""
>> +
>> +        s = [
>> +            f"{linesep}",
>> +            "=" * 80,
>> +            f"Macro Test Type:  {self.category_description}",
>> +            f"{linesep}Macro:            {self.macro}",
>> +            f"{linesep}Expected Result:  {self.result}"
>> +        ]
>> +
>> +        if self.description:
>> +            s.insert(3, f"Test Description: {self.description}")
>> +
>> +        return f'{linesep}'.join(s)
>> +
>> +
>> + at dataclass(frozen=True)
>> +class NoSpecifierNoArgumentMacroTest(MacroTest):
>> +    """Test case with no print specifier and no arguments."""
>> +
>> +    @property
>> +    def category(self) -> str:
>> +        return "no_specifier_no_argument_macro_test"
>> +
>> +
>> + at dataclass(frozen=True)
>> +class EqualSpecifierEqualArgumentMacroTest(MacroTest):
>> +    """Test case with equal count of print specifiers to arguments."""
>> +
>> +    @property
>> +    def category(self) -> str:
>> +        return "equal_specifier_equal_argument_macro_test"
>> +
>> +
>> + at dataclass(frozen=True)
>> +class MoreSpecifiersThanArgumentsMacroTest(MacroTest):
>> +    """Test case with more print specifiers than arguments."""
>> +
>> +    @property
>> +    def category(self) -> str:
>> +        return "more_specifiers_than_arguments_macro_test"
>> +
>> +
>> + at dataclass(frozen=True)
>> +class LessSpecifiersThanArgumentsMacroTest(MacroTest):
>> +    """Test case with less print specifiers than arguments."""
>> +
>> +    @property
>> +    def category(self) -> str:
>> +        return "less_specifiers_than_arguments_macro_test"
>> +
>> +
>> + at dataclass(frozen=True)
>> +class IgnoredSpecifiersMacroTest(MacroTest):
>> +    """Test case to test ignored print specifiers."""
>> +
>> +    @property
>> +    def category(self) -> str:
>> +        return "ignored_specifiers_macro_test"
>> +
>> +
>> + at dataclass(frozen=True)
>> +class SpecialParsingMacroTest(MacroTest):
>> +    """Test case with special (complicated) parsing scenarios."""
>> +
>> +    @property
>> +    def category(self) -> str:
>> +        return "special_parsing_macro_test"
>> +
>> +
>> + at dataclass(frozen=True)
>> +class CodeSnippetMacroTest(MacroTest):
>> +    """Test case within a larger code snippet."""
>> +
>> +    @property
>> +    def category(self) -> str:
>> +        return "code_snippet_macro_test"
>> diff --git a/.pytool/Plugin/DebugMacroCheck/tests/__init__.py
>> b/.pytool/Plugin/DebugMacroCheck/tests/__init__.py
>> new file mode 100644
>> index 000000000000..e69de29bb2d1
>> diff --git a/.pytool/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py
>> b/.pytool/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py
>> new file mode 100644
>> index 000000000000..db51d23e03d3
>> --- /dev/null
>> +++ b/.pytool/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py
>> @@ -0,0 +1,201 @@
>> +# @file test_DebugMacroCheck.py
>> +#
>> +# Contains unit tests for the DebugMacroCheck build plugin.
>> +#
>> +# An example of running these tests from the root of the workspace:
>> +#   python -m unittest discover -s
>> ./.pytool/Plugin/DebugMacroCheck/tests -v
>> +#
>> +# Copyright (c) Microsoft Corporation. All rights reserved.
>> +# SPDX-License-Identifier: BSD-2-Clause-Patent
>> +##
>> +
>> +import inspect
>> +import pathlib
>> +import sys
>> +import unittest
>> +
>> +# Import the build plugin
>> +test_file = pathlib.Path(__file__)
>> +sys.path.append(str(test_file.parent.parent))
>> +
>> +# flake8 (E402): Ignore flake8 module level import not at top of file
>> +import DebugMacroCheck                          # noqa: E402
>> +
>> +from os import linesep                          # noqa: E402
>> +from tests import DebugMacroDataSet             # noqa: E402
>> +from tests import MacroTest                     # noqa: E402
>> +from typing import Callable, Tuple              # noqa: E402
>> +
>> +
>> +#
>> +# This metaclass is provided to dynamically produce test case container
>> +# classes. The main purpose of this approach is to:
>> +#   1. Allow categories of test cases to be defined (test container
>> classes)
>> +#   2. Allow test cases to automatically (dynamically) be assigned to
>> their
>> +#      corresponding test container class when new test data is defined.
>> +#
>> +#      The idea being that infrastructure and test data are separated.
>> Adding
>> +#      / removing / modifying test data does not require an
>> infrastructure
>> +#      change (unless new categories are defined).
>> +#   3. To work with the unittest discovery algorithm and VS Code Test
>> Explorer.
>> +#
>> +# Notes:
>> +#  - (1) can roughly be achieved with unittest test suites. In another
>> +#    implementation approach, this solution was tested with relatively
>> minor
>> +#    modifications to use test suites. However, this became a bit overly
>> +#    complicated with the dynamic test case method generation and did
>> not
>> +#    work as well with VS Code Test Explorer.
>> +#  - For (2) and (3), particularly for VS Code Test Explorer to work,
>> the
>> +#    dynamic population of the container class namespace needed to
>> happen prior
>> +#    to class object creation. That is why the metaclass assigns test
>> methods
>> +#    to the new classes based upon the test category specified in the
>> +#    corresponding data class.
>> +#  - This could have been simplified a bit by either using one test case
>> +#    container class and/or testing data in a single, monolithic test
>> function
>> +#    that iterates over the data set. However, the dynamic hierarchy
>> greatly
>> +#    helps organize test results and reporting. The infrastructure
>> though
>> +#    inheriting some complexity to support it, should not need to change
>> (much)
>> +#    as the data set expands.
>> +#  - Test case categories (container classes) are derived from the
>> overall
>> +#    type of macro conditions under test.
>> +#
>> +#  - This implementation assumes unittest will discover test cases
>> +#    (classes derived from unittest.TestCase) with the name pattern
>> "Test_*"
>> +#    and test functions with the name pattern "test_x". Individual tests
>> are
>> +#    dynamically numbered monotonically within a category.
>> +#  - The final test case description is also able to return fairly clean
>> +#    context information.
>> +#
>> +class Meta_TestDebugMacroCheck(type):
>> +    """
>> +    Metaclass for debug macro test case class factory.
>> +    """
>> +    @classmethod
>> +    def __prepare__(mcls, name, bases, **kwargs):
>> +        """Returns the test case namespace for this class."""
>> +        candidate_macros, cls_ns, cnt = [], {}, 0
>> +
>> +        if "category" in kwargs.keys():
>> +            candidate_macros = [m for m in
>> DebugMacroDataSet.DEBUG_MACROS if
>> +                                m.category == kwargs["category"]]
>> +        else:
>> +            candidate_macros = DebugMacroDataSet.DEBUG_MACROS
>> +
>> +        for cnt, macro_test in enumerate(candidate_macros):
>> +            f_name = f'test_{macro_test.category}_{cnt}'
>> +            t_desc = f'{macro_test!s}'
>> +            cls_ns[f_name] = mcls.build_macro_test(macro_test, t_desc)
>> +        return cls_ns
>> +
>> +    def __new__(mcls, name, bases, ns, **kwargs):
>> +        """Defined to prevent variable args from bubbling to the base
>> class."""
>> +        return super().__new__(mcls, name, bases, ns)
>> +
>> +    def __init__(mcls, name, bases, ns, **kwargs):
>> +        """Defined to prevent variable args from bubbling to the base
>> class."""
>> +        return super().__init__(name, bases, ns)
>> +
>> +    @classmethod
>> +    def build_macro_test(cls, macro_test: MacroTest.MacroTest,
>> +                         test_desc: str) -> Callable[[None], None]:
>> +        """Returns a test function for this macro test data."
>> +
>> +        Args:
>> +            macro_test (MacroTest.MacroTest): The macro test class.
>> +
>> +            test_desc (str): A test description string.
>> +
>> +        Returns:
>> +            Callable[[None], None]: A test case function.
>> +        """
>> +        def test_func(self):
>> +            act_result = cls.check_regex(macro_test.macro)
>> +            self.assertCountEqual(
>> +                act_result,
>> +                macro_test.result,
>> +                test_desc + f'{linesep}'.join(
>> +                    ["", f"Actual Result:    {act_result}", "=" * 80,
>> ""]))
>> +
>> +        return test_func
>> +
>> +    @classmethod
>> +    def check_regex(cls, source_str: str) -> Tuple[int, int, int]:
>> +        """Returns the plugin result for the given macro string.
>> +
>> +        Args:
>> +            source_str (str): A string containing debug macros.
>> +
>> +        Returns:
>> +            Tuple[int, int, int]: A tuple of the number of formatting
>> errors,
>> +            number of print specifiers, and number of arguments for the
>> macros
>> +            given.
>> +        """
>> +        return DebugMacroCheck.check_debug_macros(
>> +            DebugMacroCheck.get_debug_macros(source_str),
>> +            cls._get_function_name())
>> +
>> +    @classmethod
>> +    def _get_function_name(cls) -> str:
>> +        """Returns the function name from one level of call depth.
>> +
>> +        Returns:
>> +            str: The caller function name.
>> +        """
>> +        return "function: " +
>> inspect.currentframe().f_back.f_code.co_name
>> +
>> +
>> +# Test container classes for dynamically generated macro test cases.
>> +# A class can be removed below to skip / remove it from testing.
>> +# Test case functions will be added to the appropriate class as they are
>> +# created.
>> +class Test_NoSpecifierNoArgument(
>> +        unittest.TestCase,
>> +        metaclass=Meta_TestDebugMacroCheck,
>> +        category="no_specifier_no_argument_macro_test"):
>> +    pass
>> +
>> +
>> +class Test_EqualSpecifierEqualArgument(
>> +        unittest.TestCase,
>> +        metaclass=Meta_TestDebugMacroCheck,
>> +        category="equal_specifier_equal_argument_macro_test"):
>> +    pass
>> +
>> +
>> +class Test_MoreSpecifiersThanArguments(
>> +        unittest.TestCase,
>> +        metaclass=Meta_TestDebugMacroCheck,
>> +        category="more_specifiers_than_arguments_macro_test"):
>> +    pass
>> +
>> +
>> +class Test_LessSpecifiersThanArguments(
>> +        unittest.TestCase,
>> +        metaclass=Meta_TestDebugMacroCheck,
>> +        category="less_specifiers_than_arguments_macro_test"):
>> +    pass
>> +
>> +
>> +class Test_IgnoredSpecifiers(
>> +        unittest.TestCase,
>> +        metaclass=Meta_TestDebugMacroCheck,
>> +        category="ignored_specifiers_macro_test"):
>> +    pass
>> +
>> +
>> +class Test_SpecialParsingMacroTest(
>> +        unittest.TestCase,
>> +        metaclass=Meta_TestDebugMacroCheck,
>> +        category="special_parsing_macro_test"):
>> +    pass
>> +
>> +
>> +class Test_CodeSnippetMacroTest(
>> +        unittest.TestCase,
>> +        metaclass=Meta_TestDebugMacroCheck,
>> +        category="code_snippet_macro_test"):
>> +    pass
>> +
>> +
>> +if __name__ == '__main__':
>> +    unittest.main()
>> --
>> 2.41.0.windows.3
> 
> 
> 
> 
> 


-=-=-=-=-=-=-=-=-=-=-=-
Groups.io Links: You receive all messages sent to this group.
View/Reply Online (#108420): https://edk2.groups.io/g/devel/message/108420
Mute This Topic: https://groups.io/mt/100745711/1813853
Group Owner: devel+owner at edk2.groups.io
Unsubscribe: https://edk2.groups.io/g/devel/unsub [edk2-devel-archive at redhat.com]
-=-=-=-=-=-=-=-=-=-=-=-




More information about the edk2-devel-archive mailing list