1# Copyright (C) 2011, 2016 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17import re
18import sys
19import textwrap
20
21from io import StringIO
22
23from .. import (
24    commands,
25    export_pot,
26    option,
27    registry,
28    tests,
29    )
30
31
32class TestEscape(tests.TestCase):
33
34    def test_simple_escape(self):
35        self.assertEqual(
36            export_pot._escape('foobar'),
37            'foobar')
38
39        s = '''foo\nbar\r\tbaz\\"spam"'''
40        e = '''foo\\nbar\\r\\tbaz\\\\\\"spam\\"'''
41        self.assertEqual(export_pot._escape(s), e)
42
43    def test_complex_escape(self):
44        s = '''\\r \\\n'''
45        e = '''\\\\r \\\\\\n'''
46        self.assertEqual(export_pot._escape(s), e)
47
48
49class TestNormalize(tests.TestCase):
50
51    def test_single_line(self):
52        s = 'foobar'
53        e = '"foobar"'
54        self.assertEqual(export_pot._normalize(s), e)
55
56        s = 'foo"bar'
57        e = '"foo\\"bar"'
58        self.assertEqual(export_pot._normalize(s), e)
59
60    def test_multi_lines(self):
61        s = 'foo\nbar\n'
62        e = '""\n"foo\\n"\n"bar\\n"'
63        self.assertEqual(export_pot._normalize(s), e)
64
65        s = '\nfoo\nbar\n'
66        e = ('""\n'
67             '"\\n"\n'
68             '"foo\\n"\n'
69             '"bar\\n"')
70        self.assertEqual(export_pot._normalize(s), e)
71
72
73class TestParseSource(tests.TestCase):
74    """Check mappings to line numbers generated from python source"""
75
76    def test_classes(self):
77        src = '''
78class Ancient:
79    """Old style class"""
80
81class Modern(object):
82    """New style class"""
83'''
84        cls_lines, _ = export_pot._parse_source(src)
85        self.assertEqual(cls_lines,
86                         {"Ancient": 2, "Modern": 5})
87
88    def test_classes_nested(self):
89        src = '''
90class Matroska(object):
91    class Smaller(object):
92        class Smallest(object):
93            pass
94'''
95        cls_lines, _ = export_pot._parse_source(src)
96        self.assertEqual(cls_lines,
97                         {"Matroska": 2, "Smaller": 3, "Smallest": 4})
98
99    def test_strings_docstrings(self):
100        src = '''\
101"""Module"""
102
103def function():
104    """Function"""
105
106class Class(object):
107    """Class"""
108
109    def method(self):
110        """Method"""
111'''
112        _, str_lines = export_pot._parse_source(src)
113        self.assertEqual(str_lines,
114                         {"Module": 1, "Function": 4, "Class": 7, "Method": 10})
115
116    def test_strings_literals(self):
117        src = '''\
118s = "One"
119t = (2, "Two")
120f = dict(key="Three")
121'''
122        _, str_lines = export_pot._parse_source(src)
123        self.assertEqual(str_lines,
124                         {"One": 1, "Two": 2, "Three": 3})
125
126    def test_strings_multiline(self):
127        src = '''\
128"""Start
129
130End
131"""
132t = (
133    "A"
134    "B"
135    "C"
136    )
137'''
138        _, str_lines = export_pot._parse_source(src)
139        self.assertEqual(str_lines,
140                         {"Start\n\nEnd\n": 1, "ABC": 6})
141
142    def test_strings_multiline_escapes(self):
143        src = '''\
144s = "Escaped\\n"
145r = r"Raw\\n"
146t = (
147    "A\\n\\n"
148    "B\\n\\n"
149    "C\\n\\n"
150    )
151'''
152        _, str_lines = export_pot._parse_source(src)
153        if sys.version_info < (3, 8):
154            self.expectFailure("Escaped newlines confuses the multiline handling",
155                               self.assertNotEqual, str_lines,
156                               {"Escaped\n": 0, "Raw\\n": 2, "A\n\nB\n\nC\n\n": -2})
157        else:
158            self.assertEqual(
159                str_lines, {"Escaped\n": 1, "Raw\\n": 2, "A\n\nB\n\nC\n\n": 4})
160
161
162class TestModuleContext(tests.TestCase):
163    """Checks for source context tracking objects"""
164
165    def check_context(self, context, path, lineno):
166        self.assertEqual((context.path, context.lineno), (path, lineno))
167
168    def test___init__(self):
169        context = export_pot._ModuleContext("one.py")
170        self.check_context(context, "one.py", 1)
171        context = export_pot._ModuleContext("two.py", 5)
172        self.check_context(context, "two.py", 5)
173
174    def test_from_class(self):
175        """New context returned with lineno updated from class"""
176        path = "cls.py"
177
178        class A(object):
179            pass
180
181        class B(object):
182            pass
183        cls_lines = {"A": 5, "B": 7}
184        context = export_pot._ModuleContext(path, _source_info=(cls_lines, {}))
185        contextA = context.from_class(A)
186        self.check_context(contextA, path, 5)
187        contextB1 = context.from_class(B)
188        self.check_context(contextB1, path, 7)
189        contextB2 = contextA.from_class(B)
190        self.check_context(contextB2, path, 7)
191        self.check_context(context, path, 1)
192        self.assertEqual("", self.get_log())
193
194    def test_from_class_missing(self):
195        """When class has no lineno the old context details are returned"""
196        path = "cls_missing.py"
197
198        class A(object):
199            pass
200
201        class M(object):
202            pass
203        context = export_pot._ModuleContext(path, 3, ({"A": 15}, {}))
204        contextA = context.from_class(A)
205        contextM1 = context.from_class(M)
206        self.check_context(contextM1, path, 3)
207        contextM2 = contextA.from_class(M)
208        self.check_context(contextM2, path, 15)
209        self.assertContainsRe(self.get_log(), "Definition of <.*M'> not found")
210
211    def test_from_string(self):
212        """New context returned with lineno updated from string"""
213        path = "str.py"
214        str_lines = {"one": 14, "two": 42}
215        context = export_pot._ModuleContext(path, _source_info=({}, str_lines))
216        context1 = context.from_string("one")
217        self.check_context(context1, path, 14)
218        context2A = context.from_string("two")
219        self.check_context(context2A, path, 42)
220        context2B = context1.from_string("two")
221        self.check_context(context2B, path, 42)
222        self.check_context(context, path, 1)
223        self.assertEqual("", self.get_log())
224
225    def test_from_string_missing(self):
226        """When string has no lineno the old context details are returned"""
227        path = "str_missing.py"
228        context = export_pot._ModuleContext(path, 4, ({}, {"line\n": 21}))
229        context1 = context.from_string("line\n")
230        context2A = context.from_string("not there")
231        self.check_context(context2A, path, 4)
232        context2B = context1.from_string("not there")
233        self.check_context(context2B, path, 21)
234        self.assertContainsRe(self.get_log(), "String b?'not there' not found")
235
236
237class TestWriteOption(tests.TestCase):
238    """Tests for writing texts extracted from options in pot format"""
239
240    def pot_from_option(self, opt, context=None, note="test"):
241        sio = StringIO()
242        exporter = export_pot._PotExporter(sio)
243        if context is None:
244            context = export_pot._ModuleContext("nowhere", 0)
245        export_pot._write_option(exporter, context, opt, note)
246        return sio.getvalue()
247
248    def test_option_without_help(self):
249        opt = option.Option("helpless")
250        self.assertEqual("", self.pot_from_option(opt))
251
252    def test_option_with_help(self):
253        opt = option.Option("helpful", help="Info.")
254        self.assertContainsString(self.pot_from_option(opt), "\n"
255                                  "# help of 'helpful' test\n"
256                                  "msgid \"Info.\"\n")
257
258    def test_option_hidden(self):
259        opt = option.Option("hidden", help="Unseen.", hidden=True)
260        self.assertEqual("", self.pot_from_option(opt))
261
262    def test_option_context_missing(self):
263        context = export_pot._ModuleContext("remote.py", 3)
264        opt = option.Option("metaphor", help="Not a literal in the source.")
265        self.assertContainsString(self.pot_from_option(opt, context),
266                                  "#: remote.py:3\n"
267                                  "# help of 'metaphor' test\n")
268
269    def test_option_context_string(self):
270        s = "Literally."
271        context = export_pot._ModuleContext("local.py", 3, ({}, {s: 17}))
272        opt = option.Option("example", help=s)
273        self.assertContainsString(self.pot_from_option(opt, context),
274                                  "#: local.py:17\n"
275                                  "# help of 'example' test\n")
276
277    def test_registry_option_title(self):
278        opt = option.RegistryOption.from_kwargs("group", help="Pick one.",
279                                                title="Choose!")
280        pot = self.pot_from_option(opt)
281        self.assertContainsString(pot, "\n"
282                                  "# title of 'group' test\n"
283                                  "msgid \"Choose!\"\n")
284        self.assertContainsString(pot, "\n"
285                                  "# help of 'group' test\n"
286                                  "msgid \"Pick one.\"\n")
287
288    def test_registry_option_title_context_missing(self):
289        context = export_pot._ModuleContext("theory.py", 3)
290        opt = option.RegistryOption.from_kwargs("abstract", title="Unfounded!")
291        self.assertContainsString(self.pot_from_option(opt, context),
292                                  "#: theory.py:3\n"
293                                  "# title of 'abstract' test\n")
294
295    def test_registry_option_title_context_string(self):
296        s = "Grounded!"
297        context = export_pot._ModuleContext("practice.py", 3, ({}, {s: 144}))
298        opt = option.RegistryOption.from_kwargs("concrete", title=s)
299        self.assertContainsString(self.pot_from_option(opt, context),
300                                  "#: practice.py:144\n"
301                                  "# title of 'concrete' test\n")
302
303    def test_registry_option_value_switches(self):
304        opt = option.RegistryOption.from_kwargs("switch", help="Flip one.",
305                                                value_switches=True, enum_switch=False,
306                                                red="Big.", green="Small.")
307        pot = self.pot_from_option(opt)
308        self.assertContainsString(pot, "\n"
309                                  "# help of 'switch' test\n"
310                                  "msgid \"Flip one.\"\n")
311        self.assertContainsString(pot, "\n"
312                                  "# help of 'switch=red' test\n"
313                                  "msgid \"Big.\"\n")
314        self.assertContainsString(pot, "\n"
315                                  "# help of 'switch=green' test\n"
316                                  "msgid \"Small.\"\n")
317
318    def test_registry_option_value_switches_hidden(self):
319        reg = registry.Registry()
320
321        class Hider(object):
322            hidden = True
323        reg.register("new", 1, "Current.")
324        reg.register("old", 0, "Legacy.", info=Hider())
325        opt = option.RegistryOption("protocol", "Talking.", reg,
326                                    value_switches=True, enum_switch=False)
327        pot = self.pot_from_option(opt)
328        self.assertContainsString(pot, "\n"
329                                  "# help of 'protocol' test\n"
330                                  "msgid \"Talking.\"\n")
331        self.assertContainsString(pot, "\n"
332                                  "# help of 'protocol=new' test\n"
333                                  "msgid \"Current.\"\n")
334        self.assertNotContainsString(pot, "'protocol=old'")
335
336
337class TestPotExporter(tests.TestCase):
338    """Test for logic specific to the _PotExporter class"""
339
340    # This test duplicates test_duplicates below
341    def test_duplicates(self):
342        exporter = export_pot._PotExporter(StringIO())
343        context = export_pot._ModuleContext("mod.py", 1)
344        exporter.poentry_in_context(context, "Common line.")
345        context.lineno = 3
346        exporter.poentry_in_context(context, "Common line.")
347        self.assertEqual(1, exporter.outf.getvalue().count("Common line."))
348
349    def test_duplicates_included(self):
350        exporter = export_pot._PotExporter(StringIO(), True)
351        context = export_pot._ModuleContext("mod.py", 1)
352        exporter.poentry_in_context(context, "Common line.")
353        context.lineno = 3
354        exporter.poentry_in_context(context, "Common line.")
355        self.assertEqual(2, exporter.outf.getvalue().count("Common line."))
356
357
358class PoEntryTestCase(tests.TestCase):
359
360    def setUp(self):
361        super(PoEntryTestCase, self).setUp()
362        self.exporter = export_pot._PotExporter(StringIO())
363
364    def check_output(self, expected):
365        self.assertEqual(
366            self.exporter.outf.getvalue(),
367            textwrap.dedent(expected)
368            )
369
370
371class TestPoEntry(PoEntryTestCase):
372
373    def test_simple(self):
374        self.exporter.poentry('dummy', 1, "spam")
375        self.exporter.poentry('dummy', 2, "ham", 'EGG')
376        self.check_output('''\
377                #: dummy:1
378                msgid "spam"
379                msgstr ""
380
381                #: dummy:2
382                # EGG
383                msgid "ham"
384                msgstr ""
385
386                ''')
387
388    def test_duplicate(self):
389        self.exporter.poentry('dummy', 1, "spam")
390        # This should be ignored.
391        self.exporter.poentry('dummy', 2, "spam", 'EGG')
392
393        self.check_output('''\
394                #: dummy:1
395                msgid "spam"
396                msgstr ""\n
397                ''')
398
399
400class TestPoentryPerPergraph(PoEntryTestCase):
401
402    def test_single(self):
403        self.exporter.poentry_per_paragraph(
404            'dummy',
405            10,
406            '''foo\nbar\nbaz\n'''
407            )
408        self.check_output('''\
409                #: dummy:10
410                msgid ""
411                "foo\\n"
412                "bar\\n"
413                "baz\\n"
414                msgstr ""\n
415                ''')
416
417    def test_multi(self):
418        self.exporter.poentry_per_paragraph(
419            'dummy',
420            10,
421            '''spam\nham\negg\n\nSPAM\nHAM\nEGG\n'''
422            )
423        self.check_output('''\
424                #: dummy:10
425                msgid ""
426                "spam\\n"
427                "ham\\n"
428                "egg"
429                msgstr ""
430
431                #: dummy:14
432                msgid ""
433                "SPAM\\n"
434                "HAM\\n"
435                "EGG\\n"
436                msgstr ""\n
437                ''')
438
439
440class TestExportCommandHelp(PoEntryTestCase):
441
442    def test_command_help(self):
443
444        class cmd_Demo(commands.Command):
445            __doc__ = """A sample command.
446
447            :Usage:
448                bzr demo
449
450            :Examples:
451                Example 1::
452
453                    cmd arg1
454
455            Blah Blah Blah
456            """
457
458        export_pot._write_command_help(self.exporter, cmd_Demo())
459        result = self.exporter.outf.getvalue()
460        # We don't care about filename and lineno here.
461        result = re.sub(r'(?m)^#: [^\n]+\n', '', result)
462
463        self.assertEqualDiff(
464            'msgid "A sample command."\n'
465            'msgstr ""\n'
466            '\n'                # :Usage: should not be translated.
467            'msgid ""\n'
468            '":Examples:\\n"\n'
469            '"    Example 1::"\n'
470            'msgstr ""\n'
471            '\n'
472            'msgid "        cmd arg1"\n'
473            'msgstr ""\n'
474            '\n'
475            'msgid "Blah Blah Blah"\n'
476            'msgstr ""\n'
477            '\n',
478            result
479            )
480