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