Skip to content

Commit ae775dc

Browse files
authored
[3.13] gh-130197: Improve test coverage of msgfmt.py (GH-133048) (GH-133255)
(cherry picked from commit c73d460)
1 parent 766c5f7 commit ae775dc

File tree

1 file changed

+113
-12
lines changed

1 file changed

+113
-12
lines changed

Lib/test/test_tools/test_msgfmt.py

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@
88

99
from test.support.os_helper import temp_cwd
1010
from test.support.script_helper import assert_python_failure, assert_python_ok
11-
from test.test_tools import skip_if_missing, toolsdir
11+
from test.test_tools import imports_under_tool, skip_if_missing, toolsdir
1212

1313

1414
skip_if_missing('i18n')
1515

1616
data_dir = (Path(__file__).parent / 'msgfmt_data').resolve()
1717
script_dir = Path(toolsdir) / 'i18n'
18-
msgfmt = script_dir / 'msgfmt.py'
18+
msgfmt_py = script_dir / 'msgfmt.py'
19+
20+
with imports_under_tool("i18n"):
21+
import msgfmt
1922

2023

2124
def compile_messages(po_file, mo_file):
22-
assert_python_ok(msgfmt, '-o', mo_file, po_file)
25+
assert_python_ok(msgfmt_py, '-o', mo_file, po_file)
2326

2427

2528
class CompilationTest(unittest.TestCase):
@@ -69,7 +72,7 @@ def test_invalid_msgid_plural(self):
6972
msgstr[0] "singular"
7073
''')
7174

72-
res = assert_python_failure(msgfmt, 'invalid.po')
75+
res = assert_python_failure(msgfmt_py, 'invalid.po')
7376
err = res.err.decode('utf-8')
7477
self.assertIn('msgid_plural not preceded by msgid', err)
7578

@@ -80,7 +83,7 @@ def test_plural_without_msgid_plural(self):
8083
msgstr[0] "bar"
8184
''')
8285

83-
res = assert_python_failure(msgfmt, 'invalid.po')
86+
res = assert_python_failure(msgfmt_py, 'invalid.po')
8487
err = res.err.decode('utf-8')
8588
self.assertIn('plural without msgid_plural', err)
8689

@@ -92,7 +95,7 @@ def test_indexed_msgstr_without_msgid_plural(self):
9295
msgstr "bar"
9396
''')
9497

95-
res = assert_python_failure(msgfmt, 'invalid.po')
98+
res = assert_python_failure(msgfmt_py, 'invalid.po')
9699
err = res.err.decode('utf-8')
97100
self.assertIn('indexed msgstr required for plural', err)
98101

@@ -102,38 +105,136 @@ def test_generic_syntax_error(self):
102105
"foo"
103106
''')
104107

105-
res = assert_python_failure(msgfmt, 'invalid.po')
108+
res = assert_python_failure(msgfmt_py, 'invalid.po')
106109
err = res.err.decode('utf-8')
107110
self.assertIn('Syntax error', err)
108111

