Skip to content

Commit 6883007

Browse files
authored
bpo-4080: unittest durations (#12271)
1 parent a0305c5 commit 6883007

File tree

12 files changed

+227
-34
lines changed

12 files changed

+227
-34
lines changed

Doc/library/unittest.rst

+36-6
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ Command-line options
244244

245245
Show local variables in tracebacks.
246246

247+
.. cmdoption:: --durations N
248+
249+
Show the N slowest test cases (N=0 for all).
250+
247251
.. versionadded:: 3.2
248252
The command-line options ``-b``, ``-c`` and ``-f`` were added.
249253

@@ -253,10 +257,12 @@ Command-line options
253257
.. versionadded:: 3.7
254258
The command-line option ``-k``.
255259

260+
.. versionadded:: 3.12
261+
The command-line option ``--durations``.
262+
256263
The command line can also be used for test discovery, for running all of the
257264
tests in a project or just a subset.
258265

259-
260266
.. _unittest-test-discovery:
261267

262268
Test Discovery
@@ -2009,6 +2015,13 @@ Loading and running tests
20092015
A list containing :class:`TestCase` instances that were marked as expected
20102016
failures, but succeeded.
20112017

2018+
.. attribute:: collectedDurations
2019+
2020+
A list containing 2-tuples of :class:`TestCase` instances and floats
2021+
representing the elapsed time of each test which was run.
2022+
2023+
.. versionadded:: 3.12
2024+
20122025
.. attribute:: shouldStop
20132026

20142027
Set to ``True`` when the execution of tests should stop by :meth:`stop`.
@@ -2160,14 +2173,27 @@ Loading and running tests
21602173

21612174
.. versionadded:: 3.4
21622175

2176+
.. method:: addDuration(test, elapsed)
2177+
2178+
Called when the test case finishes. *elapsed* is the time represented in
2179+
seconds, and it includes the execution of cleanup functions.
2180+
2181+
.. versionadded:: 3.12
21632182

2164-
.. class:: TextTestResult(stream, descriptions, verbosity)
2183+
.. class:: TextTestResult(stream, descriptions, verbosity, *, durations=None)
21652184

21662185
A concrete implementation of :class:`TestResult` used by the
2167-
:class:`TextTestRunner`.
2186+
:class:`TextTestRunner`. Subclasses should accept ``**kwargs`` to ensure
2187+
compatibility as the interface changes.
21682188

21692189
.. versionadded:: 3.2
21702190

2191+
.. versionadded:: 3.12
2192+
Added *durations* keyword argument.
2193+
2194+
.. versionchanged:: 3.12
2195+
Subclasses should accept ``**kwargs`` to ensure compatibility as the
2196+
interface changes.
21712197

21722198
.. data:: defaultTestLoader
21732199

@@ -2177,7 +2203,8 @@ Loading and running tests
21772203

21782204

21792205
.. class:: TextTestRunner(stream=None, descriptions=True, verbosity=1, failfast=False, \
2180-
buffer=False, resultclass=None, warnings=None, *, tb_locals=False)
2206+
buffer=False, resultclass=None, warnings=None, *, \
2207+
tb_locals=False, durations=None)
21812208
21822209
A basic test runner implementation that outputs results to a stream. If *stream*
21832210
is ``None``, the default, :data:`sys.stderr` is used as the output stream. This class
@@ -2195,14 +2222,17 @@ Loading and running tests
21952222
*warnings* to ``None``.
21962223

21972224
.. versionchanged:: 3.2
2198-
Added the ``warnings`` argument.
2225+
Added the *warnings* parameter.
21992226

22002227
.. versionchanged:: 3.2
22012228
The default stream is set to :data:`sys.stderr` at instantiation time rather
22022229
than import time.
22032230

22042231
.. versionchanged:: 3.5
2205-
Added the tb_locals parameter.
2232+
Added the *tb_locals* parameter.
2233+
2234+
.. versionchanged:: 3.12
2235+
Added the *durations* parameter.
22062236

22072237
.. method:: _makeResult()
22082238

Doc/whatsnew/3.12.rst

+21
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,27 @@ unicodedata
371371
* The Unicode database has been updated to version 15.0.0. (Contributed by
372372
Benjamin Peterson in :gh:`96734`).
373373

374+
unittest
375+
--------
376+
377+
Added ``--durations`` command line option, showing the N slowest test cases::
378+
379+
python3 -m unittest --durations=3 lib.tests.test_threading
380+
.....
381+
Slowest test durations
382+
----------------------------------------------------------------------
383+
1.210s test_timeout (Lib.test.test_threading.BarrierTests)
384+
1.003s test_default_timeout (Lib.test.test_threading.BarrierTests)
385+
0.518s test_timeout (Lib.test.test_threading.EventTests)
386+
387+
(0.000 durations hidden. Use -v to show these durations.)
388+
----------------------------------------------------------------------
389+
Ran 158 tests in 9.869s
390+
391+
OK (skipped=3)
392+
393+
(Contributed by Giampaolo Rodola in :issue:`4080`)
394+
374395
uuid
375396
----
376397

Lib/test/test_unittest/support.py

+16
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,19 @@ def addSuccess(self, test):
136136

137137
def wasSuccessful(self):
138138
return True
139+
140+
141+
class BufferedWriter:
142+
def __init__(self):
143+
self.result = ''
144+
self.buffer = ''
145+
146+
def write(self, arg):
147+
self.buffer += arg
148+
149+
def flush(self):
150+
self.result += self.buffer
151+
self.buffer = ''
152+
153+
def getvalue(self):
154+
return self.result

Lib/test/test_unittest/test_break.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def __init__(self, catchbreak):
236236
self.testRunner = FakeRunner
237237
self.test = test
238238
self.result = None
239+
self.durations = None
239240

240241
p = Program(False)
241242
p.runTests()
@@ -244,7 +245,8 @@ def __init__(self, catchbreak):
244245
'verbosity': verbosity,
245246
'failfast': failfast,
246247
'tb_locals': False,
247-
'warnings': None})])
248+
'warnings': None,
249+
'durations': None})])
248250
self.assertEqual(FakeRunner.runArgs, [test])
249251
self.assertEqual(p.result, result)
250252

@@ -259,7 +261,8 @@ def __init__(self, catchbreak):
259261
'verbosity': verbosity,
260262
'failfast': failfast,
261263
'tb_locals': False,
262-
'warnings': None})])
264+
'warnings': None,
265+
'durations': None})])
263266
self.assertEqual(FakeRunner.runArgs, [test])
264267
self.assertEqual(p.result, result)
265268

Lib/test/test_unittest/test_program.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,16 @@ def testRunTestsRunnerClass(self):
284284
program.failfast = 'failfast'
285285
program.buffer = 'buffer'
286286
program.warnings = 'warnings'
287+
program.durations = '5'
287288

288289
program.runTests()
289290

290291
self.assertEqual(FakeRunner.initArgs, {'verbosity': 'verbosity',
291292
'failfast': 'failfast',
292293
'buffer': 'buffer',
293294
'tb_locals': False,
294-
'warnings': 'warnings'})
295+
'warnings': 'warnings',
296+
'durations': '5'})
295297
self.assertEqual(FakeRunner.test, 'test')
296298
self.assertIs(program.result, RESULT)
297299

@@ -320,7 +322,8 @@ def test_locals(self):
320322
'failfast': False,
321323
'tb_locals': True,
322324
'verbosity': 1,
323-
'warnings': None})
325+
'warnings': None,
326+
'durations': None})
324327

325328
def testRunTestsOldRunnerClass(self):
326329
program = self.program
@@ -333,6 +336,7 @@ def testRunTestsOldRunnerClass(self):
333336
program.failfast = 'failfast'
334337
program.buffer = 'buffer'
335338
program.test = 'test'
339+
program.durations = '0'
336340

337341
program.runTests()
338342

@@ -356,6 +360,7 @@ def fakeInstallHandler():
356360

357361
program = self.program
358362
program.catchbreak = True
363+
program.durations = None
359364

360365
program.testRunner = FakeRunner
361366

Lib/test/test_unittest/test_result.py

+2-16
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import traceback
88
import unittest
9+
from unittest import mock
910
from unittest.util import strclass
11+
from test.test_unittest.support import BufferedWriter
1012

1113

1214
class MockTraceback(object):
@@ -33,22 +35,6 @@ def bad_cleanup2():
3335
raise ValueError('bad cleanup2')
3436

3537

36-
class BufferedWriter:
37-
def __init__(self):
38-
self.result = ''
39-
self.buffer = ''
40-
41-
def write(self, arg):
42-
self.buffer += arg
43-
44-
def flush(self):
45-
self.result += self.buffer
46-
self.buffer = ''
47-
48-
def getvalue(self):
49-
return self.result
50-
51-
5238
class Test_TestResult(unittest.TestCase):
5339
# Note: there are not separate tests for TestResult.wasSuccessful(),
5440
# TestResult.errors, TestResult.failures, TestResult.testsRun or

