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