History

docs: test_kdoc_parser: add support for dynamic test creation

Use the content of kdoc-test.yaml to generate unittests to verify that kernel-doc internal methods are parsing C code and generating output the expected way. Depending on what is written at the parser file at kdoc-test.yaml, up to 5 tests can be generated from a single test entry inside the YAML file: 1. from source to kdoc_item: test KernelDoc class; 2. from kdoc_item to man: test ManOutput class; 3. from kdoc_item to rst: test RestOutput class; 4. from source to man without checking expected KdocItem; 5. from source to rst without checking expected KdocItem.

Mauro Carvalho Chehab committed 8 months ago

Commit eea0d801 parent 8b69f521 file · +350 −18Browse files

Unified · Split

tools/unittests/test_kdoc_parser.py+350−18
@@ -8,24 +8,43 @@ · 7 unmodified lines
8Unit tests for kernel-doc parser.8Unit tests for kernel-doc parser.
9"""9"""
10 10
11import logging
11import os12import os
12import unittest
13import re13import re
14import shlex
14import sys15import sys
16import unittest
15 17
16from textwrap import dedent18from textwrap import dedent
17from unittest.mock import patch, MagicMock, mock_open19from unittest.mock import patch, MagicMock, mock_open
18 20
21import yaml
22
19SRC_DIR = os.path.dirname(os.path.realpath(__file__))23SRC_DIR = os.path.dirname(os.path.realpath(__file__))
20sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python"))24sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python"))
21 25
22from kdoc.kdoc_parser import KernelDoc26from kdoc.kdoc_files import KdocConfig
23from kdoc.kdoc_item import KdocItem27from kdoc.kdoc_item import KdocItem
28from kdoc.kdoc_parser import KernelDoc
29from kdoc.kdoc_output import RestFormat, ManFormat
30
24from kdoc.xforms_lists import CTransforms31from kdoc.xforms_lists import CTransforms
32
25from unittest_helper import run_unittest33from unittest_helper import run_unittest
26 34
35
36#
37# Test file
38#
39TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml")
40
41#
42# Ancillary logic to clean whitespaces
43#
27#: Regex to help cleaning whitespaces44#: Regex to help cleaning whitespaces
28RE_WHITESPC = re.compile(r"\s++")45RE_WHITESPC = re.compile(r"[ \t]++")
46RE_BEGINSPC = re.compile(r"^\s+", re.MULTILINE)
47RE_ENDSPC = re.compile(r"\s+$", re.MULTILINE)
29 48
30def clean_whitespc(val, relax_whitespace=False):49def clean_whitespc(val, relax_whitespace=False):
31 """50 """
@@ -38,7 +57,9 @@ def clean_whitespc(val, relax_whitespace=False): · 6 unmodified lines
38 if isinstance(val, str):57 if isinstance(val, str):
39 val = val.strip()58 val = val.strip()
40 if relax_whitespace:59 if relax_whitespace:
41 val = RE_WHITESPC.sub("", val)60 val = RE_WHITESPC.sub(" ", val)
61 val = RE_BEGINSPC.sub("", val)
62 val = RE_ENDSPC.sub("", val)
42 elif isinstance(val, list):63 elif isinstance(val, list):
43 val = [clean_whitespc(item, relax_whitespace) for item in val]64 val = [clean_whitespc(item, relax_whitespace) for item in val]
44 elif isinstance(val, dict):65 elif isinstance(val, dict):
@@ -46,22 +67,59 @@ def clean_whitespc(val, relax_whitespace=False): · 1 unmodified line
46 return val67 return val
47 68
48#69#
49# Helper class to help mocking with70# Helper classes to help mocking with logger and config
71#
72class MockLogging(logging.Handler):
73 """
74 Simple class to store everything on a list
75 """
76
77 def __init__(self, level=logging.NOTSET):
78 super().__init__(level)
79 self.messages = []
80 self.formatter = logging.Formatter()
81
82 def emit(self, record: logging.LogRecord) -> None:
83 """
84 Append a formatted record to self.messages.
85 """
86 try:
87 # The `format` method uses the handler's formatter.
88 message = self.format(record)
89 self.messages.append(message)
90 except Exception:
91 self.handleError(record)
92
93class MockKdocConfig(KdocConfig):
94 def __init__(self, *args, **kwargs):
95 super().__init__(*args, **kwargs)
96
97 self.log = logging.getLogger(__file__)
98 self.handler = MockLogging()
99 self.log.addHandler(self.handler)
100
101 def warning(self, msg):
102 """Ancillary routine to output a warning and increment error count."""
103
104 self.log.warning(msg)
105
106#
107# Helper class to generate KdocItem and validate its contents
108#
109# TODO: check self.config.handler.messages content
50#110#
51class KdocParser(unittest.TestCase):111class GenerateKdocItem(unittest.TestCase):
52 """112 """
53 Base class to run KernelDoc parser class113 Base class to run KernelDoc parser class
54 """114 """
55 115
56 DEFAULT = vars(KdocItem("", "", "", 0))116 DEFAULT = vars(KdocItem("", "", "", 0))
57 117
118 config = MockKdocConfig()
119 xforms = CTransforms()
120
58 def setUp(self):121 def setUp(self):
59 self.maxDiff = None122 self.maxDiff = None
60 self.config = MagicMock()
61 self.config.log = MagicMock()
62 self.config.log.debug = MagicMock()
63 self.xforms = CTransforms()
64
65 123
66 def run_test(self, source, __expected_list, exports={}, fname="test.c",124 def run_test(self, source, __expected_list, exports={}, fname="test.c",
67 relax_whitespace=False):125 relax_whitespace=False):
@@ -75,6 +133,9 @@ class KdocParser(unittest.TestCase): · 7 unmodified lines
75 # Ensure that default values will be there133 # Ensure that default values will be there
76 expected_list = []134 expected_list = []
77 for e in __expected_list:135 for e in __expected_list:
136 if not isinstance(e, dict):
137 e = vars(e)
138
78 new_e = self.DEFAULT.copy()139 new_e = self.DEFAULT.copy()
79 new_e["fname"] = fname140 new_e["fname"] = fname
80 for key, value in e.items():141 for key, value in e.items():
@@ -111,13 +172,155 @@ class KdocParser(unittest.TestCase): · 30 unmodified lines
111 172
112 self.assertEqual(result, value, msg=f"at {key}")173 self.assertEqual(result, value, msg=f"at {key}")
113 174
175#
176# Ancillary function that replicates kdoc_files way to generate output
177#
178def cleanup_timestamp(text):
179 lines = text.split("\n")
180
181 for i, line in enumerate(lines):
182 if not line.startswith('.TH'):
183 continue
184
185 parts = shlex.split(line)
186 if len(parts) > 3:
187 parts[3] = ""
188
189 lines[i] = " ".join(parts)
190
191
192 return "\n".join(lines)
193
194def gen_output(fname, out_style, symbols, expected,
195 config=None, relax_whitespace=False):
196 """
197 Use the output class to return an output content from KdocItem symbols.
198 """
199
200 if not config:
201 config = MockKdocConfig()
202
203 out_style.set_config(config)
204
205 msg = out_style.output_symbols(fname, symbols)
206
207 result = clean_whitespc(msg, relax_whitespace)
208 result = cleanup_timestamp(result)
209
210 expected = clean_whitespc(expected, relax_whitespace)
211 expected = cleanup_timestamp(expected)
212
213 return result, expected
214
215#
216# Classes to be used by dynamic test generation from YAML
217#
218class CToKdocItem(GenerateKdocItem):
219 def setUp(self):
220 self.maxDiff = None
221
222 def run_parser_test(self, source, symbols, exports, fname):
223 if isinstance(symbols, dict):
224 symbols = [symbols]
225
226 if isinstance(exports, str):
227 exports=set([exports])
228 elif isinstance(exports, list):
229 exports=set(exports)
230
231 self.run_test(source, symbols, exports=exports,
232 fname=fname, relax_whitespace=True)
233
234class KdocItemToMan(unittest.TestCase):
235 out_style = ManFormat()
236
237 def setUp(self):
238 self.maxDiff = None
239
240 def run_out_test(self, fname, symbols, expected):
241 """
242 Generate output using out_style,
243 """
244 result, expected = gen_output(fname, self.out_style,
245 symbols, expected)
246
247 self.assertEqual(result, expected)
248
249class KdocItemToRest(unittest.TestCase):
250 out_style = RestFormat()
251
252 def setUp(self):
253 self.maxDiff = None
254
255 def run_out_test(self, fname, symbols, expected):
256 """
257 Generate output using out_style,
258 """
259 result, expected = gen_output(fname, self.out_style, symbols,
260 expected, relax_whitespace=True)
261
262 self.assertEqual(result, expected)
263
264
265class CToMan(unittest.TestCase):
266 out_style = ManFormat()
267 config = MockKdocConfig()
268 xforms = CTransforms()
269
270 def setUp(self):
271 self.maxDiff = None
272
273 def run_out_test(self, fname, source, expected):
274 """
275 Generate output using out_style,
276 """
277 patcher = patch('builtins.open',
278 new_callable=mock_open, read_data=source)
279
280 kernel_doc = KernelDoc(self.config, fname, self.xforms)
281
282 with patcher:
283 export_table, entries = kernel_doc.parse_kdoc()
284
285 result, expected = gen_output(fname, self.out_style,
286 entries, expected, config=self.config)
287
288 self.assertEqual(result, expected)
289
290
291class CToRest(unittest.TestCase):
292 out_style = RestFormat()
293 config = MockKdocConfig()
294 xforms = CTransforms()
295
296 def setUp(self):
297 self.maxDiff = None
298
299 def run_out_test(self, fname, source, expected):
300 """
301 Generate output using out_style,
302 """
303 patcher = patch('builtins.open',
304 new_callable=mock_open, read_data=source)
305
306 kernel_doc = KernelDoc(self.config, fname, self.xforms)
307
308 with patcher:
309 export_table, entries = kernel_doc.parse_kdoc()
310
311 result, expected = gen_output(fname, self.out_style, entries,
312 expected, relax_whitespace=True,
313 config=self.config)
314
315 self.assertEqual(result, expected)
316
114 317
115#318#
116# Selttest class319# Selftest class
117#320#
118class TestSelfValidate(KdocParser):321class TestSelfValidate(GenerateKdocItem):
119 """322 """
120 Tests to check if logic inside KdocParser.run_test() is working.323 Tests to check if logic inside GenerateKdocItem.run_test() is working.
121 """324 """
122 325
123 SOURCE = """326 SOURCE = """
@@ -147,16 +350,23 @@ class TestSelfValidate(KdocParser): · 23 unmodified lines
147 'Description': 'Does nothing\n\n',350 'Description': 'Does nothing\n\n',
148 'Return': '\nalways return 0.\n'351 'Return': '\nalways return 0.\n'
149 },352 },
353
354 'sections_start_lines': {
355 'Description': 4,
356 'Return': 7,
357 },
358
359 'parameterdescs': {'arg1': '@arg1 does nothing\n'},
360 'parameterlist': ['arg1'],
361 'parameterdesc_start_lines': {'arg1': 3},
362 'parametertypes': {'arg1': 'char *arg1'},
363
150 'other_stuff': {364 'other_stuff': {
151 'func_macro': False,365 'func_macro': False,
152 'functiontype': 'int',366 'functiontype': 'int',
153 'purpose': 'Exported function',367 'purpose': 'Exported function',
154 'typedef': False368 'typedef': False
155 },369 },
156 'parameterdescs': {'arg1': '@arg1 does nothing\n'},
157 'parameterlist': ['arg1'],
158 'parameterdesc_start_lines': {'arg1': 3},
159 'parametertypes': {'arg1': 'char *arg1'},
160 }]370 }]
161 371
162 EXPORTS = {"function3"}372 EXPORTS = {"function3"}
@@ -195,6 +405,128 @@ class TestSelfValidate(KdocParser): · 32 unmodified lines
195 """405 """
196 self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS)406 self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS)
197 407
408#
409# Class and logic to create dynamic tests from YAML
410#
411
412class KernelDocDynamicTests():
413 """
414 Dynamically create a set of tests from a YAML file.
415 """
416
417 @classmethod
418 def create_parser_test(cls, name, fname, source, symbols, exports):
419 """
420 Return a function that will be attached to the test class.
421 """
422 def test_method(self):
423 """Lambda-like function to run tests with provided vars"""
424 self.run_parser_test(source, symbols, exports, fname)
425
426 test_method.__name__ = f"test_gen_{name}"
427
428 setattr(CToKdocItem, test_method.__name__, test_method)
429
430 @classmethod
431 def create_out_test(cls, name, fname, symbols, out_type, data):
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_out_test(fname, symbols, data)
438
439 test_method.__name__ = f"test_{out_type}_{name}"
440
441 if out_type == "man":
442 setattr(KdocItemToMan, test_method.__name__, test_method)
443 else:
444 setattr(KdocItemToRest, test_method.__name__, test_method)
445
446 @classmethod
447 def create_src2out_test(cls, name, fname, source, out_type, data):
448 """
449 Return a function that will be attached to the test class.
450 """
451 def test_method(self):
452 """Lambda-like function to run tests with provided vars"""
453 self.run_out_test(fname, source, data)
454
455 test_method.__name__ = f"test_{out_type}_{name}"
456
457 if out_type == "man":
458 setattr(CToMan, test_method.__name__, test_method)
459 else:
460 setattr(CToRest, test_method.__name__, test_method)
461
462 @classmethod
463 def create_tests(cls):
464 """
465 Iterate over all scenarios and add a method to the class for each.
466
467 The logic in this function assumes a valid test that are compliant
468 with kdoc-test-schema.yaml. There is an unit test to check that.
469 As such, it picks mandatory values directly, and uses get() for the
470 optional ones.
471 """
472
473 with open(TEST_FILE, encoding="utf-8") as fp:
474 testset = yaml.safe_load(fp)
475
476 tests = testset["tests"]
477
478 for idx, test in enumerate(tests):
479 name = test["name"]
480 fname = test["fname"]
481 source = test["source"]
482 expected_list = test["expected"]
483
484 exports = test.get("exports", [])
485
486 #
487 # The logic below allows setting up to 5 types of test:
488 # 1. from source to kdoc_item: test KernelDoc class;
489 # 2. from kdoc_item to man: test ManOutput class;
490 # 3. from kdoc_item to rst: test RestOutput class;
491 # 4. from source to man without checking expected KdocItem;
492 # 5. from source to rst without checking expected KdocItem.
493 #
494 for expected in expected_list:
495 kdoc_item = expected.get("kdoc_item")
496 man = expected.get("man", [])
497 rst = expected.get("rst", [])
498
499 if kdoc_item:
500 if isinstance(kdoc_item, dict):
501 kdoc_item = [kdoc_item]
502
503 symbols = []
504
505 for arg in kdoc_item:
506 arg["fname"] = fname
507 arg["start_line"] = 1
508
509 symbols.append(KdocItem.from_dict(arg))
510
511 if source:
512 cls.create_parser_test(name, fname, source,
513 symbols, exports)
514
515 if man:
516 cls.create_out_test(name, fname, symbols, "man", man)
517
518 if rst:
519 cls.create_out_test(name, fname, symbols, "rst", rst)
520
521 elif source:
522 if man:
523 cls.create_src2out_test(name, fname, source, "man", man)
524
525 if rst:
526 cls.create_src2out_test(name, fname, source, "rst", rst)
527
528KernelDocDynamicTests.create_tests()
529
198#530#
199# Run all tests531# Run all tests
200#532#

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t