1import warnings
2from io import BytesIO
3
4import pytest
5
6from translate.convert import dtd2po, po2dtd, test_convert
7from translate.storage import dtd, po
8
9
10class TestPO2DTD:
11    def setup_method(self, method):
12        warnings.resetwarnings()
13
14    def teardown_method(self, method):
15        warnings.resetwarnings()
16
17    def po2dtd(self, posource, remove_untranslated=False):
18        """helper that converts po source to dtd source without requiring files"""
19        inputfile = BytesIO(posource.encode())
20        inputpo = po.pofile(inputfile)
21        convertor = po2dtd.po2dtd(remove_untranslated=remove_untranslated)
22        return convertor.convertstore(inputpo)
23
24    def merge2dtd(self, dtdsource, posource):
25        """helper that merges po translations to dtd source without requiring files"""
26        inputfile = BytesIO(posource.encode())
27        inputpo = po.pofile(inputfile)
28        templatefile = BytesIO(dtdsource.encode())
29        templatedtd = dtd.dtdfile(templatefile)
30        convertor = po2dtd.redtd(templatedtd)
31        return convertor.convertstore(inputpo)
32
33    def convertdtd(self, posource, dtdtemplate, remove_untranslated=False):
34        """helper to exercise the command line function"""
35        inputfile = BytesIO(posource.encode())
36        outputfile = BytesIO()
37        templatefile = BytesIO(dtdtemplate.encode())
38        assert po2dtd.convertdtd(
39            inputfile, outputfile, templatefile, remove_untranslated=remove_untranslated
40        )
41        return outputfile.getvalue().decode("utf-8")
42
43    def roundtripsource(self, dtdsource):
44        """converts dtd source to po and back again, returning the resulting source"""
45        dtdinputfile = BytesIO(dtdsource.encode())
46        dtdinputfile2 = BytesIO(dtdsource.encode())
47        pooutputfile = BytesIO()
48        dtd2po.convertdtd(dtdinputfile, pooutputfile, dtdinputfile2)
49        posource = pooutputfile.getvalue()
50        poinputfile = BytesIO(posource)
51        dtdtemplatefile = BytesIO(dtdsource.encode())
52        dtdoutputfile = BytesIO()
53        po2dtd.convertdtd(poinputfile, dtdoutputfile, dtdtemplatefile)
54        dtdresult = dtdoutputfile.getvalue().decode("utf-8")
55        print_string = "Original DTD:\n%s\n\nPO version:\n%s\n\n"
56        print_string = print_string + "Output DTD:\n%s\n################"
57        print(print_string % (dtdsource, posource, dtdresult))
58        return dtdresult
59
60    def roundtripstring(self, entitystring):
61        """Just takes the contents of a ENTITY definition (with quotes) and does a roundtrip on that"""
62        dtdintro, dtdoutro = "<!ENTITY Test.RoundTrip ", ">\n"
63        dtdsource = dtdintro + entitystring + dtdoutro
64        dtdresult = self.roundtripsource(dtdsource)
65        assert dtdresult.startswith(dtdintro) and dtdresult.endswith(dtdoutro)
66        return dtdresult[len(dtdintro) : -len(dtdoutro)]
67
68    def check_roundtrip(self, dtdsource, dtdcompare=None):
69        """Checks that the round-tripped string is the same as dtdcompare.
70
71        If no dtdcompare string is provided then the round-tripped string is
72        compared with the original string.
73
74        The reason why sometimes another string is provided to compare with the
75        resulting string from the roundtrip is that if the original string
76        contains some characters, like " character, or escapes like &quot;,
77        then when the roundtrip is performed those characters or escapes are
78        escaped, rendering a round-tripped string which differs from the
79        original one.
80        """
81        if not dtdcompare:
82            dtdcompare = dtdsource
83        assert self.roundtripstring(dtdsource) == dtdcompare
84
85    def test_joinlines(self):
86        """tests that po lines are joined seamlessly (bug 16)"""
87        multilinepo = """#: pref.menuPath\nmsgid ""\n"<span>Tools &gt; Options</"\n"span>"\nmsgstr ""\n"""
88        dtdfile = self.po2dtd(multilinepo)
89        dtdsource = bytes(dtdfile)
90        assert b"</span>" in dtdsource
91
92    def test_escapedstr(self):
93        r"""tests that \n in msgstr is escaped correctly in dtd"""
94        multilinepo = (
95            """#: pref.menuPath\nmsgid "Hello\\nEveryone"\nmsgstr "Good day\\nAll"\n"""
96        )
97        dtdfile = self.po2dtd(multilinepo)
98        dtdsource = bytes(dtdfile)
99        assert b"Good day\nAll" in dtdsource
100
101    def test_missingaccesskey(self):
102        """tests that proper warnings are given if access key is missing"""
103        simplepo = """#: simple.label
104#: simple.accesskey
105msgid "Simple &String"
106msgstr "Dimpled Ring"
107"""
108        simpledtd = """<!ENTITY simple.label "Simple String">
109<!ENTITY simple.accesskey "S">"""
110        warnings.simplefilter("error")
111        with pytest.raises(Warning):
112            self.merge2dtd(simpledtd, simplepo)
113
114    def test_accesskeycase(self):
115        """tests that access keys come out with the same case as the original, regardless"""
116        simplepo_template = (
117            """#: simple.label\n#: simple.accesskey\nmsgid "%s"\nmsgstr "%s"\n"""
118        )
119        simpledtd_template = (
120            """<!ENTITY simple.label "Simple %s">\n<!ENTITY simple.accesskey "%s">"""
121        )
122        possibilities = [
123            # (en label, en akey, en po, af po, af label, expected af akey)
124            ("Sis", "S", "&Sis", "&Sies", "Sies", "S"),
125            ("Sis", "s", "Si&s", "&Sies", "Sies", "S"),
126            ("Sis", "S", "&Sis", "Sie&s", "Sies", "s"),
127            ("Sis", "s", "Si&s", "Sie&s", "Sies", "s"),
128            # untranslated strings should have the casing of the source
129            ("Sis", "S", "&Sis", "", "Sis", "S"),
130            ("Sis", "s", "Si&s", "", "Sis", "s"),
131            ("Suck", "S", "&Suck", "", "Suck", "S"),
132            ("Suck", "s", "&Suck", "", "Suck", "s"),
133        ]
134        for (
135            en_label,
136            en_akey,
137            po_source,
138            po_target,
139            target_label,
140            target_akey,
141        ) in possibilities:
142            simplepo = simplepo_template % (po_source, po_target)
143            simpledtd = simpledtd_template % (en_label, en_akey)
144            dtdfile = self.merge2dtd(simpledtd, simplepo)
145            dtdfile.makeindex()
146            accel = dtd.unquotefromdtd(dtdfile.id_index["simple.accesskey"].definition)
147            assert accel == target_akey
148
149    def test_accesskey_types(self):
150        """tests that we can detect the various styles of accesskey"""
151        simplepo_template = (
152            """#: simple.%s\n#: simple.%s\nmsgid "&File"\nmsgstr "F&aele"\n"""
153        )
154        simpledtd_template = """<!ENTITY simple.%s "File">\n<!ENTITY simple.%s "a">"""
155        for label in ("label", "title"):
156            for accesskey in ("accesskey", "accessKey", "akey"):
157                simplepo = simplepo_template % (label, accesskey)
158                simpledtd = simpledtd_template % (label, accesskey)
159                dtdfile = self.merge2dtd(simpledtd, simplepo)
160                dtdfile.makeindex()
161                assert (
162                    dtd.unquotefromdtd(
163                        dtdfile.id_index["simple.%s" % accesskey].definition
164                    )
165                    == "a"
166                )
167
168    def test_accesskey_missing(self):
169        """tests that missing ampersands use the source accesskey"""
170        po_snippet = r"""#: key.label
171#: key.accesskey
172msgid "&Search"
173msgstr "Ileti"
174"""
175        dtd_snippet = r"""<!ENTITY key.accesskey      "S">
176<!ENTITY key.label       "Ileti">"""
177        dtdfile = self.merge2dtd(dtd_snippet, po_snippet)
178        dtdsource = bytes(dtdfile).decode("utf-8")
179        print(dtdsource)
180        assert '"Ileti"' in dtdsource
181        assert '""' not in dtdsource
182        assert '"S"' in dtdsource
183
184    def test_accesskey_and_amp_case_no_accesskey(self):
185        """
186        tests that accesskey and &amp; can work together
187
188        If missing we use the source accesskey
189        """
190        po_snippet = r"""#: key.label
191#: key.accesskey
192msgid "Colour & &Light"
193msgstr "Lig en Kleur"
194"""
195        dtd_snippet = r"""<!ENTITY key.accesskey      "L">
196<!ENTITY key.label       "Colour &amp; Light">"""
197        dtdfile = self.merge2dtd(dtd_snippet, po_snippet)
198        dtdsource = bytes(dtdfile).decode("utf-8")
199        print(dtdsource)
200        assert '"Lig en Kleur"' in dtdsource
201        assert '"L"' in dtdsource
202
203    def test_accesskey_and_amp_source_no_amp_in_target(self):
204        """
205        tests that accesskey and &amp; can work together
206
207        If present we use the target accesskey
208        """
209        po_snippet = r"""#: key.label
210#: key.accesskey
211msgid "Colour & &Light"
212msgstr "Lig en &Kleur"
213"""
214        dtd_snippet = r"""<!ENTITY key.accesskey      "L">
215<!ENTITY key.label       "Colour &amp; Light">"""
216        dtdfile = self.merge2dtd(dtd_snippet, po_snippet)
217        dtdsource = bytes(dtdfile).decode("utf-8")
218        print(dtdsource)
219        assert '"Lig en Kleur"' in dtdsource
220        assert '"K"' in dtdsource
221
222    def test_accesskey_and_amp_case_both_amp_and_accesskey(self):
223        """
224        tests that accesskey and &amp; can work together
225
226        If present both & (and) and a marker then we use the correct source
227        accesskey
228        """
229        po_snippet = r"""#: key.label
230#: key.accesskey
231msgid "Colour & &Light"
232msgstr "Lig & &Kleur"
233"""
234        dtd_snippet = r"""<!ENTITY key.accesskey      "L">
235<!ENTITY key.label       "Colour &amp; Light">"""
236        dtdfile = self.merge2dtd(dtd_snippet, po_snippet)
237        dtdsource = bytes(dtdfile).decode("utf-8")
238        print(dtdsource)
239        assert '"Lig &amp; Kleur"' in dtdsource
240        assert '"K"' in dtdsource
241
242    def test_accesskey_and_amp_case_amp_no_accesskey(self):
243        """
244        tests that accesskey and &amp; can work together
245
246        If present both & (and) and a no marker then we use the correct source
247        accesskey
248        """
249        po_snippet = r"""#: key.label
250#: key.accesskey
251msgid "Colour & &Light"
252msgstr "Lig & Kleur"
253"""
254        dtd_snippet = r"""<!ENTITY key.accesskey      "L">
255<!ENTITY key.label       "Colour &amp; Light">"""
256        dtdfile = self.merge2dtd(dtd_snippet, po_snippet)
257        dtdsource = bytes(dtdfile).decode("utf-8")
258        print(dtdsource)
259        assert '"Lig &amp; Kleur"' in dtdsource
260        assert '"L"' in dtdsource
261
262    def test_entities_two(self):
263        """test the error ouput when we find two entities"""
264        simplestring = """#: simple.string second.string\nmsgid "Simple String"\nmsgstr "Dimpled Ring"\n"""
265        dtdfile = self.po2dtd(simplestring)
266        dtdsource = bytes(dtdfile)
267        assert b"CONVERSION NOTE - multiple entities" in dtdsource
268
269    def test_entities(self):
270        """tests that entities are correctly idnetified in the dtd"""
271        simplestring = (
272            """#: simple.string\nmsgid "Simple String"\nmsgstr "Dimpled Ring"\n"""
273        )
274        dtdfile = self.po2dtd(simplestring)
275        dtdsource = bytes(dtdfile)
276        assert dtdsource.startswith(b"<!ENTITY simple.string")
277
278    def test_comments_translator(self):
279        """tests for translator comments"""
280        simplestring = """# Comment1\n# Comment2\n#: simple.string\nmsgid "Simple String"\nmsgstr "Dimpled Ring"\n"""
281        dtdfile = self.po2dtd(simplestring)
282        dtdsource = bytes(dtdfile)
283        assert dtdsource.startswith(b"<!-- Comment1 -->")
284
285    def test_retains_hashprefix(self):
286        """tests that hash prefixes in the dtd are retained"""
287        hashpo = """#: lang.version\nmsgid "__MOZILLA_LOCALE_VERSION__"\nmsgstr "__MOZILLA_LOCALE_VERSION__"\n"""
288        hashdtd = '#expand <!ENTITY lang.version "__MOZILLA_LOCALE_VERSION__">\n'
289        dtdfile = self.merge2dtd(hashdtd, hashpo)
290        regendtd = bytes(dtdfile).decode("utf-8")
291        assert regendtd == hashdtd
292
293    def test_convertdtd(self):
294        """checks that the convertdtd function is working"""
295        posource = """#: simple.label\n#: simple.accesskey\nmsgid "Simple &String"\nmsgstr "Dimpled &Ring"\n"""
296        dtdtemplate = """<!ENTITY simple.label "Simple String">\n<!ENTITY simple.accesskey "S">\n"""
297        dtdexpected = """<!ENTITY simple.label "Dimpled Ring">\n<!ENTITY simple.accesskey "R">\n"""
298        newdtd = self.convertdtd(posource, dtdtemplate)
299        print(newdtd)
300        assert newdtd == dtdexpected
301
302    def test_untranslated_with_template(self):
303        """test removing of untranslated entries in redtd"""
304        posource = """#: simple.label
305msgid "Simple string"
306msgstr "Dimpled ring"
307
308#: simple.label2
309msgid "Simple string 2"
310msgstr ""
311
312#: simple.label3
313msgid "Simple string 3"
314msgstr "Simple string 3"
315
316#: simple.label4
317#, fuzzy
318msgid "Simple string 4"
319msgstr "simple string four"
320"""
321        dtdtemplate = """<!ENTITY simple.label "Simple string">
322<!ENTITY simple.label2 "Simple string 2">
323<!ENTITY simple.label3 "Simple string 3">
324<!ENTITY simple.label4 "Simple string 4">
325"""
326        dtdexpected = """<!ENTITY simple.label "Dimpled ring">
327
328<!ENTITY simple.label3 "Simple string 3">
329
330"""
331        newdtd = self.convertdtd(posource, dtdtemplate, remove_untranslated=True)
332        print(newdtd)
333        assert newdtd == dtdexpected
334
335    def test_untranslated_without_template(self):
336        """test removing of untranslated entries in po2dtd"""
337        posource = """#: simple.label
338msgid "Simple string"
339msgstr "Dimpled ring"
340
341#: simple.label2
342msgid "Simple string 2"
343msgstr ""
344
345#: simple.label3
346msgid "Simple string 3"
347msgstr "Simple string 3"
348
349#: simple.label4
350#, fuzzy
351msgid "Simple string 4"
352msgstr "simple string four"
353"""
354        dtdexpected = """<!ENTITY simple.label "Dimpled ring">
355<!ENTITY simple.label3 "Simple string 3">
356"""
357        newdtd = self.po2dtd(posource, remove_untranslated=True)
358        print(bytes(newdtd))
359        assert bytes(newdtd).decode("utf-8") == dtdexpected
360
361    def test_blank_source(self):
362        """test removing of untranslated entries where source is blank"""
363        posource = """#: simple.label
364msgid "Simple string"
365msgstr "Dimpled ring"
366
367#: simple.label2
368msgid ""
369msgstr ""
370
371#: simple.label3
372msgid "Simple string 3"
373msgstr "Simple string 3"
374"""
375        dtdtemplate = """<!ENTITY simple.label "Simple string">
376<!ENTITY simple.label2 "">
377<!ENTITY simple.label3 "Simple string 3">
378"""
379        dtdexpected_with_template = """<!ENTITY simple.label "Dimpled ring">
380<!ENTITY simple.label2 "">
381<!ENTITY simple.label3 "Simple string 3">
382"""
383
384        dtdexpected_no_template = """<!ENTITY simple.label "Dimpled ring">
385<!ENTITY simple.label3 "Simple string 3">
386"""
387        newdtd_with_template = self.convertdtd(
388            posource, dtdtemplate, remove_untranslated=True
389        )
390        print(newdtd_with_template)
391        assert newdtd_with_template == dtdexpected_with_template
392        newdtd_no_template = self.po2dtd(posource, remove_untranslated=True)
393        print(bytes(newdtd_no_template))
394        assert bytes(newdtd_no_template).decode("utf-8") == dtdexpected_no_template
395
396    def test_newlines_escapes(self):
397        r"""check that we can handle a \n in the PO file"""
398        posource = """#: simple.label\n#: simple.accesskey\nmsgid "A hard coded newline.\\n"\nmsgstr "Hart gekoeerde nuwe lyne\\n"\n"""
399        dtdtemplate = '<!ENTITY  simple.label "A hard coded newline.\n">\n'
400        dtdexpected = """<!ENTITY  simple.label "Hart gekoeerde nuwe lyne\n">\n"""
401        dtdfile = self.merge2dtd(dtdtemplate, posource)
402        print(bytes(dtdfile))
403        assert bytes(dtdfile).decode("utf-8") == dtdexpected
404
405    def test_roundtrip_simple(self):
406        """checks that simple strings make it through a dtd->po->dtd roundtrip"""
407        self.check_roundtrip('"Hello"')
408        self.check_roundtrip('"Hello Everybody"')
409
410    def test_roundtrip_escape(self):
411        """checks that escapes in strings make it through a dtd->po->dtd roundtrip"""
412        self.check_roundtrip(r'"Simple Escape \ \n \\ \: \t \r "')
413        self.check_roundtrip(r'"End Line Escape \"')
414
415    def test_roundtrip_quotes(self):
416        """Checks that quotes make it through a DTD->PO->DTD roundtrip.
417
418        Quotes may be escaped or not.
419        """
420        # NOTE: during the roundtrip, if " quote mark is present, then it is
421        # converted to &quot; and the resulting string is always enclosed
422        # between " characters independently of which quotation marks the
423        # original string is enclosed between. Thus the string cannot be
424        # compared with itself and therefore other string should be provided to
425        # compare with the result.
426        #
427        # Thus the string cannot be compared with itself and therefore another
428        # string should be provided to compare with the roundtrip result.
429        self.check_roundtrip(
430            r"""'Quote Escape "" '""", r'''"Quote Escape &quot;&quot; "'''
431        )
432        self.check_roundtrip(r'''"Double-Quote Escape &quot;&quot; "''')
433        self.check_roundtrip(r'''"Single-Quote ' "''')
434        self.check_roundtrip(r'''"Single-Quote Escape \' "''')
435        # NOTE: during the roundtrip, if " quote mark is present, then ' is
436        # converted to &apos; and " is converted to &quot; Also the resulting
437        # string is always enclosed between " characters independently of which
438        # quotation marks the original string is enclosed between. Thus the
439        # string cannot be compared with itself and therefore another string
440        # should be provided to compare with the result.
441        #
442        # Thus the string cannot be compared with itself and therefore another
443        # string should be provided to compare with the roundtrip result.
444        self.check_roundtrip(
445            r"""'Both Quotes "" &apos;&apos; '""",
446            r'''"Both Quotes &quot;&quot; &apos;&apos; "''',
447        )
448        self.check_roundtrip(r'''"Both Quotes &quot;&quot; &apos;&apos; "''')
449        # NOTE: during the roundtrip, if &quot; is present, then ' is converted
450        # to &apos; Also the resulting string is always enclosed between "
451        # characters independently of which quotation marks the original string
452        # is enclosed between.
453        #
454        # Thus the string cannot be compared with itself and therefore another
455        # string should be provided to compare with the roundtrip result.
456        self.check_roundtrip(
457            r'''"Both Quotes &quot;&quot; '' "''',
458            r'''"Both Quotes &quot;&quot; &apos;&apos; "''',
459        )
460
461    def test_roundtrip_amp(self):
462        """Checks that quotes make it through a DTD->PO->DTD roundtrip.
463
464        Quotes may be escaped or not.
465        """
466        self.check_roundtrip('"Colour &amp; Light"')
467
468    def test_merging_entries_with_spaces_removed(self):
469        """dtd2po removes pretty printed spaces, this tests that we can merge this back into the pretty printed dtd"""
470        posource = """#: simple.label\nmsgid "First line then "\n"next lines."\nmsgstr "Eerste lyne en dan volgende lyne."\n"""
471        dtdtemplate = (
472            '<!ENTITY simple.label "First line then\n'
473            '                           next lines.">\n'
474        )
475        dtdexpected = '<!ENTITY simple.label "Eerste lyne en dan volgende lyne.">\n'
476        dtdfile = self.merge2dtd(dtdtemplate, posource)
477        print(bytes(dtdfile))
478        assert bytes(dtdfile).decode("utf-8") == dtdexpected
479
480    def test_preserving_spaces(self):
481        """ensure that we preserve spaces between entity and value. Bug 1662"""
482        posource = """#: simple.label\nmsgid "One"\nmsgstr "Een"\n"""
483        dtdtemplate = '<!ENTITY     simple.label         "One">\n'
484        dtdexpected = '<!ENTITY     simple.label         "Een">\n'
485        dtdfile = self.merge2dtd(dtdtemplate, posource)
486        print(bytes(dtdfile))
487        assert bytes(dtdfile).decode("utf-8") == dtdexpected
488
489    def test_preserving_spaces_after_value(self):
490        """Preserve spaces after value. Bug 1662"""
491        # Space between value and >
492        posource = """#: simple.label\nmsgid "One"\nmsgstr "Een"\n"""
493        dtdtemplate = '<!ENTITY simple.label "One" >\n'
494        dtdexpected = '<!ENTITY simple.label "Een" >\n'
495        dtdfile = self.merge2dtd(dtdtemplate, posource)
496        print(bytes(dtdfile))
497        assert bytes(dtdfile).decode("utf-8") == dtdexpected
498        # Space after >
499        dtdtemplate = '<!ENTITY simple.label "One"> \n'
500        dtdexpected = '<!ENTITY simple.label "Een"> \n'
501        dtdfile = self.merge2dtd(dtdtemplate, posource)
502        print(dtdfile)
503        assert bytes(dtdfile).decode("utf-8") == dtdexpected
504
505    def test_comments(self):
506        """test that we preserve comments, bug 351"""
507        posource = '''#: name\nmsgid "Text"\nmsgstr "Teks"'''
508        dtdtemplate = """<!ENTITY name "%s">\n<!-- \n\nexample -->\n"""
509        dtdfile = self.merge2dtd(dtdtemplate % "Text", posource)
510        print(bytes(dtdfile))
511        assert bytes(dtdfile).decode("utf-8") == dtdtemplate % "Teks"
512
513    def test_duplicates(self):
514        """test that we convert duplicates back correctly to their respective entries."""
515        posource = r"""#: bookmarksMenu.label bookmarksMenu.accesskey
516msgctxt "bookmarksMenu.label bookmarksMenu.accesskey"
517msgid "&Bookmarks"
518msgstr "Dipu&kutshwayo1"
519
520#: bookmarksItem.title
521msgctxt "bookmarksItem.title"
522msgid "Bookmarks"
523msgstr "Dipukutshwayo2"
524
525#: bookmarksButton.label
526msgctxt "bookmarksButton.label"
527msgid "Bookmarks"
528msgstr "Dipukutshwayo3"
529"""
530        dtdtemplate = r"""<!ENTITY bookmarksMenu.label "Bookmarks">
531<!ENTITY bookmarksMenu.accesskey "B">
532<!ENTITY bookmarksItem.title "Bookmarks">
533<!ENTITY bookmarksButton.label "Bookmarks">
534"""
535        dtdexpected = r"""<!ENTITY bookmarksMenu.label "Dipukutshwayo1">
536<!ENTITY bookmarksMenu.accesskey "k">
537<!ENTITY bookmarksItem.title "Dipukutshwayo2">
538<!ENTITY bookmarksButton.label "Dipukutshwayo3">
539"""
540        dtdfile = self.merge2dtd(dtdtemplate, posource)
541        print(bytes(dtdfile))
542        assert bytes(dtdfile).decode("utf-8") == dtdexpected
543
544
545class TestPO2DTDCommand(test_convert.TestConvertCommand, TestPO2DTD):
546    """Tests running actual po2dtd commands on files"""
547
548    convertmodule = po2dtd
549    defaultoptions = {"progress": "none"}
550    # TODO: because of having 2 base classes, we need to call all their setup and teardown methods
551    # (otherwise we won't reset the warnings etc)
552
553    def setup_method(self, method):
554        """call both base classes setup_methods"""
555        super().setup_method(method)
556        TestPO2DTD.setup_method(self, method)
557
558    def teardown_method(self, method):
559        """call both base classes teardown_methods"""
560        super().teardown_method(method)
561        TestPO2DTD.teardown_method(self, method)
562
563    def test_help(self, capsys):
564        """tests getting help"""
565        options = super().test_help(capsys)
566        options = self.help_check(options, "-t TEMPLATE, --template=TEMPLATE")
567        options = self.help_check(options, "--fuzzy")
568        options = self.help_check(options, "--threshold=PERCENT")
569        options = self.help_check(options, "--removeuntranslated")
570        options = self.help_check(options, "--nofuzzy", last=True)
571