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
88 Unit tests for kernel-doc parser.
99 """
1010
11+import logging
1112 import os
12−import unittest
1313 import re
14+import shlex
1415 import sys
16+import unittest
1517
1618 from textwrap import dedent
1719 from unittest.mock import patch, MagicMock, mock_open
1820
21+import yaml
22+
1923 SRC_DIR = os.path.dirname(os.path.realpath(__file__))
2024 sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python"))
2125
22−from kdoc.kdoc_parser import KernelDoc
26+from kdoc.kdoc_files import KdocConfig
2327 from kdoc.kdoc_item import KdocItem
28+from kdoc.kdoc_parser import KernelDoc
29+from kdoc.kdoc_output import RestFormat, ManFormat
30+
2431 from kdoc.xforms_lists import CTransforms
32+
2533 from unittest_helper import run_unittest
2634
35+
36+#
37+# Test file
38+#
39+TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml")
40+
41+#
42+# Ancillary logic to clean whitespaces
43+#
2744 #: Regex to help cleaning whitespaces
28−RE_WHITESPC = re.compile(r"\s++")
45+RE_WHITESPC = re.compile(r"[ \t]++")
46+RE_BEGINSPC = re.compile(r"^\s+", re.MULTILINE)
47+RE_ENDSPC = re.compile(r"\s+$", re.MULTILINE)
2948
3049 def clean_whitespc(val, relax_whitespace=False):
3150 """
@@ -38,7 +57,9 @@ def clean_whitespc(val, relax_whitespace=False): · 6 unmodified lines
3857 if isinstance(val, str):
3958 val = val.strip()
4059 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)
4263 elif isinstance(val, list):
4364 val = [clean_whitespc(item, relax_whitespace) for item in val]
4465 elif isinstance(val, dict):
@@ -46,22 +67,59 @@ def clean_whitespc(val, relax_whitespace=False): · 1 unmodified line
4667 return val
4768
4869 #
49−# Helper class to help mocking with
70+# Helper classes to help mocking with logger and config
71+#
72+class 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+
93+class 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
50110 #
51−class KdocParser(unittest.TestCase):
111+class GenerateKdocItem(unittest.TestCase):
52112 """
53113 Base class to run KernelDoc parser class
54114 """
55115
56116 DEFAULT = vars(KdocItem("", "", "", 0))
57117
118+ config = MockKdocConfig()
119+ xforms = CTransforms()
120+
58121 def setUp(self):
59122 self.maxDiff = None
60− self.config = MagicMock()
61− self.config.log = MagicMock()
62− self.config.log.debug = MagicMock()
63− self.xforms = CTransforms()
64−
65123
66124 def run_test(self, source, __expected_list, exports={}, fname="test.c",
67125 relax_whitespace=False):
@@ -75,6 +133,9 @@ class KdocParser(unittest.TestCase): · 7 unmodified lines
75133 # Ensure that default values will be there
76134 expected_list = []
77135 for e in __expected_list:
136+ if not isinstance(e, dict):
137+ e = vars(e)
138+
78139 new_e = self.DEFAULT.copy()
79140 new_e["fname"] = fname
80141 for key, value in e.items():
@@ -111,13 +172,155 @@ class KdocParser(unittest.TestCase): · 30 unmodified lines
111172
112173 self.assertEqual(result, value, msg=f"at {key}")
113174
175+#
176+# Ancillary function that replicates kdoc_files way to generate output
177+#
178+def 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+
194+def 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+#
218+class 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+
234+class 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+
249+class 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+
265+class 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+
291+class 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+
114317
115318 #
116−# Selttest class
319+# Selftest class
117320 #
118−class TestSelfValidate(KdocParser):
321+class TestSelfValidate(GenerateKdocItem):
119322 """
120− Tests to check if logic inside KdocParser.run_test() is working.
323+ Tests to check if logic inside GenerateKdocItem.run_test() is working.
121324 """
122325
123326 SOURCE = """
@@ -147,16 +350,23 @@ class TestSelfValidate(KdocParser): · 23 unmodified lines
147350 'Description': 'Does nothing\n\n',
148351 'Return': '\nalways return 0.\n'
149352 },
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+
150364 'other_stuff': {
151365 'func_macro': False,
152366 'functiontype': 'int',
153367 'purpose': 'Exported function',
154368 'typedef': False
155369 },
156− 'parameterdescs': {'arg1': '@arg1 does nothing\n'},
157− 'parameterlist': ['arg1'],
158− 'parameterdesc_start_lines': {'arg1': 3},
159− 'parametertypes': {'arg1': 'char *arg1'},
160370 }]
161371
162372 EXPORTS = {"function3"}
@@ -195,6 +405,128 @@ class TestSelfValidate(KdocParser): · 32 unmodified lines
195405 """
196406 self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS)
197407
408+#
409+# Class and logic to create dynamic tests from YAML
410+#
411+
412+class 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+
528+KernelDocDynamicTests.create_tests()
529+
198530 #
199531 # Run all tests
200532 #

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