summaryrefslogtreecommitdiffstats
path: root/BaseTools/Plugin/DebugMacroCheck/tests/test_DebugMacroCheck.py
blob: 7abc0d2b878568fa3cf1eb48d1270ed94f527484 (plain)
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()