112+
113+
class POParserTest(unittest.TestCase):
114+
@classmethod
115+
def tearDownClass(cls):
116+
# msgfmt uses a global variable to store messages,
117+
# clear it after the tests.
118+
msgfmt.MESSAGES.clear()
119+
120+
def test_strings(self):
121+
# Test that the PO parser correctly handles and unescape
122+
# strings in the PO file.
123+
# The PO file format allows for a variety of escape sequences,
124+
# octal and hex escapes.
125+
valid_strings = (
126+
# empty strings
127+
('""', ''),
128+
('"" "" ""', ''),
129+
# allowed escape sequences
130+
(r'"\\"', '\\'),
131+
(r'"\""', '"'),
132+
(r'"\t"', '\t'),
133+
(r'"\n"', '\n'),
134+
(r'"\r"', '\r'),
135+
(r'"\f"', '\f'),
136+
(r'"\a"', '\a'),
137+
(r'"\b"', '\b'),
138+
(r'"\v"', '\v'),
139+
# non-empty strings
140+
('"foo"', 'foo'),
141+
('"foo" "bar"', 'foobar'),
142+
('"foo""bar"', 'foobar'),
143+
('"" "foo" ""', 'foo'),
144+
# newlines and tabs
145+
(r'"foo\nbar"', 'foo\nbar'),
146+
(r'"foo\n" "bar"', 'foo\nbar'),
147+
(r'"foo\tbar"', 'foo\tbar'),
148+
(r'"foo\t" "bar"', 'foo\tbar'),
149+
# escaped quotes
150+
(r'"foo\"bar"', 'foo"bar'),
151+
(r'"foo\"" "bar"', 'foo"bar'),
152+
(r'"foo\\" "bar"', 'foo\\bar'),
153+
# octal escapes
154+
(r'"\120\171\164\150\157\156"', 'Python'),
155+
(r'"\120\171\164" "\150\157\156"', 'Python'),
156+
(r'"\"\120\171\164" "\150\157\156\""', '"Python"'),
157+
# hex escapes
158+
(r'"\x50\x79\x74\x68\x6f\x6e"', 'Python'),
159+
(r'"\x50\x79\x74" "\x68\x6f\x6e"', 'Python'),
160+
(r'"\"\x50\x79\x74" "\x68\x6f\x6e\""', '"Python"'),
161+
)
162+
163+
with temp_cwd():
164+
for po_string, expected in valid_strings:
165+
with self.subTest(po_string=po_string):
166+
# Construct a PO file with a single entry,
167+
# compile it, read it into a catalog and
168+
# check the result.
169+
po = f'msgid {po_string}\nmsgstr "translation"'
170+
Path('messages.po').write_text(po)
171+
# Reset the global MESSAGES dictionary
172+
msgfmt.MESSAGES.clear()
173+
msgfmt.make('messages.po', 'messages.mo')
174+
175+
with open('messages.mo', 'rb') as f:
176+
actual = GNUTranslations(f)
177+
178+
self.assertDictEqual(actual._catalog, {expected: 'translation'})
179+
180+
invalid_strings = (
181+
# "''", # invalid but currently accepted
182+
'"',
183+
'"""',
184+
'"" "',
185+
'foo',
186+
'"" "foo',
187+
'"foo" foo',
188+
'42',
189+
'"" 42 ""',
190+
# disallowed escape sequences
191+
# r'"\'"', # invalid but currently accepted
192+
# r'"\e"', # invalid but currently accepted
193+
# r'"\8"', # invalid but currently accepted
194+
# r'"\9"', # invalid but currently accepted
195+
r'"\x"',
196+
r'"\u1234"',
197+
r'"\N{ROMAN NUMERAL NINE}"'
198+
)
199+
with temp_cwd():
200+
for invalid_string in invalid_strings:
201+
with self.subTest(string=invalid_string):
202+
po = f'msgid {invalid_string}\nmsgstr "translation"'
203+
Path('messages.po').write_text(po)
204+
# Reset the global MESSAGES dictionary
205+
msgfmt.MESSAGES.clear()
206+
with self.assertRaises(Exception):
207+
msgfmt.make('messages.po', 'messages.mo')
208+
209+
109210
class CLITest(unittest.TestCase):
110211

111212
def test_help(self):
112213
for option in ('--help', '-h'):
113-
res = assert_python_ok(msgfmt, option)
214+
res = assert_python_ok(msgfmt_py, option)
114215
err = res.err.decode('utf-8')
115216
self.assertIn('Generate binary message catalog from textual translation description.', err)
116217

117218
def test_version(self):
118219
for option in ('--version', '-V'):
119-
res = assert_python_ok(msgfmt, option)
220+
res = assert_python_ok(msgfmt_py, option)
120221
out = res.out.decode('utf-8').strip()
121222
self.assertEqual('msgfmt.py 1.2', out)
122223

123224
def test_invalid_option(self):
124-
res = assert_python_failure(msgfmt, '--invalid-option')
225+
res = assert_python_failure(msgfmt_py, '--invalid-option')
125226
err = res.err.decode('utf-8')
126227
self.assertIn('Generate binary message catalog from textual translation description.', err)
127228
self.assertIn('option --invalid-option not recognized', err)
128229

129230
def test_no_input_file(self):
130-
res = assert_python_ok(msgfmt)
231+
res = assert_python_ok(msgfmt_py)
131232
err = res.err.decode('utf-8').replace('\r\n', '\n')
132233
self.assertIn('No input file given\n'
133234
"Try `msgfmt --help' for more information.", err)
134235

135236
def test_nonexistent_file(self):
136-
assert_python_failure(msgfmt, 'nonexistent.po')
237+
assert_python_failure(msgfmt_py, 'nonexistent.po')
137238

138239

139240
def update_catalog_snapshots():

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy