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