| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 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 | |
| 38 | |
| 39 | TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml")
|
| 40 |
|
| 41 | env = {
|
| 42 | "yaml_file": TEST_FILE
|
| 43 | }
|
| 44 |
|
| 45 | |
| 46 | |
| 47 | |
| 48 | |
| 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 | |
| 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 | |
| 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 |
|
| 107 |
|
| 108 | self.log.warning(msg)
|
| 109 |
|
| 110 | |
| 111 | |
| 112 | |
| 113 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | class TestContextLockStruct(GenerateKdocItem):
|
| 422 | def test_context_lock_struct_declaration(self):
|
| 423 | source = """
|
| 424 | /**
|
| 425 | * struct debugfs_cancellation - cancellation data
|
| 426 | * @list: internal, for keeping track
|
| 427 | * @cancel: callback to call
|
| 428 | * @cancel_data: extra data for the callback to call
|
| 429 | */
|
| 430 | context_lock_struct(debugfs_cancellation) {
|
| 431 | struct list_head list;
|
| 432 | void (*cancel)(struct dentry *, void *);
|
| 433 | void *cancel_data;
|
| 434 | };
|
| 435 | """
|
| 436 |
|
| 437 | kernel_doc = KernelDoc(self.config, "test.c", self.xforms)
|
| 438 | patcher = patch('builtins.open', new_callable=mock_open,
|
| 439 | read_data=dedent(source))
|
| 440 | with patcher:
|
| 441 | _, entries = kernel_doc.parse_kdoc()
|
| 442 |
|
| 443 | self.assertEqual(len(entries), 1)
|
| 444 | self.assertEqual(entries[0].name, "debugfs_cancellation")
|
| 445 | self.assertEqual(entries[0].warnings, [])
|
| 446 |
|
| 447 | |
| 448 | |
| 449 | |
| 450 |
|
| 451 | class KernelDocDynamicTests():
|
| 452 | |
| 453 | Dynamically create a set of tests from a YAML file.
|
| 454 |
|
| 455 |
|
| 456 | @classmethod
|
| 457 | def create_parser_test(cls, name, fname, source, symbols, exports):
|
| 458 | |
| 459 | Return a function that will be attached to the test class.
|
| 460 |
|
| 461 | def test_method(self):
|
| 462 |
|
| 463 | self.run_parser_test(source, symbols, exports, fname)
|
| 464 |
|
| 465 | test_method.__name__ = f"test_gen_{name}"
|
| 466 |
|
| 467 | setattr(CToKdocItem, test_method.__name__, test_method)
|
| 468 |
|
| 469 | @classmethod
|
| 470 | def create_out_test(cls, name, fname, symbols, out_type, data):
|
| 471 | |
| 472 | Return a function that will be attached to the test class.
|
| 473 |
|
| 474 | def test_method(self):
|
| 475 |
|
| 476 | self.run_out_test(fname, symbols, data)
|
| 477 |
|
| 478 | test_method.__name__ = f"test_{out_type}_{name}"
|
| 479 |
|
| 480 | if out_type == "man":
|
| 481 | setattr(KdocItemToMan, test_method.__name__, test_method)
|
| 482 | else:
|
| 483 | setattr(KdocItemToRest, test_method.__name__, test_method)
|
| 484 |
|
| 485 | @classmethod
|
| 486 | def create_src2out_test(cls, name, fname, source, out_type, data):
|
| 487 | |
| 488 | Return a function that will be attached to the test class.
|
| 489 |
|
| 490 | def test_method(self):
|
| 491 |
|
| 492 | self.run_out_test(fname, source, data)
|
| 493 |
|
| 494 | test_method.__name__ = f"test_{out_type}_{name}"
|
| 495 |
|
| 496 | if out_type == "man":
|
| 497 | setattr(CToMan, test_method.__name__, test_method)
|
| 498 | else:
|
| 499 | setattr(CToRest, test_method.__name__, test_method)
|
| 500 |
|
| 501 | @classmethod
|
| 502 | def create_tests(cls):
|
| 503 | |
| 504 | Iterate over all scenarios and add a method to the class for each.
|
| 505 |
|
| 506 | The logic in this function assumes a valid test that are compliant
|
| 507 | with kdoc-test-schema.yaml. There is an unit test to check that.
|
| 508 | As such, it picks mandatory values directly, and uses get() for the
|
| 509 | optional ones.
|
| 510 |
|
| 511 |
|
| 512 | test_file = os.environ.get("yaml_file", TEST_FILE)
|
| 513 |
|
| 514 | with open(test_file, encoding="utf-8") as fp:
|
| 515 | testset = yaml.safe_load(fp)
|
| 516 |
|
| 517 | tests = testset["tests"]
|
| 518 |
|
| 519 | for idx, test in enumerate(tests):
|
| 520 | name = test["name"]
|
| 521 | fname = test["fname"]
|
| 522 | source = test["source"]
|
| 523 | expected_list = test["expected"]
|
| 524 |
|
| 525 | exports = test.get("exports", [])
|
| 526 |
|
| 527 | |
| 528 | |
| 529 | |
| 530 | |
| 531 | |
| 532 | |
| 533 | |
| 534 | |
| 535 | for expected in expected_list:
|
| 536 | kdoc_item = expected.get("kdoc_item")
|
| 537 | man = expected.get("man", [])
|
| 538 | rst = expected.get("rst", [])
|
| 539 |
|
| 540 | if kdoc_item:
|
| 541 | if isinstance(kdoc_item, dict):
|
| 542 | kdoc_item = [kdoc_item]
|
| 543 |
|
| 544 | symbols = []
|
| 545 |
|
| 546 | for arg in kdoc_item:
|
| 547 | arg["fname"] = fname
|
| 548 | arg["start_line"] = 1
|
| 549 |
|
| 550 | symbols.append(KdocItem.from_dict(arg))
|
| 551 |
|
| 552 | if source:
|
| 553 | cls.create_parser_test(name, fname, source,
|
| 554 | symbols, exports)
|
| 555 |
|
| 556 | if man:
|
| 557 | cls.create_out_test(name, fname, symbols, "man", man)
|
| 558 |
|
| 559 | if rst:
|
| 560 | cls.create_out_test(name, fname, symbols, "rst", rst)
|
| 561 |
|
| 562 | elif source:
|
| 563 | if man:
|
| 564 | cls.create_src2out_test(name, fname, source, "man", man)
|
| 565 |
|
| 566 | if rst:
|
| 567 | cls.create_src2out_test(name, fname, source, "rst", rst)
|
| 568 |
|
| 569 | KernelDocDynamicTests.create_tests()
|
| 570 |
|
| 571 | |
| 572 | |
| 573 | |
| 574 | if __name__ == "__main__":
|
| 575 | runner = TestUnits()
|
| 576 | parser = runner.parse_args()
|
| 577 | parser.add_argument("-y", "--yaml-file", "--yaml",
|
| 578 | help='Name of the yaml file to load')
|
| 579 |
|
| 580 | args = parser.parse_args()
|
| 581 |
|
| 582 | if args.yaml_file:
|
| 583 | env["yaml_file"] = os.path.expanduser(args.yaml_file)
|
| 584 |
|
| 585 | |
| 586 | runner.run(__file__, parser=parser, args=args, env=env)
|