Skip to content

Commit

Permalink
gh-127221: Add colour to unittest output (#127223)
Browse files Browse the repository at this point in the history
Co-authored-by: Kirill Podoprigora <[email protected]>
  • Loading branch information
hugovk and Eclips4 authored Dec 5, 2024
1 parent d958d9f commit 23f2e8f
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 60 deletions.
7 changes: 7 additions & 0 deletions Doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@
.. |python_version_literal| replace:: ``Python {version}``
.. |python_x_dot_y_literal| replace:: ``python{version}``
.. |usr_local_bin_python_x_dot_y_literal| replace:: ``/usr/local/bin/python{version}``
.. Apparently this how you hack together a formatted link:
(https://www.docutils.org/docs/ref/rst/directives.html#replacement-text)
.. |FORCE_COLOR| replace:: ``FORCE_COLOR``
.. _FORCE_COLOR: https://force-color.org/
.. |NO_COLOR| replace:: ``NO_COLOR``
.. _NO_COLOR: https://no-color.org/
"""

# There are two options for replacing |today|. Either, you set today to some
Expand Down
4 changes: 4 additions & 0 deletions Doc/library/doctest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ examples of doctests in the standard Python test suite and libraries.
Especially useful examples can be found in the standard test file
:file:`Lib/test/test_doctest/test_doctest.py`.

.. versionadded:: 3.13
Output is colorized by default and can be
:ref:`controlled using environment variables <using-on-controlling-color>`.


.. _doctest-simple-testmod:

Expand Down
4 changes: 4 additions & 0 deletions Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ The module's API can be divided into two parts:
necessary for later formatting without holding references to actual exception
and traceback objects.

.. versionadded:: 3.13
Output is colorized by default and can be
:ref:`controlled using environment variables <using-on-controlling-color>`.


Module-Level Functions
----------------------
Expand Down
4 changes: 3 additions & 1 deletion Doc/library/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ test runner
a textual interface, or return a special value to indicate the results of
executing the tests.


.. seealso::

Module :mod:`doctest`
Expand Down Expand Up @@ -198,6 +197,9 @@ For a list of all the command-line options::
In earlier versions it was only possible to run individual test methods and
not modules or classes.

.. versionadded:: 3.14
Output is colorized by default and can be
:ref:`controlled using environment variables <using-on-controlling-color>`.

Command-line options
~~~~~~~~~~~~~~~~~~~~
Expand Down
8 changes: 0 additions & 8 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -663,14 +663,6 @@ output. To control the color output only in the Python interpreter, the
precedence over ``NO_COLOR``, which in turn takes precedence over
``FORCE_COLOR``.

.. Apparently this how you hack together a formatted link:
.. |FORCE_COLOR| replace:: ``FORCE_COLOR``
.. _FORCE_COLOR: https://force-color.org/

.. |NO_COLOR| replace:: ``NO_COLOR``
.. _NO_COLOR: https://no-color.org/

Options you shouldn't use
~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
9 changes: 0 additions & 9 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,6 @@ Improved error messages
the canonical |NO_COLOR|_ and |FORCE_COLOR|_ environment variables.
(Contributed by Pablo Galindo Salgado in :gh:`112730`.)

.. Apparently this how you hack together a formatted link:
(https://www.docutils.org/docs/ref/rst/directives.html#replacement-text)
.. |FORCE_COLOR| replace:: ``FORCE_COLOR``
.. _FORCE_COLOR: https://force-color.org/

.. |NO_COLOR| replace:: ``NO_COLOR``
.. _NO_COLOR: https://no-color.org/

* A common mistake is to write a script with the same name as a
standard library module. When this results in errors, we now
display a more helpful error message:
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,13 @@ unicodedata
unittest
--------

* :mod:`unittest` output is now colored by default.
This can be controlled via the :envvar:`PYTHON_COLORS` environment
variable as well as the canonical |NO_COLOR|_
and |FORCE_COLOR|_ environment variables.
See also :ref:`using-on-controlling-color`.
(Contributed by Hugo van Kemenade in :gh:`127221`.)

* unittest discovery supports :term:`namespace package` as start
directory again. It was removed in Python 3.11.
(Contributed by Jacob Walls in :gh:`80958`.)
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_unittest/test_async_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import contextvars
import unittest
from test import support
from test.support import force_not_colorized

support.requires_working_socket(module=True)

Expand Down Expand Up @@ -252,6 +253,7 @@ async def on_cleanup(self):
test.doCleanups()
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup'])

@force_not_colorized
def test_exception_in_tear_clean_up(self):
class Test(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_unittest/test_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from test import support
import unittest
import test.test_unittest
from test.support import force_not_colorized
from test.test_unittest.test_result import BufferedWriter


Expand Down Expand Up @@ -120,6 +121,7 @@ def run(self, test):
self.assertEqual(['test.test_unittest', 'test.test_unittest2'],
program.testNames)

@force_not_colorized
def test_NonExit(self):
stream = BufferedWriter()
program = unittest.main(exit=False,
Expand All @@ -135,6 +137,7 @@ def test_NonExit(self):
'expected failures=1, unexpected successes=1)\n')
self.assertTrue(out.endswith(expected))

@force_not_colorized
def test_Exit(self):
stream = BufferedWriter()
with self.assertRaises(SystemExit) as cm:
Expand All @@ -152,6 +155,7 @@ def test_Exit(self):
'expected failures=1, unexpected successes=1)\n')
self.assertTrue(out.endswith(expected))

@force_not_colorized
def test_ExitAsDefault(self):
stream = BufferedWriter()
with self.assertRaises(SystemExit):
Expand All @@ -167,6 +171,7 @@ def test_ExitAsDefault(self):
'expected failures=1, unexpected successes=1)\n')
self.assertTrue(out.endswith(expected))

@force_not_colorized
def test_ExitSkippedSuite(self):
stream = BufferedWriter()
with self.assertRaises(SystemExit) as cm:
Expand All @@ -179,6 +184,7 @@ def test_ExitSkippedSuite(self):
expected = '\n\nOK (skipped=1)\n'
self.assertTrue(out.endswith(expected))

@force_not_colorized
def test_ExitEmptySuite(self):
stream = BufferedWriter()
with self.assertRaises(SystemExit) as cm:
Expand Down
16 changes: 15 additions & 1 deletion Lib/test/test_unittest/test_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import traceback
import unittest
from unittest.util import strclass
from test.support import force_not_colorized
from test.test_unittest.support import BufferedWriter


class MockTraceback(object):
class TracebackException:
def __init__(self, *args, **kwargs):
self.capture_locals = kwargs.get('capture_locals', False)
def format(self):
def format(self, **kwargs):
result = ['A traceback']
if self.capture_locals:
result.append('locals')
Expand Down Expand Up @@ -205,6 +206,7 @@ def test_1(self):
self.assertIs(test_case, test)
self.assertIsInstance(formatted_exc, str)

@force_not_colorized
def test_addFailure_filter_traceback_frames(self):
class Foo(unittest.TestCase):
def test_1(self):
Expand All @@ -231,6 +233,7 @@ def get_exc_info():
self.assertEqual(len(dropped), 1)
self.assertIn("raise self.failureException(msg)", dropped[0])

@force_not_colorized
def test_addFailure_filter_traceback_frames_context(self):
class Foo(unittest.TestCase):
def test_1(self):
Expand Down Expand Up @@ -260,6 +263,7 @@ def get_exc_info():
self.assertEqual(len(dropped), 1)
self.assertIn("raise self.failureException(msg)", dropped[0])

@force_not_colorized
def test_addFailure_filter_traceback_frames_chained_exception_self_loop(self):
class Foo(unittest.TestCase):
def test_1(self):
Expand All @@ -285,6 +289,7 @@ def get_exc_info():
formatted_exc = result.failures[0][1]
self.assertEqual(formatted_exc.count("Exception: Loop\n"), 1)

@force_not_colorized
def test_addFailure_filter_traceback_frames_chained_exception_cycle(self):
class Foo(unittest.TestCase):
def test_1(self):
Expand Down Expand Up @@ -446,6 +451,7 @@ def testFailFast(self):
result.addUnexpectedSuccess(None)
self.assertTrue(result.shouldStop)

@force_not_colorized
def testFailFastSetByRunner(self):
stream = BufferedWriter()
runner = unittest.TextTestRunner(stream=stream, failfast=True)
Expand Down Expand Up @@ -619,6 +625,7 @@ def _run_test(self, test_name, verbosity, tearDownError=None):
test.run(result)
return stream.getvalue()

@force_not_colorized
def testDotsOutput(self):
self.assertEqual(self._run_test('testSuccess', 1), '.')
self.assertEqual(self._run_test('testSkip', 1), 's')
Expand All @@ -627,6 +634,7 @@ def testDotsOutput(self):
self.assertEqual(self._run_test('testExpectedFailure', 1), 'x')
self.assertEqual(self._run_test('testUnexpectedSuccess', 1), 'u')

@force_not_colorized
def testLongOutput(self):
classname = f'{__name__}.{self.Test.__qualname__}'
self.assertEqual(self._run_test('testSuccess', 2),
Expand All @@ -642,17 +650,21 @@ def testLongOutput(self):
self.assertEqual(self._run_test('testUnexpectedSuccess', 2),
f'testUnexpectedSuccess ({classname}.testUnexpectedSuccess) ... unexpected success\n')

@force_not_colorized
def testDotsOutputSubTestSuccess(self):
self.assertEqual(self._run_test('testSubTestSuccess', 1), '.')

@force_not_colorized
def testLongOutputSubTestSuccess(self):
classname = f'{__name__}.{self.Test.__qualname__}'
self.assertEqual(self._run_test('testSubTestSuccess', 2),
f'testSubTestSuccess ({classname}.testSubTestSuccess) ... ok\n')

@force_not_colorized
def testDotsOutputSubTestMixed(self):
self.assertEqual(self._run_test('testSubTestMixed', 1), 'sFE')

@force_not_colorized
def testLongOutputSubTestMixed(self):
classname = f'{__name__}.{self.Test.__qualname__}'
self.assertEqual(self._run_test('testSubTestMixed', 2),
Expand All @@ -661,6 +673,7 @@ def testLongOutputSubTestMixed(self):
f' testSubTestMixed ({classname}.testSubTestMixed) [fail] (c=3) ... FAIL\n'
f' testSubTestMixed ({classname}.testSubTestMixed) [error] (d=4) ... ERROR\n')

@force_not_colorized
def testDotsOutputTearDownFail(self):
out = self._run_test('testSuccess', 1, AssertionError('fail'))
self.assertEqual(out, 'F')
Expand All @@ -671,6 +684,7 @@ def testDotsOutputTearDownFail(self):
out = self._run_test('testSkip', 1, AssertionError('fail'))
self.assertEqual(out, 'sF')

@force_not_colorized
def testLongOutputTearDownFail(self):
classname = f'{__name__}.{self.Test.__qualname__}'
out = self._run_test('testSuccess', 2, AssertionError('fail'))
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_unittest/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pickle
import subprocess
from test import support
from test.support import force_not_colorized

import unittest
from unittest.case import _Outcome
Expand Down Expand Up @@ -106,6 +107,7 @@ def cleanup2(*args, **kwargs):
self.assertTrue(test.doCleanups())
self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))])

@force_not_colorized
def testCleanUpWithErrors(self):
class TestableTest(unittest.TestCase):
def testNothing(self):
Expand Down Expand Up @@ -416,6 +418,7 @@ def cleanup2():
self.assertIsInstance(e2[1], CustomError)
self.assertEqual(str(e2[1]), 'cleanup1')

@force_not_colorized
def test_with_errors_addCleanUp(self):
ordering = []
class TestableTest(unittest.TestCase):
Expand All @@ -439,6 +442,7 @@ def tearDownClass(cls):
['setUpClass', 'setUp', 'cleanup_exc',
'tearDownClass', 'cleanup_good'])

@force_not_colorized
def test_run_with_errors_addClassCleanUp(self):
ordering = []
class TestableTest(unittest.TestCase):
Expand All @@ -462,6 +466,7 @@ def tearDownClass(cls):
['setUpClass', 'setUp', 'test', 'cleanup_good',
'tearDownClass', 'cleanup_exc'])

@force_not_colorized
def test_with_errors_in_addClassCleanup_and_setUps(self):
ordering = []
class_blow_up = False
Expand Down Expand Up @@ -514,6 +519,7 @@ def tearDownClass(cls):
['setUpClass', 'setUp', 'tearDownClass',
'cleanup_exc'])

@force_not_colorized
def test_with_errors_in_tearDownClass(self):
ordering = []
class TestableTest(unittest.TestCase):
Expand Down Expand Up @@ -590,6 +596,7 @@ def test(self):
'inner setup', 'inner test', 'inner cleanup',
'end outer test', 'outer cleanup'])

@force_not_colorized
def test_run_empty_suite_error_message(self):
class EmptyTest(unittest.TestCase):
pass
Expand Down Expand Up @@ -663,6 +670,7 @@ class Module(object):
self.assertEqual(cleanups,
[((1, 2), {'function': 'hello'})])

@force_not_colorized
def test_run_module_cleanUp(self):
blowUp = True
ordering = []
Expand Down Expand Up @@ -802,6 +810,7 @@ def tearDownClass(cls):
'tearDownClass', 'cleanup_good'])
self.assertEqual(unittest.case._module_cleanups, [])

@force_not_colorized
def test_run_module_cleanUp_when_teardown_exception(self):
ordering = []
class Module(object):
Expand Down Expand Up @@ -963,6 +972,7 @@ def testNothing(self):
self.assertEqual(cleanups,
[((1, 2), {'function': 3, 'self': 4})])

@force_not_colorized
def test_with_errors_in_addClassCleanup(self):
ordering = []

Expand Down Expand Up @@ -996,6 +1006,7 @@ def tearDownClass(cls):
['setUpModule', 'setUpClass', 'test', 'tearDownClass',
'cleanup_exc', 'tearDownModule', 'cleanup_good'])

@force_not_colorized
def test_with_errors_in_addCleanup(self):
ordering = []
class Module(object):
Expand Down Expand Up @@ -1026,6 +1037,7 @@ def tearDown(self):
['setUpModule', 'setUp', 'test', 'tearDown',
'cleanup_exc', 'tearDownModule', 'cleanup_good'])

@force_not_colorized
def test_with_errors_in_addModuleCleanup_and_setUps(self):
ordering = []
module_blow_up = False
Expand Down Expand Up @@ -1318,6 +1330,7 @@ def MockResultClass(*args):
expectedresult = (runner.stream, DESCRIPTIONS, VERBOSITY)
self.assertEqual(runner._makeResult(), expectedresult)

@force_not_colorized
@support.requires_subprocess()
def test_warnings(self):
"""
Expand Down
Loading

0 comments on commit 23f2e8f

Please sign in to comment.