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

+113-12
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)