88
99from test .support .os_helper import temp_cwd
1010from 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
1414skip_if_missing ('i18n' )
1515
1616data_dir = (Path (__file__ ).parent / 'msgfmt_data' ).resolve ()
1717script_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
2124def 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
2528class CompilationTest (unittest .TestCase ):
@@ -69,7 +72,7 @@ def test_invalid_msgid_plural(self):
6972msgstr[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):
8083msgstr[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):
9295msgstr "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\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+
109210class 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
139240def update_catalog_snapshots ():
0 commit comments