summaryrefslogtreecommitdiffstats
path: root/Lib/unittest
diff options
context:
space:
mode:
authorGiampaolo Rodola <g.rodola@gmail.com>2023-04-02 22:12:51 (GMT)
committerGitHub <noreply@github.com>2023-04-02 22:12:51 (GMT)
commit6883007a86bdf0d7cf4560b949fd5e577dab1013 (patch)
treeb5585b8135e1136d9b567dda62e887a27033b392 /Lib/unittest
parenta0305c5fdfdef7a362d0262c54399c4a6013d1ea (diff)
downloadcpython-6883007a86bdf0d7cf4560b949fd5e577dab1013.zip
cpython-6883007a86bdf0d7cf4560b949fd5e577dab1013.tar.gz
cpython-6883007a86bdf0d7cf4560b949fd5e577dab1013.tar.bz2
bpo-4080: unittest durations (#12271)
Diffstat (limited to 'Lib/unittest')
-rw-r--r--Lib/unittest/case.py12
-rw-r--r--Lib/unittest/main.py12
-rw-r--r--Lib/unittest/result.py7
-rw-r--r--Lib/unittest/runner.py42
4 files changed, 67 insertions, 6 deletions
diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py
index 5167c5f..018f22e 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -9,6 +9,7 @@ import warnings
import collections
import contextlib
import traceback
+import time
import types
from . import result
@@ -572,6 +573,15 @@ class TestCase(object):
else:
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):
self.setUp()
@@ -612,6 +622,7 @@ class TestCase(object):
getattr(testMethod, "__unittest_expecting_failure__", False)
)
outcome = _Outcome(result)
+ start_time = time.perf_counter()
try:
self._outcome = outcome
@@ -625,6 +636,7 @@ class TestCase(object):
with outcome.testPartExecutor(self):
self._callTearDown()
self.doCleanups()
+ self._addDuration(result, (time.perf_counter() - start_time))
if outcome.success:
if expecting_failure:
diff --git a/Lib/unittest/main.py b/Lib/unittest/main.py
index 046fbd3..0792750 100644
--- a/Lib/unittest/main.py
+++ b/Lib/unittest/main.py
@@ -66,7 +66,8 @@ class TestProgram(object):
def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
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):
self.module = __import__(module)
for part in module.split('.')[1:]:
@@ -82,6 +83,7 @@ class TestProgram(object):
self.verbosity = verbosity
self.buffer = buffer
self.tb_locals = tb_locals
+ self.durations = durations
if warnings is None and not sys.warnoptions:
# even if DeprecationWarnings are ignored by default
# print them anyway unless other warnings settings are
@@ -178,6 +180,9 @@ class TestProgram(object):
parser.add_argument('--locals', dest='tb_locals',
action='store_true',
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:
parser.add_argument('-f', '--failfast', dest='failfast',
action='store_true',
@@ -258,9 +263,10 @@ class TestProgram(object):
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings,
- tb_locals=self.tb_locals)
+ tb_locals=self.tb_locals,
+ durations=self.durations)
except TypeError:
- # didn't accept the tb_locals argument
+ # didn't accept the tb_locals or durations argument
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py
index 5ca4c23..fa9bea4 100644
--- a/Lib/unittest/result.py
+++ b/Lib/unittest/result.py
@@ -43,6 +43,7 @@ class TestResult(object):
self.skipped = []
self.expectedFailures = []
self.unexpectedSuccesses = []
+ self.collectedDurations = []
self.shouldStop = False
self.buffer = False
self.tb_locals = False
@@ -157,6 +158,12 @@ class TestResult(object):
"""Called when a test was expected to fail, but succeed."""
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):
"""Tells whether or not this result was a success."""
# The hasattr check is for test_result's OldResult test. That
diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py
index 6678adb..a51c5c5 100644
--- a/Lib/unittest/runner.py
+++ b/Lib/unittest/runner.py
@@ -35,13 +35,16 @@ class TextTestResult(result.TestResult):
separator1 = '=' * 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)
self.stream = stream
self.showAll = verbosity > 1
self.dots = verbosity == 1
self.descriptions = descriptions
self._newline = True
+ self.durations = durations
def getDescription(self, test):
doc_first_line = test.shortDescription()
@@ -168,7 +171,7 @@ class TextTestRunner(object):
def __init__(self, stream=None, descriptions=True, verbosity=1,
failfast=False, buffer=False, resultclass=None, warnings=None,
- *, tb_locals=False):
+ *, tb_locals=False, durations=None):
"""Construct a TextTestRunner.
Subclasses should accept **kwargs to ensure compatibility as the
@@ -182,12 +185,41 @@ class TextTestRunner(object):
self.failfast = failfast
self.buffer = buffer
self.tb_locals = tb_locals
+ self.durations = durations
self.warnings = warnings
if resultclass is not None:
self.resultclass = resultclass
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):
"Run the given test case or test suite."
@@ -213,8 +245,12 @@ class TextTestRunner(object):
stopTime = time.perf_counter()
timeTaken = stopTime - startTime
result.printErrors()
+ if self.durations is not None:
+ self._printDurations(result)
+
if hasattr(result, 'separator2'):
self.stream.writeln(result.separator2)
+
run = result.testsRun
self.stream.writeln("Ran %d test%s in %.3fs" %
(run, run != 1 and "s" or "", timeTaken))