python (3.12.0)

(root)/
lib/
python3.12/
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 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          )