Skip to content

Commit 345df99

Browse files
committed
Show session duration in human-readable format
Fix #5707
1 parent 8665f56 commit 345df99

File tree

6 files changed

+43
-12
lines changed

6 files changed

+43
-12
lines changed

changelog/5707.improvement.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Time taken to run the test suite now includes a human-readable representation when it takes over
2+
60 seconds, for example::
3+
4+
===== 2 failed in 102.70s (0:01:42) =====

src/_pytest/pytester.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,10 @@ def _config_for_test():
340340
config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
341341

342342

343-
rex_outcome = re.compile(r"(\d+) ([\w-]+)")
343+
# regex to match the session duration string in the summary: "74.34s"
344+
rex_session_duration = re.compile(r"\d+\.\d\ds")
345+
# regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped"
346+
rex_outcome = re.compile(r"(\d+) (\w+)")
344347

345348

346349
class RunResult:
@@ -379,14 +382,11 @@ def parseoutcomes(self):
379382
380383
"""
381384
for line in reversed(self.outlines):
382-
if "seconds" in line:
385+
if rex_session_duration.search(line):
383386
outcomes = rex_outcome.findall(line)
384-
if outcomes:
385-
d = {}
386-
for num, cat in outcomes:
387-
d[cat] = int(num)
388-
return d
389-
raise ValueError("Pytest terminal report not found")
387+
return {noun: int(count) for (count, noun) in outcomes}
388+
389+
raise ValueError("Pytest terminal summary report not found")
390390

391391
def assert_outcomes(
392392
self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0

src/_pytest/terminal.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
import argparse
66
import collections
7+
import datetime
78
import platform
89
import sys
910
import time
@@ -861,7 +862,7 @@ def _outrep_summary(self, rep):
861862
def summary_stats(self):
862863
session_duration = time.time() - self._sessionstarttime
863864
(line, color) = build_summary_stats_line(self.stats)
864-
msg = "{} in {:.2f} seconds".format(line, session_duration)
865+
msg = "{} in {}".format(line, format_session_duration(session_duration))
865866
markup = {color: True, "bold": True}
866867

867868
if self.verbosity >= 0:
@@ -1055,3 +1056,12 @@ def _plugin_nameversions(plugininfo):
10551056
if name not in values:
10561057
values.append(name)
10571058
return values
1059+
1060+
1061+
def format_session_duration(seconds):
1062+
"""Format the given seconds in a human readable manner to show in the final summary"""
1063+
if seconds < 60:
1064+
return "{:.2f}s".format(seconds)
1065+
else:
1066+
dt = datetime.timedelta(seconds=int(seconds))
1067+
return "{:.2f}s ({})".format(seconds, dt)

testing/logging/test_reporting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ def test_simple():
946946
expected_lines.extend(
947947
[
948948
"*test_collection_collect_only_live_logging.py::test_simple*",
949-
"no tests ran in * seconds",
949+
"no tests ran in 0.[0-9][0-9]s",
950950
]
951951
)
952952
elif verbose == "-qq":

testing/test_pytester.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def test_assert_outcomes_after_pytest_error(testdir):
278278
testdir.makepyfile("def test_foo(): assert True")
279279

280280
result = testdir.runpytest("--unexpected-argument")
281-
with pytest.raises(ValueError, match="Pytest terminal report not found"):
281+
with pytest.raises(ValueError, match="Pytest terminal summary report not found"):
282282
result.assert_outcomes(passed=0)
283283

284284

testing/test_terminal.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ def test_passes():
617617
pluggy.__version__,
618618
),
619619
"*test_header_trailer_info.py .*",
620-
"=* 1 passed*in *.[0-9][0-9] seconds *=",
620+
"=* 1 passed*in *.[0-9][0-9]s *=",
621621
]
622622
)
623623
if request.config.pluginmanager.list_plugin_distinfo():
@@ -1678,3 +1678,20 @@ def check(msg, width, expected):
16781678
check("😄😄😄😄😄\n2nd line", 41, "FAILED nodeid::😄::withunicode - 😄😄...")
16791679
check("😄😄😄😄😄\n2nd line", 42, "FAILED nodeid::😄::withunicode - 😄😄😄...")
16801680
check("😄😄😄😄😄\n2nd line", 80, "FAILED nodeid::😄::withunicode - 😄😄😄😄😄")
1681+
1682+
1683+
@pytest.mark.parametrize(
1684+
"seconds, expected",
1685+
[
1686+
(10.0, "10.00s"),
1687+
(10.34, "10.34s"),
1688+
(59.99, "59.99s"),
1689+
(60.55, "60.55s (0:01:00)"),
1690+
(123.55, "123.55s (0:02:03)"),
1691+
(60 * 60 + 0.5, "3600.50s (1:00:00)"),
1692+
],
1693+
)
1694+
def test_format_session_duration(seconds, expected):
1695+
from _pytest.terminal import format_session_duration
1696+
1697+
assert format_session_duration(seconds) == expected

0 commit comments

Comments
 (0)