Skip to content

gh-112730: Make the test suite resilient to color-activation environment variables #117672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/reusable-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-20.04
env:
FORCE_COLOR: 1
OPENSSL_VER: 3.0.13
PYTHONSTRICTEXTENSIONBUILD: 1
steps:
Expand Down
10 changes: 9 additions & 1 deletion Lib/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1540,7 +1540,11 @@ def out(s):
# Make sure sys.displayhook just prints the value to stdout
save_displayhook = sys.displayhook
sys.displayhook = sys.__displayhook__

saved_can_colorize = traceback._can_colorize
traceback._can_colorize = lambda: False
color_variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
for key in color_variables:
color_variables[key] = os.environ.pop(key, None)
try:
return self.__run(test, compileflags, out)
finally:
Expand All @@ -1549,6 +1553,10 @@ def out(s):
sys.settrace(save_trace)
linecache.getlines = self.save_linecache_getlines
sys.displayhook = save_displayhook
traceback._can_colorize = saved_can_colorize
for key, value in color_variables.items():
if value is not None:
os.environ[key] = value
if clear_globs:
test.globs.clear()
import builtins
Expand Down
3 changes: 3 additions & 0 deletions Lib/idlelib/idle_test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from unittest import mock
import idlelib
from idlelib.idle_test.mock_idle import Func
from test.support import force_not_colorized

idlelib.testing = True # Use {} for executing test user code.

Expand Down Expand Up @@ -46,6 +47,7 @@ def __eq__(self, other):
"Did you mean: 'real'?\n"),
)

@force_not_colorized
def test_get_message(self):
for code, exc, msg in self.data:
with self.subTest(code=code):
Expand All @@ -57,6 +59,7 @@ def test_get_message(self):
expect = f'{exc.__name__}: {msg}'
self.assertEqual(actual, expect)

@force_not_colorized
@mock.patch.object(run, 'cleanup_traceback',
new_callable=lambda: (lambda t, e: None))
def test_get_multiple_message(self, mock):
Expand Down
22 changes: 21 additions & 1 deletion Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
"LOOPBACK_TIMEOUT", "INTERNET_TIMEOUT", "SHORT_TIMEOUT", "LONG_TIMEOUT",
"Py_DEBUG", "EXCEEDS_RECURSION_LIMIT", "Py_C_RECURSION_LIMIT",
"skip_on_s390x",
"without_optimizer",
"without_optimizer", "reset_colorized_globally",
"force_not_colorized"
]


Expand Down Expand Up @@ -2555,3 +2556,22 @@ def copy_python_src_ignore(path, names):
'build',
}
return ignored

def force_not_colorized(func):
"""Force the terminal not to be colorized."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
import traceback
original_fn = traceback._can_colorize
variables = {"PYTHON_COLORS": None, "FORCE_COLOR": None}
try:
for key in variables:
variables[key] = os.environ.pop(key, None)
traceback._can_colorize = lambda: False
return func(*args, **kwargs)
finally:
traceback._can_colorize = original_fn
for key, value in variables.items():
if value is not None:
os.environ[key] = value
return wrapper
4 changes: 4 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from test.support import import_helper
from test.support import threading_helper
from test.support import warnings_helper
from test.support import force_not_colorized
from test.support import requires_limited_api
from test.support.script_helper import assert_python_failure, assert_python_ok, run_python_until_end
try:
Expand Down Expand Up @@ -205,6 +206,7 @@ def test_c_type_with_ipow(self):
self.assertEqual(o.__ipow__(1), (1, None))
self.assertEqual(o.__ipow__(2, 2), (2, 2))

@force_not_colorized
def test_return_null_without_error(self):
# Issue #23571: A function must not return NULL without setting an
# error
Expand Down Expand Up @@ -234,6 +236,7 @@ def test_return_null_without_error(self):
'return_null_without_error.* '
'returned NULL without setting an exception')

@force_not_colorized
def test_return_result_with_error(self):
# Issue #23571: A function must not return a result with an error set
if support.Py_DEBUG:
Expand Down Expand Up @@ -268,6 +271,7 @@ def test_return_result_with_error(self):
'return_result_with_error.* '
'returned a result with an exception set')

@force_not_colorized
def test_getitem_with_error(self):
# Test _Py_CheckSlotResult(). Raise an exception and then calls
# PyObject_GetItem(): check that the assertion catches the bug.
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import unittest
from test import support
from test.support import os_helper
from test.support import force_not_colorized
from test.support.script_helper import (
spawn_python, kill_python, assert_python_ok, assert_python_failure,
interpreter_requires_environment
Expand Down Expand Up @@ -1027,6 +1028,7 @@ def test_sys_flags_not_set(self):


class SyntaxErrorTests(unittest.TestCase):
@force_not_colorized
def check_string(self, code):
proc = subprocess.run([sys.executable, "-"], input=code,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
Expand Down
12 changes: 11 additions & 1 deletion Lib/test/test_cmd_line_script.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's enough decorators in this test file I suggest just disabling it via setUpModule / tearDownModule instead.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import textwrap
from test import support
from test.support import import_helper, is_apple, os_helper
from test.support import import_helper, is_apple, os_helper, force_not_colorized
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)
Expand Down Expand Up @@ -195,6 +195,7 @@ def check_repl_stdout_flush(self, separate_stderr=False):
p.stdin.flush()
self.assertEqual(b'foo', p.stdout.readline().strip())

@force_not_colorized
def check_repl_stderr_flush(self, separate_stderr=False):
with self.interactive_python(separate_stderr) as p:
p.stdin.write(b"1/0\n")
Expand Down Expand Up @@ -537,6 +538,7 @@ def test_dash_m_main_traceback(self):
self.assertIn(b'Exception in __main__ module', err)
self.assertIn(b'Traceback', err)

@force_not_colorized
def test_pep_409_verbiage(self):
# Make sure PEP 409 syntax properly suppresses
# the context of an exception
Expand Down Expand Up @@ -602,6 +604,7 @@ def test_issue20500_exit_with_exception_value(self):
text = stderr.decode('ascii')
self.assertEqual(text.rstrip(), "some text")

@force_not_colorized
def test_syntaxerror_unindented_caret_position(self):
script = "1 + 1 = 2\n"
with os_helper.temp_dir() as script_dir:
Expand All @@ -611,6 +614,7 @@ def test_syntaxerror_unindented_caret_position(self):
# Confirm that the caret is located under the '=' sign
self.assertIn("\n ^^^^^\n", text)

@force_not_colorized
def test_syntaxerror_indented_caret_position(self):
script = textwrap.dedent("""\
if True:
Expand All @@ -634,6 +638,7 @@ def test_syntaxerror_indented_caret_position(self):
self.assertNotIn("\f", text)
self.assertIn("\n 1 + 1 = 2\n ^^^^^\n", text)

@force_not_colorized
def test_syntaxerror_multi_line_fstring(self):
script = 'foo = f"""{}\nfoo"""\n'
with os_helper.temp_dir() as script_dir:
Expand All @@ -648,6 +653,7 @@ def test_syntaxerror_multi_line_fstring(self):
],
)

@force_not_colorized
def test_syntaxerror_invalid_escape_sequence_multi_line(self):
script = 'foo = """\\q"""\n'
with os_helper.temp_dir() as script_dir:
Expand All @@ -663,6 +669,7 @@ def test_syntaxerror_invalid_escape_sequence_multi_line(self):
],
)

@force_not_colorized
def test_syntaxerror_null_bytes(self):
script = "x = '\0' nothing to see here\n';import os;os.system('echo pwnd')\n"
with os_helper.temp_dir() as script_dir:
Expand All @@ -675,6 +682,7 @@ def test_syntaxerror_null_bytes(self):
],
)

@force_not_colorized
def test_syntaxerror_null_bytes_in_multiline_string(self):
scripts = ["\n'''\nmultilinestring\0\n'''", "\nf'''\nmultilinestring\0\n'''"] # Both normal and f-strings
with os_helper.temp_dir() as script_dir:
Expand All @@ -688,6 +696,7 @@ def test_syntaxerror_null_bytes_in_multiline_string(self):
]
)

@force_not_colorized
def test_source_lines_are_shown_when_running_source(self):
_, _, stderr = assert_python_failure("-c", "1/0")
expected_lines = [
Expand All @@ -698,6 +707,7 @@ def test_source_lines_are_shown_when_running_source(self):
b'ZeroDivisionError: division by zero']
self.assertEqual(stderr.splitlines(), expected_lines)

@force_not_colorized
def test_syntaxerror_does_not_crash(self):
script = "nonlocal x\n"
with os_helper.temp_dir() as script_dir:
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_compileall.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from test import support
from test.support import os_helper
from test.support import script_helper
from test.support import force_not_colorized
from test.test_py_compile import without_source_date_epoch
from test.test_py_compile import SourceDateEpochTestMeta

Expand Down Expand Up @@ -760,6 +761,7 @@ def test_d_compile_error(self):
rc, out, err = self.assertRunNotOK('-q', '-d', 'dinsdale', self.pkgdir)
self.assertRegex(out, b'File "dinsdale')

@force_not_colorized
def test_d_runtime_error(self):
bazfn = script_helper.make_script(self.pkgdir, 'baz', 'raise Exception')
self.assertRunOK('-q', '-d', 'dinsdale', self.pkgdir)
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_eof.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from test.support import os_helper
from test.support import script_helper
from test.support import warnings_helper
from test.support import force_not_colorized
import unittest

class EOFTestCase(unittest.TestCase):
Expand Down Expand Up @@ -58,6 +59,7 @@ def test_line_continuation_EOF(self):
self.assertEqual(str(excinfo.exception), expect)

