1import contextlib 2import itertools 3import os 4import re 5import shutil 6import subprocess 7import sys 8import sysconfig 9import tempfile 10import textwrap 11import unittest 12from pathlib import Path 13from test import support 14 15if sys.platform != "win32": 16 raise unittest.SkipTest("test only applies to Windows") 17 18# Get winreg after the platform check 19import winreg 20 21 22PY_EXE = "py.exe" 23if 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. 28TEST_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 74TEST_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 81TEST_PY_DEFAULTS = "\n".join([ 82 "[defaults]", 83 *[f"{k[3:].lower()}={v}" for k, v in TEST_PY_ENV.items()], 84]) 85 86 87TEST_PY_COMMANDS = "\n".join([ 88 "[commands]", 89 "test-command=TEST_EXE.exe", 90]) 91 92def 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 109def 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 119def 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 127def 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 142class PreservePyIni: 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 162class RunPyMixin: 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 285class TestLauncher(unittest.TestCase, RunPyMixin): 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 ) 721