• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Run the tests in Programs/_testembed.c (tests for the CPython embedding APIs)
2from test import support
3from test.support import import_helper
4from test.support import os_helper
5import unittest
6
7from collections import namedtuple
8import contextlib
9import json
10import os
11import re
12import shutil
13import subprocess
14import sys
15import tempfile
16import textwrap
17
18
19MS_WINDOWS = (os.name == 'nt')
20MACOS = (sys.platform == 'darwin')
21
22PYMEM_ALLOCATOR_NOT_SET = 0
23PYMEM_ALLOCATOR_DEBUG = 2
24PYMEM_ALLOCATOR_MALLOC = 3
25
26# _PyCoreConfig_InitCompatConfig()
27API_COMPAT = 1
28# _PyCoreConfig_InitPythonConfig()
29API_PYTHON = 2
30# _PyCoreConfig_InitIsolatedConfig()
31API_ISOLATED = 3
32
33INIT_LOOPS = 16
34MAX_HASH_SEED = 4294967295
35
36
37def debug_build(program):
38    program = os.path.basename(program)
39    name = os.path.splitext(program)[0]
40    return name.casefold().endswith("_d".casefold())
41
42
43def remove_python_envvars():
44    env = dict(os.environ)
45    # Remove PYTHON* environment variables to get deterministic environment
46    for key in list(env):
47        if key.startswith('PYTHON'):
48            del env[key]
49    return env
50
51
52class EmbeddingTestsMixin:
53    def setUp(self):
54        here = os.path.abspath(__file__)
55        basepath = os.path.dirname(os.path.dirname(os.path.dirname(here)))
56        exename = "_testembed"
57        if MS_WINDOWS:
58            ext = ("_d" if debug_build(sys.executable) else "") + ".exe"
59            exename += ext
60            exepath = os.path.dirname(sys.executable)
61        else:
62            exepath = os.path.join(basepath, "Programs")
63        self.test_exe = exe = os.path.join(exepath, exename)
64        if not os.path.exists(exe):
65            self.skipTest("%r doesn't exist" % exe)
66        # This is needed otherwise we get a fatal error:
67        # "Py_Initialize: Unable to get the locale encoding
68        # LookupError: no codec search functions registered: can't find encoding"
69        self.oldcwd = os.getcwd()
70        os.chdir(basepath)
71
72    def tearDown(self):
73        os.chdir(self.oldcwd)
74
75    def run_embedded_interpreter(self, *args, env=None,
76                                 timeout=None, returncode=0, input=None,
77                                 cwd=None):
78        """Runs a test in the embedded interpreter"""
79        cmd = [self.test_exe]
80        cmd.extend(args)
81        if env is not None and MS_WINDOWS:
82            # Windows requires at least the SYSTEMROOT environment variable to
83            # start Python.
84            env = env.copy()
85            env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
86
87        p = subprocess.Popen(cmd,
88                             stdout=subprocess.PIPE,
89                             stderr=subprocess.PIPE,
90                             universal_newlines=True,
91                             env=env,
92                             cwd=cwd)
93        try:
94            (out, err) = p.communicate(input=input, timeout=timeout)
95        except:
96            p.terminate()
97            p.wait()
98            raise
99        if p.returncode != returncode and support.verbose:
100            print(f"--- {cmd} failed ---")
101            print(f"stdout:\n{out}")
102            print(f"stderr:\n{err}")
103            print(f"------")
104
105        self.assertEqual(p.returncode, returncode,
106                         "bad returncode %d, stderr is %r" %
107                         (p.returncode, err))
108        return out, err
109
110    def run_repeated_init_and_subinterpreters(self):
111        out, err = self.run_embedded_interpreter("test_repeated_init_and_subinterpreters")
112        self.assertEqual(err, "")
113
114        # The output from _testembed looks like this:
115        # --- Pass 1 ---
116        # interp 0 <0x1cf9330>, thread state <0x1cf9700>: id(modules) = 139650431942728
117        # interp 1 <0x1d4f690>, thread state <0x1d35350>: id(modules) = 139650431165784
118        # interp 2 <0x1d5a690>, thread state <0x1d99ed0>: id(modules) = 139650413140368
119        # interp 3 <0x1d4f690>, thread state <0x1dc3340>: id(modules) = 139650412862200
120        # interp 0 <0x1cf9330>, thread state <0x1cf9700>: id(modules) = 139650431942728
121        # --- Pass 2 ---
122        # ...
123
124        interp_pat = (r"^interp (\d+) <(0x[\dA-F]+)>, "
125                      r"thread state <(0x[\dA-F]+)>: "
126                      r"id\(modules\) = ([\d]+)$")
127        Interp = namedtuple("Interp", "id interp tstate modules")
128
129        numloops = 1
130        current_run = []
131        for line in out.splitlines():
132            if line == "--- Pass {} ---".format(numloops):
133                self.assertEqual(len(current_run), 0)
134                if support.verbose > 1:
135                    print(line)
136                numloops += 1
137                continue
138
139            self.assertLess(len(current_run), 5)
140            match = re.match(interp_pat, line)
141            if match is None:
142                self.assertRegex(line, interp_pat)
143
144            # Parse the line from the loop.  The first line is the main
145            # interpreter and the 3 afterward are subinterpreters.
146            interp = Interp(*match.groups())
147            if support.verbose > 1:
148                print(interp)
149            self.assertTrue(interp.interp)
150            self.assertTrue(interp.tstate)
151            self.assertTrue(interp.modules)
152            current_run.append(interp)
153
154            # The last line in the loop should be the same as the first.
155            if len(current_run) == 5:
156                main = current_run[0]
157                self.assertEqual(interp, main)
158                yield current_run
159                current_run = []
160
161
162class EmbeddingTests(EmbeddingTestsMixin, unittest.TestCase):
163    maxDiff = 100 * 50
164
165    def test_subinterps_main(self):
166        for run in self.run_repeated_init_and_subinterpreters():
167            main = run[0]
168
169            self.assertEqual(main.id, '0')
170
171    def test_subinterps_different_ids(self):
172        for run in self.run_repeated_init_and_subinterpreters():
173            main, *subs, _ = run
174
175            mainid = int(main.id)
176            for i, sub in enumerate(subs):
177                self.assertEqual(sub.id, str(mainid + i + 1))
178
179    def test_subinterps_distinct_state(self):
180        for run in self.run_repeated_init_and_subinterpreters():
181            main, *subs, _ = run
182
183            if '0x0' in main:
184                # XXX Fix on Windows (and other platforms): something
185                # is going on with the pointers in Programs/_testembed.c.
186                # interp.interp is 0x0 and interp.modules is the same
187                # between interpreters.
188                raise unittest.SkipTest('platform prints pointers as 0x0')
189
190            for sub in subs:
191                # A new subinterpreter may have the same
192                # PyInterpreterState pointer as a previous one if
193                # the earlier one has already been destroyed.  So
194                # we compare with the main interpreter.  The same
195                # applies to tstate.
196                self.assertNotEqual(sub.interp, main.interp)
197                self.assertNotEqual(sub.tstate, main.tstate)
198                self.assertNotEqual(sub.modules, main.modules)
199
200    def test_repeated_init_and_inittab(self):
201        out, err = self.run_embedded_interpreter("test_repeated_init_and_inittab")
202        self.assertEqual(err, "")
203
204        lines = [f"--- Pass {i} ---" for i in range(1, INIT_LOOPS+1)]
205        lines = "\n".join(lines) + "\n"
206        self.assertEqual(out, lines)
207
208    def test_forced_io_encoding(self):
209        # Checks forced configuration of embedded interpreter IO streams
210        env = dict(os.environ, PYTHONIOENCODING="utf-8:surrogateescape")
211        out, err = self.run_embedded_interpreter("test_forced_io_encoding", env=env)
212        if support.verbose > 1:
213            print()
214            print(out)
215            print(err)
216        expected_stream_encoding = "utf-8"
217        expected_errors = "surrogateescape"
218        expected_output = '\n'.join([
219        "--- Use defaults ---",
220        "Expected encoding: default",
221        "Expected errors: default",
222        "stdin: {in_encoding}:{errors}",
223        "stdout: {out_encoding}:{errors}",
224        "stderr: {out_encoding}:backslashreplace",
225        "--- Set errors only ---",
226        "Expected encoding: default",
227        "Expected errors: ignore",
228        "stdin: {in_encoding}:ignore",
229        "stdout: {out_encoding}:ignore",
230        "stderr: {out_encoding}:backslashreplace",
231        "--- Set encoding only ---",
232        "Expected encoding: iso8859-1",
233        "Expected errors: default",
234        "stdin: iso8859-1:{errors}",
235        "stdout: iso8859-1:{errors}",
236        "stderr: iso8859-1:backslashreplace",
237        "--- Set encoding and errors ---",
238        "Expected encoding: iso8859-1",
239        "Expected errors: replace",
240        "stdin: iso8859-1:replace",
241        "stdout: iso8859-1:replace",
242        "stderr: iso8859-1:backslashreplace"])
243        expected_output = expected_output.format(
244                                in_encoding=expected_stream_encoding,
245                                out_encoding=expected_stream_encoding,
246                                errors=expected_errors)
247        # This is useful if we ever trip over odd platform behaviour
248        self.maxDiff = None
249        self.assertEqual(out.strip(), expected_output)
250
251    def test_pre_initialization_api(self):
252        """
253        Checks some key parts of the C-API that need to work before the runtime
254        is initialized (via Py_Initialize()).
255        """
256        env = dict(os.environ, PYTHONPATH=os.pathsep.join(sys.path))
257        out, err = self.run_embedded_interpreter("test_pre_initialization_api", env=env)
258        if MS_WINDOWS:
259            expected_path = self.test_exe
260        else:
261            expected_path = os.path.join(os.getcwd(), "spam")
262        expected_output = f"sys.executable: {expected_path}\n"
263        self.assertIn(expected_output, out)
264        self.assertEqual(err, '')
265
266    def test_pre_initialization_sys_options(self):
267        """
268        Checks that sys.warnoptions and sys._xoptions can be set before the
269        runtime is initialized (otherwise they won't be effective).
270        """
271        env = remove_python_envvars()
272        env['PYTHONPATH'] = os.pathsep.join(sys.path)
273        out, err = self.run_embedded_interpreter(
274                        "test_pre_initialization_sys_options", env=env)
275        expected_output = (
276            "sys.warnoptions: ['once', 'module', 'default']\n"
277            "sys._xoptions: {'not_an_option': '1', 'also_not_an_option': '2'}\n"
278            "warnings.filters[:3]: ['default', 'module', 'once']\n"
279        )
280        self.assertIn(expected_output, out)
281        self.assertEqual(err, '')
282
283    def test_bpo20891(self):
284        """
285        bpo-20891: Calling PyGILState_Ensure in a non-Python thread must not
286        crash.
287        """
288        out, err = self.run_embedded_interpreter("test_bpo20891")
289        self.assertEqual(out, '')
290        self.assertEqual(err, '')
291
292    def test_initialize_twice(self):
293        """
294        bpo-33932: Calling Py_Initialize() twice should do nothing (and not
295        crash!).
296        """
297        out, err = self.run_embedded_interpreter("test_initialize_twice")
298        self.assertEqual(out, '')
299        self.assertEqual(err, '')
300
301    def test_initialize_pymain(self):
302        """
303        bpo-34008: Calling Py_Main() after Py_Initialize() must not fail.
304        """
305        out, err = self.run_embedded_interpreter("test_initialize_pymain")
306        self.assertEqual(out.rstrip(), "Py_Main() after Py_Initialize: sys.argv=['-c', 'arg2']")
307        self.assertEqual(err, '')
308
309    def test_run_main(self):
310        out, err = self.run_embedded_interpreter("test_run_main")
311        self.assertEqual(out.rstrip(), "Py_RunMain(): sys.argv=['-c', 'arg2']")
312        self.assertEqual(err, '')
313
314    def test_run_main_loop(self):
315        # bpo-40413: Calling Py_InitializeFromConfig()+Py_RunMain() multiple
316        # times must not crash.
317        nloop = 5
318        out, err = self.run_embedded_interpreter("test_run_main_loop")
319        self.assertEqual(out, "Py_RunMain(): sys.argv=['-c', 'arg2']\n" * nloop)
320        self.assertEqual(err, '')
321
322
323class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
324    maxDiff = 4096
325    UTF8_MODE_ERRORS = ('surrogatepass' if MS_WINDOWS else 'surrogateescape')
326
327    # Marker to read the default configuration: get_default_config()
328    GET_DEFAULT_CONFIG = object()
329
330    # Marker to ignore a configuration parameter
331    IGNORE_CONFIG = object()
332
333    PRE_CONFIG_COMPAT = {
334        '_config_init': API_COMPAT,
335        'allocator': PYMEM_ALLOCATOR_NOT_SET,
336        'parse_argv': 0,
337        'configure_locale': 1,
338        'coerce_c_locale': 0,
339        'coerce_c_locale_warn': 0,
340        'utf8_mode': 0,
341    }
342    if MS_WINDOWS:
343        PRE_CONFIG_COMPAT.update({
344            'legacy_windows_fs_encoding': 0,
345        })
346    PRE_CONFIG_PYTHON = dict(PRE_CONFIG_COMPAT,
347        _config_init=API_PYTHON,
348        parse_argv=1,
349        coerce_c_locale=GET_DEFAULT_CONFIG,
350        utf8_mode=GET_DEFAULT_CONFIG,
351    )
352    PRE_CONFIG_ISOLATED = dict(PRE_CONFIG_COMPAT,
353        _config_init=API_ISOLATED,
354        configure_locale=0,
355        isolated=1,
356        use_environment=0,
357        utf8_mode=0,
358        dev_mode=0,
359        coerce_c_locale=0,
360    )
361
362    COPY_PRE_CONFIG = [
363        'dev_mode',
364        'isolated',
365        'use_environment',
366    ]
367
368    CONFIG_COMPAT = {
369        '_config_init': API_COMPAT,
370        'isolated': 0,
371        'use_environment': 1,
372        'dev_mode': 0,
373
374        'install_signal_handlers': 1,
375        'use_hash_seed': 0,
376        'hash_seed': 0,
377        'faulthandler': 0,
378        'tracemalloc': 0,
379        'import_time': 0,
380        'show_ref_count': 0,
381        'dump_refs': 0,
382        'malloc_stats': 0,
383
384        'filesystem_encoding': GET_DEFAULT_CONFIG,
385        'filesystem_errors': GET_DEFAULT_CONFIG,
386
387        'pycache_prefix': None,
388        'program_name': GET_DEFAULT_CONFIG,
389        'parse_argv': 0,
390        'argv': [""],
391        'orig_argv': [],
392
393        'xoptions': [],
394        'warnoptions': [],
395
396        'pythonpath_env': None,
397        'home': None,
398        'executable': GET_DEFAULT_CONFIG,
399        'base_executable': GET_DEFAULT_CONFIG,
400
401        'prefix': GET_DEFAULT_CONFIG,
402        'base_prefix': GET_DEFAULT_CONFIG,
403        'exec_prefix': GET_DEFAULT_CONFIG,
404        'base_exec_prefix': GET_DEFAULT_CONFIG,
405        'module_search_paths': GET_DEFAULT_CONFIG,
406        'module_search_paths_set': 1,
407        'platlibdir': sys.platlibdir,
408
409        'site_import': 1,
410        'bytes_warning': 0,
411        'warn_default_encoding': 0,
412        'inspect': 0,
413        'interactive': 0,
414        'optimization_level': 0,
415        'parser_debug': 0,
416        'write_bytecode': 1,
417        'verbose': 0,
418        'quiet': 0,
419        'user_site_directory': 1,
420        'configure_c_stdio': 0,
421        'buffered_stdio': 1,
422
423        'stdio_encoding': GET_DEFAULT_CONFIG,
424        'stdio_errors': GET_DEFAULT_CONFIG,
425
426        'skip_source_first_line': 0,
427        'run_command': None,
428        'run_module': None,
429        'run_filename': None,
430
431        '_install_importlib': 1,
432        'check_hash_pycs_mode': 'default',
433        'pathconfig_warnings': 1,
434        '_init_main': 1,
435        '_isolated_interpreter': 0,
436    }
437    if MS_WINDOWS:
438        CONFIG_COMPAT.update({
439            'legacy_windows_stdio': 0,
440        })
441
442    CONFIG_PYTHON = dict(CONFIG_COMPAT,
443        _config_init=API_PYTHON,
444        configure_c_stdio=1,
445        parse_argv=2,
446    )
447    CONFIG_ISOLATED = dict(CONFIG_COMPAT,
448        _config_init=API_ISOLATED,
449        isolated=1,
450        use_environment=0,
451        user_site_directory=0,
452        dev_mode=0,
453        install_signal_handlers=0,
454        use_hash_seed=0,
455        faulthandler=0,
456        tracemalloc=0,
457        pathconfig_warnings=0,
458    )
459    if MS_WINDOWS:
460        CONFIG_ISOLATED['legacy_windows_stdio'] = 0
461
462    # global config
463    DEFAULT_GLOBAL_CONFIG = {
464        'Py_HasFileSystemDefaultEncoding': 0,
465        'Py_HashRandomizationFlag': 1,
466        '_Py_HasFileSystemDefaultEncodeErrors': 0,
467    }
468    COPY_GLOBAL_PRE_CONFIG = [
469        ('Py_UTF8Mode', 'utf8_mode'),
470    ]
471    COPY_GLOBAL_CONFIG = [
472        # Copy core config to global config for expected values
473        # True means that the core config value is inverted (0 => 1 and 1 => 0)
474        ('Py_BytesWarningFlag', 'bytes_warning'),
475        ('Py_DebugFlag', 'parser_debug'),
476        ('Py_DontWriteBytecodeFlag', 'write_bytecode', True),
477        ('Py_FileSystemDefaultEncodeErrors', 'filesystem_errors'),
478        ('Py_FileSystemDefaultEncoding', 'filesystem_encoding'),
479        ('Py_FrozenFlag', 'pathconfig_warnings', True),
480        ('Py_IgnoreEnvironmentFlag', 'use_environment', True),
481        ('Py_InspectFlag', 'inspect'),
482        ('Py_InteractiveFlag', 'interactive'),
483        ('Py_IsolatedFlag', 'isolated'),
484        ('Py_NoSiteFlag', 'site_import', True),
485        ('Py_NoUserSiteDirectory', 'user_site_directory', True),
486        ('Py_OptimizeFlag', 'optimization_level'),
487        ('Py_QuietFlag', 'quiet'),
488        ('Py_UnbufferedStdioFlag', 'buffered_stdio', True),
489        ('Py_VerboseFlag', 'verbose'),
490    ]
491    if MS_WINDOWS:
492        COPY_GLOBAL_PRE_CONFIG.extend((
493            ('Py_LegacyWindowsFSEncodingFlag', 'legacy_windows_fs_encoding'),
494        ))
495        COPY_GLOBAL_CONFIG.extend((
496            ('Py_LegacyWindowsStdioFlag', 'legacy_windows_stdio'),
497        ))
498
499    # path config
500    if MS_WINDOWS:
501        PATH_CONFIG = {
502            'isolated': -1,
503            'site_import': -1,
504            'python3_dll': GET_DEFAULT_CONFIG,
505        }
506    else:
507        PATH_CONFIG = {}
508    # other keys are copied by COPY_PATH_CONFIG
509
510    COPY_PATH_CONFIG = [
511        # Copy core config to global config for expected values
512        'prefix',
513        'exec_prefix',
514        'program_name',
515        'home',
516        # program_full_path and module_search_path are copied indirectly from
517        # the core configuration in check_path_config().
518    ]
519    if MS_WINDOWS:
520        COPY_PATH_CONFIG.extend((
521            'base_executable',
522        ))
523
524    EXPECTED_CONFIG = None
525
526    @classmethod
527    def tearDownClass(cls):
528        # clear cache
529        cls.EXPECTED_CONFIG = None
530
531    def main_xoptions(self, xoptions_list):
532        xoptions = {}
533        for opt in xoptions_list:
534            if '=' in opt:
535                key, value = opt.split('=', 1)
536                xoptions[key] = value
537            else:
538                xoptions[opt] = True
539        return xoptions
540
541    def _get_expected_config_impl(self):
542        env = remove_python_envvars()
543        code = textwrap.dedent('''
544            import json
545            import sys
546            import _testinternalcapi
547
548            configs = _testinternalcapi.get_configs()
549
550            data = json.dumps(configs)
551            data = data.encode('utf-8')
552            sys.stdout.buffer.write(data)
553            sys.stdout.buffer.flush()
554        ''')
555
556        # Use -S to not import the site module: get the proper configuration
557        # when test_embed is run from a venv (bpo-35313)
558        args = [sys.executable, '-S', '-c', code]
559        proc = subprocess.run(args, env=env,
560                              stdout=subprocess.PIPE,
561                              stderr=subprocess.PIPE)
562        if proc.returncode:
563            raise Exception(f"failed to get the default config: "
564                            f"stdout={proc.stdout!r} stderr={proc.stderr!r}")
565        stdout = proc.stdout.decode('utf-8')
566        # ignore stderr
567        try:
568            return json.loads(stdout)
569        except json.JSONDecodeError:
570            self.fail(f"fail to decode stdout: {stdout!r}")
571
572    def _get_expected_config(self):
573        cls = InitConfigTests
574        if cls.EXPECTED_CONFIG is None:
575            cls.EXPECTED_CONFIG = self._get_expected_config_impl()
576
577        # get a copy
578        configs = {}
579        for config_key, config_value in cls.EXPECTED_CONFIG.items():
580            config = {}
581            for key, value in config_value.items():
582                if isinstance(value, list):
583                    value = value.copy()
584                config[key] = value
585            configs[config_key] = config
586        return configs
587
588    def get_expected_config(self, expected_preconfig, expected,
589                            expected_pathconfig, env, api,
590                            modify_path_cb=None):
591        configs = self._get_expected_config()
592
593        pre_config = configs['pre_config']
594        for key, value in expected_preconfig.items():
595            if value is self.GET_DEFAULT_CONFIG:
596                expected_preconfig[key] = pre_config[key]
597
598        path_config = configs['path_config']
599        for key, value in expected_pathconfig.items():
600            if value is self.GET_DEFAULT_CONFIG:
601                expected_pathconfig[key] = path_config[key]
602
603        if not expected_preconfig['configure_locale'] or api == API_COMPAT:
604            # there is no easy way to get the locale encoding before
605            # setlocale(LC_CTYPE, "") is called: don't test encodings
606            for key in ('filesystem_encoding', 'filesystem_errors',
607                        'stdio_encoding', 'stdio_errors'):
608                expected[key] = self.IGNORE_CONFIG
609
610        if not expected_preconfig['configure_locale']:
611            # UTF-8 Mode depends on the locale. There is no easy way
612            # to guess if UTF-8 Mode will be enabled or not if the locale
613            # is not configured.
614            expected_preconfig['utf8_mode'] = self.IGNORE_CONFIG
615
616        if expected_preconfig['utf8_mode'] == 1:
617            if expected['filesystem_encoding'] is self.GET_DEFAULT_CONFIG:
618                expected['filesystem_encoding'] = 'utf-8'
619            if expected['filesystem_errors'] is self.GET_DEFAULT_CONFIG:
620                expected['filesystem_errors'] = self.UTF8_MODE_ERRORS
621            if expected['stdio_encoding'] is self.GET_DEFAULT_CONFIG:
622                expected['stdio_encoding'] = 'utf-8'
623            if expected['stdio_errors'] is self.GET_DEFAULT_CONFIG:
624                expected['stdio_errors'] = 'surrogateescape'
625
626        if MS_WINDOWS:
627            default_executable = self.test_exe
628        elif expected['program_name'] is not self.GET_DEFAULT_CONFIG:
629            default_executable = os.path.abspath(expected['program_name'])
630        else:
631            default_executable = os.path.join(os.getcwd(), '_testembed')
632        if expected['executable'] is self.GET_DEFAULT_CONFIG:
633            expected['executable'] = default_executable
634        if expected['base_executable'] is self.GET_DEFAULT_CONFIG:
635            expected['base_executable'] = default_executable
636        if expected['program_name'] is self.GET_DEFAULT_CONFIG:
637            expected['program_name'] = './_testembed'
638
639        config = configs['config']
640        for key, value in expected.items():
641            if value is self.GET_DEFAULT_CONFIG:
642                expected[key] = config[key]
643
644        if expected['module_search_paths'] is not self.IGNORE_CONFIG:
645            pythonpath_env = expected['pythonpath_env']
646            if pythonpath_env is not None:
647                paths = pythonpath_env.split(os.path.pathsep)
648                expected['module_search_paths'] = [*paths, *expected['module_search_paths']]
649            if modify_path_cb is not None:
650                expected['module_search_paths'] = expected['module_search_paths'].copy()
651                modify_path_cb(expected['module_search_paths'])
652
653        for key in self.COPY_PRE_CONFIG:
654            if key not in expected_preconfig:
655                expected_preconfig[key] = expected[key]
656
657    def check_pre_config(self, configs, expected):
658        pre_config = dict(configs['pre_config'])
659        for key, value in list(expected.items()):
660            if value is self.IGNORE_CONFIG:
661                pre_config.pop(key, None)
662                del expected[key]
663        self.assertEqual(pre_config, expected)
664
665    def check_config(self, configs, expected):
666        config = dict(configs['config'])
667        for key, value in list(expected.items()):
668            if value is self.IGNORE_CONFIG:
669                config.pop(key, None)
670                del expected[key]
671        self.assertEqual(config, expected)
672
673    def check_global_config(self, configs):
674        pre_config = configs['pre_config']
675        config = configs['config']
676
677        expected = dict(self.DEFAULT_GLOBAL_CONFIG)
678        for item in self.COPY_GLOBAL_CONFIG:
679            if len(item) == 3:
680                global_key, core_key, opposite = item
681                expected[global_key] = 0 if config[core_key] else 1
682            else:
683                global_key, core_key = item
684                expected[global_key] = config[core_key]
685        for item in self.COPY_GLOBAL_PRE_CONFIG:
686            if len(item) == 3:
687                global_key, core_key, opposite = item
688                expected[global_key] = 0 if pre_config[core_key] else 1
689            else:
690                global_key, core_key = item
691                expected[global_key] = pre_config[core_key]
692
693        self.assertEqual(configs['global_config'], expected)
694
695    def check_path_config(self, configs, expected):
696        config = configs['config']
697
698        for key in self.COPY_PATH_CONFIG:
699            expected[key] = config[key]
700        expected['module_search_path'] = os.path.pathsep.join(config['module_search_paths'])
701        expected['program_full_path'] = config['executable']
702
703        self.assertEqual(configs['path_config'], expected)
704
705    def check_all_configs(self, testname, expected_config=None,
706                          expected_preconfig=None, expected_pathconfig=None,
707                          modify_path_cb=None,
708                          stderr=None, *, api, preconfig_api=None,
709                          env=None, ignore_stderr=False, cwd=None):
710        new_env = remove_python_envvars()
711        if env is not None:
712            new_env.update(env)
713        env = new_env
714
715        if preconfig_api is None:
716            preconfig_api = api
717        if preconfig_api == API_ISOLATED:
718            default_preconfig = self.PRE_CONFIG_ISOLATED
719        elif preconfig_api == API_PYTHON:
720            default_preconfig = self.PRE_CONFIG_PYTHON
721        else:
722            default_preconfig = self.PRE_CONFIG_COMPAT
723        if expected_preconfig is None:
724            expected_preconfig = {}
725        expected_preconfig = dict(default_preconfig, **expected_preconfig)
726
727        if expected_config is None:
728            expected_config = {}
729
730        if expected_pathconfig is None:
731            expected_pathconfig = {}
732        expected_pathconfig = dict(self.PATH_CONFIG, **expected_pathconfig)
733
734        if api == API_PYTHON:
735            default_config = self.CONFIG_PYTHON
736        elif api == API_ISOLATED:
737            default_config = self.CONFIG_ISOLATED
738        else:
739            default_config = self.CONFIG_COMPAT
740        expected_config = dict(default_config, **expected_config)
741
742        self.get_expected_config(expected_preconfig,
743                                 expected_config,
744                                 expected_pathconfig,
745                                 env,
746                                 api, modify_path_cb)
747
748        out, err = self.run_embedded_interpreter(testname,
749                                                 env=env, cwd=cwd)
750        if stderr is None and not expected_config['verbose']:
751            stderr = ""
752        if stderr is not None and not ignore_stderr:
753            self.assertEqual(err.rstrip(), stderr)
754        try:
755            configs = json.loads(out)
756        except json.JSONDecodeError:
757            self.fail(f"fail to decode stdout: {out!r}")
758
759        self.check_pre_config(configs, expected_preconfig)
760        self.check_config(configs, expected_config)
761        self.check_global_config(configs)
762        self.check_path_config(configs, expected_pathconfig)
763        return configs
764
765    def test_init_default_config(self):
766        self.check_all_configs("test_init_initialize_config", api=API_COMPAT)
767
768    def test_preinit_compat_config(self):
769        self.check_all_configs("test_preinit_compat_config", api=API_COMPAT)
770
771    def test_init_compat_config(self):
772        self.check_all_configs("test_init_compat_config", api=API_COMPAT)
773
774    def test_init_global_config(self):
775        preconfig = {
776            'utf8_mode': 1,
777        }
778        config = {
779            'program_name': './globalvar',
780            'site_import': 0,
781            'bytes_warning': 1,
782            'warnoptions': ['default::BytesWarning'],
783            'inspect': 1,
784            'interactive': 1,
785            'optimization_level': 2,
786            'write_bytecode': 0,
787            'verbose': 1,
788            'quiet': 1,
789            'buffered_stdio': 0,
790
791            'user_site_directory': 0,
792            'pathconfig_warnings': 0,
793        }
794        self.check_all_configs("test_init_global_config", config, preconfig,
795                               api=API_COMPAT)
796
797    def test_init_from_config(self):
798        preconfig = {
799            'allocator': PYMEM_ALLOCATOR_MALLOC,
800            'utf8_mode': 1,
801        }
802        config = {
803            'install_signal_handlers': 0,
804            'use_hash_seed': 1,
805            'hash_seed': 123,
806            'tracemalloc': 2,
807            'import_time': 1,
808            'show_ref_count': 1,
809            'malloc_stats': 1,
810
811            'stdio_encoding': 'iso8859-1',
812            'stdio_errors': 'replace',
813
814            'pycache_prefix': 'conf_pycache_prefix',
815            'program_name': './conf_program_name',
816            'argv': ['-c', 'arg2'],
817            'orig_argv': ['python3',
818                          '-W', 'cmdline_warnoption',
819                          '-X', 'cmdline_xoption',
820                          '-c', 'pass',
821                          'arg2'],
822            'parse_argv': 2,
823            'xoptions': [
824                'config_xoption1=3',
825                'config_xoption2=',
826                'config_xoption3',
827                'cmdline_xoption',
828            ],
829            'warnoptions': [
830                'cmdline_warnoption',
831                'default::BytesWarning',
832                'config_warnoption',
833            ],
834            'run_command': 'pass\n',
835
836            'site_import': 0,
837            'bytes_warning': 1,
838            'inspect': 1,
839            'interactive': 1,
840            'optimization_level': 2,
841            'write_bytecode': 0,
842            'verbose': 1,
843            'quiet': 1,
844            'configure_c_stdio': 1,
845            'buffered_stdio': 0,
846            'user_site_directory': 0,
847            'faulthandler': 1,
848            'platlibdir': 'my_platlibdir',
849            'module_search_paths': self.IGNORE_CONFIG,
850
851            'check_hash_pycs_mode': 'always',
852            'pathconfig_warnings': 0,
853
854            '_isolated_interpreter': 1,
855        }
856        self.check_all_configs("test_init_from_config", config, preconfig,
857                               api=API_COMPAT)
858
859    def test_init_compat_env(self):
860        preconfig = {
861            'allocator': PYMEM_ALLOCATOR_MALLOC,
862        }
863        config = {
864            'use_hash_seed': 1,
865            'hash_seed': 42,
866            'tracemalloc': 2,
867            'import_time': 1,
868            'malloc_stats': 1,
869            'inspect': 1,
870            'optimization_level': 2,
871            'pythonpath_env': '/my/path',
872            'pycache_prefix': 'env_pycache_prefix',
873            'write_bytecode': 0,
874            'verbose': 1,
875            'buffered_stdio': 0,
876            'stdio_encoding': 'iso8859-1',
877            'stdio_errors': 'replace',
878            'user_site_directory': 0,
879            'faulthandler': 1,
880            'warnoptions': ['EnvVar'],
881            'platlibdir': 'env_platlibdir',
882            'module_search_paths': self.IGNORE_CONFIG,
883        }
884        self.check_all_configs("test_init_compat_env", config, preconfig,
885                               api=API_COMPAT)
886
887    def test_init_python_env(self):
888        preconfig = {
889            'allocator': PYMEM_ALLOCATOR_MALLOC,
890            'utf8_mode': 1,
891        }
892        config = {
893            'use_hash_seed': 1,
894            'hash_seed': 42,
895            'tracemalloc': 2,
896            'import_time': 1,
897            'malloc_stats': 1,
898            'inspect': 1,
899            'optimization_level': 2,
900            'pythonpath_env': '/my/path',
901            'pycache_prefix': 'env_pycache_prefix',
902            'write_bytecode': 0,
903            'verbose': 1,
904            'buffered_stdio': 0,
905            'stdio_encoding': 'iso8859-1',
906            'stdio_errors': 'replace',
907            'user_site_directory': 0,
908            'faulthandler': 1,
909            'warnoptions': ['EnvVar'],
910            'platlibdir': 'env_platlibdir',
911            'module_search_paths': self.IGNORE_CONFIG,
912        }
913        self.check_all_configs("test_init_python_env", config, preconfig,
914                               api=API_PYTHON)
915
916    def test_init_env_dev_mode(self):
917        preconfig = dict(allocator=PYMEM_ALLOCATOR_DEBUG)
918        config = dict(dev_mode=1,
919                      faulthandler=1,
920                      warnoptions=['default'])
921        self.check_all_configs("test_init_env_dev_mode", config, preconfig,
922                               api=API_COMPAT)
923
924    def test_init_env_dev_mode_alloc(self):
925        preconfig = dict(allocator=PYMEM_ALLOCATOR_MALLOC)
926        config = dict(dev_mode=1,
927                      faulthandler=1,
928                      warnoptions=['default'])
929        self.check_all_configs("test_init_env_dev_mode_alloc", config, preconfig,
930                               api=API_COMPAT)
931
932    def test_init_dev_mode(self):
933        preconfig = {
934            'allocator': PYMEM_ALLOCATOR_DEBUG,
935        }
936        config = {
937            'faulthandler': 1,
938            'dev_mode': 1,
939            'warnoptions': ['default'],
940        }
941        self.check_all_configs("test_init_dev_mode", config, preconfig,
942                               api=API_PYTHON)
943
944    def test_preinit_parse_argv(self):
945        # Pre-initialize implicitly using argv: make sure that -X dev
946        # is used to configure the allocation in preinitialization
947        preconfig = {
948            'allocator': PYMEM_ALLOCATOR_DEBUG,
949        }
950        config = {
951            'argv': ['script.py'],
952            'orig_argv': ['python3', '-X', 'dev', 'script.py'],
953            'run_filename': os.path.abspath('script.py'),
954            'dev_mode': 1,
955            'faulthandler': 1,
956            'warnoptions': ['default'],
957            'xoptions': ['dev'],
958        }
959        self.check_all_configs("test_preinit_parse_argv", config, preconfig,
960                               api=API_PYTHON)
961
962    def test_preinit_dont_parse_argv(self):
963        # -X dev must be ignored by isolated preconfiguration
964        preconfig = {
965            'isolated': 0,
966        }
967        argv = ["python3",
968               "-E", "-I",
969               "-X", "dev",
970               "-X", "utf8",
971               "script.py"]
972        config = {
973            'argv': argv,
974            'orig_argv': argv,
975            'isolated': 0,
976        }
977        self.check_all_configs("test_preinit_dont_parse_argv", config, preconfig,
978                               api=API_ISOLATED)
979
980    def test_init_isolated_flag(self):
981        config = {
982            'isolated': 1,
983            'use_environment': 0,
984            'user_site_directory': 0,
985        }
986        self.check_all_configs("test_init_isolated_flag", config, api=API_PYTHON)
987
988    def test_preinit_isolated1(self):
989        # _PyPreConfig.isolated=1, _PyCoreConfig.isolated not set
990        config = {
991            'isolated': 1,
992            'use_environment': 0,
993            'user_site_directory': 0,
994        }
995        self.check_all_configs("test_preinit_isolated1", config, api=API_COMPAT)
996
997    def test_preinit_isolated2(self):
998        # _PyPreConfig.isolated=0, _PyCoreConfig.isolated=1
999        config = {
1000            'isolated': 1,
1001            'use_environment': 0,
1002            'user_site_directory': 0,
1003        }
1004        self.check_all_configs("test_preinit_isolated2", config, api=API_COMPAT)
1005
1006    def test_preinit_isolated_config(self):
1007        self.check_all_configs("test_preinit_isolated_config", api=API_ISOLATED)
1008
1009    def test_init_isolated_config(self):
1010        self.check_all_configs("test_init_isolated_config", api=API_ISOLATED)
1011
1012    def test_preinit_python_config(self):
1013        self.check_all_configs("test_preinit_python_config", api=API_PYTHON)
1014
1015    def test_init_python_config(self):
1016        self.check_all_configs("test_init_python_config", api=API_PYTHON)
1017
1018    def test_init_dont_configure_locale(self):
1019        # _PyPreConfig.configure_locale=0
1020        preconfig = {
1021            'configure_locale': 0,
1022            'coerce_c_locale': 0,
1023        }
1024        self.check_all_configs("test_init_dont_configure_locale", {}, preconfig,
1025                               api=API_PYTHON)
1026
1027    def test_init_read_set(self):
1028        config = {
1029            'program_name': './init_read_set',
1030            'executable': 'my_executable',
1031        }
1032        def modify_path(path):
1033            path.insert(1, "test_path_insert1")
1034            path.append("test_path_append")
1035        self.check_all_configs("test_init_read_set", config,
1036                               api=API_PYTHON,
1037                               modify_path_cb=modify_path)
1038
1039    def test_init_sys_add(self):
1040        config = {
1041            'faulthandler': 1,
1042            'xoptions': [
1043                'config_xoption',
1044                'cmdline_xoption',
1045                'sysadd_xoption',
1046                'faulthandler',
1047            ],
1048            'warnoptions': [
1049                'ignore:::cmdline_warnoption',
1050                'ignore:::sysadd_warnoption',
1051                'ignore:::config_warnoption',
1052            ],
1053            'orig_argv': ['python3',
1054                          '-W', 'ignore:::cmdline_warnoption',
1055                          '-X', 'cmdline_xoption'],
1056        }
1057        self.check_all_configs("test_init_sys_add", config, api=API_PYTHON)
1058
1059    def test_init_run_main(self):
1060        code = ('import _testinternalcapi, json; '
1061                'print(json.dumps(_testinternalcapi.get_configs()))')
1062        config = {
1063            'argv': ['-c', 'arg2'],
1064            'orig_argv': ['python3', '-c', code, 'arg2'],
1065            'program_name': './python3',
1066            'run_command': code + '\n',
1067            'parse_argv': 2,
1068        }
1069        self.check_all_configs("test_init_run_main", config, api=API_PYTHON)
1070
1071    def test_init_main(self):
1072        code = ('import _testinternalcapi, json; '
1073                'print(json.dumps(_testinternalcapi.get_configs()))')
1074        config = {
1075            'argv': ['-c', 'arg2'],
1076            'orig_argv': ['python3',
1077                          '-c', code,
1078                          'arg2'],
1079            'program_name': './python3',
1080            'run_command': code + '\n',
1081            'parse_argv': 2,
1082            '_init_main': 0,
1083        }
1084        self.check_all_configs("test_init_main", config,
1085                               api=API_PYTHON,
1086                               stderr="Run Python code before _Py_InitializeMain")
1087
1088    def test_init_parse_argv(self):
1089        config = {
1090            'parse_argv': 2,
1091            'argv': ['-c', 'arg1', '-v', 'arg3'],
1092            'orig_argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'],
1093            'program_name': './argv0',
1094            'run_command': 'pass\n',
1095            'use_environment': 0,
1096        }
1097        self.check_all_configs("test_init_parse_argv", config, api=API_PYTHON)
1098
1099    def test_init_dont_parse_argv(self):
1100        pre_config = {
1101            'parse_argv': 0,
1102        }
1103        config = {
1104            'parse_argv': 0,
1105            'argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'],
1106            'orig_argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'],
1107            'program_name': './argv0',
1108        }
1109        self.check_all_configs("test_init_dont_parse_argv", config, pre_config,
1110                               api=API_PYTHON)
1111
1112    def default_program_name(self, config):
1113        if MS_WINDOWS:
1114            program_name = 'python'
1115            executable = self.test_exe
1116        else:
1117            program_name = 'python3'
1118            if MACOS:
1119                executable = self.test_exe
1120            else:
1121                executable = shutil.which(program_name) or ''
1122        config.update({
1123            'program_name': program_name,
1124            'base_executable': executable,
1125            'executable': executable,
1126        })
1127
1128    def test_init_setpath(self):
1129        # Test Py_SetPath()
1130        config = self._get_expected_config()
1131        paths = config['config']['module_search_paths']
1132
1133        config = {
1134            'module_search_paths': paths,
1135            'prefix': '',
1136            'base_prefix': '',
1137            'exec_prefix': '',
1138            'base_exec_prefix': '',
1139        }
1140        self.default_program_name(config)
1141        env = {'TESTPATH': os.path.pathsep.join(paths)}
1142
1143        self.check_all_configs("test_init_setpath", config,
1144                               api=API_COMPAT, env=env,
1145                               ignore_stderr=True)
1146
1147    def test_init_setpath_config(self):
1148        # Test Py_SetPath() with PyConfig
1149        config = self._get_expected_config()
1150        paths = config['config']['module_search_paths']
1151
1152        config = {
1153            # set by Py_SetPath()
1154            'module_search_paths': paths,
1155            'prefix': '',
1156            'base_prefix': '',
1157            'exec_prefix': '',
1158            'base_exec_prefix': '',
1159            # overridden by PyConfig
1160            'program_name': 'conf_program_name',
1161            'base_executable': 'conf_executable',
1162            'executable': 'conf_executable',
1163        }
1164        env = {'TESTPATH': os.path.pathsep.join(paths)}
1165        self.check_all_configs("test_init_setpath_config", config,
1166                               api=API_PYTHON, env=env, ignore_stderr=True)
1167
1168    def module_search_paths(self, prefix=None, exec_prefix=None):
1169        config = self._get_expected_config()
1170        if prefix is None:
1171            prefix = config['config']['prefix']
1172        if exec_prefix is None:
1173            exec_prefix = config['config']['prefix']
1174        if MS_WINDOWS:
1175            return config['config']['module_search_paths']
1176        else:
1177            ver = sys.version_info
1178            return [
1179                os.path.join(prefix, sys.platlibdir,
1180                             f'python{ver.major}{ver.minor}.zip'),
1181                os.path.join(prefix, sys.platlibdir,
1182                             f'python{ver.major}.{ver.minor}'),
1183                os.path.join(exec_prefix, sys.platlibdir,
1184                             f'python{ver.major}.{ver.minor}', 'lib-dynload'),
1185            ]
1186
1187    @contextlib.contextmanager
1188    def tmpdir_with_python(self):
1189        # Temporary directory with a copy of the Python program
1190        with tempfile.TemporaryDirectory() as tmpdir:
1191            # bpo-38234: On macOS and FreeBSD, the temporary directory
1192            # can be symbolic link. For example, /tmp can be a symbolic link
1193            # to /var/tmp. Call realpath() to resolve all symbolic links.
1194            tmpdir = os.path.realpath(tmpdir)
1195
1196            if MS_WINDOWS:
1197                # Copy pythonXY.dll (or pythonXY_d.dll)
1198                ver = sys.version_info
1199                dll = f'python{ver.major}{ver.minor}'
1200                dll3 = f'python{ver.major}'
1201                if debug_build(sys.executable):
1202                    dll += '_d'
1203                    dll3 += '_d'
1204                dll += '.dll'
1205                dll3 += '.dll'
1206                dll = os.path.join(os.path.dirname(self.test_exe), dll)
1207                dll3 = os.path.join(os.path.dirname(self.test_exe), dll3)
1208                dll_copy = os.path.join(tmpdir, os.path.basename(dll))
1209                dll3_copy = os.path.join(tmpdir, os.path.basename(dll3))
1210                shutil.copyfile(dll, dll_copy)
1211                shutil.copyfile(dll3, dll3_copy)
1212
1213            # Copy Python program
1214            exec_copy = os.path.join(tmpdir, os.path.basename(self.test_exe))
1215            shutil.copyfile(self.test_exe, exec_copy)
1216            shutil.copystat(self.test_exe, exec_copy)
1217            self.test_exe = exec_copy
1218
1219            yield tmpdir
1220
1221    def test_init_setpythonhome(self):
1222        # Test Py_SetPythonHome(home) with PYTHONPATH env var
1223        config = self._get_expected_config()
1224        paths = config['config']['module_search_paths']
1225        paths_str = os.path.pathsep.join(paths)
1226
1227        for path in paths:
1228            if not os.path.isdir(path):
1229                continue
1230            if os.path.exists(os.path.join(path, 'os.py')):
1231                home = os.path.dirname(path)
1232                break
1233        else:
1234            self.fail(f"Unable to find home in {paths!r}")
1235
1236        prefix = exec_prefix = home
1237        expected_paths = self.module_search_paths(prefix=home, exec_prefix=home)
1238
1239        config = {
1240            'home': home,
1241            'module_search_paths': expected_paths,
1242            'prefix': prefix,
1243            'base_prefix': prefix,
1244            'exec_prefix': exec_prefix,
1245            'base_exec_prefix': exec_prefix,
1246            'pythonpath_env': paths_str,
1247        }
1248        self.default_program_name(config)
1249        env = {'TESTHOME': home, 'PYTHONPATH': paths_str}
1250        self.check_all_configs("test_init_setpythonhome", config,
1251                               api=API_COMPAT, env=env)
1252
1253    def copy_paths_by_env(self, config):
1254        all_configs = self._get_expected_config()
1255        paths = all_configs['config']['module_search_paths']
1256        paths_str = os.path.pathsep.join(paths)
1257        config['pythonpath_env'] = paths_str
1258        env = {'PYTHONPATH': paths_str}
1259        return env
1260
1261    @unittest.skipIf(MS_WINDOWS, 'Windows does not use pybuilddir.txt')
1262    def test_init_pybuilddir(self):
1263        # Test path configuration with pybuilddir.txt configuration file
1264
1265        with self.tmpdir_with_python() as tmpdir:
1266            # pybuilddir.txt is a sub-directory relative to the current
1267            # directory (tmpdir)
1268            subdir = 'libdir'
1269            libdir = os.path.join(tmpdir, subdir)
1270            os.mkdir(libdir)
1271
1272            filename = os.path.join(tmpdir, 'pybuilddir.txt')
1273            with open(filename, "w", encoding="utf8") as fp:
1274                fp.write(subdir)
1275
1276            module_search_paths = self.module_search_paths()
1277            module_search_paths[-1] = libdir
1278
1279            executable = self.test_exe
1280            config = {
1281                'base_executable': executable,
1282                'executable': executable,
1283                'module_search_paths': module_search_paths,
1284            }
1285            env = self.copy_paths_by_env(config)
1286            self.check_all_configs("test_init_compat_config", config,
1287                                   api=API_COMPAT, env=env,
1288                                   ignore_stderr=True, cwd=tmpdir)
1289
1290    def test_init_pyvenv_cfg(self):
1291        # Test path configuration with pyvenv.cfg configuration file
1292
1293        with self.tmpdir_with_python() as tmpdir, \
1294             tempfile.TemporaryDirectory() as pyvenv_home:
1295            ver = sys.version_info
1296
1297            if not MS_WINDOWS:
1298                lib_dynload = os.path.join(pyvenv_home,
1299                                           sys.platlibdir,
1300                                           f'python{ver.major}.{ver.minor}',
1301                                           'lib-dynload')
1302                os.makedirs(lib_dynload)
1303            else:
1304                lib_dynload = os.path.join(pyvenv_home, 'lib')
1305                os.makedirs(lib_dynload)
1306                # getpathp.c uses Lib\os.py as the LANDMARK
1307                shutil.copyfile(os.__file__, os.path.join(lib_dynload, 'os.py'))
1308
1309            filename = os.path.join(tmpdir, 'pyvenv.cfg')
1310            with open(filename, "w", encoding="utf8") as fp:
1311                print("home = %s" % pyvenv_home, file=fp)
1312                print("include-system-site-packages = false", file=fp)
1313
1314            paths = self.module_search_paths()
1315            if not MS_WINDOWS:
1316                paths[-1] = lib_dynload
1317            else:
1318                for index, path in enumerate(paths):
1319                    if index == 0:
1320                        paths[index] = os.path.join(tmpdir, os.path.basename(path))
1321                    else:
1322                        paths[index] = os.path.join(pyvenv_home, os.path.basename(path))
1323                paths[-1] = pyvenv_home
1324
1325            executable = self.test_exe
1326            exec_prefix = pyvenv_home
1327            config = {
1328                'base_exec_prefix': exec_prefix,
1329                'exec_prefix': exec_prefix,
1330                'base_executable': executable,
1331                'executable': executable,
1332                'module_search_paths': paths,
1333            }
1334            path_config = {}
1335            if MS_WINDOWS:
1336                config['base_prefix'] = pyvenv_home
1337                config['prefix'] = pyvenv_home
1338
1339                ver = sys.version_info
1340                dll = f'python{ver.major}'
1341                if debug_build(executable):
1342                    dll += '_d'
1343                dll += '.DLL'
1344                dll = os.path.join(os.path.dirname(executable), dll)
1345                path_config['python3_dll'] = dll
1346
1347            env = self.copy_paths_by_env(config)
1348            self.check_all_configs("test_init_compat_config", config,
1349                                   expected_pathconfig=path_config,
1350                                   api=API_COMPAT, env=env,
1351                                   ignore_stderr=True, cwd=tmpdir)
1352
1353    def test_global_pathconfig(self):
1354        # Test C API functions getting the path configuration:
1355        #
1356        # - Py_GetExecPrefix()
1357        # - Py_GetPath()
1358        # - Py_GetPrefix()
1359        # - Py_GetProgramFullPath()
1360        # - Py_GetProgramName()
1361        # - Py_GetPythonHome()
1362        #
1363        # The global path configuration (_Py_path_config) must be a copy
1364        # of the path configuration of PyInterpreter.config (PyConfig).
1365        ctypes = import_helper.import_module('ctypes')
1366        _testinternalcapi = import_helper.import_module('_testinternalcapi')
1367
1368        def get_func(name):
1369            func = getattr(ctypes.pythonapi, name)
1370            func.argtypes = ()
1371            func.restype = ctypes.c_wchar_p
1372            return func
1373
1374        Py_GetPath = get_func('Py_GetPath')
1375        Py_GetPrefix = get_func('Py_GetPrefix')
1376        Py_GetExecPrefix = get_func('Py_GetExecPrefix')
1377        Py_GetProgramName = get_func('Py_GetProgramName')
1378        Py_GetProgramFullPath = get_func('Py_GetProgramFullPath')
1379        Py_GetPythonHome = get_func('Py_GetPythonHome')
1380
1381        config = _testinternalcapi.get_configs()['config']
1382
1383        self.assertEqual(Py_GetPath().split(os.path.pathsep),
1384                         config['module_search_paths'])
1385        self.assertEqual(Py_GetPrefix(), config['prefix'])
1386        self.assertEqual(Py_GetExecPrefix(), config['exec_prefix'])
1387        self.assertEqual(Py_GetProgramName(), config['program_name'])
1388        self.assertEqual(Py_GetProgramFullPath(), config['executable'])
1389        self.assertEqual(Py_GetPythonHome(), config['home'])
1390
1391    def test_init_warnoptions(self):
1392        # lowest to highest priority
1393        warnoptions = [
1394            'ignore:::PyConfig_Insert0',      # PyWideStringList_Insert(0)
1395            'default',                        # PyConfig.dev_mode=1
1396            'ignore:::env1',                  # PYTHONWARNINGS env var
1397            'ignore:::env2',                  # PYTHONWARNINGS env var
1398            'ignore:::cmdline1',              # -W opt command line option
1399            'ignore:::cmdline2',              # -W opt command line option
1400            'default::BytesWarning',          # PyConfig.bytes_warnings=1
1401            'ignore:::PySys_AddWarnOption1',  # PySys_AddWarnOption()
1402            'ignore:::PySys_AddWarnOption2',  # PySys_AddWarnOption()
1403            'ignore:::PyConfig_BeforeRead',   # PyConfig.warnoptions
1404            'ignore:::PyConfig_AfterRead']    # PyWideStringList_Append()
1405        preconfig = dict(allocator=PYMEM_ALLOCATOR_DEBUG)
1406        config = {
1407            'dev_mode': 1,
1408            'faulthandler': 1,
1409            'bytes_warning': 1,
1410            'warnoptions': warnoptions,
1411            'orig_argv': ['python3',
1412                          '-Wignore:::cmdline1',
1413                          '-Wignore:::cmdline2'],
1414        }
1415        self.check_all_configs("test_init_warnoptions", config, preconfig,
1416                               api=API_PYTHON)
1417
1418    def test_init_set_config(self):
1419        config = {
1420            '_init_main': 0,
1421            'bytes_warning': 2,
1422            'warnoptions': ['error::BytesWarning'],
1423        }
1424        self.check_all_configs("test_init_set_config", config,
1425                               api=API_ISOLATED)
1426
1427    def test_get_argc_argv(self):
1428        self.run_embedded_interpreter("test_get_argc_argv")
1429        # ignore output
1430
1431
1432class SetConfigTests(unittest.TestCase):
1433    def test_set_config(self):
1434        # bpo-42260: Test _PyInterpreterState_SetConfig()
1435        cmd = [sys.executable, '-I', '-m', 'test._test_embed_set_config']
1436        proc = subprocess.run(cmd,
1437                              stdout=subprocess.PIPE,
1438                              stderr=subprocess.PIPE)
1439        self.assertEqual(proc.returncode, 0,
1440                         (proc.returncode, proc.stdout, proc.stderr))
1441
1442
1443class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
1444    def test_open_code_hook(self):
1445        self.run_embedded_interpreter("test_open_code_hook")
1446
1447    def test_audit(self):
1448        self.run_embedded_interpreter("test_audit")
1449
1450    def test_audit_subinterpreter(self):
1451        self.run_embedded_interpreter("test_audit_subinterpreter")
1452
1453    def test_audit_run_command(self):
1454        self.run_embedded_interpreter("test_audit_run_command",
1455                                      timeout=support.SHORT_TIMEOUT,
1456                                      returncode=1)
1457
1458    def test_audit_run_file(self):
1459        self.run_embedded_interpreter("test_audit_run_file",
1460                                      timeout=support.SHORT_TIMEOUT,
1461                                      returncode=1)
1462
1463    def test_audit_run_interactivehook(self):
1464        startup = os.path.join(self.oldcwd, os_helper.TESTFN) + ".py"
1465        with open(startup, "w", encoding="utf-8") as f:
1466            print("import sys", file=f)
1467            print("sys.__interactivehook__ = lambda: None", file=f)
1468        try:
1469            env = {**remove_python_envvars(), "PYTHONSTARTUP": startup}
1470            self.run_embedded_interpreter("test_audit_run_interactivehook",
1471                                          timeout=support.SHORT_TIMEOUT,
1472                                          returncode=10, env=env)
1473        finally:
1474            os.unlink(startup)
1475
1476    def test_audit_run_startup(self):
1477        startup = os.path.join(self.oldcwd, os_helper.TESTFN) + ".py"
1478        with open(startup, "w", encoding="utf-8") as f:
1479            print("pass", file=f)
1480        try:
1481            env = {**remove_python_envvars(), "PYTHONSTARTUP": startup}
1482            self.run_embedded_interpreter("test_audit_run_startup",
1483                                          timeout=support.SHORT_TIMEOUT,
1484                                          returncode=10, env=env)
1485        finally:
1486            os.unlink(startup)
1487
1488    def test_audit_run_stdin(self):
1489        self.run_embedded_interpreter("test_audit_run_stdin",
1490                                      timeout=support.SHORT_TIMEOUT,
1491                                      returncode=1)
1492
1493
1494class MiscTests(EmbeddingTestsMixin, unittest.TestCase):
1495    def test_unicode_id_init(self):
1496        # bpo-42882: Test that _PyUnicode_FromId() works
1497        # when Python is initialized multiples times.
1498        self.run_embedded_interpreter("test_unicode_id_init")
1499
1500
1501class StdPrinterTests(EmbeddingTestsMixin, unittest.TestCase):
1502    # Test PyStdPrinter_Type which is used by _PySys_SetPreliminaryStderr():
1503    #   "Set up a preliminary stderr printer until we have enough
1504    #    infrastructure for the io module in place."
1505
1506    STDOUT_FD = 1
1507
1508    def create_printer(self, fd):
1509        ctypes = import_helper.import_module('ctypes')
1510        PyFile_NewStdPrinter = ctypes.pythonapi.PyFile_NewStdPrinter
1511        PyFile_NewStdPrinter.argtypes = (ctypes.c_int,)
1512        PyFile_NewStdPrinter.restype = ctypes.py_object
1513        return PyFile_NewStdPrinter(fd)
1514
1515    def test_write(self):
1516        message = "unicode:\xe9-\u20ac-\udc80!\n"
1517
1518        stdout_fd = self.STDOUT_FD
1519        stdout_fd_copy = os.dup(stdout_fd)
1520        self.addCleanup(os.close, stdout_fd_copy)
1521
1522        rfd, wfd = os.pipe()
1523        self.addCleanup(os.close, rfd)
1524        self.addCleanup(os.close, wfd)
1525        try:
1526            # PyFile_NewStdPrinter() only accepts fileno(stdout)
1527            # or fileno(stderr) file descriptor.
1528            os.dup2(wfd, stdout_fd)
1529
1530            printer = self.create_printer(stdout_fd)
1531            printer.write(message)
1532        finally:
1533            os.dup2(stdout_fd_copy, stdout_fd)
1534
1535        data = os.read(rfd, 100)
1536        self.assertEqual(data, message.encode('utf8', 'backslashreplace'))
1537
1538    def test_methods(self):
1539        fd = self.STDOUT_FD
1540        printer = self.create_printer(fd)
1541        self.assertEqual(printer.fileno(), fd)
1542        self.assertEqual(printer.isatty(), os.isatty(fd))
1543        printer.flush()  # noop
1544        printer.close()  # noop
1545
1546    def test_disallow_instantiation(self):
1547        fd = self.STDOUT_FD
1548        printer = self.create_printer(fd)
1549        support.check_disallow_instantiation(self, type(printer))
1550
1551
1552if __name__ == "__main__":
1553    unittest.main()
1554