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