Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
torvalds
GitHub Repository: torvalds/linux
Path: blob/master/tools/unittests/test_kdoc_parser.py
170831 views
1
#!/usr/bin/env python3
2
# SPDX-License-Identifier: GPL-2.0
3
# Copyright(c) 2026: Mauro Carvalho Chehab <[email protected]>.
4
#
5
# pylint: disable=C0200,C0413,W0102,R0914
6
7
"""
8
Unit tests for kernel-doc parser.
9
"""
10
11
import logging
12
import os
13
import re
14
import shlex
15
import sys
16
import unittest
17
18
from textwrap import dedent
19
from unittest.mock import patch, MagicMock, mock_open
20
21
import yaml
22
23
SRC_DIR = os.path.dirname(os.path.realpath(__file__))
24
sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python"))
25
26
from kdoc.kdoc_files import KdocConfig
27
from kdoc.kdoc_item import KdocItem
28
from kdoc.kdoc_parser import KernelDoc
29
from kdoc.kdoc_output import RestFormat, ManFormat
30
31
from kdoc.xforms_lists import CTransforms
32
33
from unittest_helper import TestUnits
34
35
36
#
37
# Test file
38
#
39
TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml")
40
41
env = {
42
"yaml_file": TEST_FILE
43
}
44
45
#
46
# Ancillary logic to clean whitespaces
47
#
48
#: Regex to help cleaning whitespaces
49
RE_WHITESPC = re.compile(r"[ \t]++")
50
RE_BEGINSPC = re.compile(r"^\s+", re.MULTILINE)
51
RE_ENDSPC = re.compile(r"\s+$", re.MULTILINE)
52
53
def 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
#
76
class 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
97
class 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
#
115
class 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
#
191
def 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
207
def 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
#
231
class 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
247
class 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
262
class 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
278
class 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
304
class 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
#
334
class 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
425
class 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
543
KernelDocDynamicTests.create_tests()
544
545
#
546
# Run all tests
547
#
548
if __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)
561
562