1# Argument Clinic 2# Copyright 2012-2013 by Larry Hastings. 3# Licensed to the PSF under a contributor agreement. 4 5from test import support, test_tools 6from unittest import TestCase 7import collections 8import inspect 9import os.path 10import sys 11import unittest 12 13test_tools.skip_if_missing('clinic') 14with test_tools.imports_under_tool('clinic'): 15 import clinic 16 from clinic import DSLParser 17 18 19class FakeConverter: 20 def __init__(self, name, args): 21 self.name = name 22 self.args = args 23 24 25class FakeConverterFactory: 26 def __init__(self, name): 27 self.name = name 28 29 def __call__(self, name, default, **kwargs): 30 return FakeConverter(self.name, kwargs) 31 32 33class FakeConvertersDict: 34 def __init__(self): 35 self.used_converters = {} 36 37 def get(self, name, default): 38 return self.used_converters.setdefault(name, FakeConverterFactory(name)) 39 40c = clinic.Clinic(language='C', filename = "file") 41 42class FakeClinic: 43 def __init__(self): 44 self.converters = FakeConvertersDict() 45 self.legacy_converters = FakeConvertersDict() 46 self.language = clinic.CLanguage(None) 47 self.filename = None 48 self.destination_buffers = {} 49 self.block_parser = clinic.BlockParser('', self.language) 50 self.modules = collections.OrderedDict() 51 self.classes = collections.OrderedDict() 52 clinic.clinic = self 53 self.name = "FakeClinic" 54 self.line_prefix = self.line_suffix = '' 55 self.destinations = {} 56 self.add_destination("block", "buffer") 57 self.add_destination("file", "buffer") 58 self.add_destination("suppress", "suppress") 59 d = self.destinations.get 60 self.field_destinations = collections.OrderedDict(( 61 ('docstring_prototype', d('suppress')), 62 ('docstring_definition', d('block')), 63 ('methoddef_define', d('block')), 64 ('impl_prototype', d('block')), 65 ('parser_prototype', d('suppress')), 66 ('parser_definition', d('block')), 67 ('impl_definition', d('block')), 68 )) 69 70 def get_destination(self, name): 71 d = self.destinations.get(name) 72 if not d: 73 sys.exit("Destination does not exist: " + repr(name)) 74 return d 75 76 def add_destination(self, name, type, *args): 77 if name in self.destinations: 78 sys.exit("Destination already exists: " + repr(name)) 79 self.destinations[name] = clinic.Destination(name, type, self, *args) 80 81 def is_directive(self, name): 82 return name == "module" 83 84 def directive(self, name, args): 85 self.called_directives[name] = args 86 87 _module_and_class = clinic.Clinic._module_and_class 88 89class ClinicWholeFileTest(TestCase): 90 def test_eol(self): 91 # regression test: 92 # clinic's block parser didn't recognize 93 # the "end line" for the block if it 94 # didn't end in "\n" (as in, the last) 95 # byte of the file was '/'. 96 # so it would spit out an end line for you. 97 # and since you really already had one, 98 # the last line of the block got corrupted. 99 c = clinic.Clinic(clinic.CLanguage(None), filename="file") 100 raw = "/*[clinic]\nfoo\n[clinic]*/" 101 cooked = c.parse(raw).splitlines() 102 end_line = cooked[2].rstrip() 103 # this test is redundant, it's just here explicitly to catch 104 # the regression test so we don't forget what it looked like 105 self.assertNotEqual(end_line, "[clinic]*/[clinic]*/") 106 self.assertEqual(end_line, "[clinic]*/") 107 108 109 110class ClinicGroupPermuterTest(TestCase): 111 def _test(self, l, m, r, output): 112 computed = clinic.permute_optional_groups(l, m, r) 113 self.assertEqual(output, computed) 114 115 def test_range(self): 116 self._test([['start']], ['stop'], [['step']], 117 ( 118 ('stop',), 119 ('start', 'stop',), 120 ('start', 'stop', 'step',), 121 )) 122 123 def test_add_window(self): 124 self._test([['x', 'y']], ['ch'], [['attr']], 125 ( 126 ('ch',), 127 ('ch', 'attr'), 128 ('x', 'y', 'ch',), 129 ('x', 'y', 'ch', 'attr'), 130 )) 131 132 def test_ludicrous(self): 133 self._test([['a1', 'a2', 'a3'], ['b1', 'b2']], ['c1'], [['d1', 'd2'], ['e1', 'e2', 'e3']], 134 ( 135 ('c1',), 136 ('b1', 'b2', 'c1'), 137 ('b1', 'b2', 'c1', 'd1', 'd2'), 138 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1'), 139 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1', 'd1', 'd2'), 140 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1', 'd1', 'd2', 'e1', 'e2', 'e3'), 141 )) 142 143 def test_right_only(self): 144 self._test([], [], [['a'],['b'],['c']], 145 ( 146 (), 147 ('a',), 148 ('a', 'b'), 149 ('a', 'b', 'c') 150 )) 151 152 def test_have_left_options_but_required_is_empty(self): 153 def fn(): 154 clinic.permute_optional_groups(['a'], [], []) 155 self.assertRaises(AssertionError, fn) 156 157 158class ClinicLinearFormatTest(TestCase): 159 def _test(self, input, output, **kwargs): 160 computed = clinic.linear_format(input, **kwargs) 161 self.assertEqual(output, computed) 162 163 def test_empty_strings(self): 164 self._test('', '') 165 166 def test_solo_newline(self): 167 self._test('\n', '\n') 168 169 def test_no_substitution(self): 170 self._test(""" 171 abc 172 """, """ 173 abc 174 """) 175 176 def test_empty_substitution(self): 177 self._test(""" 178 abc 179 {name} 180 def 181 """, """ 182 abc 183 def 184 """, name='') 185 186 def test_single_line_substitution(self): 187 self._test(""" 188 abc 189 {name} 190 def 191 """, """ 192 abc 193 GARGLE 194 def 195 """, name='GARGLE') 196 197 def test_multiline_substitution(self): 198 self._test(""" 199 abc 200 {name} 201 def 202 """, """ 203 abc 204 bingle 205 bungle 206 207 def 208 """, name='bingle\nbungle\n') 209 210class InertParser: 211 def __init__(self, clinic): 212 pass 213 214 def parse(self, block): 215 pass 216 217class CopyParser: 218 def __init__(self, clinic): 219 pass 220 221 def parse(self, block): 222 block.output = block.input 223 224 225class ClinicBlockParserTest(TestCase): 226 def _test(self, input, output): 227 language = clinic.CLanguage(None) 228 229 blocks = list(clinic.BlockParser(input, language)) 230 writer = clinic.BlockPrinter(language) 231 for block in blocks: 232 writer.print_block(block) 233 output = writer.f.getvalue() 234 assert output == input, "output != input!\n\noutput " + repr(output) + "\n\n input " + repr(input) 235 236 def round_trip(self, input): 237 return self._test(input, input) 238 239 def test_round_trip_1(self): 240 self.round_trip(""" 241 verbatim text here 242 lah dee dah 243""") 244 def test_round_trip_2(self): 245 self.round_trip(""" 246 verbatim text here 247 lah dee dah 248/*[inert] 249abc 250[inert]*/ 251def 252/*[inert checksum: 7b18d017f89f61cf17d47f92749ea6930a3f1deb]*/ 253xyz 254""") 255 256 def _test_clinic(self, input, output): 257 language = clinic.CLanguage(None) 258 c = clinic.Clinic(language, filename="file") 259 c.parsers['inert'] = InertParser(c) 260 c.parsers['copy'] = CopyParser(c) 261 computed = c.parse(input) 262 self.assertEqual(output, computed) 263 264 def test_clinic_1(self): 265 self._test_clinic(""" 266 verbatim text here 267 lah dee dah 268/*[copy input] 269def 270[copy start generated code]*/ 271abc 272/*[copy end generated code: output=03cfd743661f0797 input=7b18d017f89f61cf]*/ 273xyz 274""", """ 275 verbatim text here 276 lah dee dah 277/*[copy input] 278def 279[copy start generated code]*/ 280def 281/*[copy end generated code: output=7b18d017f89f61cf input=7b18d017f89f61cf]*/ 282xyz 283""") 284 285 286class ClinicParserTest(TestCase): 287 def test_trivial(self): 288 parser = DSLParser(FakeClinic()) 289 block = clinic.Block("module os\nos.access") 290 parser.parse(block) 291 module, function = block.signatures 292 self.assertEqual("access", function.name) 293 self.assertEqual("os", module.name) 294 295 def test_ignore_line(self): 296 block = self.parse("#\nmodule os\nos.access") 297 module, function = block.signatures 298 self.assertEqual("access", function.name) 299 self.assertEqual("os", module.name) 300 301 def test_param(self): 302 function = self.parse_function("module os\nos.access\n path: int") 303 self.assertEqual("access", function.name) 304 self.assertEqual(2, len(function.parameters)) 305 p = function.parameters['path'] 306 self.assertEqual('path', p.name) 307 self.assertIsInstance(p.converter, clinic.int_converter) 308 309 def test_param_default(self): 310 function = self.parse_function("module os\nos.access\n follow_symlinks: bool = True") 311 p = function.parameters['follow_symlinks'] 312 self.assertEqual(True, p.default) 313 314 def test_param_with_continuations(self): 315 function = self.parse_function("module os\nos.access\n follow_symlinks: \\\n bool \\\n =\\\n True") 316 p = function.parameters['follow_symlinks'] 317 self.assertEqual(True, p.default) 318 319 def test_param_default_expression(self): 320 function = self.parse_function("module os\nos.access\n follow_symlinks: int(c_default='MAXSIZE') = sys.maxsize") 321 p = function.parameters['follow_symlinks'] 322 self.assertEqual(sys.maxsize, p.default) 323 self.assertEqual("MAXSIZE", p.converter.c_default) 324 325 s = self.parse_function_should_fail("module os\nos.access\n follow_symlinks: int = sys.maxsize") 326 self.assertEqual(s, "Error on line 0:\nWhen you specify a named constant ('sys.maxsize') as your default value,\nyou MUST specify a valid c_default.\n") 327 328 def test_param_no_docstring(self): 329 function = self.parse_function(""" 330module os 331os.access 332 follow_symlinks: bool = True 333 something_else: str = ''""") 334 p = function.parameters['follow_symlinks'] 335 self.assertEqual(3, len(function.parameters)) 336 self.assertIsInstance(function.parameters['something_else'].converter, clinic.str_converter) 337 338 def test_param_default_parameters_out_of_order(self): 339 s = self.parse_function_should_fail(""" 340module os 341os.access 342 follow_symlinks: bool = True 343 something_else: str""") 344 self.assertEqual(s, """Error on line 0: 345Can't have a parameter without a default ('something_else') 346after a parameter with a default! 347""") 348 349 def disabled_test_converter_arguments(self): 350 function = self.parse_function("module os\nos.access\n path: path_t(allow_fd=1)") 351 p = function.parameters['path'] 352 self.assertEqual(1, p.converter.args['allow_fd']) 353 354 def test_function_docstring(self): 355 function = self.parse_function(""" 356module os 357os.stat as os_stat_fn 358 359 path: str 360 Path to be examined 361 362Perform a stat system call on the given path.""") 363 self.assertEqual(""" 364stat($module, /, path) 365-- 366 367Perform a stat system call on the given path. 368 369 path 370 Path to be examined 371""".strip(), function.docstring) 372 373 def test_explicit_parameters_in_docstring(self): 374 function = self.parse_function(""" 375module foo 376foo.bar 377 x: int 378 Documentation for x. 379 y: int 380 381This is the documentation for foo. 382 383Okay, we're done here. 384""") 385 self.assertEqual(""" 386bar($module, /, x, y) 387-- 388 389This is the documentation for foo. 390 391 x 392 Documentation for x. 393 394Okay, we're done here. 395""".strip(), function.docstring) 396 397 def test_parser_regression_special_character_in_parameter_column_of_docstring_first_line(self): 398 function = self.parse_function(""" 399module os 400os.stat 401 path: str 402This/used to break Clinic! 403""") 404 self.assertEqual("stat($module, /, path)\n--\n\nThis/used to break Clinic!", function.docstring) 405 406 def test_c_name(self): 407 function = self.parse_function("module os\nos.stat as os_stat_fn") 408 self.assertEqual("os_stat_fn", function.c_basename) 409 410 def test_return_converter(self): 411 function = self.parse_function("module os\nos.stat -> int") 412 self.assertIsInstance(function.return_converter, clinic.int_return_converter) 413 414 def test_star(self): 415 function = self.parse_function("module os\nos.access\n *\n follow_symlinks: bool = True") 416 p = function.parameters['follow_symlinks'] 417 self.assertEqual(inspect.Parameter.KEYWORD_ONLY, p.kind) 418 self.assertEqual(0, p.group) 419 420 def test_group(self): 421 function = self.parse_function("module window\nwindow.border\n [\n ls : int\n ]\n /\n") 422 p = function.parameters['ls'] 423 self.assertEqual(1, p.group) 424 425 def test_left_group(self): 426 function = self.parse_function(""" 427module curses 428curses.addch 429 [ 430 y: int 431 Y-coordinate. 432 x: int 433 X-coordinate. 434 ] 435 ch: char 436 Character to add. 437 [ 438 attr: long 439 Attributes for the character. 440 ] 441 / 442""") 443 for name, group in ( 444 ('y', -1), ('x', -1), 445 ('ch', 0), 446 ('attr', 1), 447 ): 448 p = function.parameters[name] 449 self.assertEqual(p.group, group) 450 self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY) 451 self.assertEqual(function.docstring.strip(), """ 452addch([y, x,] ch, [attr]) 453 454 455 y 456 Y-coordinate. 457 x 458 X-coordinate. 459 ch 460 Character to add. 461 attr 462 Attributes for the character. 463 """.strip()) 464 465 def test_nested_groups(self): 466 function = self.parse_function(""" 467module curses 468curses.imaginary 469 [ 470 [ 471 y1: int 472 Y-coordinate. 473 y2: int 474 Y-coordinate. 475 ] 476 x1: int 477 X-coordinate. 478 x2: int 479 X-coordinate. 480 ] 481 ch: char 482 Character to add. 483 [ 484 attr1: long 485 Attributes for the character. 486 attr2: long 487 Attributes for the character. 488 attr3: long 489 Attributes for the character. 490 [ 491 attr4: long 492 Attributes for the character. 493 attr5: long 494 Attributes for the character. 495 attr6: long 496 Attributes for the character. 497 ] 498 ] 499 / 500""") 501 for name, group in ( 502 ('y1', -2), ('y2', -2), 503 ('x1', -1), ('x2', -1), 504 ('ch', 0), 505 ('attr1', 1), ('attr2', 1), ('attr3', 1), 506 ('attr4', 2), ('attr5', 2), ('attr6', 2), 507 ): 508 p = function.parameters[name] 509 self.assertEqual(p.group, group) 510 self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY) 511 512 self.assertEqual(function.docstring.strip(), """ 513imaginary([[y1, y2,] x1, x2,] ch, [attr1, attr2, attr3, [attr4, attr5, 514 attr6]]) 515 516 517 y1 518 Y-coordinate. 519 y2 520 Y-coordinate. 521 x1 522 X-coordinate. 523 x2 524 X-coordinate. 525 ch 526 Character to add. 527 attr1 528 Attributes for the character. 529 attr2 530 Attributes for the character. 531 attr3 532 Attributes for the character. 533 attr4 534 Attributes for the character. 535 attr5 536 Attributes for the character. 537 attr6 538 Attributes for the character. 539 """.strip()) 540 541 def parse_function_should_fail(self, s): 542 with support.captured_stdout() as stdout: 543 with self.assertRaises(SystemExit): 544 self.parse_function(s) 545 return stdout.getvalue() 546 547 def test_disallowed_grouping__two_top_groups_on_left(self): 548 s = self.parse_function_should_fail(""" 549module foo 550foo.two_top_groups_on_left 551 [ 552 group1 : int 553 ] 554 [ 555 group2 : int 556 ] 557 param: int 558 """) 559 self.assertEqual(s, 560 ('Error on line 0:\n' 561 'Function two_top_groups_on_left has an unsupported group configuration. (Unexpected state 2.b)\n')) 562 563 def test_disallowed_grouping__two_top_groups_on_right(self): 564 self.parse_function_should_fail(""" 565module foo 566foo.two_top_groups_on_right 567 param: int 568 [ 569 group1 : int 570 ] 571 [ 572 group2 : int 573 ] 574 """) 575 576 def test_disallowed_grouping__parameter_after_group_on_right(self): 577 self.parse_function_should_fail(""" 578module foo 579foo.parameter_after_group_on_right 580 param: int 581 [ 582 [ 583 group1 : int 584 ] 585 group2 : int 586 ] 587 """) 588 589 def test_disallowed_grouping__group_after_parameter_on_left(self): 590 self.parse_function_should_fail(""" 591module foo 592foo.group_after_parameter_on_left 593 [ 594 group2 : int 595 [ 596 group1 : int 597 ] 598 ] 599 param: int 600 """) 601 602 def test_disallowed_grouping__empty_group_on_left(self): 603 self.parse_function_should_fail(""" 604module foo 605foo.empty_group 606 [ 607 [ 608 ] 609 group2 : int 610 ] 611 param: int 612 """) 613 614 def test_disallowed_grouping__empty_group_on_right(self): 615 self.parse_function_should_fail(""" 616module foo 617foo.empty_group 618 param: int 619 [ 620 [ 621 ] 622 group2 : int 623 ] 624 """) 625 626 def test_no_parameters(self): 627 function = self.parse_function(""" 628module foo 629foo.bar 630 631Docstring 632 633""") 634 self.assertEqual("bar($module, /)\n--\n\nDocstring", function.docstring) 635 self.assertEqual(1, len(function.parameters)) # self! 636 637 def test_init_with_no_parameters(self): 638 function = self.parse_function(""" 639module foo 640class foo.Bar "unused" "notneeded" 641foo.Bar.__init__ 642 643Docstring 644 645""", signatures_in_block=3, function_index=2) 646 # self is not in the signature 647 self.assertEqual("Bar()\n--\n\nDocstring", function.docstring) 648 # but it *is* a parameter 649 self.assertEqual(1, len(function.parameters)) 650 651 def test_illegal_module_line(self): 652 self.parse_function_should_fail(""" 653module foo 654foo.bar => int 655 / 656""") 657 658 def test_illegal_c_basename(self): 659 self.parse_function_should_fail(""" 660module foo 661foo.bar as 935 662 / 663""") 664 665 def test_single_star(self): 666 self.parse_function_should_fail(""" 667module foo 668foo.bar 669 * 670 * 671""") 672 673 def test_parameters_required_after_star_without_initial_parameters_or_docstring(self): 674 self.parse_function_should_fail(""" 675module foo 676foo.bar 677 * 678""") 679 680 def test_parameters_required_after_star_without_initial_parameters_with_docstring(self): 681 self.parse_function_should_fail(""" 682module foo 683foo.bar 684 * 685Docstring here. 686""") 687 688 def test_parameters_required_after_star_with_initial_parameters_without_docstring(self): 689 self.parse_function_should_fail(""" 690module foo 691foo.bar 692 this: int 693 * 694""") 695 696 def test_parameters_required_after_star_with_initial_parameters_and_docstring(self): 697 self.parse_function_should_fail(""" 698module foo 699foo.bar 700 this: int 701 * 702Docstring. 703""") 704 705 def test_single_slash(self): 706 self.parse_function_should_fail(""" 707module foo 708foo.bar 709 / 710 / 711""") 712 713 def test_mix_star_and_slash(self): 714 self.parse_function_should_fail(""" 715module foo 716foo.bar 717 x: int 718 y: int 719 * 720 z: int 721 / 722""") 723 724 def test_parameters_not_permitted_after_slash_for_now(self): 725 self.parse_function_should_fail(""" 726module foo 727foo.bar 728 / 729 x: int 730""") 731 732 def test_function_not_at_column_0(self): 733 function = self.parse_function(""" 734 module foo 735 foo.bar 736 x: int 737 Nested docstring here, goeth. 738 * 739 y: str 740 Not at column 0! 741""") 742 self.assertEqual(""" 743bar($module, /, x, *, y) 744-- 745 746Not at column 0! 747 748 x 749 Nested docstring here, goeth. 750""".strip(), function.docstring) 751 752 def test_directive(self): 753 c = FakeClinic() 754 parser = DSLParser(c) 755 parser.flag = False 756 parser.directives['setflag'] = lambda : setattr(parser, 'flag', True) 757 block = clinic.Block("setflag") 758 parser.parse(block) 759 self.assertTrue(parser.flag) 760 761 def test_legacy_converters(self): 762 block = self.parse('module os\nos.access\n path: "s"') 763 module, function = block.signatures 764 self.assertIsInstance((function.parameters['path']).converter, clinic.str_converter) 765 766 def parse(self, text): 767 c = FakeClinic() 768 parser = DSLParser(c) 769 block = clinic.Block(text) 770 parser.parse(block) 771 return block 772 773 def parse_function(self, text, signatures_in_block=2, function_index=1): 774 block = self.parse(text) 775 s = block.signatures 776 self.assertEqual(len(s), signatures_in_block) 777 assert isinstance(s[0], clinic.Module) 778 assert isinstance(s[function_index], clinic.Function) 779 return s[function_index] 780 781 def test_scaffolding(self): 782 # test repr on special values 783 self.assertEqual(repr(clinic.unspecified), '<Unspecified>') 784 self.assertEqual(repr(clinic.NULL), '<Null>') 785 786 # test that fail fails 787 with support.captured_stdout() as stdout: 788 with self.assertRaises(SystemExit): 789 clinic.fail('The igloos are melting!', filename='clown.txt', line_number=69) 790 self.assertEqual(stdout.getvalue(), 'Error in file "clown.txt" on line 69:\nThe igloos are melting!\n') 791 792 793class ClinicExternalTest(TestCase): 794 maxDiff = None 795 796 def test_external(self): 797 # bpo-42398: Test that the destination file is left unchanged if the 798 # content does not change. Moreover, check also that the file 799 # modification time does not change in this case. 800 source = support.findfile('clinic.test') 801 with open(source, 'r', encoding='utf-8') as f: 802 orig_contents = f.read() 803 804 with support.temp_dir() as tmp_dir: 805 testfile = os.path.join(tmp_dir, 'clinic.test.c') 806 with open(testfile, 'w', encoding='utf-8') as f: 807 f.write(orig_contents) 808 old_mtime_ns = os.stat(testfile).st_mtime_ns 809 810 clinic.parse_file(testfile) 811 812 with open(testfile, 'r', encoding='utf-8') as f: 813 new_contents = f.read() 814 new_mtime_ns = os.stat(testfile).st_mtime_ns 815 816 self.assertEqual(new_contents, orig_contents) 817 # Don't change the file modification time 818 # if the content does not change 819 self.assertEqual(new_mtime_ns, old_mtime_ns) 820 821 822if __name__ == "__main__": 823 unittest.main() 824