Skip to content
49 changes: 49 additions & 0 deletions Lib/test/support/script_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@

import collections
import importlib
import site
import stat
import sys
import os
import os.path
import subprocess
import py_compile
import unittest
import unittest.mock as mock

from importlib.util import source_from_cache
from test import support
Expand Down Expand Up @@ -322,3 +326,48 @@ def title(text):
raise AssertionError(f"{name} failed")
else:
assert_python_ok("-u", script, "-v")


_site_gethistoryfile = site.gethistoryfile
def _gethistoryfile():
"""Patch site.gethistoryfile() to ignore -I for PYTHON_HISTORY.

The -I option is necessary for test_no_memory() but using it
forbids using a custom PYTHON_HISTORY.
"""
history = os.environ.get("PYTHON_HISTORY")
return history or os.path.join(os.path.expanduser('~'), '.python_history')


def patch_gethistoryfile(sitemodule=site):
return mock.patch.object(sitemodule, "gethistoryfile", _gethistoryfile)


def _file_signature(file):
st = os.stat(file)
return (stat.S_IFMT(st.st_mode), st.st_size)


class EnsureSafeUserHistory(unittest.TestCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.__history_file = cls.__history_stat = None
history_file = _site_gethistoryfile()
if os.path.exists(history_file):
cls.__history_file = history_file
cls.__history_stat = _file_signature(history_file)

def tearDown(self):
if self.__history_file is not None:
self.assertTrue(
os.path.exists(self.__history_file),
f"PYTHON_HISTORY file ({self.__history_file!r}) was deleted"
)
self.assertEqual(
self.__history_stat,
_file_signature(self.__history_file),
f"PYTHON_HISTORY file ({self.__history_file!r}) was altered"
)
super().tearDown()
7 changes: 5 additions & 2 deletions Lib/test/test_cmd_line_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from test.support import import_helper, is_apple, os_helper
from test.support.script_helper import (
make_pkg, make_script, make_zip_pkg, make_zip_script,
assert_python_ok, assert_python_failure, spawn_python, kill_python)
assert_python_ok, assert_python_failure, spawn_python, kill_python,
patch_gethistoryfile, EnsureSafeUserHistory
)

verbose = support.verbose

