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