mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
bpo-4080: unittest durations (#12271)
This commit is contained in:
parent
a0305c5fdf
commit
6883007a86
12 changed files with 227 additions and 34 deletions
|
@ -244,6 +244,10 @@ Command-line options
|
||||||
|
|
||||||
Show local variables in tracebacks.
|
Show local variables in tracebacks.
|
||||||
|
|
||||||
|
.. cmdoption:: --durations N
|
||||||
|
|
||||||
|
Show the N slowest test cases (N=0 for all).
|
||||||
|
|
||||||
.. versionadded:: 3.2
|
.. versionadded:: 3.2
|
||||||
The command-line options ``-b``, ``-c`` and ``-f`` were added.
|
The command-line options ``-b``, ``-c`` and ``-f`` were added.
|
||||||
|
|
||||||
|
@ -253,10 +257,12 @@ Command-line options
|
||||||
.. versionadded:: 3.7
|
.. versionadded:: 3.7
|
||||||
The command-line option ``-k``.
|
The command-line option ``-k``.
|
||||||
|
|
||||||
|
.. versionadded:: 3.12
|
||||||
|
The command-line option ``--durations``.
|
||||||
|
|
||||||
The command line can also be used for test discovery, for running all of the
|
The command line can also be used for test discovery, for running all of the
|
||||||
tests in a project or just a subset.
|
tests in a project or just a subset.
|
||||||
|
|
||||||
|
|
||||||
.. _unittest-test-discovery:
|
.. _unittest-test-discovery:
|
||||||
|
|
||||||
Test Discovery
|
Test Discovery
|
||||||
|
@ -2009,6 +2015,13 @@ Loading and running tests
|
||||||
A list containing :class:`TestCase` instances that were marked as expected
|
A list containing :class:`TestCase` instances that were marked as expected
|
||||||
failures, but succeeded.
|
failures, but succeeded.
|
||||||
|
|
||||||
|
.. attribute:: collectedDurations
|
||||||
|
|
||||||
|
A list containing 2-tuples of :class:`TestCase` instances and floats
|
||||||
|
representing the elapsed time of each test which was run.
|
||||||
|
|
||||||
|
.. versionadded:: 3.12
|
||||||
|
|
||||||
.. attribute:: shouldStop
|
.. attribute:: shouldStop
|
||||||
|
|
||||||
Set to ``True`` when the execution of tests should stop by :meth:`stop`.
|
Set to ``True`` when the execution of tests should stop by :meth:`stop`.
|
||||||
|
@ -2160,14 +2173,27 @@ Loading and running tests
|
||||||
|
|
||||||
.. versionadded:: 3.4
|
.. versionadded:: 3.4
|
||||||
|
|
||||||
|
.. method:: addDuration(test, elapsed)
|
||||||
|
|
||||||
.. class:: TextTestResult(stream, descriptions, verbosity)
|
Called when the test case finishes. *elapsed* is the time represented in
|
||||||
|
seconds, and it includes the execution of cleanup functions.
|
||||||
|
|
||||||
|
.. versionadded:: 3.12
|
||||||
|
|
||||||
|
.. class:: TextTestResult(stream, descriptions, verbosity, *, durations=None)
|
||||||
|
|
||||||
A concrete implementation of :class:`TestResult` used by the
|
A concrete implementation of :class:`TestResult` used by the
|
||||||
:class:`TextTestRunner`.
|
:class:`TextTestRunner`. Subclasses should accept ``**kwargs`` to ensure
|
||||||
|
compatibility as the interface changes.
|
||||||
|
|
||||||
.. versionadded:: 3.2
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
.. versionadded:: 3.12
|
||||||
|
Added *durations* keyword argument.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.12
|
||||||
|
Subclasses should accept ``**kwargs`` to ensure compatibility as the
|
||||||
|
interface changes.
|
||||||
|
|
||||||
.. data:: defaultTestLoader
|
.. data:: defaultTestLoader
|
||||||
|
|
||||||
|
@ -2177,7 +2203,8 @@ Loading and running tests
|
||||||
|
|
||||||
|
|
||||||
.. class:: TextTestRunner(stream=None, descriptions=True, verbosity=1, failfast=False, \
|
.. class:: TextTestRunner(stream=None, descriptions=True, verbosity=1, failfast=False, \
|
||||||
buffer=False, resultclass=None, warnings=None, *, tb_locals=False)
|
buffer=False, resultclass=None, warnings=None, *, \
|
||||||
|
tb_locals=False, durations=None)
|
||||||
|
|
||||||
A basic test runner implementation that outputs results to a stream. If *stream*
|
A basic test runner implementation that outputs results to a stream. If *stream*
|
||||||
is ``None``, the default, :data:`sys.stderr` is used as the output stream. This class
|
is ``None``, the default, :data:`sys.stderr` is used as the output stream. This class
|
||||||
|
@ -2195,14 +2222,17 @@ Loading and running tests
|
||||||
*warnings* to ``None``.
|
*warnings* to ``None``.
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
.. versionchanged:: 3.2
|
||||||
Added the ``warnings`` argument.
|
Added the *warnings* parameter.
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
.. versionchanged:: 3.2
|
||||||
The default stream is set to :data:`sys.stderr` at instantiation time rather
|
The default stream is set to :data:`sys.stderr` at instantiation time rather
|
||||||
than import time.
|
than import time.
|
||||||
|
|
||||||
.. versionchanged:: 3.5
|
.. versionchanged:: 3.5
|
||||||
Added the tb_locals parameter.
|
Added the *tb_locals* parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.12
|
||||||
|
Added the *durations* parameter.
|
||||||
|
|
||||||
.. method:: _makeResult()
|
.. method:: _makeResult()
|
||||||
|
|
||||||
|
|
|
@ -371,6 +371,27 @@ unicodedata
|
||||||
* The Unicode database has been updated to version 15.0.0. (Contributed by
|
* The Unicode database has been updated to version 15.0.0. (Contributed by
|
||||||
Benjamin Peterson in :gh:`96734`).
|
Benjamin Peterson in :gh:`96734`).
|
||||||
|
|
||||||
|
unittest
|
||||||
|
--------
|
||||||
|
|
||||||
|
Added ``--durations`` command line option, showing the N slowest test cases::
|
||||||
|
|
||||||
|
python3 -m unittest --durations=3 lib.tests.test_threading
|
||||||
|
.....
|
||||||
|
Slowest test durations
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
1.210s test_timeout (Lib.test.test_threading.BarrierTests)
|
||||||
|
1.003s test_default_timeout (Lib.test.test_threading.BarrierTests)
|
||||||
|
0.518s test_timeout (Lib.test.test_threading.EventTests)
|
||||||
|
|
||||||
|
(0.000 durations hidden. Use -v to show these durations.)
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Ran 158 tests in 9.869s
|
||||||
|
|
||||||
|
OK (skipped=3)
|
||||||
|
|
||||||
|
(Contributed by Giampaolo Rodola in :issue:`4080`)
|
||||||
|
|
||||||
uuid
|
uuid
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
|
@ -136,3 +136,19 @@ class ResultWithNoStartTestRunStopTestRun(object):
|
||||||
|
|
||||||
def wasSuccessful(self):
|
def wasSuccessful(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class BufferedWriter:
|
||||||
|
def __init__(self):
|
||||||
|
self.result = ''
|
||||||
|
self.buffer = ''
|
||||||
|
|
||||||
|
def write(self, arg):
|
||||||
|
self.buffer += arg
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.result += self.buffer
|
||||||
|
self.buffer = ''
|
||||||
|
|
||||||
|
def getvalue(self):
|
||||||
|
return self.result
|
||||||
|
|
|
@ -236,6 +236,7 @@ class TestBreak(unittest.TestCase):
|
||||||
self.testRunner = FakeRunner
|
self.testRunner = FakeRunner
|
||||||
self.test = test
|
self.test = test
|
||||||
self.result = None
|
self.result = None
|
||||||
|
self.durations = None
|
||||||
|
|
||||||
p = Program(False)
|
p = Program(False)
|
||||||
p.runTests()
|
p.runTests()
|
||||||
|
@ -244,7 +245,8 @@ class TestBreak(unittest.TestCase):
|
||||||
'verbosity': verbosity,
|
'verbosity': verbosity,
|
||||||
'failfast': failfast,
|
'failfast': failfast,
|
||||||
'tb_locals': False,
|
'tb_locals': False,
|
||||||
'warnings': None})])
|
'warnings': None,
|
||||||
|
'durations': None})])
|
||||||
self.assertEqual(FakeRunner.runArgs, [test])
|
self.assertEqual(FakeRunner.runArgs, [test])
|
||||||
self.assertEqual(p.result, result)
|
self.assertEqual(p.result, result)
|
||||||
|
|
||||||
|
@ -259,7 +261,8 @@ class TestBreak(unittest.TestCase):
|
||||||
'verbosity': verbosity,
|
'verbosity': verbosity,
|
||||||
'failfast': failfast,
|
'failfast': failfast,
|
||||||
'tb_locals': False,
|
'tb_locals': False,
|
||||||
'warnings': None})])
|
'warnings': None,
|
||||||
|
'durations': None})])
|
||||||
self.assertEqual(FakeRunner.runArgs, [test])
|
self.assertEqual(FakeRunner.runArgs, [test])
|
||||||
self.assertEqual(p.result, result)
|
self.assertEqual(p.result, result)
|
||||||
|
|
||||||
|
|
|
@ -284,6 +284,7 @@ class TestCommandLineArgs(unittest.TestCase):
|
||||||
program.failfast = 'failfast'
|
program.failfast = 'failfast'
|
||||||
program.buffer = 'buffer'
|
program.buffer = 'buffer'
|
||||||
program.warnings = 'warnings'
|
program.warnings = 'warnings'
|
||||||
|
program.durations = '5'
|
||||||
|
|
||||||
program.runTests()
|
program.runTests()
|
||||||
|
|
||||||
|
@ -291,7 +292,8 @@ class TestCommandLineArgs(unittest.TestCase):
|
||||||
'failfast': 'failfast',
|
'failfast': 'failfast',
|
||||||
'buffer': 'buffer',
|
'buffer': 'buffer',
|
||||||
'tb_locals': False,
|
'tb_locals': False,
|
||||||
'warnings': 'warnings'})
|
'warnings': 'warnings',
|
||||||
|
'durations': '5'})
|
||||||
self.assertEqual(FakeRunner.test, 'test')
|
self.assertEqual(FakeRunner.test, 'test')
|
||||||
self.assertIs(program.result, RESULT)
|
self.assertIs(program.result, RESULT)
|
||||||
|
|
||||||
|
@ -320,7 +322,8 @@ class TestCommandLineArgs(unittest.TestCase):
|
||||||
'failfast': False,
|
'failfast': False,
|
||||||
'tb_locals': True,
|
'tb_locals': True,
|
||||||
'verbosity': 1,
|
'verbosity': 1,
|
||||||
'warnings': None})
|
'warnings': None,
|
||||||
|
'durations': None})
|
||||||
|
|
||||||
def testRunTestsOldRunnerClass(self):
|
def testRunTestsOldRunnerClass(self):
|
||||||
program = self.program
|
program = self.program
|
||||||
|
@ -333,6 +336,7 @@ class TestCommandLineArgs(unittest.TestCase):
|
||||||
program.failfast = 'failfast'
|
program.failfast = 'failfast'
|
||||||
program.buffer = 'buffer'
|
program.buffer = 'buffer'
|
||||||
program.test = 'test'
|
program.test = 'test'
|
||||||
|
program.durations = '0'
|
||||||
|
|
||||||
program.runTests()
|
program.runTests()
|
||||||
|
|
||||||
|
@ -356,6 +360,7 @@ class TestCommandLineArgs(unittest.TestCase):
|
||||||
|
|
||||||
program = self.program
|
program = self.program
|
||||||
program.catchbreak = True
|
program.catchbreak = True
|
||||||
|
program.durations = None
|
||||||
|
|
||||||
program.testRunner = FakeRunner
|
program.testRunner = FakeRunner
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,9 @@ from test.support import warnings_helper, captured_stdout
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
from unittest.util import strclass
|
from unittest.util import strclass
|
||||||
|
from test.test_unittest.support import BufferedWriter
|
||||||
|
|
||||||
|
|
||||||
class MockTraceback(object):
|
class MockTraceback(object):
|
||||||
|
@ -33,22 +35,6 @@ def bad_cleanup2():
|
||||||
raise ValueError('bad cleanup2')
|
raise ValueError('bad cleanup2')
|
||||||
|
|
||||||
|
|
||||||
class BufferedWriter:
|
|
||||||
def __init__(self):
|
|
||||||
self.result = ''
|
|
||||||
self.buffer = ''
|
|
||||||
|
|
||||||
def write(self, arg):
|
|
||||||
self.buffer += arg
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
self.result += self.buffer
|
|
||||||
self.buffer = ''
|
|
||||||
|
|
||||||
def getvalue(self):
|
|
||||||
return self.result
|
|
||||||
|
|
||||||
|
|
||||||
class Test_TestResult(unittest.TestCase):
|
class Test_TestResult(unittest.TestCase):
|
||||||
# Note: there are not separate tests for TestResult.wasSuccessful(),
|
# Note: there are not separate tests for TestResult.wasSuccessful(),
|
||||||
# TestResult.errors, TestResult.failures, TestResult.testsRun or
|
# TestResult.errors, TestResult.failures, TestResult.testsRun or
|
||||||
|
|
|
@ -8,8 +8,11 @@ from test import support
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.case import _Outcome
|
from unittest.case import _Outcome
|
||||||
|
|
||||||
from test.test_unittest.support import (LoggingResult,
|
from test.test_unittest.support import (
|
||||||
ResultWithNoStartTestRunStopTestRun)
|
BufferedWriter,
|
||||||
|
LoggingResult,
|
||||||
|
ResultWithNoStartTestRunStopTestRun,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def resultFactory(*_):
|
def resultFactory(*_):
|
||||||
|
@ -1176,6 +1179,7 @@ class Test_TextTestRunner(unittest.TestCase):
|
||||||
self.assertTrue(runner.descriptions)
|
self.assertTrue(runner.descriptions)
|
||||||
self.assertEqual(runner.resultclass, unittest.TextTestResult)
|
self.assertEqual(runner.resultclass, unittest.TextTestResult)
|
||||||
self.assertFalse(runner.tb_locals)
|
self.assertFalse(runner.tb_locals)
|
||||||
|
self.assertIsNone(runner.durations)
|
||||||
|
|
||||||
def test_multiple_inheritance(self):
|
def test_multiple_inheritance(self):
|
||||||
class AResult(unittest.TestResult):
|
class AResult(unittest.TestResult):
|
||||||
|
@ -1362,6 +1366,65 @@ class Test_TextTestRunner(unittest.TestCase):
|
||||||
runner = unittest.TextTestRunner(f)
|
runner = unittest.TextTestRunner(f)
|
||||||
self.assertTrue(runner.stream.stream is f)
|
self.assertTrue(runner.stream.stream is f)
|
||||||
|
|
||||||
|
def test_durations(self):
|
||||||
|
def run(test, expect_durations):
|
||||||
|
stream = BufferedWriter()
|
||||||
|
runner = unittest.TextTestRunner(stream=stream, durations=5, verbosity=2)
|
||||||
|
result = runner.run(test)
|
||||||
|
self.assertEqual(result.durations, 5)
|
||||||
|
stream.flush()
|
||||||
|
text = stream.getvalue()
|
||||||
|
regex = r"\n\d+.\d\d\ds"
|
||||||
|
if expect_durations:
|
||||||
|
self.assertEqual(len(result.collectedDurations), 1)
|
||||||
|
self.assertIn('Slowest test durations', text)
|
||||||
|
self.assertRegex(text, regex)
|
||||||
|
else:
|
||||||
|
self.assertEqual(len(result.collectedDurations), 0)
|
||||||
|
self.assertNotIn('Slowest test durations', text)
|
||||||
|
self.assertNotRegex(text, regex)
|
||||||
|
|
||||||
|
# success
|
||||||
|
class Foo(unittest.TestCase):
|
||||||
|
def test_1(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
run(Foo('test_1'), True)
|
||||||
|
|
||||||
|
# failure
|
||||||
|
class Foo(unittest.TestCase):
|
||||||
|
def test_1(self):
|
||||||
|
self.assertEqual(0, 1)
|
||||||
|
|
||||||
|
run(Foo('test_1'), True)
|
||||||
|
|
||||||
|
# error
|
||||||
|
class Foo(unittest.TestCase):
|
||||||
|
def test_1(self):
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
run(Foo('test_1'), True)
|
||||||
|
|
||||||
|
|
||||||
|
# error in setUp and tearDown
|
||||||
|
class Foo(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
1 / 0
|
||||||
|
tearDown = setUp
|
||||||
|
def test_1(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
run(Foo('test_1'), True)
|
||||||
|
|
||||||
|
# skip (expect no durations)
|
||||||
|
class Foo(unittest.TestCase):
|
||||||
|
@unittest.skip("reason")
|
||||||
|
def test_1(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
run(Foo('test_1'), False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -9,6 +9,7 @@ import warnings
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import traceback
|
import traceback
|
||||||
|
import time
|
||||||
import types
|
import types
|
||||||
|
|
||||||
from . import result
|
from . import result
|
||||||
|
@ -572,6 +573,15 @@ class TestCase(object):
|
||||||
else:
|
else:
|
||||||
addUnexpectedSuccess(self)
|
addUnexpectedSuccess(self)
|
||||||
|
|
||||||
|
def _addDuration(self, result, elapsed):
|
||||||
|
try:
|
||||||
|
addDuration = result.addDuration
|
||||||
|
except AttributeError:
|
||||||
|
warnings.warn("TestResult has no addDuration method",
|
||||||
|
RuntimeWarning)
|
||||||
|
else:
|
||||||
|
addDuration(self, elapsed)
|
||||||
|
|
||||||
def _callSetUp(self):
|
def _callSetUp(self):
|
||||||
self.setUp()
|
self.setUp()
|
||||||
|
|
||||||
|
@ -612,6 +622,7 @@ class TestCase(object):
|
||||||
getattr(testMethod, "__unittest_expecting_failure__", False)
|
getattr(testMethod, "__unittest_expecting_failure__", False)
|
||||||
)
|
)
|
||||||
outcome = _Outcome(result)
|
outcome = _Outcome(result)
|
||||||
|
start_time = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
self._outcome = outcome
|
self._outcome = outcome
|
||||||
|
|
||||||
|
@ -625,6 +636,7 @@ class TestCase(object):
|
||||||
with outcome.testPartExecutor(self):
|
with outcome.testPartExecutor(self):
|
||||||
self._callTearDown()
|
self._callTearDown()
|
||||||
self.doCleanups()
|
self.doCleanups()
|
||||||
|
self._addDuration(result, (time.perf_counter() - start_time))
|
||||||
|
|
||||||
if outcome.success:
|
if outcome.success:
|
||||||
if expecting_failure:
|
if expecting_failure:
|
||||||
|
|
|
@ -66,7 +66,8 @@ class TestProgram(object):
|
||||||
def __init__(self, module='__main__', defaultTest=None, argv=None,
|
def __init__(self, module='__main__', defaultTest=None, argv=None,
|
||||||
testRunner=None, testLoader=loader.defaultTestLoader,
|
testRunner=None, testLoader=loader.defaultTestLoader,
|
||||||
exit=True, verbosity=1, failfast=None, catchbreak=None,
|
exit=True, verbosity=1, failfast=None, catchbreak=None,
|
||||||
buffer=None, warnings=None, *, tb_locals=False):
|
buffer=None, warnings=None, *, tb_locals=False,
|
||||||
|
durations=None):
|
||||||
if isinstance(module, str):
|
if isinstance(module, str):
|
||||||
self.module = __import__(module)
|
self.module = __import__(module)
|
||||||
for part in module.split('.')[1:]:
|
for part in module.split('.')[1:]:
|
||||||
|
@ -82,6 +83,7 @@ class TestProgram(object):
|
||||||
self.verbosity = verbosity
|
self.verbosity = verbosity
|
||||||
self.buffer = buffer
|
self.buffer = buffer
|
||||||
self.tb_locals = tb_locals
|
self.tb_locals = tb_locals
|
||||||
|
self.durations = durations
|
||||||
if warnings is None and not sys.warnoptions:
|
if warnings is None and not sys.warnoptions:
|
||||||
# even if DeprecationWarnings are ignored by default
|
# even if DeprecationWarnings are ignored by default
|
||||||
# print them anyway unless other warnings settings are
|
# print them anyway unless other warnings settings are
|
||||||
|
@ -178,6 +180,9 @@ class TestProgram(object):
|
||||||
parser.add_argument('--locals', dest='tb_locals',
|
parser.add_argument('--locals', dest='tb_locals',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Show local variables in tracebacks')
|
help='Show local variables in tracebacks')
|
||||||
|
parser.add_argument('--durations', dest='durations', type=int,
|
||||||
|
default=None, metavar="N",
|
||||||
|
help='Show the N slowest test cases (N=0 for all)')
|
||||||
if self.failfast is None:
|
if self.failfast is None:
|
||||||
parser.add_argument('-f', '--failfast', dest='failfast',
|
parser.add_argument('-f', '--failfast', dest='failfast',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
@ -258,9 +263,10 @@ class TestProgram(object):
|
||||||
failfast=self.failfast,
|
failfast=self.failfast,
|
||||||
buffer=self.buffer,
|
buffer=self.buffer,
|
||||||
warnings=self.warnings,
|
warnings=self.warnings,
|
||||||
tb_locals=self.tb_locals)
|
tb_locals=self.tb_locals,
|
||||||
|
durations=self.durations)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# didn't accept the tb_locals argument
|
# didn't accept the tb_locals or durations argument
|
||||||
testRunner = self.testRunner(verbosity=self.verbosity,
|
testRunner = self.testRunner(verbosity=self.verbosity,
|
||||||
failfast=self.failfast,
|
failfast=self.failfast,
|
||||||
buffer=self.buffer,
|
buffer=self.buffer,
|
||||||
|
|
|
@ -43,6 +43,7 @@ class TestResult(object):
|
||||||
self.skipped = []
|
self.skipped = []
|
||||||
self.expectedFailures = []
|
self.expectedFailures = []
|
||||||
self.unexpectedSuccesses = []
|
self.unexpectedSuccesses = []
|
||||||
|
self.collectedDurations = []
|
||||||
self.shouldStop = False
|
self.shouldStop = False
|
||||||
self.buffer = False
|
self.buffer = False
|
||||||
self.tb_locals = False
|
self.tb_locals = False
|
||||||
|
@ -157,6 +158,12 @@ class TestResult(object):
|
||||||
"""Called when a test was expected to fail, but succeed."""
|
"""Called when a test was expected to fail, but succeed."""
|
||||||
self.unexpectedSuccesses.append(test)
|
self.unexpectedSuccesses.append(test)
|
||||||
|
|
||||||
|
def addDuration(self, test, elapsed):
|
||||||
|
"""Called when a test finished to run, regardless of its outcome."""
|
||||||
|
# support for a TextTestRunner using an old TestResult class
|
||||||
|
if hasattr(self, "collectedDurations"):
|
||||||
|
self.collectedDurations.append((test, elapsed))
|
||||||
|
|
||||||
def wasSuccessful(self):
|
def wasSuccessful(self):
|
||||||
"""Tells whether or not this result was a success."""
|
"""Tells whether or not this result was a success."""
|
||||||
# The hasattr check is for test_result's OldResult test. That
|
# The hasattr check is for test_result's OldResult test. That
|
||||||
|
|
|
@ -35,13 +35,16 @@ class TextTestResult(result.TestResult):
|
||||||
separator1 = '=' * 70
|
separator1 = '=' * 70
|
||||||
separator2 = '-' * 70
|
separator2 = '-' * 70
|
||||||
|
|
||||||
def __init__(self, stream, descriptions, verbosity):
|
def __init__(self, stream, descriptions, verbosity, *, durations=None):
|
||||||
|
"""Construct a TextTestResult. Subclasses should accept **kwargs
|
||||||
|
to ensure compatibility as the interface changes."""
|
||||||
super(TextTestResult, self).__init__(stream, descriptions, verbosity)
|
super(TextTestResult, self).__init__(stream, descriptions, verbosity)
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
self.showAll = verbosity > 1
|
self.showAll = verbosity > 1
|
||||||
self.dots = verbosity == 1
|
self.dots = verbosity == 1
|
||||||
self.descriptions = descriptions
|
self.descriptions = descriptions
|
||||||
self._newline = True
|
self._newline = True
|
||||||
|
self.durations = durations
|
||||||
|
|
||||||
def getDescription(self, test):
|
def getDescription(self, test):
|
||||||
doc_first_line = test.shortDescription()
|
doc_first_line = test.shortDescription()
|
||||||
|
@ -168,7 +171,7 @@ class TextTestRunner(object):
|
||||||
|
|
||||||
def __init__(self, stream=None, descriptions=True, verbosity=1,
|
def __init__(self, stream=None, descriptions=True, verbosity=1,
|
||||||
failfast=False, buffer=False, resultclass=None, warnings=None,
|
failfast=False, buffer=False, resultclass=None, warnings=None,
|
||||||
*, tb_locals=False):
|
*, tb_locals=False, durations=None):
|
||||||
"""Construct a TextTestRunner.
|
"""Construct a TextTestRunner.
|
||||||
|
|
||||||
Subclasses should accept **kwargs to ensure compatibility as the
|
Subclasses should accept **kwargs to ensure compatibility as the
|
||||||
|
@ -182,12 +185,41 @@ class TextTestRunner(object):
|
||||||
self.failfast = failfast
|
self.failfast = failfast
|
||||||
self.buffer = buffer
|
self.buffer = buffer
|
||||||
self.tb_locals = tb_locals
|
self.tb_locals = tb_locals
|
||||||
|
self.durations = durations
|
||||||
self.warnings = warnings
|
self.warnings = warnings
|
||||||
if resultclass is not None:
|
if resultclass is not None:
|
||||||
self.resultclass = resultclass
|
self.resultclass = resultclass
|
||||||
|
|
||||||
def _makeResult(self):
|
def _makeResult(self):
|
||||||
return self.resultclass(self.stream, self.descriptions, self.verbosity)
|
try:
|
||||||
|
return self.resultclass(self.stream, self.descriptions,
|
||||||
|
self.verbosity, durations=self.durations)
|
||||||
|
except TypeError:
|
||||||
|
# didn't accept the durations argument
|
||||||
|
return self.resultclass(self.stream, self.descriptions,
|
||||||
|
self.verbosity)
|
||||||
|
|
||||||
|
def _printDurations(self, result):
|
||||||
|
if not result.collectedDurations:
|
||||||
|
return
|
||||||
|
ls = sorted(result.collectedDurations, key=lambda x: x[1],
|
||||||
|
reverse=True)
|
||||||
|
if self.durations > 0:
|
||||||
|
ls = ls[:self.durations]
|
||||||
|
self.stream.writeln("Slowest test durations")
|
||||||
|
if hasattr(result, 'separator2'):
|
||||||
|
self.stream.writeln(result.separator2)
|
||||||
|
hidden = False
|
||||||
|
for test, elapsed in ls:
|
||||||
|
if self.verbosity < 2 and elapsed < 0.001:
|
||||||
|
hidden = True
|
||||||
|
continue
|
||||||
|
self.stream.writeln("%-10s %s" % ("%.3fs" % elapsed, test))
|
||||||
|
if hidden:
|
||||||
|
self.stream.writeln("\n(durations < 0.001s were hidden; "
|
||||||
|
"use -v to show these durations)")
|
||||||
|
else:
|
||||||
|
self.stream.writeln("")
|
||||||
|
|
||||||
def run(self, test):
|
def run(self, test):
|
||||||
"Run the given test case or test suite."
|
"Run the given test case or test suite."
|
||||||
|
@ -213,8 +245,12 @@ class TextTestRunner(object):
|
||||||
stopTime = time.perf_counter()
|
stopTime = time.perf_counter()
|
||||||
timeTaken = stopTime - startTime
|
timeTaken = stopTime - startTime
|
||||||
result.printErrors()
|
result.printErrors()
|
||||||
|
if self.durations is not None:
|
||||||
|
self._printDurations(result)
|
||||||
|
|
||||||
if hasattr(result, 'separator2'):
|
if hasattr(result, 'separator2'):
|
||||||
self.stream.writeln(result.separator2)
|
self.stream.writeln(result.separator2)
|
||||||
|
|
||||||
run = result.testsRun
|
run = result.testsRun
|
||||||
self.stream.writeln("Ran %d test%s in %.3fs" %
|
self.stream.writeln("Ran %d test%s in %.3fs" %
|
||||||
(run, run != 1 and "s" or "", timeTaken))
|
(run, run != 1 and "s" or "", timeTaken))
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
Added ``--durations`` command line option, showing the N slowest test cases.
|
||||||
|
:class:`unittest.TextTestRunner` and :class:`unittest.TextTestResult`
|
||||||
|
constructors accept a new *durations* keyword argument. Subclasses should take
|
||||||
|
this into account or accept ``**kwargs``. Added
|
||||||
|
:meth:`unittest.TestResult.addDuration` method and
|
||||||
|
:attr:`unittest.TestResult.collectedDurations` attribute.
|
||||||
|
|
||||||
|
(Contributed by Giampaolo Rodola)
|
Loading…
Add table
Add a link
Reference in a new issue