python (3.12.0)
1 import contextlib
2 import itertools
3 import os
4 import re
5 import shutil
6 import subprocess
7 import sys
8 import sysconfig
9 import tempfile
10 import unittest
11 from pathlib import Path
12 from test import support
13
14 if sys.platform != "win32":
15 raise unittest.SkipTest("test only applies to Windows")
16
17 # Get winreg after the platform check
18 import winreg
19
20
21 PY_EXE = "py.exe"
22 if sys.executable.casefold().endswith("_d.exe".casefold()):
23 PY_EXE = "py_d.exe"
24
25 # Registry data to create. On removal, everything beneath top-level names will
26 # be deleted.
27 TEST_DATA = {
28 "PythonTestSuite": {
29 "DisplayName": "Python Test Suite",
30 "SupportUrl": "https://www.python.org/",
31 "3.100": {
32 "DisplayName": "X.Y version",
33 "InstallPath": {
34 None: sys.prefix,
35 "ExecutablePath": "X.Y.exe",
36 }
37 },
38 "3.100-32": {
39 "DisplayName": "X.Y-32 version",
40 "InstallPath": {
41 None: sys.prefix,
42 "ExecutablePath": "X.Y-32.exe",
43 }
44 },
45 "3.100-arm64": {
46 "DisplayName": "X.Y-arm64 version",
47 "InstallPath": {
48 None: sys.prefix,
49 "ExecutablePath": "X.Y-arm64.exe",
50 "ExecutableArguments": "-X fake_arg_for_test",
51 }
52 },
53 "ignored": {
54 "DisplayName": "Ignored because no ExecutablePath",
55 "InstallPath": {
56 None: sys.prefix,
57 }
58 },
59 },
60 "PythonTestSuite1": {
61 "DisplayName": "Python Test Suite Single",
62 "3.100": {
63 "DisplayName": "Single Interpreter",
64 "InstallPath": {
65 None: sys.prefix,
66 "ExecutablePath": sys.executable,
67 }
68 }
69 },
70 }
71
72
73 TEST_PY_ENV = dict(
74 PY_PYTHON="PythonTestSuite/3.100",
75 PY_PYTHON2="PythonTestSuite/3.100-32",
76 PY_PYTHON3="PythonTestSuite/3.100-arm64",
77 )
78
79
80 TEST_PY_DEFAULTS = "\n".join([
81 "[defaults]",
82 *[f"{k[3:].lower()}={v}" for k, v in TEST_PY_ENV.items()],
83 ])
84
85
86 TEST_PY_COMMANDS = "\n".join([
87 "[commands]",
88 "test-command=TEST_EXE.exe",
89 ])
90
91 def create_registry_data(root, data):
92 def _create_registry_data(root, key, value):
93 if isinstance(value, dict):
94 # For a dict, we recursively create keys
95 with winreg.CreateKeyEx(root, key) as hkey:
96 for k, v in value.items():
97 _create_registry_data(hkey, k, v)
98 elif isinstance(value, str):
99 # For strings, we set values. 'key' may be None in this case
100 winreg.SetValueEx(root, key, None, winreg.REG_SZ, value)
101 else:
102 raise TypeError("don't know how to create data for '{}'".format(value))
103
104 for k, v in data.items():
105 _create_registry_data(root, k, v)
106
107
108 def enum_keys(root):
109 for i in itertools.count():
110 try:
111 yield winreg.EnumKey(root, i)
112 except OSError as ex:
113 if ex.winerror == 259:
114 break
115 raise
116
117
118 def delete_registry_data(root, keys):
119 ACCESS = winreg.KEY_WRITE | winreg.KEY_ENUMERATE_SUB_KEYS
120 for key in list(keys):
121 with winreg.OpenKey(root, key, access=ACCESS) as hkey:
122 delete_registry_data(hkey, enum_keys(hkey))
123 winreg.DeleteKey(root, key)
124
125
126 def is_installed(tag):
127 key = rf"Software\Python\PythonCore\{tag}\InstallPath"
128 for root, flag in [
129 (winreg.HKEY_CURRENT_USER, 0),
130 (winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_64KEY),
131 (winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_32KEY),
132 ]:
133 try:
134 winreg.CloseKey(winreg.OpenKey(root, key, access=winreg.KEY_READ | flag))
135 return True
136 except OSError:
137 pass
138 return False
139
140
141 class ESC[4;38;5;81mPreservePyIni:
142 def __init__(self, path, content):
143 self.path = Path(path)
144 self.content = content
145 self._preserved = None
146
147 def __enter__(self):
148 try:
149 self._preserved = self.path.read_bytes()
150 except FileNotFoundError:
151 self._preserved = None
152 self.path.write_text(self.content, encoding="utf-16")
153
154 def __exit__(self, *exc_info):
155 if self._preserved is None:
156 self.path.unlink()
157 else:
158 self.path.write_bytes(self._preserved)
159
160
161 class ESC[4;38;5;81mRunPyMixin:
162 py_exe = None
163
164 @classmethod
165 def find_py(cls):
166 py_exe = None
167 if sysconfig.is_python_build():
168 py_exe = Path(sys.executable).parent / PY_EXE
169 else:
170 for p in os.getenv("PATH").split(";"):
171 if p:
172 py_exe = Path(p) / PY_EXE
173 if py_exe.is_file():
174 break
175 else:
176 py_exe = None
177
178 # Test launch and check version, to exclude installs of older
179 # releases when running outside of a source tree
180 if py_exe:
181 try:
182 with subprocess.Popen(
183 [py_exe, "-h"],
184 stdin=subprocess.PIPE,
185 stdout=subprocess.PIPE,
186 stderr=subprocess.PIPE,
187 encoding="ascii",
188 errors="ignore",
189 ) as p:
190 p.stdin.close()
191 version = next(p.stdout, "\n").splitlines()[0].rpartition(" ")[2]
192 p.stdout.read()
193 p.wait(10)
194 if not sys.version.startswith(version):
195 py_exe = None
196 except OSError:
197 py_exe = None
198
199 if not py_exe:
200 raise unittest.SkipTest(
201 "cannot locate '{}' for test".format(PY_EXE)
202 )
203 return py_exe
204
205 def get_py_exe(self):
206 if not self.py_exe:
207 self.py_exe = self.find_py()
208 return self.py_exe
209
210 def run_py(self, args, env=None, allow_fail=False, expect_returncode=0, argv=None):
211 if not self.py_exe:
212 self.py_exe = self.find_py()
213
214 ignore = {"VIRTUAL_ENV", "PY_PYTHON", "PY_PYTHON2", "PY_PYTHON3"}
215 env = {
216 **{k.upper(): v for k, v in os.environ.items() if k.upper() not in ignore},
217 "PYLAUNCHER_DEBUG": "1",
218 "PYLAUNCHER_DRYRUN": "1",
219 "PYLAUNCHER_LIMIT_TO_COMPANY": "",
220 **{k.upper(): v for k, v in (env or {}).items()},
221 }
222 if not argv:
223 argv = [self.py_exe, *args]
224 with subprocess.Popen(
225 argv,
226 env=env,
227 executable=self.py_exe,
228 stdin=subprocess.PIPE,
229 stdout=subprocess.PIPE,
230 stderr=subprocess.PIPE,
231 ) as p:
232 p.stdin.close()
233 p.wait(10)
234 out = p.stdout.read().decode("utf-8", "replace")
235 err = p.stderr.read().decode("ascii", "replace")
236 if p.returncode != expect_returncode and support.verbose and not allow_fail:
237 print("++ COMMAND ++")
238 print([self.py_exe, *args])
239 print("++ STDOUT ++")
240 print(out)
241 print("++ STDERR ++")
242 print(err)
243 if allow_fail and p.returncode != expect_returncode:
244 raise subprocess.CalledProcessError(p.returncode, [self.py_exe, *args], out, err)
245 else:
246 self.assertEqual(expect_returncode, p.returncode)
247 data = {
248 s.partition(":")[0]: s.partition(":")[2].lstrip()
249 for s in err.splitlines()
250 if not s.startswith("#") and ":" in s
251 }
252 data["stdout"] = out
253 data["stderr"] = err
254 return data
255
256 def py_ini(self, content):
257 local_appdata = os.environ.get("LOCALAPPDATA")
258 if not local_appdata:
259 raise unittest.SkipTest("LOCALAPPDATA environment variable is "
260 "missing or empty")
261 return PreservePyIni(Path(local_appdata) / "py.ini", content)
262
263 @contextlib.contextmanager
264 def script(self, content, encoding="utf-8"):
265 file = Path(tempfile.mktemp(dir=os.getcwd()) + ".py")
266 file.write_text(content, encoding=encoding)
267 try:
268 yield file
269 finally:
270 file.unlink()
271
272 @contextlib.contextmanager
273 def fake_venv(self):
274 venv = Path.cwd() / "Scripts"
275 venv.mkdir(exist_ok=True, parents=True)
276 venv_exe = (venv / Path(sys.executable).name)
277 venv_exe.touch()
278 try:
279 yield venv_exe, {"VIRTUAL_ENV": str(venv.parent)}
280 finally:
281 shutil.rmtree(venv)
282
283
284 class ESC[4;38;5;81mTestLauncher(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase, ESC[4;38;5;149mRunPyMixin):
285 @classmethod
286 def setUpClass(cls):
287 with winreg.CreateKey(winreg.HKEY_CURRENT_USER, rf"Software\Python") as key:
288 create_registry_data(key, TEST_DATA)
289
290 if support.verbose:
291 p = subprocess.check_output("reg query HKCU\\Software\\Python /s")
292 #print(p.decode('mbcs'))
293
294
295 @classmethod
296 def tearDownClass(cls):
297 with winreg.OpenKey(winreg.HKEY_CURRENT_USER, rf"Software\Python", access=winreg.KEY_WRITE | winreg.KEY_ENUMERATE_SUB_KEYS) as key:
298 delete_registry_data(key, TEST_DATA)
299
300
301 def test_version(self):
302 data = self.run_py(["-0"])
303 self.assertEqual(self.py_exe, Path(data["argv0"]))
304 self.assertEqual(sys.version.partition(" ")[0], data["version"])
305
306 def test_help_option(self):
307 data = self.run_py(["-h"])
308 self.assertEqual("True", data["SearchInfo.help"])
309
310 def test_list_option(self):
311 for opt, v1, v2 in [
312 ("-0", "True", "False"),
313 ("-0p", "False", "True"),
314 ("--list", "True", "False"),
315 ("--list-paths", "False", "True"),
316 ]:
317 with self.subTest(opt):
318 data = self.run_py([opt])
319 self.assertEqual(v1, data["SearchInfo.list"])
320 self.assertEqual(v2, data["SearchInfo.listPaths"])
321
322 def test_list(self):
323 data = self.run_py(["--list"])
324 found = {}
325 expect = {}
326 for line in data["stdout"].splitlines():
327 m = re.match(r"\s*(.+?)\s+?(\*\s+)?(.+)$", line)
328 if m:
329 found[m.group(1)] = m.group(3)
330 for company in TEST_DATA:
331 company_data = TEST_DATA[company]
332 tags = [t for t in company_data if isinstance(company_data[t], dict)]
333 for tag in tags:
334 arg = f"-V:{company}/{tag}"
335 expect[arg] = company_data[tag]["DisplayName"]
336 expect.pop(f"-V:{company}/ignored", None)
337
338 actual = {k: v for k, v in found.items() if k in expect}
339 try:
340 self.assertDictEqual(expect, actual)
341 except:
342 if support.verbose:
343 print("*** STDOUT ***")
344 print(data["stdout"])
345 raise
346
347 def test_list_paths(self):
348 data = self.run_py(["--list-paths"])
349 found = {}
350 expect = {}
351 for line in data["stdout"].splitlines():
352 m = re.match(r"\s*(.+?)\s+?(\*\s+)?(.+)$", line)
353 if m:
354 found[m.group(1)] = m.group(3)
355 for company in TEST_DATA:
356 company_data = TEST_DATA[company]
357 tags = [t for t in company_data if isinstance(company_data[t], dict)]
358 for tag in tags:
359 arg = f"-V:{company}/{tag}"
360 install = company_data[tag]["InstallPath"]
361 try:
362 expect[arg] = install["ExecutablePath"]
363 try:
364 expect[arg] += " " + install["ExecutableArguments"]
365 except KeyError:
366 pass
367 except KeyError:
368 expect[arg] = str(Path(install[None]) / Path(sys.executable).name)
369
370 expect.pop(f"-V:{company}/ignored", None)
371
372 actual = {k: v for k, v in found.items() if k in expect}
373 try:
374 self.assertDictEqual(expect, actual)
375 except:
376 if support.verbose:
377 print("*** STDOUT ***")
378 print(data["stdout"])
379 raise
380
381 def test_filter_to_company(self):
382 company = "PythonTestSuite"
383 data = self.run_py([f"-V:{company}/"])
384 self.assertEqual("X.Y.exe", data["LaunchCommand"])
385 self.assertEqual(company, data["env.company"])
386 self.assertEqual("3.100", data["env.tag"])
387
388 def test_filter_to_company_with_default(self):
389 company = "PythonTestSuite"
390 data = self.run_py([f"-V:{company}/"], env=dict(PY_PYTHON="3.0"))
391 self.assertEqual("X.Y.exe", data["LaunchCommand"])
392 self.assertEqual(company, data["env.company"])
393 self.assertEqual("3.100", data["env.tag"])
394
395 def test_filter_to_tag(self):
396 company = "PythonTestSuite"
397 data = self.run_py(["-V:3.100"])
398 self.assertEqual("X.Y.exe", data["LaunchCommand"])
399 self.assertEqual(company, data["env.company"])
400 self.assertEqual("3.100", data["env.tag"])
401
402 data = self.run_py(["-V:3.100-32"])
403 self.assertEqual("X.Y-32.exe", data["LaunchCommand"])
404 self.assertEqual(company, data["env.company"])
405 self.assertEqual("3.100-32", data["env.tag"])
406
407 data = self.run_py(["-V:3.100-arm64"])
408 self.assertEqual("X.Y-arm64.exe -X fake_arg_for_test", data["LaunchCommand"])
409 self.assertEqual(company, data["env.company"])
410 self.assertEqual("3.100-arm64", data["env.tag"])
411
412 def test_filter_to_company_and_tag(self):
413 company = "PythonTestSuite"
414 data = self.run_py([f"-V:{company}/3.1"], expect_returncode=103)
415
416 data = self.run_py([f"-V:{company}/3.100"])
417 self.assertEqual("X.Y.exe", data["LaunchCommand"])
418 self.assertEqual(company, data["env.company"])
419 self.assertEqual("3.100", data["env.tag"])
420
421 def test_filter_with_single_install(self):
422 company = "PythonTestSuite1"
423 data = self.run_py(
424 ["-V:Nonexistent"],
425 env={"PYLAUNCHER_LIMIT_TO_COMPANY": company},
426 expect_returncode=103,
427 )
428
429 def test_search_major_3(self):
430 try:
431 data = self.run_py(["-3"], allow_fail=True)
432 except subprocess.CalledProcessError:
433 raise unittest.SkipTest("requires at least one Python 3.x install")
434 self.assertEqual("PythonCore", data["env.company"])
435 self.assertTrue(data["env.tag"].startswith("3."), data["env.tag"])
436
437 def test_search_major_3_32(self):
438 try:
439 data = self.run_py(["-3-32"], allow_fail=True)
440 except subprocess.CalledProcessError:
441 if not any(is_installed(f"3.{i}-32") for i in range(5, 11)):
442 raise unittest.SkipTest("requires at least one 32-bit Python 3.x install")
443 raise
444 self.assertEqual("PythonCore", data["env.company"])
445 self.assertTrue(data["env.tag"].startswith("3."), data["env.tag"])
446 self.assertTrue(data["env.tag"].endswith("-32"), data["env.tag"])
447
448 def test_search_major_2(self):
449 try:
450 data = self.run_py(["-2"], allow_fail=True)
451 except subprocess.CalledProcessError:
452 if not is_installed("2.7"):
453 raise unittest.SkipTest("requires at least one Python 2.x install")
454 self.assertEqual("PythonCore", data["env.company"])
455 self.assertTrue(data["env.tag"].startswith("2."), data["env.tag"])
456
457 def test_py_default(self):
458 with self.py_ini(TEST_PY_DEFAULTS):
459 data = self.run_py(["-arg"])
460 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
461 self.assertEqual("3.100", data["SearchInfo.tag"])
462 self.assertEqual("X.Y.exe -arg", data["stdout"].strip())
463
464 def test_py2_default(self):
465 with self.py_ini(TEST_PY_DEFAULTS):
466 data = self.run_py(["-2", "-arg"])
467 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
468 self.assertEqual("3.100-32", data["SearchInfo.tag"])
469 self.assertEqual("X.Y-32.exe -arg", data["stdout"].strip())
470
471 def test_py3_default(self):
472 with self.py_ini(TEST_PY_DEFAULTS):
473 data = self.run_py(["-3", "-arg"])
474 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
475 self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
476 self.assertEqual("X.Y-arm64.exe -X fake_arg_for_test -arg", data["stdout"].strip())
477
478 def test_py_default_env(self):
479 data = self.run_py(["-arg"], env=TEST_PY_ENV)
480 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
481 self.assertEqual("3.100", data["SearchInfo.tag"])
482 self.assertEqual("X.Y.exe -arg", data["stdout"].strip())
483
484 def test_py2_default_env(self):
485 data = self.run_py(["-2", "-arg"], env=TEST_PY_ENV)
486 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
487 self.assertEqual("3.100-32", data["SearchInfo.tag"])
488 self.assertEqual("X.Y-32.exe -arg", data["stdout"].strip())
489
490 def test_py3_default_env(self):
491 data = self.run_py(["-3", "-arg"], env=TEST_PY_ENV)
492 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
493 self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
494 self.assertEqual("X.Y-arm64.exe -X fake_arg_for_test -arg", data["stdout"].strip())
495
496 def test_py_default_short_argv0(self):
497 with self.py_ini(TEST_PY_DEFAULTS):
498 for argv0 in ['"py.exe"', 'py.exe', '"py"', 'py']:
499 with self.subTest(argv0):
500 data = self.run_py(["--version"], argv=f'{argv0} --version')
501 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
502 self.assertEqual("3.100", data["SearchInfo.tag"])
503 self.assertEqual("X.Y.exe --version", data["stdout"].strip())
504
505 def test_py_default_in_list(self):
506 data = self.run_py(["-0"], env=TEST_PY_ENV)
507 default = None
508 for line in data["stdout"].splitlines():
509 m = re.match(r"\s*-V:(.+?)\s+?\*\s+(.+)$", line)
510 if m:
511 default = m.group(1)
512 break
513 self.assertEqual("PythonTestSuite/3.100", default)
514
515 def test_virtualenv_in_list(self):
516 with self.fake_venv() as (venv_exe, env):
517 data = self.run_py(["-0p"], env=env)
518 for line in data["stdout"].splitlines():
519 m = re.match(r"\s*\*\s+(.+)$", line)
520 if m:
521 self.assertEqual(str(venv_exe), m.group(1))
522 break
523 else:
524 self.fail("did not find active venv path")
525
526 data = self.run_py(["-0"], env=env)
527 for line in data["stdout"].splitlines():
528 m = re.match(r"\s*\*\s+(.+)$", line)
529 if m:
530 self.assertEqual("Active venv", m.group(1))
531 break
532 else:
533 self.fail("did not find active venv entry")
534
535 def test_virtualenv_with_env(self):
536 with self.fake_venv() as (venv_exe, env):
537 data1 = self.run_py([], env={**env, "PY_PYTHON": "PythonTestSuite/3"})
538 data2 = self.run_py(["-V:PythonTestSuite/3"], env={**env, "PY_PYTHON": "PythonTestSuite/3"})
539 # Compare stdout, because stderr goes via ascii
540 self.assertEqual(data1["stdout"].strip(), str(venv_exe))
541 self.assertEqual(data1["SearchInfo.lowPriorityTag"], "True")
542 # Ensure passing the argument doesn't trigger the same behaviour
543 self.assertNotEqual(data2["stdout"].strip(), str(venv_exe))
544 self.assertNotEqual(data2["SearchInfo.lowPriorityTag"], "True")
545
546 def test_py_shebang(self):
547 with self.py_ini(TEST_PY_DEFAULTS):
548 with self.script("#! /usr/bin/python -prearg") as script:
549 data = self.run_py([script, "-postarg"])
550 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
551 self.assertEqual("3.100", data["SearchInfo.tag"])
552 self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
553
554 def test_python_shebang(self):
555 with self.py_ini(TEST_PY_DEFAULTS):
556 with self.script("#! python -prearg") as script:
557 data = self.run_py([script, "-postarg"])
558 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
559 self.assertEqual("3.100", data["SearchInfo.tag"])
560 self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
561
562 def test_py2_shebang(self):
563 with self.py_ini(TEST_PY_DEFAULTS):
564 with self.script("#! /usr/bin/python2 -prearg") as script:
565 data = self.run_py([script, "-postarg"])
566 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
567 self.assertEqual("3.100-32", data["SearchInfo.tag"])
568 self.assertEqual(f"X.Y-32.exe -prearg {script} -postarg", data["stdout"].strip())
569
570 def test_py3_shebang(self):
571 with self.py_ini(TEST_PY_DEFAULTS):
572 with self.script("#! /usr/bin/python3 -prearg") as script:
573 data = self.run_py([script, "-postarg"])
574 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
575 self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
576 self.assertEqual(f"X.Y-arm64.exe -X fake_arg_for_test -prearg {script} -postarg", data["stdout"].strip())
577
578 def test_py_shebang_nl(self):
579 with self.py_ini(TEST_PY_DEFAULTS):
580 with self.script("#! /usr/bin/python -prearg\n") as script:
581 data = self.run_py([script, "-postarg"])
582 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
583 self.assertEqual("3.100", data["SearchInfo.tag"])
584 self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
585
586 def test_py2_shebang_nl(self):
587 with self.py_ini(TEST_PY_DEFAULTS):
588 with self.script("#! /usr/bin/python2 -prearg\n") as script:
589 data = self.run_py([script, "-postarg"])
590 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
591 self.assertEqual("3.100-32", data["SearchInfo.tag"])
592 self.assertEqual(f"X.Y-32.exe -prearg {script} -postarg", data["stdout"].strip())
593
594 def test_py3_shebang_nl(self):
595 with self.py_ini(TEST_PY_DEFAULTS):
596 with self.script("#! /usr/bin/python3 -prearg\n") as script:
597 data = self.run_py([script, "-postarg"])
598 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
599 self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
600 self.assertEqual(f"X.Y-arm64.exe -X fake_arg_for_test -prearg {script} -postarg", data["stdout"].strip())
601
602 def test_py_shebang_short_argv0(self):
603 with self.py_ini(TEST_PY_DEFAULTS):
604 with self.script("#! /usr/bin/python -prearg") as script:
605 # Override argv to only pass "py.exe" as the command
606 data = self.run_py([script, "-postarg"], argv=f'"py.exe" "{script}" -postarg')
607 self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
608 self.assertEqual("3.100", data["SearchInfo.tag"])
609 self.assertEqual(f'X.Y.exe -prearg "{script}" -postarg', data["stdout"].strip())
610
611 def test_py_handle_64_in_ini(self):
612 with self.py_ini("\n".join(["[defaults]", "python=3.999-64"])):
613 # Expect this to fail, but should get oldStyleTag flipped on
614 data = self.run_py([], allow_fail=True, expect_returncode=103)
615 self.assertEqual("3.999-64", data["SearchInfo.tag"])
616 self.assertEqual("True", data["SearchInfo.oldStyleTag"])
617
618 def test_search_path(self):
619 stem = Path(sys.executable).stem
620 with self.py_ini(TEST_PY_DEFAULTS):
621 with self.script(f"#! /usr/bin/env {stem} -prearg") as script:
622 data = self.run_py(
623 [script, "-postarg"],
624 env={"PATH": f"{Path(sys.executable).parent};{os.getenv('PATH')}"},
625 )
626 self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())
627
628 def test_search_path_exe(self):
629 # Leave the .exe on the name to ensure we don't add it a second time
630 name = Path(sys.executable).name
631 with self.py_ini(TEST_PY_DEFAULTS):
632 with self.script(f"#! /usr/bin/env {name} -prearg") as script:
633 data = self.run_py(
634 [script, "-postarg"],
635 env={"PATH": f"{Path(sys.executable).parent};{os.getenv('PATH')}"},
636 )
637 self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())
638
639 def test_recursive_search_path(self):
640 stem = self.get_py_exe().stem
641 with self.py_ini(TEST_PY_DEFAULTS):
642 with self.script(f"#! /usr/bin/env {stem}") as script:
643 data = self.run_py(
644 [script],
645 env={"PATH": f"{self.get_py_exe().parent};{os.getenv('PATH')}"},
646 )
647 # The recursive search is ignored and we get normal "py" behavior
648 self.assertEqual(f"X.Y.exe {script}", data["stdout"].strip())
649
650 def test_install(self):
651 data = self.run_py(["-V:3.10"], env={"PYLAUNCHER_ALWAYS_INSTALL": "1"}, expect_returncode=111)
652 cmd = data["stdout"].strip()
653 # If winget is runnable, we should find it. Otherwise, we'll be trying
654 # to open the Store.
655 try:
656 subprocess.check_call(["winget.exe", "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
657 except FileNotFoundError:
658 self.assertIn("ms-windows-store://", cmd)
659 else:
660 self.assertIn("winget.exe", cmd)
661 # Both command lines include the store ID
662 self.assertIn("9PJPW5LDXLZ5", cmd)
663
664 def test_literal_shebang_absolute(self):
665 with self.script("#! C:/some_random_app -witharg") as script:
666 data = self.run_py([script])
667 self.assertEqual(
668 f"C:\\some_random_app -witharg {script}",
669 data["stdout"].strip(),
670 )
671
672 def test_literal_shebang_relative(self):
673 with self.script("#! ..\\some_random_app -witharg") as script:
674 data = self.run_py([script])
675 self.assertEqual(
676 f"{script.parent.parent}\\some_random_app -witharg {script}",
677 data["stdout"].strip(),
678 )
679
680 def test_literal_shebang_quoted(self):
681 with self.script('#! "some random app" -witharg') as script:
682 data = self.run_py([script])
683 self.assertEqual(
684 f'"{script.parent}\\some random app" -witharg {script}',
685 data["stdout"].strip(),
686 )
687
688 with self.script('#! some" random "app -witharg') as script:
689 data = self.run_py([script])
690 self.assertEqual(
691 f'"{script.parent}\\some random app" -witharg {script}',
692 data["stdout"].strip(),
693 )
694
695 def test_literal_shebang_quoted_escape(self):
696 with self.script('#! some\\" random "app -witharg') as script:
697 data = self.run_py([script])
698 self.assertEqual(
699 f'"{script.parent}\\some\\ random app" -witharg {script}',
700 data["stdout"].strip(),
701 )
702
703 def test_literal_shebang_command(self):
704 with self.py_ini(TEST_PY_COMMANDS):
705 with self.script('#! test-command arg1') as script:
706 data = self.run_py([script])
707 self.assertEqual(
708 f"TEST_EXE.exe arg1 {script}",
709 data["stdout"].strip(),
710 )
711
712 def test_literal_shebang_invalid_template(self):
713 with self.script('#! /usr/bin/not-python arg1') as script:
714 data = self.run_py([script])
715 expect = script.parent / "/usr/bin/not-python"
716 self.assertEqual(
717 f"{expect} arg1 {script}",
718 data["stdout"].strip(),
719 )