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