bpo-4080: unittest durations (#12271) · python/cpython@6883007 · GitHub
Skip to content

Commit 6883007

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

12 files changed

Lines changed: 227 additions & 34 deletions

File tree

Doc/library/unittest.rst

Lines changed: 36 additions & 6 deletions

Doc/whatsnew/3.12.rst

Lines changed: 21 additions & 0 deletions
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

Lines changed: 16 additions & 0 deletions
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

Lines changed: 5 additions & 2 deletions
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

Lines changed: 7 additions & 2 deletions
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

Lines changed: 2 additions & 16 deletions
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

Lines changed: 65 additions & 2 deletions
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

Lines changed: 12 additions & 0 deletions

0 commit comments

Comments
 (0)