1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 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()
|