9
9
10
10
from test .support .os_helper import temp_cwd
11
11
from test .support .script_helper import assert_python_failure , assert_python_ok
12
- from test .test_tools import skip_if_missing , toolsdir
12
+ from test .test_tools import imports_under_tool , skip_if_missing , toolsdir
13
13
14
14
15
15
skip_if_missing ('i18n' )
16
16
17
17
data_dir = (Path (__file__ ).parent / 'msgfmt_data' ).resolve ()
18
18
script_dir = Path (toolsdir ) / 'i18n'
19
- msgfmt = script_dir / 'msgfmt.py'
19
+ msgfmt_py = script_dir / 'msgfmt.py'
20
+
21
+ with imports_under_tool ("i18n" ):
22
+ import msgfmt
20
23
21
24
22
25
def compile_messages (po_file , mo_file ):
23
- assert_python_ok (msgfmt , '-o' , mo_file , po_file )
26
+ assert_python_ok (msgfmt_py , '-o' , mo_file , po_file )
24
27
25
28
26
29
class CompilationTest (unittest .TestCase ):
@@ -92,7 +95,7 @@ def test_po_with_bom(self):
92
95
with temp_cwd ():
93
96
Path ('bom.po' ).write_bytes (b'\xef \xbb \xbf msgid "Python"\n msgstr "Pioton"\n ' )
94
97
95
- res = assert_python_failure (msgfmt , 'bom.po' )
98
+ res = assert_python_failure (msgfmt_py , 'bom.po' )
96
99
err = res .err .decode ('utf-8' )
97
100
self .assertIn ('The file bom.po starts with a UTF-8 BOM' , err )
98
101
@@ -103,7 +106,7 @@ def test_invalid_msgid_plural(self):
103
106
msgstr[0] "singular"
104
107
''' )
105
108
106
- res = assert_python_failure (msgfmt , 'invalid.po' )
109
+ res = assert_python_failure (msgfmt_py , 'invalid.po' )
107
110
err = res .err .decode ('utf-8' )
108
111
self .assertIn ('msgid_plural not preceded by msgid' , err )
109
112
@@ -114,7 +117,7 @@ def test_plural_without_msgid_plural(self):
114
117
msgstr[0] "bar"
115
118
''' )
116
119
117
- res = assert_python_failure (msgfmt , 'invalid.po' )
120
+ res = assert_python_failure (msgfmt_py , 'invalid.po' )
118
121
err = res .err .decode ('utf-8' )
119
122
self .assertIn ('plural without msgid_plural' , err )
120
123
@@ -126,7 +129,7 @@ def test_indexed_msgstr_without_msgid_plural(self):
126
129
msgstr "bar"
127
130
''' )
128
131
129
- res = assert_python_failure (msgfmt , 'invalid.po' )
132
+ res = assert_python_failure (msgfmt_py , 'invalid.po' )
130
133
err = res .err .decode ('utf-8' )
131
134
self .assertIn ('indexed msgstr required for plural' , err )
132
135
@@ -136,38 +139,136 @@ def test_generic_syntax_error(self):
136
139
"foo"
137
140
''' )
138
141
139
- res = assert_python_failure (msgfmt , 'invalid.po' )
142
+ res = assert_python_failure (msgfmt_py , 'invalid.po' )
140
143
err = res .err .decode ('utf-8' )
141
144
self .assertIn ('Syntax error' , err )
142
145
146
+
147
+ class POParserTest (unittest .TestCase ):
148
+ @classmethod
149
+ def tearDownClass (cls ):
150
+ # msgfmt uses a global variable to store messages,
151
+ # clear it after the tests.
152
+ msgfmt .MESSAGES .clear ()
153
+
154
+ def test_strings (self ):
155
+ # Test that the PO parser correctly handles and unescape
156
+ # strings in the PO file.
157
+ # The PO file format allows for a variety of escape sequences,
158
+ # octal and hex escapes.
159
+ valid_strings = (
160
+ # empty strings
161
+ ('""' , '' ),
162
+ ('"" "" ""' , '' ),
163
+ # allowed escape sequences
164
+ (r'"\\"' , '\\ ' ),
165
+ (r'"\""' , '"' ),
166
+ (r'"\t"' , '\t ' ),
167
+ (r'"\n"' , '\n ' ),
168
+ (r'"\r"' , '\r ' ),
169
+ (r'"\f"' , '\f ' ),
170
+ (r'"\a"' , '\a ' ),
171
+ (r'"\b"' , '\b ' ),
172
+ (r'"\v"' , '\v ' ),
173
+ # non-empty strings
174
+ ('"foo"' , 'foo' ),
175
+ ('"foo" "bar"' , 'foobar' ),
176
+ ('"foo""bar"' , 'foobar' ),
177
+ ('"" "foo" ""' , 'foo' ),
178
+ # newlines and tabs
179
+ (r'"foo\nbar"' , 'foo\n bar' ),
180
+ (r'"foo\n" "bar"' , 'foo\n bar' ),
181
+ (r'"foo\tbar"' , 'foo\t bar' ),
182
+ (r'"foo\t" "bar"' , 'foo\t bar' ),
183
+ # escaped quotes
184
+ (r'"foo\"bar"' , 'foo"bar' ),
185
+ (r'"foo\"" "bar"' , 'foo"bar' ),
186
+ (r'"foo\\" "bar"' , 'foo\\ bar' ),
187
+ # octal escapes
188
+ (r'"\120\171\164\150\157\156"' , 'Python' ),
189
+ (r'"\120\171\164" "\150\157\156"' , 'Python' ),
190
+ (r'"\"\120\171\164" "\150\157\156\""' , '"Python"' ),
191
+ # hex escapes
192
+ (r'"\x50\x79\x74\x68\x6f\x6e"' , 'Python' ),
193
+ (r'"\x50\x79\x74" "\x68\x6f\x6e"' , 'Python' ),
194
+ (r'"\"\x50\x79\x74" "\x68\x6f\x6e\""' , '"Python"' ),
195
+ )
196
+
197
+ with temp_cwd ():
198
+ for po_string , expected in valid_strings :
199
+ with self .subTest (po_string = po_string ):
200
+ # Construct a PO file with a single entry,
201
+ # compile it, read it into a catalog and
202
+ # check the result.
203
+ po = f'msgid { po_string } \n msgstr "translation"'
204
+ Path ('messages.po' ).write_text (po )
205
+ # Reset the global MESSAGES dictionary
206
+ msgfmt .MESSAGES .clear ()
207
+ msgfmt .make ('messages.po' , 'messages.mo' )
208
+
209
+ with open ('messages.mo' , 'rb' ) as f :
210
+ actual = GNUTranslations (f )
211
+
212
+ self .assertDictEqual (actual ._catalog , {expected : 'translation' })
213
+
214
+ invalid_strings = (
215
+ # "''", # invalid but currently accepted
216
+ '"' ,
217
+ '"""' ,
218
+ '"" "' ,
219
+ 'foo' ,
220
+ '"" "foo' ,
221
+ '"foo" foo' ,
222
+ '42' ,
223
+ '"" 42 ""' ,
224
+ # disallowed escape sequences
225
+ # r'"\'"', # invalid but currently accepted
226
+ # r'"\e"', # invalid but currently accepted
227
+ # r'"\8"', # invalid but currently accepted
228
+ # r'"\9"', # invalid but currently accepted
229
+ r'"\x"' ,
230
+ r'"\u1234"' ,
231
+ r'"\N{ROMAN NUMERAL NINE}"'
232
+ )
233
+ with temp_cwd ():
234
+ for invalid_string in invalid_strings :
235
+ with self .subTest (string = invalid_string ):
236
+ po = f'msgid { invalid_string } \n msgstr "translation"'
237
+ Path ('messages.po' ).write_text (po )
238
+ # Reset the global MESSAGES dictionary
239
+ msgfmt .MESSAGES .clear ()
240
+ with self .assertRaises (Exception ):
241
+ msgfmt .make ('messages.po' , 'messages.mo' )
242
+
243
+
143
244
class CLITest (unittest .TestCase ):
144
245
145
246
def test_help (self ):
146
247
for option in ('--help' , '-h' ):
147
- res = assert_python_ok (msgfmt , option )
248
+ res = assert_python_ok (msgfmt_py , option )
148
249
err = res .err .decode ('utf-8' )
149
250
self .assertIn ('Generate binary message catalog from textual translation description.' , err )
150
251
151
252
def test_version (self ):
152
253
for option in ('--version' , '-V' ):
153
- res = assert_python_ok (msgfmt , option )
254
+ res = assert_python_ok (msgfmt_py , option )
154
255
out = res .out .decode ('utf-8' ).strip ()
155
256
self .assertEqual ('msgfmt.py 1.2' , out )
156
257
157
258
def test_invalid_option (self ):
158
- res = assert_python_failure (msgfmt , '--invalid-option' )
259
+ res = assert_python_failure (msgfmt_py , '--invalid-option' )
159
260
err = res .err .decode ('utf-8' )
160
261
self .assertIn ('Generate binary message catalog from textual translation description.' , err )
161
262
self .assertIn ('option --invalid-option not recognized' , err )
162
263
163
264
def test_no_input_file (self ):
164
- res = assert_python_ok (msgfmt )
265
+ res = assert_python_ok (msgfmt_py )
165
266
err = res .err .decode ('utf-8' ).replace ('\r \n ' , '\n ' )
166
267
self .assertIn ('No input file given\n '
167
268
"Try `msgfmt --help' for more information." , err )
168
269
169
270
def test_nonexistent_file (self ):
170
- assert_python_failure (msgfmt , 'nonexistent.po' )
271
+ assert_python_failure (msgfmt_py , 'nonexistent.po' )
171
272
172
273
173
274
def update_catalog_snapshots ():
0 commit comments