audk/BaseTools/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py

202 lines
7.4 KiB
Python

# @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()