560 lines · 15.7 KB · Python
RawDownload
#!/usr/bin/env python3# SPDX-License-Identifier: GPL-2.0# Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>.## pylint: disable=C0200,C0413,W0102,R0914 """Unit tests for kernel-doc parser.""" import loggingimport osimport reimport shleximport sysimport unittest from textwrap import dedentfrom unittest.mock import patch, MagicMock, mock_open import yaml SRC_DIR = os.path.dirname(os.path.realpath(__file__))sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python")) from kdoc.kdoc_files import KdocConfigfrom kdoc.kdoc_item import KdocItemfrom kdoc.kdoc_parser import KernelDocfrom kdoc.kdoc_output import RestFormat, ManFormat from kdoc.xforms_lists import CTransforms from unittest_helper import TestUnits  ## Test file#TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml") env = {    "yaml_file": TEST_FILE} ## Ancillary logic to clean whitespaces##: Regex to help cleaning whitespacesRE_WHITESPC = re.compile(r"[ \t]++")RE_BEGINSPC = re.compile(r"^\s+", re.MULTILINE)RE_ENDSPC = re.compile(r"\s+$", re.MULTILINE) def clean_whitespc(val, relax_whitespace=False):    """    Cleanup whitespaces to avoid false positives.     By default, strip only bein/end whitespaces, but, when relax_whitespace    is true, also replace multiple whitespaces in the middle.    """     if isinstance(val, str):        val = val.strip()        if relax_whitespace:            val = RE_WHITESPC.sub(" ", val)            val = RE_BEGINSPC.sub("", val)            val = RE_ENDSPC.sub("", val)    elif isinstance(val, list):        val = [clean_whitespc(item, relax_whitespace) for item in val]    elif isinstance(val, dict):        val = {k: clean_whitespc(v, relax_whitespace) for k, v in val.items()}    return val ## Helper classes to help mocking with logger and config#class MockLogging(logging.Handler):    """    Simple class to store everything on a list    """     def __init__(self, level=logging.NOTSET):        super().__init__(level)        self.messages = []        self.formatter = logging.Formatter()     def emit(self, record: logging.LogRecord) -> None:        """        Append a formatted record to self.messages.        """        try:            # The `format` method uses the handler's formatter.            message = self.format(record)            self.messages.append(message)        except Exception:            self.handleError(record) class MockKdocConfig(KdocConfig):    def __init__(self, *args, **kwargs):        super().__init__(*args, **kwargs)         self.log = logging.getLogger(__file__)        self.handler = MockLogging()        self.log.addHandler(self.handler)     def warning(self, msg):        """Ancillary routine to output a warning and increment error count."""         self.log.warning(msg) ## Helper class to generate KdocItem and validate its contents## TODO: check self.config.handler.messages content#class GenerateKdocItem(unittest.TestCase):    """    Base class to run KernelDoc parser class    """     DEFAULT = vars(KdocItem("", "", "", 0))     config = MockKdocConfig()    xforms = CTransforms()     def setUp(self):        self.maxDiff = None     def run_test(self, source, __expected_list, exports={}, fname="test.c",                 relax_whitespace=False):        """        Stores expected values and patch the test to use source as        a "file" input.        """        debug_level = int(os.getenv("VERBOSE", "0"))        source = dedent(source)         # Ensure that default values will be there        expected_list = []        for e in __expected_list:            if not isinstance(e, dict):                e = vars(e)             new_e = self.DEFAULT.copy()            new_e["fname"] = fname            for key, value in e.items():                new_e[key] = value             expected_list.append(new_e)         patcher = patch('builtins.open',                        new_callable=mock_open, read_data=source)         kernel_doc = KernelDoc(self.config, fname, self.xforms)         with patcher:            export_table, entries = kernel_doc.parse_kdoc()             self.assertEqual(export_table, exports)            self.assertEqual(len(entries), len(expected_list))             for i in range(0, len(entries)):                 entry = entries[i]                expected = expected_list[i]                self.assertNotEqual(expected, None)                self.assertNotEqual(expected, {})                self.assertIsInstance(entry, KdocItem)                 d = vars(entry)                 other_stuff = d.get("other_stuff", {})                if "source" in other_stuff:                    del other_stuff["source"]                 for key, value in expected.items():                    if key == "other_stuff":                        if "source" in value:                            del value["source"]                     result = clean_whitespc(d[key], relax_whitespace)                    value = clean_whitespc(value, relax_whitespace)                     if debug_level > 1:                        sys.stderr.write(f"{key}: assert('{result}' == '{value}')\n")                     self.assertEqual(result, value, msg=f"at {key}") ## Ancillary function that replicates kdoc_files way to generate output#def cleanup_timestamp(text):    lines = text.split("\n")     for i, line in enumerate(lines):        if not line.startswith('.TH'):            continue         parts = shlex.split(line)        if len(parts) > 3:            parts[3] = ""         lines[i] = " ".join(parts)      return "\n".join(lines) def gen_output(fname, out_style, symbols, expected,               config=None, relax_whitespace=False):    """    Use the output class to return an output content from KdocItem symbols.    """     if not config:        config = MockKdocConfig()     out_style.set_config(config)     msg = out_style.output_symbols(fname, symbols)     result = clean_whitespc(msg, relax_whitespace)    result = cleanup_timestamp(result)     expected = clean_whitespc(expected, relax_whitespace)    expected = cleanup_timestamp(expected)     return result, expected ## Classes to be used by dynamic test generation from YAML#class CToKdocItem(GenerateKdocItem):    def setUp(self):        self.maxDiff = None     def run_parser_test(self, source, symbols, exports, fname):        if isinstance(symbols, dict):            symbols = [symbols]         if isinstance(exports, str):            exports=set([exports])        elif isinstance(exports, list):            exports=set(exports)         self.run_test(source, symbols, exports=exports,                      fname=fname, relax_whitespace=True) class KdocItemToMan(unittest.TestCase):    out_style = ManFormat()     def setUp(self):        self.maxDiff = None     def run_out_test(self, fname, symbols, expected):        """        Generate output using out_style,        """        result, expected = gen_output(fname, self.out_style,                                      symbols, expected)         self.assertEqual(result, expected) class KdocItemToRest(unittest.TestCase):    out_style = RestFormat()     def setUp(self):        self.maxDiff = None     def run_out_test(self, fname, symbols, expected):        """        Generate output using out_style,        """        result, expected = gen_output(fname, self.out_style, symbols,                                      expected, relax_whitespace=True)         self.assertEqual(result, expected)  class CToMan(unittest.TestCase):    out_style = ManFormat()    config = MockKdocConfig()    xforms = CTransforms()     def setUp(self):        self.maxDiff = None     def run_out_test(self, fname, source, expected):        """        Generate output using out_style,        """        patcher = patch('builtins.open',                        new_callable=mock_open, read_data=source)         kernel_doc = KernelDoc(self.config, fname, self.xforms)         with patcher:            export_table, entries = kernel_doc.parse_kdoc()         result, expected = gen_output(fname, self.out_style,                                      entries, expected, config=self.config)         self.assertEqual(result, expected)  class CToRest(unittest.TestCase):    out_style = RestFormat()    config = MockKdocConfig()    xforms = CTransforms()     def setUp(self):        self.maxDiff = None     def run_out_test(self, fname, source, expected):        """        Generate output using out_style,        """        patcher = patch('builtins.open',                        new_callable=mock_open, read_data=source)         kernel_doc = KernelDoc(self.config, fname, self.xforms)         with patcher:            export_table, entries = kernel_doc.parse_kdoc()         result, expected = gen_output(fname, self.out_style, entries,                                      expected, relax_whitespace=True,                                      config=self.config)         self.assertEqual(result, expected)  ## Selftest class#class TestSelfValidate(GenerateKdocItem):    """    Tests to check if logic inside GenerateKdocItem.run_test() is working.    """     SOURCE = """        /**         * function3: Exported function         * @arg1: @arg1 does nothing         *         * Does nothing         *         * return:         *    always return 0.         */        int function3(char *arg1) { return 0; };        EXPORT_SYMBOL(function3);    """     EXPECTED = [{        'name': 'function3',        'type': 'function',        'declaration_start_line': 2,         'sections_start_lines': {            'Description': 4,            'Return': 7,        },        'sections': {            'Description': 'Does nothing\n\n',            'Return': '\nalways return 0.\n'        },         'sections_start_lines': {            'Description': 4,            'Return': 7,        },         'parameterdescs': {'arg1': '@arg1 does nothing\n'},        'parameterlist': ['arg1'],        'parameterdesc_start_lines': {'arg1': 3},        'parametertypes': {'arg1': 'char *arg1'},         'other_stuff': {            'func_macro': False,            'functiontype': 'int',            'purpose': 'Exported function',            'typedef': False        },    }]     EXPORTS = {"function3"}     def test_parse_pass(self):        """        Test if export_symbol is properly handled.        """        self.run_test(self.SOURCE, self.EXPECTED, self.EXPORTS)     @unittest.expectedFailure    def test_no_exports(self):        """        Test if export_symbol is properly handled.        """        self.run_test(self.SOURCE, [], {})     @unittest.expectedFailure    def test_with_empty_expected(self):        """        Test if export_symbol is properly handled.        """        self.run_test(self.SOURCE, [], self.EXPORTS)     @unittest.expectedFailure    def test_with_unfilled_expected(self):        """        Test if export_symbol is properly handled.        """        self.run_test(self.SOURCE, [{}], self.EXPORTS)     @unittest.expectedFailure    def test_with_default_expected(self):        """        Test if export_symbol is properly handled.        """        self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS) ## Class and logic to create dynamic tests from YAML# class KernelDocDynamicTests():    """    Dynamically create a set of tests from a YAML file.    """     @classmethod    def create_parser_test(cls, name, fname, source, symbols, exports):        """        Return a function that will be attached to the test class.        """        def test_method(self):            """Lambda-like function to run tests with provided vars"""            self.run_parser_test(source, symbols, exports, fname)         test_method.__name__ = f"test_gen_{name}"         setattr(CToKdocItem, test_method.__name__, test_method)     @classmethod    def create_out_test(cls, name, fname, symbols, out_type, data):        """        Return a function that will be attached to the test class.        """        def test_method(self):            """Lambda-like function to run tests with provided vars"""            self.run_out_test(fname, symbols, data)         test_method.__name__ = f"test_{out_type}_{name}"         if out_type == "man":            setattr(KdocItemToMan, test_method.__name__, test_method)        else:            setattr(KdocItemToRest, test_method.__name__, test_method)     @classmethod    def create_src2out_test(cls, name, fname, source, out_type, data):        """        Return a function that will be attached to the test class.        """        def test_method(self):            """Lambda-like function to run tests with provided vars"""            self.run_out_test(fname, source,  data)         test_method.__name__ = f"test_{out_type}_{name}"         if out_type == "man":            setattr(CToMan, test_method.__name__, test_method)        else:            setattr(CToRest, test_method.__name__, test_method)     @classmethod    def create_tests(cls):        """        Iterate over all scenarios and add a method to the class for each.         The logic in this function assumes a valid test that are compliant        with kdoc-test-schema.yaml. There is an unit test to check that.        As such, it picks mandatory values directly, and uses get() for the        optional ones.        """         test_file = os.environ.get("yaml_file", TEST_FILE)         with open(test_file, encoding="utf-8") as fp:            testset = yaml.safe_load(fp)         tests = testset["tests"]         for idx, test in enumerate(tests):            name = test["name"]            fname = test["fname"]            source = test["source"]            expected_list = test["expected"]             exports = test.get("exports", []) 

Lines 1–500 of 560.