Skip to content

Commit bb6bf7d

Browse files
authored
bpo-38234: Add tests for Python init path config (GH-16358)
1 parent 1ce152a commit bb6bf7d

File tree

3 files changed

+176
-24
lines changed

3 files changed

+176
-24
lines changed

Lib/test/test_embed.py

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,8 @@ def main_xoptions(self, xoptions_list):
470470
xoptions[opt] = True
471471
return xoptions
472472

473-
def _get_expected_config(self, env):
473+
def _get_expected_config_impl(self):
474+
env = remove_python_envvars()
474475
code = textwrap.dedent('''
475476
import json
476477
import sys
@@ -499,13 +500,19 @@ def _get_expected_config(self, env):
499500
except json.JSONDecodeError:
500501
self.fail(f"fail to decode stdout: {stdout!r}")
501502

503+
def _get_expected_config(self):
504+
cls = InitConfigTests
505+
if cls.EXPECTED_CONFIG is None:
506+
cls.EXPECTED_CONFIG = self._get_expected_config_impl()
507+
508+
# get a copy
509+
return {key: dict(value)
510+
for key, value in cls.EXPECTED_CONFIG.items()}
511+
502512
def get_expected_config(self, expected_preconfig, expected, env, api,
503513
modify_path_cb=None):
504514
cls = self.__class__
505-
if cls.EXPECTED_CONFIG is None:
506-
cls.EXPECTED_CONFIG = self._get_expected_config(env)
507-
configs = {key: dict(value)
508-
for key, value in self.EXPECTED_CONFIG.items()}
515+
configs = self._get_expected_config()
509516

510517
pre_config = configs['pre_config']
511518
for key, value in expected_preconfig.items():
@@ -553,9 +560,10 @@ def get_expected_config(self, expected_preconfig, expected, env, api,
553560
if value is self.GET_DEFAULT_CONFIG:
554561
expected[key] = config[key]
555562

556-
prepend_path = expected['pythonpath_env']
557-
if prepend_path is not None:
558-
expected['module_search_paths'] = [prepend_path, *expected['module_search_paths']]
563+
pythonpath_env = expected['pythonpath_env']
564+
if pythonpath_env is not None:
565+
paths = pythonpath_env.split(os.path.pathsep)
566+
expected['module_search_paths'] = [*paths, *expected['module_search_paths']]
559567
if modify_path_cb is not None:
560568
expected['module_search_paths'] = expected['module_search_paths'].copy()
561569
modify_path_cb(expected['module_search_paths'])
@@ -604,8 +612,11 @@ def check_global_config(self, configs):
604612

605613
def check_all_configs(self, testname, expected_config=None,
606614
expected_preconfig=None, modify_path_cb=None, stderr=None,
607-
*, api):
608-
env = remove_python_envvars()
615+
*, api, env=None, ignore_stderr=False):
616+
new_env = remove_python_envvars()
617+
if env is not None:
618+
new_env.update(env)
619+
env = new_env
609620

610621
if api == API_ISOLATED:
611622
default_preconfig = self.PRE_CONFIG_ISOLATED
@@ -634,7 +645,7 @@ def check_all_configs(self, testname, expected_config=None,
634645
out, err = self.run_embedded_interpreter(testname, env=env)
635646
if stderr is None and not expected_config['verbose']:
636647
stderr = ""
637-
if stderr is not None:
648+
if stderr is not None and not ignore_stderr:
638649
self.assertEqual(err.rstrip(), stderr)
639650
try:
640651
configs = json.loads(out)
@@ -966,6 +977,62 @@ def test_init_dont_parse_argv(self):
966977
self.check_all_configs("test_init_dont_parse_argv", config, pre_config,
967978
api=API_PYTHON)
968979

980+
def test_init_setpath(self):
981+
# Test Py_SetProgramName() + Py_SetPath()
982+
config = self._get_expected_config()
983+
paths = config['config']['module_search_paths']
984+
985+
config = {
986+
'module_search_paths': paths,
987+
'prefix': '',
988+
'base_prefix': '',
989+
'exec_prefix': '',
990+
'base_exec_prefix': '',
991+
}
992+
env = {'TESTPATH': os.path.pathsep.join(paths)}
993+
self.check_all_configs("test_init_setpath", config,
994+
api=API_COMPAT, env=env,
995+
ignore_stderr=True)
996+
997+
def test_init_setpythonhome(self):
998+
# Test Py_SetPythonHome(home) + PYTHONPATH env var
999+
# + Py_SetProgramName()
1000+
config = self._get_expected_config()
1001+
paths = config['config']['module_search_paths']
1002+
paths_str = os.path.pathsep.join(paths)
1003+
1004+
for path in paths:
1005+
if not os.path.isdir(path):
1006+
continue
1007+
if os.path.exists(os.path.join(path, 'os.py')):
1008+
home = os.path.dirname(path)
1009+
break
1010+
else:
1011+
self.fail(f"Unable to find home in {paths!r}")
1012+
1013+
prefix = exec_prefix = home
1014+
ver = sys.version_info
1015+
if MS_WINDOWS:
1016+
expected_paths = paths
1017+
else:
1018+
expected_paths = [
1019+
os.path.join(prefix, 'lib', f'python{ver.major}{ver.minor}.zip'),
1020+
os.path.join(home, 'lib', f'python{ver.major}.{ver.minor}'),
1021+
os.path.join(home, 'lib', f'python{ver.major}.{ver.minor}/lib-dynload')]
1022+
1023+
config = {
1024+
'home': home,
1025+
'module_search_paths': expected_paths,
1026+
'prefix': prefix,
1027+
'base_prefix': prefix,
1028+
'exec_prefix': exec_prefix,
1029+
'base_exec_prefix': exec_prefix,
1030+
'pythonpath_env': paths_str,
1031+
}
1032+
env = {'TESTHOME': home, 'TESTPATH': paths_str}
1033+
self.check_all_configs("test_init_setpythonhome", config,
1034+
api=API_COMPAT, env=env)
1035+
9691036

9701037
class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
9711038
def test_open_code_hook(self):

Modules/getpath.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,12 +1012,12 @@ calculate_zip_path(PyCalculatePath *calculate, const wchar_t *prefix,
10121012
wchar_t *zip_path, size_t zip_path_len)
10131013
{
10141014
PyStatus status;
1015-
if (safe_wcscpy(zip_path, prefix, zip_path_len) < 0) {
1016-
return PATHLEN_ERR();
1017-
}
10181015

10191016
if (calculate->prefix_found > 0) {
10201017
/* Use the reduced prefix returned by Py_GetPrefix() */
1018+
if (safe_wcscpy(zip_path, prefix, zip_path_len) < 0) {
1019+
return PATHLEN_ERR();
1020+
}
10211021
reduce(zip_path);
10221022
reduce(zip_path);
10231023
}

Programs/_testembed.c

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@
2020
* Executed via 'EmbeddingTests' in Lib/test/test_capi.py
2121
*********************************************************/
2222

23+
/* Use path starting with "./" avoids a search along the PATH */
24+
#define PROGRAM_NAME L"./_testembed"
25+
2326
static void _testembed_Py_Initialize(void)
2427
{
25-
/* HACK: the "./" at front avoids a search along the PATH in
26-
Modules/getpath.c */
27-
Py_SetProgramName(L"./_testembed");
28+
Py_SetProgramName(PROGRAM_NAME);
2829
Py_Initialize();
2930
}
3031

@@ -363,8 +364,7 @@ config_set_wide_string_list(PyConfig *config, PyWideStringList *list,
363364

364365
static void config_set_program_name(PyConfig *config)
365366
{
366-
/* Use path starting with "./" avoids a search along the PATH */
367-
const wchar_t *program_name = L"./_testembed";
367+
const wchar_t *program_name = PROGRAM_NAME;
368368
config_set_string(config, &config->program_name, program_name);
369369
}
370370

@@ -1263,7 +1263,7 @@ static int _audit_hook_run(const char *eventName, PyObject *args, void *userData
12631263
static int test_audit_run_command(void)
12641264
{
12651265
AuditRunCommandTest test = {"cpython.run_command"};
1266-
wchar_t *argv[] = {L"./_testembed", L"-c", L"pass"};
1266+
wchar_t *argv[] = {PROGRAM_NAME, L"-c", L"pass"};
12671267

12681268
Py_IgnoreEnvironmentFlag = 0;
12691269
PySys_AddAuditHook(_audit_hook_run, (void*)&test);
@@ -1274,7 +1274,7 @@ static int test_audit_run_command(void)
12741274
static int test_audit_run_file(void)
12751275
{
12761276
AuditRunCommandTest test = {"cpython.run_file"};
1277-
wchar_t *argv[] = {L"./_testembed", L"filename.py"};
1277+
wchar_t *argv[] = {PROGRAM_NAME, L"filename.py"};
12781278

12791279
Py_IgnoreEnvironmentFlag = 0;
12801280
PySys_AddAuditHook(_audit_hook_run, (void*)&test);
@@ -1312,21 +1312,21 @@ static int run_audit_run_test(int argc, wchar_t **argv, void *test)
13121312
static int test_audit_run_interactivehook(void)
13131313
{
13141314
AuditRunCommandTest test = {"cpython.run_interactivehook", 10};
1315-
wchar_t *argv[] = {L"./_testembed"};
1315+
wchar_t *argv[] = {PROGRAM_NAME};
13161316
return run_audit_run_test(Py_ARRAY_LENGTH(argv), argv, &test);
13171317
}
13181318

13191319
static int test_audit_run_startup(void)
13201320
{
13211321
AuditRunCommandTest test = {"cpython.run_startup", 10};
1322-
wchar_t *argv[] = {L"./_testembed"};
1322+
wchar_t *argv[] = {PROGRAM_NAME};
13231323
return run_audit_run_test(Py_ARRAY_LENGTH(argv), argv, &test);
13241324
}
13251325

13261326
static int test_audit_run_stdin(void)
13271327
{
13281328
AuditRunCommandTest test = {"cpython.run_stdin"};
1329-
wchar_t *argv[] = {L"./_testembed"};
1329+
wchar_t *argv[] = {PROGRAM_NAME};
13301330
return run_audit_run_test(Py_ARRAY_LENGTH(argv), argv, &test);
13311331
}
13321332

@@ -1423,6 +1423,88 @@ static int test_init_sys_add(void)
14231423
}
14241424

14251425

1426+
static int test_init_setpath(void)
1427+
{
1428+
Py_SetProgramName(PROGRAM_NAME);
1429+
1430+
char *env = getenv("TESTPATH");
1431+
if (!env) {
1432+
fprintf(stderr, "missing TESTPATH env var\n");
1433+
return 1;
1434+
}
1435+
wchar_t *path = Py_DecodeLocale(env, NULL);
1436+
if (path == NULL) {
1437+
fprintf(stderr, "failed to decode TESTPATH\n");
1438+
return 1;
1439+
}
1440+
Py_SetPath(path);
1441+
PyMem_RawFree(path);
1442+
putenv("TESTPATH=");
1443+
1444+
Py_Initialize();
1445+
dump_config();
1446+
Py_Finalize();
1447+
return 0;
1448+
}
1449+
1450+
1451+
static int mysetenv(const char *name, const char *value)
1452+
{
1453+
size_t len = strlen(name) + 1 + strlen(value) + 1;
1454+
char *env = PyMem_RawMalloc(len);
1455+
if (env == NULL) {
1456+
fprintf(stderr, "out of memory\n");
1457+
return -1;
1458+
}
1459+
strcpy(env, name);
1460+
strcat(env, "=");
1461+
strcat(env, value);
1462+
1463+
putenv(env);
1464+
1465+
/* Don't call PyMem_RawFree(env), but leak env memory block:
1466+
putenv() does not copy the string. */
1467+
1468+
return 0;
1469+
}
1470+
1471+
1472+
static int test_init_setpythonhome(void)
1473+
{
1474+
char *env = getenv("TESTHOME");
1475+
if (!env) {
1476+
fprintf(stderr, "missing TESTHOME env var\n");
1477+
return 1;
1478+
}
1479+
wchar_t *home = Py_DecodeLocale(env, NULL);
1480+
if (home == NULL) {
1481+
fprintf(stderr, "failed to decode TESTHOME\n");
1482+
return 1;
1483+
}
1484+
Py_SetPythonHome(home);
1485+
PyMem_RawFree(home);
1486+
putenv("TESTHOME=");
1487+
1488+
char *path = getenv("TESTPATH");
1489+
if (!path) {
1490+
fprintf(stderr, "missing TESTPATH env var\n");
1491+
return 1;
1492+
}
1493+
1494+
if (mysetenv("PYTHONPATH", path) < 0) {
1495+
return 1;
1496+
}
1497+
putenv("TESTPATH=");
1498+
1499+
Py_SetProgramName(PROGRAM_NAME);
1500+
1501+
Py_Initialize();
1502+
dump_config();
1503+
Py_Finalize();
1504+
return 0;
1505+
}
1506+
1507+
14261508
static void configure_init_main(PyConfig *config)
14271509
{
14281510
wchar_t* argv[] = {
@@ -1559,7 +1641,10 @@ static struct TestCase TestCases[] = {
15591641
{"test_init_run_main", test_init_run_main},
15601642
{"test_init_main", test_init_main},
15611643
{"test_init_sys_add", test_init_sys_add},
1644+
{"test_init_setpath", test_init_setpath},
1645+
{"test_init_setpythonhome", test_init_setpythonhome},
15621646
{"test_run_main", test_run_main},
1647+
15631648
{"test_open_code_hook", test_open_code_hook},
15641649
{"test_audit", test_audit},
15651650
{"test_audit_subinterpreter", test_audit_subinterpreter},

0 commit comments

Comments
 (0)