8
8
9
9
from test .support .os_helper import temp_cwd
10
10
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
12
12
13
13
14
14
skip_if_missing ('i18n' )
15
15
16
16
data_dir = (Path (__file__ ).parent / 'msgfmt_data' ).resolve ()
17
17
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
19
22
20
23
21
24
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 )
23
26
24
27
25
28
class CompilationTest (unittest .TestCase ):
@@ -69,7 +72,7 @@ def test_invalid_msgid_plural(self):
69
72
msgstr[0] "singular"
70
73
''' )
71
74
72
- res = assert_python_failure (msgfmt , 'invalid.po' )
75
+ res = assert_python_failure (msgfmt_py , 'invalid.po' )
73
76
err = res .err .decode ('utf-8' )
74
77
self .assertIn ('msgid_plural not preceded by msgid' , err )
75
78
@@ -80,7 +83,7 @@ def test_plural_without_msgid_plural(self):
80
83
msgstr[0] "bar"
81
84
''' )
82
85
83
- res = assert_python_failure (msgfmt , 'invalid.po' )
86
+ res = assert_python_failure (msgfmt_py , 'invalid.po' )
84
87
err = res .err .decode ('utf-8' )
85
88
self .assertIn ('plural without msgid_plural' , err )
86
89
@@ -92,7 +95,7 @@ def test_indexed_msgstr_without_msgid_plural(self):
92
95
msgstr "bar"
93
96
''' )
94
97
95
- res = assert_python_failure (msgfmt , 'invalid.po' )
98
+ res = assert_python_failure (msgfmt_py , 'invalid.po' )
96
99
err = res .err .decode ('utf-8' )
97
100
self .assertIn ('indexed msgstr required for plural' , err )
98
101
@@ -102,38 +105,136 @@ def test_generic_syntax_error(self):
102
105
"foo"
103
106
''' )
104
107
105
- res = assert_python_failure (msgfmt , 'invalid.po' )
108
+ res = assert_python_failure (msgfmt_py , 'invalid.po' )
106
109
err = res .err .decode ('utf-8' )
107
110
self .assertIn ('Syntax error' , err )
108
111
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\n bar' ),
146
+ (r'"foo\n" "bar"' , 'foo\n bar' ),
147
+ (r'"foo\tbar"' , 'foo\t bar' ),
148
+ (r'"foo\t" "bar"' , 'foo\t bar' ),
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 } \n msgstr "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 } \n msgstr "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
+
109
210
class CLITest (unittest .TestCase ):
110
211
111
212
def test_help (self ):
112
213
for option in ('--help' , '-h' ):
113
- res = assert_python_ok (msgfmt , option )
214
+ res = assert_python_ok (msgfmt_py , option )
114
215
err = res .err .decode ('utf-8' )
115
216
self .assertIn ('Generate binary message catalog from textual translation description.' , err )
116
217
117
218
def test_version (self ):
118
219
for option in ('--version' , '-V' ):
119
- res = assert_python_ok (msgfmt , option )
220
+ res = assert_python_ok (msgfmt_py , option )
120
221
out = res .out .decode ('utf-8' ).strip ()
121
222
self .assertEqual ('msgfmt.py 1.2' , out )
122
223
123
224
def test_invalid_option (self ):
124
- res = assert_python_failure (msgfmt , '--invalid-option' )
225
+ res = assert_python_failure (msgfmt_py , '--invalid-option' )
125
226
err = res .err .decode ('utf-8' )
126
227
self .assertIn ('Generate binary message catalog from textual translation description.' , err )
127
228
self .assertIn ('option --invalid-option not recognized' , err )
128
229
129
230
def test_no_input_file (self ):
130
- res = assert_python_ok (msgfmt )
231
+ res = assert_python_ok (msgfmt_py )
131
232
err = res .err .decode ('utf-8' ).replace ('\r \n ' , '\n ' )
132
233
self .assertIn ('No input file given\n '
133
234
"Try `msgfmt --help' for more information." , err )
134
235
135
236
def test_nonexistent_file (self ):
136
- assert_python_failure (msgfmt , 'nonexistent.po' )
237
+ assert_python_failure (msgfmt_py , 'nonexistent.po' )
137
238
138
239
139
240
def update_catalog_snapshots ():
0 commit comments