@unittest.skipIf(not sys.executable, "sys.executable required")
@force_not_colorized
def test_line_continuation_EOF_from_file_bpo2180(self):
"""Ensure tok_nextc() does not add too many ending newlines."""
with os_helper.temp_dir() as temp_dir:
Expand Down
10 changes: 9 additions & 1 deletion Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from test.support import (captured_stderr, check_impl_detail,
cpython_only, gc_collect,
no_tracing, script_helper,
SuppressCrashReport)
SuppressCrashReport,
force_not_colorized)
from test.support.import_helper import import_module
from test.support.os_helper import TESTFN, unlink
from test.support.warnings_helper import check_warnings
Expand Down Expand Up @@ -41,6 +42,7 @@ def __str__(self):

# XXX This is not really enough, each *operation* should be tested!


class ExceptionTests(unittest.TestCase):

def raise_catch(self, exc, excname):
Expand Down Expand Up @@ -1438,6 +1440,7 @@ def gen():

@cpython_only
@unittest.skipIf(_testcapi is None, "requires _testcapi")
@force_not_colorized
def test_recursion_normalizing_infinite_exception(self):
# Issue #30697. Test that a RecursionError is raised when
# maximum recursion depth has been exceeded when creating
Expand Down Expand Up @@ -1993,6 +1996,7 @@ def write_source(self, source):
_rc, _out, err = script_helper.assert_python_failure('-Wd', '-X', 'utf8', TESTFN)
return err.decode('utf-8').splitlines()

@force_not_colorized
def test_assertion_error_location(self):
cases = [
('assert None',
Expand Down Expand Up @@ -2069,6 +2073,7 @@ def test_assertion_error_location(self):
result = self.write_source(source)
self.assertEqual(result[-3:], expected)

@force_not_colorized
def test_multiline_not_highlighted(self):
cases = [
("""
Expand Down Expand Up @@ -2101,6 +2106,7 @@ def test_multiline_not_highlighted(self):


class SyntaxErrorTests(unittest.TestCase):
@force_not_colorized
def test_range_of_offsets(self):
cases = [
# Basic range from 2->7
Expand Down Expand Up @@ -2191,6 +2197,7 @@ def test_range_of_offsets(self):
self.assertIn(expected, err.getvalue())
the_exception = exc

@force_not_colorized
def test_encodings(self):
source = (
'# -*- coding: cp437 -*-\n'
Expand Down Expand Up @@ -2220,6 +2227,7 @@ def test_encodings(self):
finally:
unlink(TESTFN)

@force_not_colorized
def test_non_utf8(self):
# Check non utf-8 characters
try:
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from test.support.os_helper import TESTFN, temp_cwd
from test.support.script_helper import assert_python_ok, assert_python_failure, kill_python
from test.support import has_subprocess_support, SuppressCrashReport
from test.support import force_not_colorized
from test import support

from test.test_inspect import inspect_fodder as mod
Expand Down Expand Up @@ -816,6 +817,7 @@ def test_getsource_on_code_object(self):
self.assertSourceEqual(mod.eggs.__code__, 12, 18)

class TestGetsourceInteractive(unittest.TestCase):
@force_not_colorized
def test_getclasses_interactive(self):
# bpo-44648: simulate a REPL session;
# there is no `__file__` in the __main__ module
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Raise SkipTest if subinterpreters not supported.
_interpreters = import_helper.import_module('_xxsubinterpreters')
from test.support import interpreters
from test.support import force_not_colorized
from test.support.interpreters import InterpreterNotFoundError
from .utils import _captured_script, _run_output, _running, TestBase

Expand Down Expand Up @@ -533,6 +534,7 @@ def test_failure(self):
with self.assertRaises(interpreters.ExecutionFailed):
interp.exec('raise Exception')

@force_not_colorized
def test_display_preserved_exception(self):
tempdir = self.temp_dir()
modfile = self.make_module('spam', tempdir, text="""
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_regrtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import unittest
from test import support
from test.support import os_helper, without_optimizer
from test.support import force_not_colorized
from test.libregrtest import cmdline
from test.libregrtest import main
from test.libregrtest import setup
Expand Down Expand Up @@ -1835,6 +1836,7 @@ def test_unraisable_exc(self):
self.assertIn("Warning -- Unraisable exception", output)
self.assertIn("Exception: weakref callback bug", output)

@force_not_colorized
def test_threading_excepthook(self):
# --fail-env-changed must catch uncaught thread exception.
# The exception must be displayed even if sys.stderr is redirected.
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from textwrap import dedent
from test import support
from test.support import cpython_only, has_subprocess_support, SuppressCrashReport
from test.support import force_not_colorized
from test.support.script_helper import kill_python
from test.support.import_helper import import_module

Expand All @@ -15,6 +16,7 @@
raise unittest.SkipTest("test module requires subprocess")


@force_not_colorized
def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw):
"""Run the Python REPL with the given arguments.

Expand Down Expand Up @@ -43,6 +45,7 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw):
stdout=stdout, stderr=stderr,
**kw)

@force_not_colorized
def run_on_interactive_mode(source):
"""Spawn a new Python interpreter, pass the given
input source code from the stdin and return the
Expand Down
Loading