Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
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 logging
12import os
13import re
14import shlex
15import sys
16import unittest
17
18from textwrap import dedent
19from unittest.mock import patch, MagicMock, mock_open
20
21import yaml
22
23SRC_DIR = os.path.dirname(os.path.realpath(__file__))
24sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python"))
25
26from kdoc.kdoc_files import KdocConfig
27from kdoc.kdoc_item import KdocItem
28from kdoc.kdoc_parser import KernelDoc
29from kdoc.kdoc_output import RestFormat, ManFormat
30
31from kdoc.xforms_lists import CTransforms
32
33from unittest_helper import TestUnits
34
35
36#
37# Test file
38#
39TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml")
40
41env = {
42 "yaml_file": TEST_FILE
43}
44
45#
46# Ancillary logic to clean whitespaces
47#
48#: Regex to help cleaning whitespaces
49RE_WHITESPC = re.compile(r"[ \t]++")
50RE_BEGINSPC = re.compile(r"^\s+", re.MULTILINE)
51RE_ENDSPC = re.compile(r"\s+$", re.MULTILINE)
52
53def clean_whitespc(val, relax_whitespace=False):
54 """
55 Cleanup whitespaces to avoid false positives.
56
57 By default, strip only bein/end whitespaces, but, when relax_whitespace
58 is true, also replace multiple whitespaces in the middle.
59 """
60
61 if isinstance(val, str):
62 val = val.strip()
63 if relax_whitespace:
64 val = RE_WHITESPC.sub(" ", val)
65 val = RE_BEGINSPC.sub("", val)
66 val = RE_ENDSPC.sub("", val)
67 elif isinstance(val, list):
68 val = [clean_whitespc(item, relax_whitespace) for item in val]
69 elif isinstance(val, dict):
70 val = {k: clean_whitespc(v, relax_whitespace) for k, v in val.items()}
71 return val
72
73#
74# Helper classes to help mocking with logger and config
75#
76class MockLogging(logging.Handler):
77 """
78 Simple class to store everything on a list
79 """
80
81 def __init__(self, level=logging.NOTSET):
82 super().__init__(level)
83 self.messages = []
84 self.formatter = logging.Formatter()
85
86 def emit(self, record: logging.LogRecord) -> None:
87 """
88 Append a formatted record to self.messages.
89 """
90 try:
91 # The `format` method uses the handler's formatter.
92 message = self.format(record)
93 self.messages.append(message)
94 except Exception:
95 self.handleError(record)
96
97class MockKdocConfig(KdocConfig):
98 def __init__(self, *args, **kwargs):
99 super().__init__(*args, **kwargs)
100
101 self.log = logging.getLogger(__file__)
102 self.handler = MockLogging()
103 self.log.addHandler(self.handler)
104
105 def warning(self, msg):
106 """Ancillary routine to output a warning and increment error count."""
107
108 self.log.warning(msg)
109
110#
111# Helper class to generate KdocItem and validate its contents
112#
113# TODO: check self.config.handler.messages content
114#
115class GenerateKdocItem(unittest.TestCase):
116 """
117 Base class to run KernelDoc parser class
118 """
119
120 DEFAULT = vars(KdocItem("", "", "", 0))
121
122 config = MockKdocConfig()
123 xforms = CTransforms()
124
125 def setUp(self):
126 self.maxDiff = None
127
128 def run_test(self, source, __expected_list, exports={}, fname="test.c",
129 relax_whitespace=False):
130 """
131 Stores expected values and patch the test to use source as
132 a "file" input.
133 """
134 debug_level = int(os.getenv("VERBOSE", "0"))
135 source = dedent(source)
136
137 # Ensure that default values will be there
138 expected_list = []
139 for e in __expected_list:
140 if not isinstance(e, dict):
141 e = vars(e)
142
143 new_e = self.DEFAULT.copy()
144 new_e["fname"] = fname
145 for key, value in e.items():
146 new_e[key] = value
147
148 expected_list.append(new_e)
149
150 patcher = patch('builtins.open',
151 new_callable=mock_open, read_data=source)
152
153 kernel_doc = KernelDoc(self.config, fname, self.xforms)
154
155 with patcher:
156 export_table, entries = kernel_doc.parse_kdoc()
157
158 self.assertEqual(export_table, exports)
159 self.assertEqual(len(entries), len(expected_list))
160
161 for i in range(0, len(entries)):
162
163 entry = entries[i]
164 expected = expected_list[i]
165 self.assertNotEqual(expected, None)
166 self.assertNotEqual(expected, {})
167 self.assertIsInstance(entry, KdocItem)
168
169 d = vars(entry)
170
171 other_stuff = d.get("other_stuff", {})
172 if "source" in other_stuff:
173 del other_stuff["source"]
174
175 for key, value in expected.items():
176 if key == "other_stuff":
177 if "source" in value:
178 del value["source"]
179
180 result = clean_whitespc(d[key], relax_whitespace)
181 value = clean_whitespc(value, relax_whitespace)
182
183 if debug_level > 1:
184 sys.stderr.write(f"{key}: assert('{result}' == '{value}')\n")
185
186 self.assertEqual(result, value, msg=f"at {key}")
187
188#
189# Ancillary function that replicates kdoc_files way to generate output
190#
191def cleanup_timestamp(text):
192 lines = text.split("\n")
193
194 for i, line in enumerate(lines):
195 if not line.startswith('.TH'):
196 continue
197
198 parts = shlex.split(line)
199 if len(parts) > 3:
200 parts[3] = ""
201
202 lines[i] = " ".join(parts)
203
204
205 return "\n".join(lines)
206
207def gen_output(fname, out_style, symbols, expected,
208 config=None, relax_whitespace=False):
209 """
210 Use the output class to return an output content from KdocItem symbols.
211 """
212
213 if not config:
214 config = MockKdocConfig()
215
216 out_style.set_config(config)
217
218 msg = out_style.output_symbols(fname, symbols)
219
220 result = clean_whitespc(msg, relax_whitespace)
221 result = cleanup_timestamp(result)
222
223 expected = clean_whitespc(expected, relax_whitespace)
224 expected = cleanup_timestamp(expected)
225
226 return result, expected
227
228#
229# Classes to be used by dynamic test generation from YAML
230#
231class CToKdocItem(GenerateKdocItem):
232 def setUp(self):
233 self.maxDiff = None
234
235 def run_parser_test(self, source, symbols, exports, fname):
236 if isinstance(symbols, dict):
237 symbols = [symbols]
238
239 if isinstance(exports, str):
240 exports=set([exports])
241 elif isinstance(exports, list):
242 exports=set(exports)
243
244 self.run_test(source, symbols, exports=exports,
245 fname=fname, relax_whitespace=True)
246
247class KdocItemToMan(unittest.TestCase):
248 out_style = ManFormat()
249
250 def setUp(self):
251 self.maxDiff = None
252
253 def run_out_test(self, fname, symbols, expected):
254 """
255 Generate output using out_style,
256 """
257 result, expected = gen_output(fname, self.out_style,
258 symbols, expected)
259
260 self.assertEqual(result, expected)
261
262class KdocItemToRest(unittest.TestCase):
263 out_style = RestFormat()
264
265 def setUp(self):
266 self.maxDiff = None
267
268 def run_out_test(self, fname, symbols, expected):
269 """
270 Generate output using out_style,
271 """
272 result, expected = gen_output(fname, self.out_style, symbols,
273 expected, relax_whitespace=True)
274
275 self.assertEqual(result, expected)
276
277
278class CToMan(unittest.TestCase):
279 out_style = ManFormat()
280 config = MockKdocConfig()
281 xforms = CTransforms()
282
283 def setUp(self):
284 self.maxDiff = None
285
286 def run_out_test(self, fname, source, expected):
287 """
288 Generate output using out_style,
289 """
290 patcher = patch('builtins.open',
291 new_callable=mock_open, read_data=source)
292
293 kernel_doc = KernelDoc(self.config, fname, self.xforms)
294
295 with patcher:
296 export_table, entries = kernel_doc.parse_kdoc()
297
298 result, expected = gen_output(fname, self.out_style,
299 entries, expected, config=self.config)
300
301 self.assertEqual(result, expected)
302
303
304class CToRest(unittest.TestCase):
305 out_style = RestFormat()
306 config = MockKdocConfig()
307 xforms = CTransforms()
308
309 def setUp(self):
310 self.maxDiff = None
311
312 def run_out_test(self, fname, source, expected):
313 """
314 Generate output using out_style,
315 """
316 patcher = patch('builtins.open',
317 new_callable=mock_open, read_data=source)
318
319 kernel_doc = KernelDoc(self.config, fname, self.xforms)
320
321 with patcher:
322 export_table, entries = kernel_doc.parse_kdoc()
323
324 result, expected = gen_output(fname, self.out_style, entries,
325 expected, relax_whitespace=True,
326 config=self.config)
327
328 self.assertEqual(result, expected)
329
330
331#
332# Selftest class
333#
334class TestSelfValidate(GenerateKdocItem):
335 """
336 Tests to check if logic inside GenerateKdocItem.run_test() is working.
337 """
338
339 SOURCE = """
340 /**
341 * function3: Exported function
342 * @arg1: @arg1 does nothing
343 *
344 * Does nothing
345 *
346 * return:
347 * always return 0.
348 */
349 int function3(char *arg1) { return 0; };
350 EXPORT_SYMBOL(function3);
351 """
352
353 EXPECTED = [{
354 'name': 'function3',
355 'type': 'function',
356 'declaration_start_line': 2,
357
358 'sections_start_lines': {
359 'Description': 4,
360 'Return': 7,
361 },
362 'sections': {
363 'Description': 'Does nothing\n\n',
364 'Return': '\nalways return 0.\n'
365 },
366
367 'sections_start_lines': {
368 'Description': 4,
369 'Return': 7,
370 },
371
372 'parameterdescs': {'arg1': '@arg1 does nothing\n'},
373 'parameterlist': ['arg1'],
374 'parameterdesc_start_lines': {'arg1': 3},
375 'parametertypes': {'arg1': 'char *arg1'},
376
377 'other_stuff': {
378 'func_macro': False,
379 'functiontype': 'int',
380 'purpose': 'Exported function',
381 'typedef': False
382 },
383 }]
384
385 EXPORTS = {"function3"}
386
387 def test_parse_pass(self):
388 """
389 Test if export_symbol is properly handled.
390 """
391 self.run_test(self.SOURCE, self.EXPECTED, self.EXPORTS)
392
393 @unittest.expectedFailure
394 def test_no_exports(self):
395 """
396 Test if export_symbol is properly handled.
397 """
398 self.run_test(self.SOURCE, [], {})
399
400 @unittest.expectedFailure
401 def test_with_empty_expected(self):
402 """
403 Test if export_symbol is properly handled.
404 """
405 self.run_test(self.SOURCE, [], self.EXPORTS)
406
407 @unittest.expectedFailure
408 def test_with_unfilled_expected(self):
409 """
410 Test if export_symbol is properly handled.
411 """
412 self.run_test(self.SOURCE, [{}], self.EXPORTS)
413
414 @unittest.expectedFailure
415 def test_with_default_expected(self):
416 """
417 Test if export_symbol is properly handled.
418 """
419 self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS)
420
421#
422# Class and logic to create dynamic tests from YAML
423#
424
425class KernelDocDynamicTests():
426 """
427 Dynamically create a set of tests from a YAML file.
428 """
429
430 @classmethod
431 def create_parser_test(cls, name, fname, source, symbols, exports):
432 """
433 Return a function that will be attached to the test class.
434 """
435 def test_method(self):
436 """Lambda-like function to run tests with provided vars"""
437 self.run_parser_test(source, symbols, exports, fname)
438
439 test_method.__name__ = f"test_gen_{name}"
440
441 setattr(CToKdocItem, test_method.__name__, test_method)
442
443 @classmethod
444 def create_out_test(cls, name, fname, symbols, out_type, data):
445 """
446 Return a function that will be attached to the test class.
447 """
448 def test_method(self):
449 """Lambda-like function to run tests with provided vars"""
450 self.run_out_test(fname, symbols, data)
451
452 test_method.__name__ = f"test_{out_type}_{name}"
453
454 if out_type == "man":
455 setattr(KdocItemToMan, test_method.__name__, test_method)
456 else:
457 setattr(KdocItemToRest, test_method.__name__, test_method)
458
459 @classmethod
460 def create_src2out_test(cls, name, fname, source, out_type, data):
461 """
462 Return a function that will be attached to the test class.
463 """
464 def test_method(self):
465 """Lambda-like function to run tests with provided vars"""
466 self.run_out_test(fname, source, data)
467
468 test_method.__name__ = f"test_{out_type}_{name}"
469
470 if out_type == "man":
471 setattr(CToMan, test_method.__name__, test_method)
472 else:
473 setattr(CToRest, test_method.__name__, test_method)
474
475 @classmethod
476 def create_tests(cls):
477 """
478 Iterate over all scenarios and add a method to the class for each.
479
480 The logic in this function assumes a valid test that are compliant
481 with kdoc-test-schema.yaml. There is an unit test to check that.
482 As such, it picks mandatory values directly, and uses get() for the
483 optional ones.
484 """
485
486 test_file = os.environ.get("yaml_file", TEST_FILE)
487
488 with open(test_file, encoding="utf-8") as fp:
489 testset = yaml.safe_load(fp)
490
491 tests = testset["tests"]
492
493 for idx, test in enumerate(tests):
494 name = test["name"]
495 fname = test["fname"]
496 source = test["source"]
497 expected_list = test["expected"]
498
499 exports = test.get("exports", [])
500
501 #
502 # The logic below allows setting up to 5 types of test:
503 # 1. from source to kdoc_item: test KernelDoc class;
504 # 2. from kdoc_item to man: test ManOutput class;
505 # 3. from kdoc_item to rst: test RestOutput class;
506 # 4. from source to man without checking expected KdocItem;
507 # 5. from source to rst without checking expected KdocItem.
508 #
509 for expected in expected_list:
510 kdoc_item = expected.get("kdoc_item")
511 man = expected.get("man", [])
512 rst = expected.get("rst", [])
513
514 if kdoc_item:
515 if isinstance(kdoc_item, dict):
516 kdoc_item = [kdoc_item]
517
518 symbols = []
519
520 for arg in kdoc_item:
521 arg["fname"] = fname
522 arg["start_line"] = 1
523
524 symbols.append(KdocItem.from_dict(arg))
525
526 if source:
527 cls.create_parser_test(name, fname, source,
528 symbols, exports)
529
530 if man:
531 cls.create_out_test(name, fname, symbols, "man", man)
532
533 if rst:
534 cls.create_out_test(name, fname, symbols, "rst", rst)
535
536 elif source:
537 if man:
538 cls.create_src2out_test(name, fname, source, "man", man)
539
540 if rst:
541 cls.create_src2out_test(name, fname, source, "rst", rst)
542
543KernelDocDynamicTests.create_tests()
544
545#
546# Run all tests
547#
548if __name__ == "__main__":
549 runner = TestUnits()
550 parser = runner.parse_args()
551 parser.add_argument("-y", "--yaml-file", "--yaml",
552 help='Name of the yaml file to load')
553
554 args = parser.parse_args()
555
556 if args.yaml_file:
557 env["yaml_file"] = os.path.expanduser(args.yaml_file)
558
559 # Run tests with customized arguments
560 runner.run(__file__, parser=parser, args=args, env=env)