Expand Down Expand Up @@ -90,7 +92,8 @@ def _make_test_zip_pkg(zip_dir, zip_basename, pkg_name, script_basename,


@support.force_not_colorized_test_class
class CmdLineTest(unittest.TestCase):
@patch_gethistoryfile()
class CmdLineTest(EnsureSafeUserHistory, unittest.TestCase):
def _check_output(self, script_name, exit_code, data,
expected_file, expected_argv0,
expected_path0, expected_package,
Expand Down
19 changes: 12 additions & 7 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from test.support import has_subprocess_support, SHORT_TIMEOUT, STDLIB_DIR
from test.support.import_helper import import_module
from test.support.os_helper import EnvironmentVarGuard, unlink
from test.support.script_helper import EnsureSafeUserHistory

from .support import (
FakeConsole,
Expand Down Expand Up @@ -45,7 +46,7 @@
pty = None


class ReplTestCase(TestCase):
class ReplTestCase(EnsureSafeUserHistory, TestCase):
def setUp(self):
if not has_subprocess_support:
raise SkipTest("test module requires subprocess")
Expand All @@ -68,7 +69,7 @@ def run_repl(
try:
return self._run_repl(
repl_input,
env=env,
env=os.environ.copy() if env is None else env,
cmdline_args=cmdline_args,
cwd=cwd,
skip=skip,
Expand All @@ -83,7 +84,7 @@ def _run_repl(
self,
repl_input: str | list[str],
*,
env: dict | None,
env: dict[str, str],
cmdline_args: list[str] | None,
cwd: str,
skip: bool,
Expand All @@ -93,11 +94,15 @@ def _run_repl(
assert pty
master_fd, slave_fd = pty.openpty()
cmd = [sys.executable, "-i", "-u"]
if env is None:
cmd.append("-I")
elif "PYTHON_HISTORY" not in env:
if "PYTHON_HISTORY" not in env:
env["PYTHON_HISTORY"] = os.path.join(cwd, ".regrtest_history")
if cmdline_args is not None:
if "PYTHON_HISTORY" in env:
for bad_option in ('-I', '-E'):
self.assertNotIn(
bad_option, cmdline_args,
f"PYTHON_HISTORY will be ignored by {bad_option}"
)
cmd.extend(cmdline_args)

try:
Expand All @@ -118,7 +123,7 @@ def _run_repl(
cwd=cwd,
text=True,
close_fds=True,
env=env if env else os.environ,
env=env,
)
os.close(slave_fd)
if isinstance(repl_input, list):
Expand Down
70 changes: 36 additions & 34 deletions Lib/test/test_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
SuppressCrashReport,
SHORT_TIMEOUT,
)
from test.support.script_helper import kill_python
from test.support.script_helper import kill_python, EnsureSafeUserHistory
from test.support.import_helper import import_module

try:
Expand Down Expand Up @@ -71,7 +71,7 @@ def run_on_interactive_mode(source):


@support.force_not_colorized_test_class
class TestInteractiveInterpreter(unittest.TestCase):
class TestInteractiveInterpreter(EnsureSafeUserHistory, unittest.TestCase):

@cpython_only
# Python built with Py_TRACE_REFS fail with a fatal error in
Expand Down Expand Up @@ -303,42 +303,44 @@ def test_asyncio_repl_reaches_python_startup_script(self):

@unittest.skipUnless(pty, "requires pty")
def test_asyncio_repl_is_ok(self):
m, s = pty.openpty()
cmd = [sys.executable, "-I", "-m", "asyncio"]
env = os.environ.copy()
proc = subprocess.Popen(
cmd,
stdin=s,
stdout=s,
stderr=s,
text=True,
close_fds=True,
env=env,
)
os.close(s)
os.write(m, b"await asyncio.sleep(0)\n")
os.write(m, b"exit()\n")
output = []
while select.select([m], [], [], SHORT_TIMEOUT)[0]:
try:
data = os.read(m, 1024).decode("utf-8")
if not data:
with os_helper.temp_dir() as tmpdir:
m, s = pty.openpty()
cmd = [sys.executable, "-m", "asyncio"]
env = os.environ.copy()
env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history")
proc = subprocess.Popen(
cmd,
stdin=s,
stdout=s,
stderr=s,
text=True,
close_fds=True,
env=env,
)
os.close(s)
os.write(m, b"await asyncio.sleep(0)\n")
os.write(m, b"exit()\n")
output = []
while select.select([m], [], [], SHORT_TIMEOUT)[0]:
try:
data = os.read(m, 1024).decode("utf-8")
if not data:
break
except OSError:
break
except OSError:
break
output.append(data)
os.close(m)
try:
exit_code = proc.wait(timeout=SHORT_TIMEOUT)
except subprocess.TimeoutExpired:
proc.kill()
exit_code = proc.wait()
output.append(data)
os.close(m)
try:
exit_code = proc.wait(timeout=SHORT_TIMEOUT)
except subprocess.TimeoutExpired:
proc.kill()
exit_code = proc.wait()

self.assertEqual(exit_code, 0, "".join(output))
self.assertEqual(exit_code, 0, "".join(output))


@support.force_not_colorized_test_class
class TestInteractiveModeSyntaxErrors(unittest.TestCase):
class TestInteractiveModeSyntaxErrors(EnsureSafeUserHistory, unittest.TestCase):

def test_interactive_syntax_error_correct_line(self):
output = run_on_interactive_mode(dedent("""\
Expand All @@ -356,7 +358,7 @@ def f():
self.assertEqual(traceback_lines, expected_lines)


class TestAsyncioREPL(unittest.TestCase):
class TestAsyncioREPL(EnsureSafeUserHistory, unittest.TestCase):
def test_multiple_statements_fail_early(self):
user_input = "1 / 0; print(f'afterwards: {1+1}')"
p = spawn_repl("-m", "asyncio")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Prevent altering the content of the :envvar:`PYTHON_HISTORY` file after
running REPL tests. Patch by Bénédikt Tran.
Loading