Commit 52ad33ab authored by Victor Stinner's avatar Victor Stinner Committed by GitHub

bpo-38234: test_embed: test pyvenv.cfg and pybuilddir.txt (GH-16366)

Add test_init_pybuilddir() and test_init_pyvenv_cfg() to test_embed
to test pyvenv.cfg and pybuilddir.txt configuration files.

Fix sysconfig._generate_posix_vars(): pybuilddir.txt uses UTF-8
encoding, not ASCII.
parent 2180f6b0
...@@ -412,7 +412,7 @@ def _generate_posix_vars(): ...@@ -412,7 +412,7 @@ def _generate_posix_vars():
pprint.pprint(vars, stream=f) pprint.pprint(vars, stream=f)
# Create file used for sys.path fixup -- see Modules/getpath.c # Create file used for sys.path fixup -- see Modules/getpath.c
with open('pybuilddir.txt', 'w', encoding='ascii') as f: with open('pybuilddir.txt', 'w', encoding='utf8') as f:
f.write(pybuilddir) f.write(pybuilddir)
def _init_posix(vars): def _init_posix(vars):
......
...@@ -3,11 +3,14 @@ from test import support ...@@ -3,11 +3,14 @@ from test import support
import unittest import unittest
from collections import namedtuple from collections import namedtuple
import contextlib
import json import json
import os import os
import re import re
import shutil
import subprocess import subprocess
import sys import sys
import tempfile
import textwrap import textwrap
...@@ -25,6 +28,12 @@ API_PYTHON = 2 ...@@ -25,6 +28,12 @@ API_PYTHON = 2
API_ISOLATED = 3 API_ISOLATED = 3
def debug_build(program):
program = os.path.basename(program)
name = os.path.splitext(program)[0]
return name.endswith("_d")
def remove_python_envvars(): def remove_python_envvars():
env = dict(os.environ) env = dict(os.environ)
# Remove PYTHON* environment variables to get deterministic environment # Remove PYTHON* environment variables to get deterministic environment
...@@ -40,7 +49,7 @@ class EmbeddingTestsMixin: ...@@ -40,7 +49,7 @@ class EmbeddingTestsMixin:
basepath = os.path.dirname(os.path.dirname(os.path.dirname(here))) basepath = os.path.dirname(os.path.dirname(os.path.dirname(here)))
exename = "_testembed" exename = "_testembed"
if MS_WINDOWS: if MS_WINDOWS:
ext = ("_d" if "_d" in sys.executable else "") + ".exe" ext = ("_d" if debug_build(sys.executable) else "") + ".exe"
exename += ext exename += ext
exepath = os.path.dirname(sys.executable) exepath = os.path.dirname(sys.executable)
else: else:
...@@ -58,7 +67,8 @@ class EmbeddingTestsMixin: ...@@ -58,7 +67,8 @@ class EmbeddingTestsMixin:
os.chdir(self.oldcwd) os.chdir(self.oldcwd)
def run_embedded_interpreter(self, *args, env=None, def run_embedded_interpreter(self, *args, env=None,
timeout=None, returncode=0, input=None): timeout=None, returncode=0, input=None,
cwd=None):
"""Runs a test in the embedded interpreter""" """Runs a test in the embedded interpreter"""
cmd = [self.test_exe] cmd = [self.test_exe]
cmd.extend(args) cmd.extend(args)
...@@ -72,7 +82,8 @@ class EmbeddingTestsMixin: ...@@ -72,7 +82,8 @@ class EmbeddingTestsMixin:
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, universal_newlines=True,
env=env) env=env,
cwd=cwd)
try: try:
(out, err) = p.communicate(input=input, timeout=timeout) (out, err) = p.communicate(input=input, timeout=timeout)
except: except:
...@@ -460,6 +471,11 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -460,6 +471,11 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
EXPECTED_CONFIG = None EXPECTED_CONFIG = None
@classmethod
def tearDownClass(cls):
# clear cache
cls.EXPECTED_CONFIG = None
def main_xoptions(self, xoptions_list): def main_xoptions(self, xoptions_list):
xoptions = {} xoptions = {}
for opt in xoptions_list: for opt in xoptions_list:
...@@ -490,11 +506,12 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -490,11 +506,12 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
args = [sys.executable, '-S', '-c', code] args = [sys.executable, '-S', '-c', code]
proc = subprocess.run(args, env=env, proc = subprocess.run(args, env=env,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT) stderr=subprocess.PIPE)
if proc.returncode: if proc.returncode:
raise Exception(f"failed to get the default config: " raise Exception(f"failed to get the default config: "
f"stdout={proc.stdout!r} stderr={proc.stderr!r}") f"stdout={proc.stdout!r} stderr={proc.stderr!r}")
stdout = proc.stdout.decode('utf-8') stdout = proc.stdout.decode('utf-8')
# ignore stderr
try: try:
return json.loads(stdout) return json.loads(stdout)
except json.JSONDecodeError: except json.JSONDecodeError:
...@@ -506,8 +523,15 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -506,8 +523,15 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
cls.EXPECTED_CONFIG = self._get_expected_config_impl() cls.EXPECTED_CONFIG = self._get_expected_config_impl()
# get a copy # get a copy
return {key: dict(value) configs = {}
for key, value in cls.EXPECTED_CONFIG.items()} for config_key, config_value in cls.EXPECTED_CONFIG.items():
config = {}
for key, value in config_value.items():
if isinstance(value, list):
value = value.copy()
config[key] = value
configs[config_key] = config
return configs
def get_expected_config(self, expected_preconfig, expected, env, api, def get_expected_config(self, expected_preconfig, expected, env, api,
modify_path_cb=None): modify_path_cb=None):
...@@ -612,7 +636,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -612,7 +636,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
def check_all_configs(self, testname, expected_config=None, def check_all_configs(self, testname, expected_config=None,
expected_preconfig=None, modify_path_cb=None, stderr=None, expected_preconfig=None, modify_path_cb=None, stderr=None,
*, api, env=None, ignore_stderr=False): *, api, env=None, ignore_stderr=False, cwd=None):
new_env = remove_python_envvars() new_env = remove_python_envvars()
if env is not None: if env is not None:
new_env.update(env) new_env.update(env)
...@@ -642,7 +666,8 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -642,7 +666,8 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
expected_config, env, expected_config, env,
api, modify_path_cb) api, modify_path_cb)
out, err = self.run_embedded_interpreter(testname, env=env) out, err = self.run_embedded_interpreter(testname,
env=env, cwd=cwd)
if stderr is None and not expected_config['verbose']: if stderr is None and not expected_config['verbose']:
stderr = "" stderr = ""
if stderr is not None and not ignore_stderr: if stderr is not None and not ignore_stderr:
...@@ -994,6 +1019,48 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -994,6 +1019,48 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
api=API_COMPAT, env=env, api=API_COMPAT, env=env,
ignore_stderr=True) ignore_stderr=True)
def module_search_paths(self, prefix=None, exec_prefix=None):
config = self._get_expected_config()
if prefix is None:
prefix = config['config']['prefix']
if exec_prefix is None:
exec_prefix = config['config']['prefix']
if MS_WINDOWS:
return config['config']['module_search_paths']
else:
ver = sys.version_info
return [
os.path.join(prefix, 'lib',
f'python{ver.major}{ver.minor}.zip'),
os.path.join(prefix, 'lib',
f'python{ver.major}.{ver.minor}'),
os.path.join(exec_prefix, 'lib',
f'python{ver.major}.{ver.minor}', 'lib-dynload'),
]
@contextlib.contextmanager
def tmpdir_with_python(self):
# Temporary directory with a copy of the Python program
with tempfile.TemporaryDirectory() as tmpdir:
if MS_WINDOWS:
# Copy pythonXY.dll (or pythonXY_d.dll)
ver = sys.version_info
dll = f'python{ver.major}{ver.minor}'
if debug_build(sys.executable):
dll += '_d'
dll += '.dll'
dll = os.path.join(os.path.dirname(self.test_exe), dll)
dll_copy = os.path.join(tmpdir, os.path.basename(dll))
shutil.copyfile(dll, dll_copy)
# Copy Python program
exec_copy = os.path.join(tmpdir, os.path.basename(self.test_exe))
shutil.copyfile(self.test_exe, exec_copy)
shutil.copystat(self.test_exe, exec_copy)
self.test_exe = exec_copy
yield tmpdir
def test_init_setpythonhome(self): def test_init_setpythonhome(self):
# Test Py_SetPythonHome(home) + PYTHONPATH env var # Test Py_SetPythonHome(home) + PYTHONPATH env var
# + Py_SetProgramName() # + Py_SetProgramName()
...@@ -1012,13 +1079,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -1012,13 +1079,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
prefix = exec_prefix = home prefix = exec_prefix = home
ver = sys.version_info ver = sys.version_info
if MS_WINDOWS: expected_paths = self.module_search_paths(prefix=home, exec_prefix=home)
expected_paths = paths
else:
expected_paths = [
os.path.join(prefix, 'lib', f'python{ver.major}{ver.minor}.zip'),
os.path.join(home, 'lib', f'python{ver.major}.{ver.minor}'),
os.path.join(home, 'lib', f'python{ver.major}.{ver.minor}/lib-dynload')]
config = { config = {
'home': home, 'home': home,
...@@ -1033,6 +1094,95 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -1033,6 +1094,95 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
self.check_all_configs("test_init_setpythonhome", config, self.check_all_configs("test_init_setpythonhome", config,
api=API_COMPAT, env=env) api=API_COMPAT, env=env)
def copy_paths_by_env(self, config):
all_configs = self._get_expected_config()
paths = all_configs['config']['module_search_paths']
paths_str = os.path.pathsep.join(paths)
config['pythonpath_env'] = paths_str
env = {'PYTHONPATH': paths_str}
return env
@unittest.skipIf(MS_WINDOWS, 'Windows does not use pybuilddir.txt')
def test_init_pybuilddir(self):
# Test path configuration with pybuilddir.txt configuration file
with self.tmpdir_with_python() as tmpdir:
# pybuilddir.txt is a sub-directory relative to the current
# directory (tmpdir)
subdir = 'libdir'
libdir = os.path.join(tmpdir, subdir)
os.mkdir(libdir)
filename = os.path.join(tmpdir, 'pybuilddir.txt')
with open(filename, "w", encoding="utf8") as fp:
fp.write(subdir)
module_search_paths = self.module_search_paths()
module_search_paths[-1] = libdir
executable = self.test_exe
config = {
'base_executable': executable,
'executable': executable,
'module_search_paths': module_search_paths,
}
env = self.copy_paths_by_env(config)
self.check_all_configs("test_init_compat_config", config,
api=API_COMPAT, env=env,
ignore_stderr=True, cwd=tmpdir)
def test_init_pyvenv_cfg(self):
# Test path configuration with pyvenv.cfg configuration file
with self.tmpdir_with_python() as tmpdir, \
tempfile.TemporaryDirectory() as pyvenv_home:
ver = sys.version_info
if not MS_WINDOWS:
lib_dynload = os.path.join(pyvenv_home,
'lib',
f'python{ver.major}.{ver.minor}',
'lib-dynload')
os.makedirs(lib_dynload)
else:
lib_dynload = os.path.join(pyvenv_home, 'lib')
os.makedirs(lib_dynload)
# getpathp.c uses Lib\os.py as the LANDMARK
shutil.copyfile(os.__file__, os.path.join(lib_dynload, 'os.py'))
filename = os.path.join(tmpdir, 'pyvenv.cfg')
with open(filename, "w", encoding="utf8") as fp:
print("home = %s" % pyvenv_home, file=fp)
print("include-system-site-packages = false", file=fp)
paths = self.module_search_paths()
if not MS_WINDOWS:
paths[-1] = lib_dynload
else:
for index, path in enumerate(paths):
if index == 0:
paths[index] = os.path.join(tmpdir, os.path.basename(path))
else:
paths[index] = os.path.join(pyvenv_home, os.path.basename(path))
paths[-1] = pyvenv_home
executable = self.test_exe
exec_prefix = pyvenv_home
config = {
'base_exec_prefix': exec_prefix,
'exec_prefix': exec_prefix,
'base_executable': executable,
'executable': executable,
'module_search_paths': paths,
}
if MS_WINDOWS:
config['base_prefix'] = pyvenv_home
config['prefix'] = pyvenv_home
env = self.copy_paths_by_env(config)
self.check_all_configs("test_init_compat_config", config,
api=API_COMPAT, env=env,
ignore_stderr=True, cwd=tmpdir)
class AuditingTests(EmbeddingTestsMixin, unittest.TestCase): class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
def test_open_code_hook(self): def test_open_code_hook(self):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment