Skip to content

Commit 95fadd5

Browse files
smarieSylvain MARIEbluetechnicoddemus
authored
Improved time counter used to compute test durations. (#6939)
Co-authored-by: Sylvain MARIE <[email protected]> Co-authored-by: Ran Benita <[email protected]> Co-authored-by: Bruno Oliveira <[email protected]>
1 parent f84742d commit 95fadd5

File tree

6 files changed

+71
-23
lines changed

6 files changed

+71
-23
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ Stefano Taschini
254254
Steffen Allner
255255
Stephan Obermann
256256
Sven-Hendrik Haase
257+
Sylvain Marié
257258
Tadek Teleżyński
258259
Takafumi Arakaki
259260
Tarcisio Fischer

changelog/4391.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved precision of test durations measurement. ``CallInfo`` items now have a new ``<CallInfo>.duration`` attribute, created using ``time.perf_counter()``. This attribute is used to fill the ``<TestReport>.duration`` attribute, which is more accurate than the previous ``<CallInfo>.stop - <CallInfo>.start`` (as these are based on ``time.time()``).

changelog/6940.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
When using the ``--duration`` option, the terminal message output is now more precise about the number and durations of hidden items.

src/_pytest/reports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def from_item_and_call(cls, item, call) -> "TestReport":
273273
Factory method to create and fill a TestReport with standard item and call info.
274274
"""
275275
when = call.when
276-
duration = call.stop - call.start
276+
duration = call.duration
277277
keywords = {x: 1 for x in item.keywords}
278278
excinfo = call.excinfo
279279
sections = []

src/_pytest/runner.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import bdb
33
import os
44
import sys
5+
from time import perf_counter
56
from time import time
67
from typing import Callable
78
from typing import Dict
@@ -59,15 +60,18 @@ def pytest_terminal_summary(terminalreporter):
5960
dlist.sort(key=lambda x: x.duration)
6061
dlist.reverse()
6162
if not durations:
62-
tr.write_sep("=", "slowest test durations")
63+
tr.write_sep("=", "slowest durations")
6364
else:
64-
tr.write_sep("=", "slowest %s test durations" % durations)
65+
tr.write_sep("=", "slowest %s durations" % durations)
6566
dlist = dlist[:durations]
6667

67-
for rep in dlist:
68+
for i, rep in enumerate(dlist):
6869
if verbose < 2 and rep.duration < 0.005:
6970
tr.write_line("")
70-
tr.write_line("(0.00 durations hidden. Use -vv to show these durations.)")
71+
tr.write_line(
72+
"(%s durations < 0.005s hidden. Use -vv to show these durations.)"
73+
% (len(dlist) - i)
74+
)
7175
break
7276
tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid))
7377

@@ -220,13 +224,23 @@ def call_runtest_hook(item, when: "Literal['setup', 'call', 'teardown']", **kwds
220224

221225
@attr.s(repr=False)
222226
class CallInfo:
223-
""" Result/Exception info a function invocation. """
227+
""" Result/Exception info a function invocation.
228+
229+
:param result: The return value of the call, if it didn't raise. Can only be accessed
230+
if excinfo is None.
231+
:param Optional[ExceptionInfo] excinfo: The captured exception of the call, if it raised.
232+
:param float start: The system time when the call started, in seconds since the epoch.
233+
:param float stop: The system time when the call ended, in seconds since the epoch.
234+
:param float duration: The call duration, in seconds.
235+
:param str when: The context of invocation: "setup", "call", "teardown", ...
236+
"""
224237

225238
_result = attr.ib()
226239
excinfo = attr.ib(type=Optional[ExceptionInfo])
227-
start = attr.ib()
228-
stop = attr.ib()
229-
when = attr.ib()
240+
start = attr.ib(type=float)
241+
stop = attr.ib(type=float)
242+
duration = attr.ib(type=float)
243+
when = attr.ib(type=str)
230244

231245
@property
232246
def result(self):
@@ -238,17 +252,28 @@ def result(self):
238252
def from_call(cls, func, when, reraise=None) -> "CallInfo":
239253
#: context of invocation: one of "setup", "call",
240254
#: "teardown", "memocollect"
241-
start = time()
242255
excinfo = None
256+
start = time()
257+
precise_start = perf_counter()
243258
try:
244259
result = func()
245260
except: # noqa
246261
excinfo = ExceptionInfo.from_current()
247262
if reraise is not None and excinfo.errisinstance(reraise):
248263
raise
249264
result = None
265+
# use the perf counter
266+
precise_stop = perf_counter()
267+
duration = precise_stop - precise_start
250268
stop = time()
251-
return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo)
269+
return cls(
270+
start=start,
271+
stop=stop,
272+
duration=duration,
273+
when=when,
274+
result=result,
275+
excinfo=excinfo,
276+
)
252277

253278
def __repr__(self):
254279
if self.excinfo is None:

testing/acceptance_test.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -896,26 +896,42 @@ def test_has_plugin(self, request):
896896
class TestDurations:
897897
source = """
898898
import time
899-
frag = 0.002
899+
frag = 0.002 # 2 ms
900900
def test_something():
901901
pass
902902
def test_2():
903-
time.sleep(frag*5)
903+
time.sleep(frag*5) # 10 ms: on windows might sleep < 0.005s
904904
def test_1():
905-
time.sleep(frag)
905+
time.sleep(frag) # 2 ms: on macOS/windows might sleep > 0.005s
906906
def test_3():
907-
time.sleep(frag*10)
907+
time.sleep(frag*10) # 20 ms
908908
"""
909909

910910
def test_calls(self, testdir):
911911
testdir.makepyfile(self.source)
912912
result = testdir.runpytest("--durations=10")
913913
assert result.ret == 0
914-
result.stdout.fnmatch_lines_random(
915-
["*durations*", "*call*test_3*", "*call*test_2*"]
916-
)
914+
915+
# on Windows, test 2 (10ms) can actually sleep less than 5ms and become hidden
916+
if sys.platform == "win32":
917+
to_match = ["*durations*", "*call*test_3*"]
918+
else:
919+
to_match = ["*durations*", "*call*test_3*", "*call*test_2*"]
920+
result.stdout.fnmatch_lines_random(to_match)
921+
922+
# The number of hidden should be 8, but on macOS and windows it sometimes is 7
923+
# - on MacOS and Windows test 1 can last longer and appear in the list
924+
# - on Windows test 2 can last less and disappear from the list
925+
if sys.platform in ("win32", "darwin"):
926+
nb_hidden = "*"
927+
else:
928+
nb_hidden = "8"
929+
917930
result.stdout.fnmatch_lines(
918-
["(0.00 durations hidden. Use -vv to show these durations.)"]
931+
[
932+
"(%s durations < 0.005s hidden. Use -vv to show these durations.)"
933+
% nb_hidden
934+
]
919935
)
920936

921937
def test_calls_show_2(self, testdir):
@@ -929,7 +945,10 @@ def test_calls_showall(self, testdir):
929945
testdir.makepyfile(self.source)
930946
result = testdir.runpytest("--durations=0")
931947
assert result.ret == 0
932-
for x in "23":
948+
949+
# on windows, test 2 (10ms) can actually sleep less than 5ms and become hidden
950+
tested = "3" if sys.platform == "win32" else "23"
951+
for x in tested:
933952
for y in ("call",): # 'setup', 'call', 'teardown':
934953
for line in result.stdout.lines:
935954
if ("test_%s" % x) in line and y in line:
@@ -951,9 +970,10 @@ def test_calls_showall_verbose(self, testdir):
951970

952971
def test_with_deselected(self, testdir):
953972
testdir.makepyfile(self.source)
954-
result = testdir.runpytest("--durations=2", "-k test_2")
973+
# on windows test 2 might sleep less than 0.005s and be hidden. Prefer test 3.
974+
result = testdir.runpytest("--durations=2", "-k test_3")
955975
assert result.ret == 0
956-
result.stdout.fnmatch_lines(["*durations*", "*call*test_2*"])
976+
result.stdout.fnmatch_lines(["*durations*", "*call*test_3*"])
957977

958978
def test_with_failing_collection(self, testdir):
959979
testdir.makepyfile(self.source)
@@ -975,7 +995,7 @@ class TestDurationWithFixture:
975995
source = """
976996
import pytest
977997
import time
978-
frag = 0.01
998+
frag = 0.02 # as on windows sleep(0.01) might take < 0.005s
979999
9801000
@pytest.fixture
9811001
def setup_fixt():

0 commit comments

Comments
 (0)