diff options
| author | kx <kx@radix.pro> | 2024-06-04 06:43:08 +0300 |
|---|---|---|
| committer | kx <kx@radix.pro> | 2024-06-04 06:43:08 +0300 |
| commit | 7e2ccccace24636f29ddc210b94606abd4c7e42b (patch) | |
| tree | 545d43ea12671048956cafeb12303e84d601e571 /BaseTools/Plugin/DebugMacroCheck | |
| parent | 27b044605cd5f6b33a3d231576003850b3fe305b (diff) | |
| download | edk2-trunk.tar.xz | |
Renormalized end-of-lines from master@27b044605cd5f6b33a3d231576003850b3fe305btrunk
Diffstat (limited to 'BaseTools/Plugin/DebugMacroCheck')
7 files changed, 2256 insertions, 2256 deletions
diff --git a/BaseTools/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheckBuildPlugin.py b/BaseTools/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheckBuildPlugin.py index aa3a2bbcab..e86c0e23ad 100644 --- a/BaseTools/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheckBuildPlugin.py +++ b/BaseTools/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheckBuildPlugin.py @@ -1,127 +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
-
- edk2 = builder.edk2path
- package = edk2.GetContainingPackage(
- builder.edk2path.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
- 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
+# @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 + + edk2 = builder.edk2path + package = edk2.GetContainingPackage( + builder.edk2path.GetAbsolutePathOnThisSystemFromEdk2RelativePath( + 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/BaseTools/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheck_plug_in.yaml b/BaseTools/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheck_plug_in.yaml index 50f97cbd39..97320f0251 100644 --- a/BaseTools/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheck_plug_in.yaml +++ b/BaseTools/Plugin/DebugMacroCheck/BuildPlugin/DebugMacroCheck_plug_in.yaml @@ -1,11 +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"
-}
+## @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/BaseTools/Plugin/DebugMacroCheck/DebugMacroCheck.py b/BaseTools/Plugin/DebugMacroCheck/DebugMacroCheck.py index ffabcdf91b..40f2083680 100644 --- a/BaseTools/Plugin/DebugMacroCheck/DebugMacroCheck.py +++ b/BaseTools/Plugin/DebugMacroCheck/DebugMacroCheck.py @@ -1,859 +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)
+# @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/BaseTools/Plugin/DebugMacroCheck/Readme.md b/BaseTools/Plugin/DebugMacroCheck/Readme.md index 33f1ad9790..ad51fbbbfc 100644 --- a/BaseTools/Plugin/DebugMacroCheck/Readme.md +++ b/BaseTools/Plugin/DebugMacroCheck/Readme.md @@ -1,253 +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/ConfigurationManagerObjectParser.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.
+# 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/ConfigurationManagerObjectParser.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/BaseTools/Plugin/DebugMacroCheck/tests/DebugMacroDataSet.py b/BaseTools/Plugin/DebugMacroCheck/tests/DebugMacroDataSet.py index 98629bb233..c3ce2faca8 100644 --- a/BaseTools/Plugin/DebugMacroCheck/tests/DebugMacroDataSet.py +++ b/BaseTools/Plugin/DebugMacroCheck/tests/DebugMacroDataSet.py @@ -1,674 +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"
- )
-]
+# @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/BaseTools/Plugin/DebugMacroCheck/tests/MacroTest.py b/BaseTools/Plugin/DebugMacroCheck/tests/MacroTest.py index 3b966d31ff..4b54a4da18 100644 --- a/BaseTools/Plugin/DebugMacroCheck/tests/MacroTest.py +++ b/BaseTools/Plugin/DebugMacroCheck/tests/MacroTest.py @@ -1,131 +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
-
-
-@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)
-
-
-@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"
-
-
-@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"
-
-
-@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"
-
-
-@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"
-
-
-@dataclass(frozen=True)
-class IgnoredSpecifiersMacroTest(MacroTest):
- """Test case to test ignored print specifiers."""
-
- @property
- def category(self) -> str:
- return "ignored_specifiers_macro_test"
-
-
-@dataclass(frozen=True)
-class SpecialParsingMacroTest(MacroTest):
- """Test case with special (complicated) parsing scenarios."""
-
- @property
- def category(self) -> str:
- return "special_parsing_macro_test"
-
-
-@dataclass(frozen=True)
-class CodeSnippetMacroTest(MacroTest):
- """Test case within a larger code snippet."""
-
- @property
- def category(self) -> str:
- return "code_snippet_macro_test"
+# @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 + + +@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) + + +@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" + + +@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" + + +@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" + + +@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" + + +@dataclass(frozen=True) +class IgnoredSpecifiersMacroTest(MacroTest): + """Test case to test ignored print specifiers.""" + + @property + def category(self) -> str: + return "ignored_specifiers_macro_test" + + +@dataclass(frozen=True) +class SpecialParsingMacroTest(MacroTest): + """Test case with special (complicated) parsing scenarios.""" + + @property + def category(self) -> str: + return "special_parsing_macro_test" + + +@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/BaseTools/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py b/BaseTools/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py index 7abc0d2b87..5e85c1da46 100644 --- a/BaseTools/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py +++ b/BaseTools/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py @@ -1,201 +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 ./BaseTools/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()
+# @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 ./BaseTools/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() |
