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 | Unit tests for kernel-doc parser. | 8 | Unit tests for kernel-doc parser. |
| 9 | """ | 9 | """ |
| 10 | 10 | ||
| 11 | import logging | ||
| 11 | import os | 12 | import os |
| 12 | import unittest | ||
| 13 | import re | 13 | import re |
| 14 | import shlex | ||
| 14 | import sys | 15 | import sys |
| 16 | import unittest | ||
| 15 | 17 | ||
| 16 | from textwrap import dedent | 18 | from textwrap import dedent |
| 17 | from unittest.mock import patch, MagicMock, mock_open | 19 | from unittest.mock import patch, MagicMock, mock_open |
| 18 | 20 | ||
| 21 | import yaml | ||
| 22 | |||
| 19 | SRC_DIR = os.path.dirname(os.path.realpath(__file__)) | 23 | SRC_DIR = os.path.dirname(os.path.realpath(__file__)) |
| 20 | sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python")) | 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 | from kdoc.kdoc_item import KdocItem | 27 | from kdoc.kdoc_item import KdocItem |
| 28 | from kdoc.kdoc_parser import KernelDoc | ||
| 29 | from kdoc.kdoc_output import RestFormat, ManFormat | ||
| 30 | |||
| 24 | from kdoc.xforms_lists import CTransforms | 31 | from kdoc.xforms_lists import CTransforms |
| 32 | |||
| 25 | from unittest_helper import run_unittest | 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 | #: Regex to help cleaning whitespaces | 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 | def clean_whitespc(val, relax_whitespace=False): | 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 | 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 val | 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 | Base class to run KernelDoc parser class | 113 | 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 = None | 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 | 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 there | 133 | # 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"] = fname | 140 | 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 | # | ||
| 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 | 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': False | 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 | 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 | |||
| 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 | # Run all tests | 531 | # Run all tests |
| 200 | # | 532 | # |