xref: /linux/tools/unittests/test_kdoc_parser.py (revision a5dea56ebd35a3027628b73db0968788e99c8412)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>.
4#
5# pylint: disable=C0200,C0413,W0102,R0914
6
7"""
8Unit tests for kernel-doc parser.
9"""
10
11import os
12import unittest
13import re
14import sys
15
16from textwrap import dedent
17from unittest.mock import patch, MagicMock, mock_open
18
19SRC_DIR = os.path.dirname(os.path.realpath(__file__))
20sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python"))
21
22from kdoc.kdoc_parser import KernelDoc
23from kdoc.kdoc_item import KdocItem
24from kdoc.xforms_lists import CTransforms
25from unittest_helper import run_unittest
26
27#: Regex to help cleaning whitespaces
28RE_WHITESPC = re.compile(r"\s++")
29
30def clean_whitespc(val, relax_whitespace=False):
31    """
32    Cleanup whitespaces to avoid false positives.
33
34    By default, strip only bein/end whitespaces, but, when relax_whitespace
35    is true, also replace multiple whitespaces in the middle.
36    """
37
38    if isinstance(val, str):
39        val = val.strip()
40        if relax_whitespace:
41            val = RE_WHITESPC.sub("", val)
42    elif isinstance(val, list):
43        val = [clean_whitespc(item, relax_whitespace) for item in val]
44    elif isinstance(val, dict):
45        val = {k: clean_whitespc(v, relax_whitespace) for k, v in val.items()}
46    return val
47
48#
49# Helper class to help mocking with
50#
51class KdocParser(unittest.TestCase):
52    """
53    Base class to run KernelDoc parser class
54    """
55
56    DEFAULT = vars(KdocItem("", "", "", 0))
57
58    def setUp(self):
59        self.maxDiff = None
60        self.config = MagicMock()
61        self.config.log = MagicMock()
62        self.config.log.debug = MagicMock()
63        self.xforms = CTransforms()
64
65
66    def run_test(self, source, __expected_list, exports={}, fname="test.c",
67                 relax_whitespace=False):
68        """
69        Stores expected values and patch the test to use source as
70        a "file" input.
71        """
72        debug_level = int(os.getenv("VERBOSE", "0"))
73        source = dedent(source)
74
75        # Ensure that default values will be there
76        expected_list = []
77        for e in __expected_list:
78            new_e = self.DEFAULT.copy()
79            new_e["fname"] = fname
80            for key, value in e.items():
81                new_e[key] = value
82
83            expected_list.append(new_e)
84
85        patcher = patch('builtins.open',
86                        new_callable=mock_open, read_data=source)
87
88        kernel_doc = KernelDoc(self.config, fname, self.xforms)
89
90        with patcher:
91            export_table, entries = kernel_doc.parse_kdoc()
92
93            self.assertEqual(export_table, exports)
94            self.assertEqual(len(entries), len(expected_list))
95
96            for i in range(0, len(entries)):
97
98                entry = entries[i]
99                expected = expected_list[i]
100                self.assertNotEqual(expected, None)
101                self.assertNotEqual(expected, {})
102                self.assertIsInstance(entry, KdocItem)
103
104                d = vars(entry)
105                for key, value in expected.items():
106                    result = clean_whitespc(d[key], relax_whitespace)
107                    value = clean_whitespc(value, relax_whitespace)
108
109                    if debug_level > 1:
110                        sys.stderr.write(f"{key}: assert('{result}' == '{value}')\n")
111
112                    self.assertEqual(result, value, msg=f"at {key}")
113
114
115#
116# Selttest class
117#
118class TestSelfValidate(KdocParser):
119    """
120    Tests to check if logic inside KdocParser.run_test() is working.
121    """
122
123    SOURCE = """
124        /**
125         * function3: Exported function
126         * @arg1: @arg1 does nothing
127         *
128         * Does nothing
129         *
130         * return:
131         *    always return 0.
132         */
133        int function3(char *arg1) { return 0; };
134        EXPORT_SYMBOL(function3);
135    """
136
137    EXPECTED = [{
138        'name': 'function3',
139        'type': 'function',
140        'declaration_start_line': 2,
141
142        'sections_start_lines': {
143            'Description': 4,
144            'Return': 7,
145        },
146        'sections': {
147            'Description': 'Does nothing\n\n',
148            'Return': '\nalways return 0.\n'
149        },
150        'other_stuff': {
151            'func_macro': False,
152            'functiontype': 'int',
153            'purpose': 'Exported function',
154            'typedef': False
155        },
156        'parameterdescs': {'arg1': '@arg1 does nothing\n'},
157        'parameterlist': ['arg1'],
158        'parameterdesc_start_lines': {'arg1': 3},
159        'parametertypes': {'arg1': 'char *arg1'},
160    }]
161
162    EXPORTS = {"function3"}
163
164    def test_parse_pass(self):
165        """
166        Test if export_symbol is properly handled.
167        """
168        self.run_test(self.SOURCE, self.EXPECTED, self.EXPORTS)
169
170    @unittest.expectedFailure
171    def test_no_exports(self):
172        """
173        Test if export_symbol is properly handled.
174        """
175        self.run_test(self.SOURCE, [], {})
176
177    @unittest.expectedFailure
178    def test_with_empty_expected(self):
179        """
180        Test if export_symbol is properly handled.
181        """
182        self.run_test(self.SOURCE, [], self.EXPORTS)
183
184    @unittest.expectedFailure
185    def test_with_unfilled_expected(self):
186        """
187        Test if export_symbol is properly handled.
188        """
189        self.run_test(self.SOURCE, [{}], self.EXPORTS)
190
191    @unittest.expectedFailure
192    def test_with_default_expected(self):
193        """
194        Test if export_symbol is properly handled.
195        """
196        self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS)
197
198#
199# Run all tests
200#
201if __name__ == "__main__":
202    run_unittest(__file__)
203