(root)/
Python-3.11.7/
Lib/
test/
test_launcher.py
       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          )