blob: 6e2e212105f3a760d2efd5acbe233e5641628fb6 [file] [log] [blame]
Nick Coghlan39f0bb52017-11-28 08:11:51 +10001# Run the tests in Programs/_testembed.c (tests for the CPython embedding APIs)
2from test import support
3import unittest
4
5from collections import namedtuple
Victor Stinner35c28d52018-11-14 02:01:52 +01006import json
Nick Coghlan39f0bb52017-11-28 08:11:51 +10007import os
8import re
9import subprocess
10import sys
11
12
Victor Stinner0c90d6f2018-08-05 12:31:59 +020013class EmbeddingTestsMixin:
Nick Coghlan39f0bb52017-11-28 08:11:51 +100014 def setUp(self):
15 here = os.path.abspath(__file__)
16 basepath = os.path.dirname(os.path.dirname(os.path.dirname(here)))
17 exename = "_testembed"
18 if sys.platform.startswith("win"):
19 ext = ("_d" if "_d" in sys.executable else "") + ".exe"
20 exename += ext
21 exepath = os.path.dirname(sys.executable)
22 else:
23 exepath = os.path.join(basepath, "Programs")
24 self.test_exe = exe = os.path.join(exepath, exename)
25 if not os.path.exists(exe):
26 self.skipTest("%r doesn't exist" % exe)
27 # This is needed otherwise we get a fatal error:
28 # "Py_Initialize: Unable to get the locale encoding
29 # LookupError: no codec search functions registered: can't find encoding"
30 self.oldcwd = os.getcwd()
31 os.chdir(basepath)
32
33 def tearDown(self):
34 os.chdir(self.oldcwd)
35
36 def run_embedded_interpreter(self, *args, env=None):
37 """Runs a test in the embedded interpreter"""
38 cmd = [self.test_exe]
39 cmd.extend(args)
40 if env is not None and sys.platform == 'win32':
41 # Windows requires at least the SYSTEMROOT environment variable to
42 # start Python.
43 env = env.copy()
44 env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
45
46 p = subprocess.Popen(cmd,
47 stdout=subprocess.PIPE,
48 stderr=subprocess.PIPE,
49 universal_newlines=True,
50 env=env)
51 (out, err) = p.communicate()
52 if p.returncode != 0 and support.verbose:
53 print(f"--- {cmd} failed ---")
54 print(f"stdout:\n{out}")
Miss Islington (bot)c6d94c32018-03-25 04:27:57 -070055 print(f"stderr:\n{err}")
Nick Coghlan39f0bb52017-11-28 08:11:51 +100056 print(f"------")
57
58 self.assertEqual(p.returncode, 0,
59 "bad returncode %d, stderr is %r" %
60 (p.returncode, err))
61 return out, err
62
63 def run_repeated_init_and_subinterpreters(self):
64 out, err = self.run_embedded_interpreter("repeated_init_and_subinterpreters")
65 self.assertEqual(err, "")
66
67 # The output from _testembed looks like this:
68 # --- Pass 0 ---
69 # interp 0 <0x1cf9330>, thread state <0x1cf9700>: id(modules) = 139650431942728
70 # interp 1 <0x1d4f690>, thread state <0x1d35350>: id(modules) = 139650431165784
71 # interp 2 <0x1d5a690>, thread state <0x1d99ed0>: id(modules) = 139650413140368
72 # interp 3 <0x1d4f690>, thread state <0x1dc3340>: id(modules) = 139650412862200
73 # interp 0 <0x1cf9330>, thread state <0x1cf9700>: id(modules) = 139650431942728
74 # --- Pass 1 ---
75 # ...
76
77 interp_pat = (r"^interp (\d+) <(0x[\dA-F]+)>, "
78 r"thread state <(0x[\dA-F]+)>: "
79 r"id\(modules\) = ([\d]+)$")
80 Interp = namedtuple("Interp", "id interp tstate modules")
81
82 numloops = 0
83 current_run = []
84 for line in out.splitlines():
85 if line == "--- Pass {} ---".format(numloops):
86 self.assertEqual(len(current_run), 0)
Miss Islington (bot)c6d94c32018-03-25 04:27:57 -070087 if support.verbose > 1:
Nick Coghlan39f0bb52017-11-28 08:11:51 +100088 print(line)
89 numloops += 1
90 continue
91
92 self.assertLess(len(current_run), 5)
93 match = re.match(interp_pat, line)
94 if match is None:
95 self.assertRegex(line, interp_pat)
96
97 # Parse the line from the loop. The first line is the main
98 # interpreter and the 3 afterward are subinterpreters.
99 interp = Interp(*match.groups())
Miss Islington (bot)c6d94c32018-03-25 04:27:57 -0700100 if support.verbose > 1:
Nick Coghlan39f0bb52017-11-28 08:11:51 +1000101 print(interp)
102 self.assertTrue(interp.interp)
103 self.assertTrue(interp.tstate)
104 self.assertTrue(interp.modules)
105 current_run.append(interp)
106
107 # The last line in the loop should be the same as the first.
108 if len(current_run) == 5:
109 main = current_run[0]
110 self.assertEqual(interp, main)
111 yield current_run
112 current_run = []
113
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200114
115class EmbeddingTests(EmbeddingTestsMixin, unittest.TestCase):
Nick Coghlan39f0bb52017-11-28 08:11:51 +1000116 def test_subinterps_main(self):
117 for run in self.run_repeated_init_and_subinterpreters():
118 main = run[0]
119
120 self.assertEqual(main.id, '0')
121
122 def test_subinterps_different_ids(self):
123 for run in self.run_repeated_init_and_subinterpreters():
124 main, *subs, _ = run
125
126 mainid = int(main.id)
127 for i, sub in enumerate(subs):
128 self.assertEqual(sub.id, str(mainid + i + 1))
129
130 def test_subinterps_distinct_state(self):
131 for run in self.run_repeated_init_and_subinterpreters():
132 main, *subs, _ = run
133
134 if '0x0' in main:
135 # XXX Fix on Windows (and other platforms): something
136 # is going on with the pointers in Programs/_testembed.c.
137 # interp.interp is 0x0 and interp.modules is the same
138 # between interpreters.
139 raise unittest.SkipTest('platform prints pointers as 0x0')
140
141 for sub in subs:
142 # A new subinterpreter may have the same
143 # PyInterpreterState pointer as a previous one if
144 # the earlier one has already been destroyed. So
145 # we compare with the main interpreter. The same
146 # applies to tstate.
147 self.assertNotEqual(sub.interp, main.interp)
148 self.assertNotEqual(sub.tstate, main.tstate)
149 self.assertNotEqual(sub.modules, main.modules)
150
151 def test_forced_io_encoding(self):
152 # Checks forced configuration of embedded interpreter IO streams
153 env = dict(os.environ, PYTHONIOENCODING="utf-8:surrogateescape")
154 out, err = self.run_embedded_interpreter("forced_io_encoding", env=env)
155 if support.verbose > 1:
156 print()
157 print(out)
158 print(err)
159 expected_stream_encoding = "utf-8"
160 expected_errors = "surrogateescape"
161 expected_output = '\n'.join([
162 "--- Use defaults ---",
163 "Expected encoding: default",
164 "Expected errors: default",
165 "stdin: {in_encoding}:{errors}",
166 "stdout: {out_encoding}:{errors}",
167 "stderr: {out_encoding}:backslashreplace",
168 "--- Set errors only ---",
169 "Expected encoding: default",
170 "Expected errors: ignore",
171 "stdin: {in_encoding}:ignore",
172 "stdout: {out_encoding}:ignore",
173 "stderr: {out_encoding}:backslashreplace",
174 "--- Set encoding only ---",
175 "Expected encoding: latin-1",
176 "Expected errors: default",
177 "stdin: latin-1:{errors}",
178 "stdout: latin-1:{errors}",
179 "stderr: latin-1:backslashreplace",
180 "--- Set encoding and errors ---",
181 "Expected encoding: latin-1",
182 "Expected errors: replace",
183 "stdin: latin-1:replace",
184 "stdout: latin-1:replace",
185 "stderr: latin-1:backslashreplace"])
186 expected_output = expected_output.format(
187 in_encoding=expected_stream_encoding,
188 out_encoding=expected_stream_encoding,
189 errors=expected_errors)
190 # This is useful if we ever trip over odd platform behaviour
191 self.maxDiff = None
192 self.assertEqual(out.strip(), expected_output)
193
194 def test_pre_initialization_api(self):
195 """
Miss Islington (bot)c6d94c32018-03-25 04:27:57 -0700196 Checks some key parts of the C-API that need to work before the runtine
Nick Coghlan39f0bb52017-11-28 08:11:51 +1000197 is initialized (via Py_Initialize()).
198 """
199 env = dict(os.environ, PYTHONPATH=os.pathsep.join(sys.path))
200 out, err = self.run_embedded_interpreter("pre_initialization_api", env=env)
Miss Islington (bot)c6d94c32018-03-25 04:27:57 -0700201 if sys.platform == "win32":
202 expected_path = self.test_exe
203 else:
204 expected_path = os.path.join(os.getcwd(), "spam")
205 expected_output = f"sys.executable: {expected_path}\n"
206 self.assertIn(expected_output, out)
207 self.assertEqual(err, '')
208
209 def test_pre_initialization_sys_options(self):
210 """
211 Checks that sys.warnoptions and sys._xoptions can be set before the
212 runtime is initialized (otherwise they won't be effective).
213 """
Miss Islington (bot)dd3ede72018-04-27 05:41:25 -0700214 env = dict(os.environ, PYTHONPATH=os.pathsep.join(sys.path))
Miss Islington (bot)c6d94c32018-03-25 04:27:57 -0700215 out, err = self.run_embedded_interpreter(
216 "pre_initialization_sys_options", env=env)
217 expected_output = (
218 "sys.warnoptions: ['once', 'module', 'default']\n"
219 "sys._xoptions: {'not_an_option': '1', 'also_not_an_option': '2'}\n"
220 "warnings.filters[:3]: ['default', 'module', 'once']\n"
221 )
222 self.assertIn(expected_output, out)
Nick Coghlan39f0bb52017-11-28 08:11:51 +1000223 self.assertEqual(err, '')
224
Victor Stinnerb4d1e1f2017-11-30 22:05:00 +0100225 def test_bpo20891(self):
226 """
227 bpo-20891: Calling PyGILState_Ensure in a non-Python thread before
228 calling PyEval_InitThreads() must not crash. PyGILState_Ensure() must
229 call PyEval_InitThreads() for us in this case.
230 """
231 out, err = self.run_embedded_interpreter("bpo20891")
232 self.assertEqual(out, '')
233 self.assertEqual(err, '')
234
Miss Islington (bot)3747dd12018-06-22 10:33:48 -0700235 def test_initialize_twice(self):
236 """
237 bpo-33932: Calling Py_Initialize() twice should do nothing (and not
238 crash!).
239 """
240 out, err = self.run_embedded_interpreter("initialize_twice")
241 self.assertEqual(out, '')
242 self.assertEqual(err, '')
243
Miss Islington (bot)03ec4df2018-07-20 17:16:22 -0700244 def test_initialize_pymain(self):
245 """
246 bpo-34008: Calling Py_Main() after Py_Initialize() must not fail.
247 """
248 out, err = self.run_embedded_interpreter("initialize_pymain")
249 self.assertEqual(out.rstrip(), "Py_Main() after Py_Initialize: sys.argv=['-c', 'arg2']")
250 self.assertEqual(err, '')
251
Nick Coghlan39f0bb52017-11-28 08:11:51 +1000252
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200253class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
254 maxDiff = 4096
Victor Stinner35c28d52018-11-14 02:01:52 +0100255 UTF8_MODE_ERRORS = ('surrogatepass' if sys.platform == 'win32'
256 else 'surrogateescape')
257 # FIXME: untested core configuration variables
258 UNTESTED_CORE_CONFIG = (
259 'base_exec_prefix',
260 'base_prefix',
261 'exec_prefix',
262 'executable',
263 'home',
264 'module_search_path_env',
265 'module_search_paths',
266 'prefix',
267 )
268 # FIXME: untested main configuration variables
269 UNTESTED_MAIN_CONFIG = (
Victor Stinner35c28d52018-11-14 02:01:52 +0100270 'module_search_path',
271 )
272 DEFAULT_GLOBAL_CONFIG = {
273 'Py_BytesWarningFlag': 0,
274 'Py_DebugFlag': 0,
275 'Py_DontWriteBytecodeFlag': 0,
276 'Py_FrozenFlag': 0,
277 'Py_HasFileSystemDefaultEncoding': 0,
278 'Py_HashRandomizationFlag': 1,
279 'Py_InspectFlag': 0,
280 'Py_InteractiveFlag': 0,
281 'Py_IsolatedFlag': 0,
282 'Py_NoSiteFlag': 0,
283 'Py_NoUserSiteDirectory': 0,
284 'Py_OptimizeFlag': 0,
285 'Py_QuietFlag': 0,
286 'Py_UnbufferedStdioFlag': 0,
287 'Py_VerboseFlag': 0,
288 }
289 if os.name == 'nt':
Victor Stinner35c28d52018-11-14 02:01:52 +0100290 DEFAULT_GLOBAL_CONFIG['Py_LegacyWindowsFSEncodingFlag'] = 0
291 DEFAULT_GLOBAL_CONFIG['Py_LegacyWindowsStdioFlag'] = 0
Victor Stinnerbc09ee82018-11-14 11:36:47 +0100292 if sys.platform in ('win32', 'darwin'):
293 DEFAULT_GLOBAL_CONFIG['Py_HasFileSystemDefaultEncoding'] = 1
Victor Stinner35c28d52018-11-14 02:01:52 +0100294
295 DEFAULT_CORE_CONFIG = {
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200296 'install_signal_handlers': 1,
Victor Stinner35c28d52018-11-14 02:01:52 +0100297 'ignore_environment': 0,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200298 'use_hash_seed': 0,
299 'hash_seed': 0,
Victor Stinner35c28d52018-11-14 02:01:52 +0100300 'allocator': None,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200301 'dev_mode': 0,
302 'faulthandler': 0,
303 'tracemalloc': 0,
304 'import_time': 0,
305 'show_ref_count': 0,
306 'show_alloc_count': 0,
307 'dump_refs': 0,
308 'malloc_stats': 0,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200309
Victor Stinner35c28d52018-11-14 02:01:52 +0100310 'utf8_mode': 0,
Victor Stinner95cc3ee2018-09-19 12:01:52 -0700311 'coerce_c_locale': 0,
312 'coerce_c_locale_warn': 0,
313
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200314 'program_name': './_testembed',
Victor Stinner35c28d52018-11-14 02:01:52 +0100315 'argv': [],
316 'program': None,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200317
Victor Stinner35c28d52018-11-14 02:01:52 +0100318 'xoptions': [],
319 'warnoptions': [],
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200320
321 '_disable_importlib': 0,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200322 }
323
Victor Stinner35c28d52018-11-14 02:01:52 +0100324 def get_filesystem_encoding(self, isolated, env):
325 code = ('import codecs, locale, sys; '
326 'print(sys.getfilesystemencoding(), '
327 'sys.getfilesystemencodeerrors())')
328 args = (sys.executable, '-c', code)
329 env = dict(env)
330 if not isolated:
331 env['PYTHONCOERCECLOCALE'] = '0'
332 env['PYTHONUTF8'] = '0'
333 proc = subprocess.run(args, text=True, env=env,
334 stdout=subprocess.PIPE,
335 stderr=subprocess.PIPE)
336 if proc.returncode:
337 raise Exception(f"failed to get the locale encoding: "
338 f"stdout={proc.stdout!r} stderr={proc.stderr!r}")
339 out = proc.stdout.rstrip()
340 return out.split()
341
342 def check_config(self, testname, expected, expected_global):
343 expected = dict(self.DEFAULT_CORE_CONFIG, **expected)
344
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200345 env = dict(os.environ)
346 for key in list(env):
347 if key.startswith('PYTHON'):
348 del env[key]
349 # Disable C locale coercion and UTF-8 mode to not depend
350 # on the current locale
351 env['PYTHONCOERCECLOCALE'] = '0'
352 env['PYTHONUTF8'] = '0'
Victor Stinner35c28d52018-11-14 02:01:52 +0100353
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200354 out, err = self.run_embedded_interpreter(testname, env=env)
355 # Ignore err
356
Victor Stinner35c28d52018-11-14 02:01:52 +0100357 config = json.loads(out)
358 core_config = config['core_config']
359 executable = core_config['executable']
360 main_config = config['main_config']
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200361
Victor Stinner35c28d52018-11-14 02:01:52 +0100362 for key in self.UNTESTED_MAIN_CONFIG:
363 del main_config[key]
364
365 expected_main = {
Victor Stinner88cbea42018-11-14 02:45:25 +0100366 'install_signal_handlers': core_config['install_signal_handlers'],
Victor Stinner35c28d52018-11-14 02:01:52 +0100367 'argv': [],
368 'prefix': sys.prefix,
369 'executable': core_config['executable'],
370 'base_prefix': sys.base_prefix,
371 'base_exec_prefix': sys.base_exec_prefix,
372 'warnoptions': core_config['warnoptions'],
373 'xoptions': {},
374 'exec_prefix': core_config['exec_prefix'],
375 }
376 self.assertEqual(main_config, expected_main)
377
378 expected_global = dict(self.DEFAULT_GLOBAL_CONFIG, **expected_global)
379
380 if 'Py_FileSystemDefaultEncoding' not in expected_global:
381 isolated = expected_global['Py_IsolatedFlag']
382 fs_encoding, fs_errors = self.get_filesystem_encoding(isolated, env)
383 expected_global['Py_FileSystemDefaultEncodeErrors'] = fs_errors
384 expected_global['Py_FileSystemDefaultEncoding'] = fs_encoding
385
386 for global_key, core_key in (
387 ('Py_UTF8Mode', 'utf8_mode'),
388 ('Py_IgnoreEnvironmentFlag', 'ignore_environment'),
389 ):
390 expected_global[global_key] = core_config[core_key]
391
392 self.assertEqual(config['global_config'], expected_global)
393
394 for key in self.UNTESTED_CORE_CONFIG:
395 core_config.pop(key, None)
396 self.assertEqual(core_config, expected)
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200397
398 def test_init_default_config(self):
Victor Stinner35c28d52018-11-14 02:01:52 +0100399 self.check_config("init_default_config", {}, {})
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200400
401 def test_init_global_config(self):
Victor Stinner35c28d52018-11-14 02:01:52 +0100402 core_config = {
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200403 'program_name': './globalvar',
Victor Stinner35c28d52018-11-14 02:01:52 +0100404 'utf8_mode': 1,
405 }
406 global_config = {
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200407 'Py_BytesWarningFlag': 1,
Victor Stinner35c28d52018-11-14 02:01:52 +0100408 'Py_DontWriteBytecodeFlag': 1,
409 'Py_HasFileSystemDefaultEncoding': 1,
410 'Py_FileSystemDefaultEncodeErrors': self.UTF8_MODE_ERRORS,
411 'Py_FileSystemDefaultEncoding': 'utf-8',
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200412 'Py_InspectFlag': 1,
413 'Py_InteractiveFlag': 1,
Victor Stinner35c28d52018-11-14 02:01:52 +0100414 'Py_NoSiteFlag': 1,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200415 'Py_NoUserSiteDirectory': 1,
Victor Stinner35c28d52018-11-14 02:01:52 +0100416 'Py_OptimizeFlag': 2,
417 'Py_QuietFlag': 1,
418 'Py_VerboseFlag': 1,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200419 'Py_FrozenFlag': 1,
Victor Stinner35c28d52018-11-14 02:01:52 +0100420 'Py_UnbufferedStdioFlag': 1,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200421 }
Victor Stinner35c28d52018-11-14 02:01:52 +0100422 self.check_config("init_global_config", core_config, global_config)
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200423
424 def test_init_from_config(self):
Victor Stinner35c28d52018-11-14 02:01:52 +0100425 core_config = {
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200426 'install_signal_handlers': 0,
427 'use_hash_seed': 1,
428 'hash_seed': 123,
429 'allocator': 'malloc_debug',
430 'tracemalloc': 2,
431 'import_time': 1,
432 'show_ref_count': 1,
433 'show_alloc_count': 1,
434 'malloc_stats': 1,
435
436 'utf8_mode': 1,
437
438 'program_name': './conf_program_name',
439 'program': 'conf_program',
440
441 'faulthandler': 1,
442 }
Victor Stinner35c28d52018-11-14 02:01:52 +0100443 global_config = {
444 'Py_HasFileSystemDefaultEncoding': 1,
445 'Py_NoUserSiteDirectory': 0,
446 }
447 self.check_config("init_from_config", core_config, global_config)
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200448
449 def test_init_env(self):
Victor Stinner35c28d52018-11-14 02:01:52 +0100450 core_config = {
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200451 'use_hash_seed': 1,
452 'hash_seed': 42,
453 'allocator': 'malloc_debug',
454 'tracemalloc': 2,
455 'import_time': 1,
456 'malloc_stats': 1,
457 'utf8_mode': 1,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200458 'faulthandler': 1,
459 'dev_mode': 1,
460 }
Victor Stinner35c28d52018-11-14 02:01:52 +0100461 global_config = {
462 'Py_DontWriteBytecodeFlag': 1,
463 'Py_HasFileSystemDefaultEncoding': 1,
464 'Py_InspectFlag': 1,
465 'Py_NoUserSiteDirectory': 1,
466 'Py_OptimizeFlag': 2,
467 'Py_UnbufferedStdioFlag': 1,
468 'Py_VerboseFlag': 1,
469 'Py_FileSystemDefaultEncoding': 'utf-8',
470 'Py_FileSystemDefaultEncodeErrors': self.UTF8_MODE_ERRORS,
471 }
472 self.check_config("init_env", core_config, global_config)
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200473
474 def test_init_dev_mode(self):
Victor Stinner35c28d52018-11-14 02:01:52 +0100475 core_config = {
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200476 'dev_mode': 1,
477 'faulthandler': 1,
478 'allocator': 'debug',
479 }
Victor Stinner35c28d52018-11-14 02:01:52 +0100480 self.check_config("init_dev_mode", core_config, {})
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200481
482 def test_init_isolated(self):
Victor Stinner35c28d52018-11-14 02:01:52 +0100483 core_config = {
484 'ignore_environment': 1,
485 }
486 global_config = {
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200487 'Py_IsolatedFlag': 1,
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200488 'Py_NoUserSiteDirectory': 1,
489 }
Victor Stinner35c28d52018-11-14 02:01:52 +0100490 self.check_config("init_isolated", core_config, global_config)
Victor Stinner0c90d6f2018-08-05 12:31:59 +0200491
492
Nick Coghlan39f0bb52017-11-28 08:11:51 +1000493if __name__ == "__main__":
494 unittest.main()