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.
Unified · Split
| @@ -8,24 +8,43 @@ · 7 unmodified lines | ||
| 8 | 8 | Unit tests for kernel-doc parser. |
| 9 | 9 | """ |
| 10 | 10 | |
| 11 | +import logging | |
| 11 | 12 | import os |
| 12 | −import unittest | |
| 13 | 13 | import re |
| 14 | +import shlex | |
| 14 | 15 | import sys |
| 16 | +import unittest | |
| 15 | 17 | |
| 16 | 18 | from textwrap import dedent |
| 17 | 19 | from unittest.mock import patch, MagicMock, mock_open |
| 18 | 20 | |
| 21 | +import yaml | |
| 22 | + | |
| 19 | 23 | SRC_DIR = os.path.dirname(os.path.realpath(__file__)) |
| 20 | 24 | sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python")) |
| 21 | 25 | |
| 22 | −from kdoc.kdoc_parser import KernelDoc | |
| 26 | +from kdoc.kdoc_files import KdocConfig | |
| 23 | 27 | from kdoc.kdoc_item import KdocItem |
| 28 | +from kdoc.kdoc_parser import KernelDoc | |
| 29 | +from kdoc.kdoc_output import RestFormat, ManFormat | |
| 30 | + | |
| 24 | 31 | from kdoc.xforms_lists import CTransforms |
| 32 | + | |
| 25 | 33 | from unittest_helper import run_unittest |
| 26 | 34 | |
| 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 | +# | |
| 27 | 44 | #: 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) | |
| 29 | 48 | |
| 30 | 49 | def clean_whitespc(val, relax_whitespace=False): |
| 31 | 50 | """ |
| @@ -38,7 +57,9 @@ def clean_whitespc(val, relax_whitespace=False): · 6 unmodified lines | ||
| 38 | 57 | if isinstance(val, str): |
| 39 | 58 | val = val.strip() |
| 40 | 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 | 63 | elif isinstance(val, list): |
| 43 | 64 | val = [clean_whitespc(item, relax_whitespace) for item in val] |
| 44 | 65 | elif isinstance(val, dict): |
| @@ -46,22 +67,59 @@ def clean_whitespc(val, relax_whitespace=False): · 1 unmodified line | ||
| 46 | 67 | return val |
| 47 | 68 | |
| 48 | 69 | # |
| 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 | |
| 50 | 110 | # |
| 51 | −class KdocParser(unittest.TestCase): | |
| 111 | +class GenerateKdocItem(unittest.TestCase): | |
| 52 | 112 | """ |
| 53 | 113 | Base class to run KernelDoc parser class |
| 54 | 114 | """ |
| 55 | 115 | |
| 56 | 116 | DEFAULT = vars(KdocItem("", "", "", 0)) |
| 57 | 117 | |
| 118 | + config = MockKdocConfig() | |
| 119 | + xforms = CTransforms() | |
| 120 | + | |
| 58 | 121 | def setUp(self): |
| 59 | 122 | 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 | 124 | def run_test(self, source, __expected_list, exports={}, fname="test.c", |
| 67 | 125 | relax_whitespace=False): |
| @@ -75,6 +133,9 @@ class KdocParser(unittest.TestCase): · 7 unmodified lines | ||
| 75 | 133 | # Ensure that default values will be there |
| 76 | 134 | expected_list = [] |
| 77 | 135 | for e in __expected_list: |
| 136 | + if not isinstance(e, dict): | |
| 137 | + e = vars(e) | |
| 138 | + | |
| 78 | 139 | new_e = self.DEFAULT.copy() |
| 79 | 140 | new_e["fname"] = fname |
| 80 | 141 | for key, value in e.items(): |
| @@ -111,13 +172,155 @@ class KdocParser(unittest.TestCase): · 30 unmodified lines | ||
| 111 | 172 | |
| 112 | 173 | self.assertEqual(result, value, msg=f"at {key}") |
| 113 | 174 | |
| 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 | + | |
| 114 | 317 | |
| 115 | 318 | # |
| 116 | −# Selttest class | |
| 319 | +# Selftest class | |
| 117 | 320 | # |
| 118 | −class TestSelfValidate(KdocParser): | |
| 321 | +class 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 | 326 | SOURCE = """ |
| @@ -147,16 +350,23 @@ class TestSelfValidate(KdocParser): · 23 unmodified lines | ||
| 147 | 350 | 'Description': 'Does nothing\n\n', |
| 148 | 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 | 364 | 'other_stuff': { |
| 151 | 365 | 'func_macro': False, |
| 152 | 366 | 'functiontype': 'int', |
| 153 | 367 | 'purpose': 'Exported function', |
| 154 | 368 | '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 | 372 | EXPORTS = {"function3"} |
| @@ -195,6 +405,128 @@ class TestSelfValidate(KdocParser): · 32 unmodified lines | ||
| 195 | 405 | """ |
| 196 | 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 | + | |
| 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 | + | |
| 198 | 530 | # |
| 199 | 531 | # Run all tests |
| 200 | 532 | # |