Lib/test/test_unittest/test_runner.py

+65-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
import unittest
99
from unittest.case import _Outcome
1010

11-
from test.test_unittest.support import (LoggingResult,
12-
ResultWithNoStartTestRunStopTestRun)
11+
from test.test_unittest.support import (
12+
BufferedWriter,
13+
LoggingResult,
14+
ResultWithNoStartTestRunStopTestRun,
15+
)
1316

1417

1518
def resultFactory(*_):
@@ -1176,6 +1179,7 @@ def test_init(self):
11761179
self.assertTrue(runner.descriptions)
11771180
self.assertEqual(runner.resultclass, unittest.TextTestResult)
11781181
self.assertFalse(runner.tb_locals)
1182+
self.assertIsNone(runner.durations)
11791183

11801184
def test_multiple_inheritance(self):
11811185
class AResult(unittest.TestResult):
@@ -1362,6 +1366,65 @@ def testSpecifiedStreamUsed(self):
13621366
runner = unittest.TextTestRunner(f)
13631367
self.assertTrue(runner.stream.stream is f)
13641368

1369+
def test_durations(self):
1370+
def run(test, expect_durations):
1371+
stream = BufferedWriter()
1372+
runner = unittest.TextTestRunner(stream=stream, durations=5, verbosity=2)
1373+
result = runner.run(test)
1374+
self.assertEqual(result.durations, 5)
1375+
stream.flush()
1376+
text = stream.getvalue()
1377+
regex = r"\n\d+.\d\d\ds"
1378+
if expect_durations:
1379+
self.assertEqual(len(result.collectedDurations), 1)
1380+
self.assertIn('Slowest test durations', text)
1381+
self.assertRegex(text, regex)
1382+
else:
1383+
self.assertEqual(len(result.collectedDurations), 0)
1384+
self.assertNotIn('Slowest test durations', text)
1385+
self.assertNotRegex(text, regex)
1386+
1387+
# success
1388+
class Foo(unittest.TestCase):
1389+
def test_1(self):
1390+
pass
1391+
1392+
run(Foo('test_1'), True)
1393+
1394+
# failure
1395+
class Foo(unittest.TestCase):
1396+
def test_1(self):
1397+
self.assertEqual(0, 1)
1398+
1399+
run(Foo('test_1'), True)
1400+
1401+
# error
1402+
class Foo(unittest.TestCase):
1403+
def test_1(self):
1404+
1 / 0
1405+
1406+
run(Foo('test_1'), True)
1407+
1408+
1409+
# error in setUp and tearDown
1410+
class Foo(unittest.TestCase):
1411+
def setUp(self):
1412+
1 / 0
1413+
tearDown = setUp
1414+
def test_1(self):
1415+
pass
1416+
1417+
run(Foo('test_1'), True)
1418+
1419+
# skip (expect no durations)
1420+
class Foo(unittest.TestCase):
1421+
@unittest.skip("reason")
1422+
def test_1(self):
1423+
pass
1424+
1425+
run(Foo('test_1'), False)
1426+
1427+
13651428

13661429
if __name__ == "__main__":
13671430
unittest.main()

Lib/unittest/case.py

+12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import collections
1010
import contextlib
1111
import traceback
12+
import time
1213
import types
1314

1415
from . import result
@@ -572,6 +573,15 @@ def _addUnexpectedSuccess(self, result):
572573
else:
573574
addUnexpectedSuccess(self)
574575

576+
def _addDuration(self, result, elapsed):
577+
try:
578+
addDuration = result.addDuration
579+
except AttributeError:
580+
warnings.warn("TestResult has no addDuration method",
581+
RuntimeWarning)
582+
else:
583+
addDuration(self, elapsed)
584+
575585
def _callSetUp(self):
576586
self.setUp()
577587

@@ -612,6 +622,7 @@ def run(self, result=None):
612622
getattr(testMethod, "__unittest_expecting_failure__", False)
613623
)
614624
outcome = _Outcome(result)
625+
start_time = time.perf_counter()
615626
try:
616627
self._outcome = outcome
617628

@@ -625,6 +636,7 @@ def run(self, result=None):
625636
with outcome.testPartExecutor(self):
626637
self._callTearDown()
627638
self.doCleanups()
639+
self._addDuration(result, (time.perf_counter() - start_time))
628640

629641
if outcome.success:
630642
if expecting_failure:

0 commit comments

Comments
 (0)