Skip to content

Improved time counter used to compute test durations. #6939

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 42 commits into from
Mar 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
04c5ca5
Improved time counter used to compute test durations. `CallInfo.start…
Mar 19, 2020
bf03b05
Added changelog and authors
Mar 19, 2020
bdb0d19
Linter fix
Mar 19, 2020
55d484c
Linter fix 2
Mar 19, 2020
8c041d3
Improved terminal message when the `--duration` option is used so as …
Mar 19, 2020
4d77bb2
Updated durations test to match the new terminal output
Mar 19, 2020
1c50d3d
make linter happy
Mar 19, 2020
47de8b8
As per code review: renamed towncrier changelog file
Mar 20, 2020
23d7a6c
New when `time.perf_counter_ns` is available (python >= 3.7) it is us…
Mar 20, 2020
481ba13
Edited changelog according to latest proposal
Mar 20, 2020
1649820
Linter/black edit
Mar 20, 2020
e1eb122
Made one test a little looser
Mar 20, 2020
5373d18
Attempt to improve terminal reporting again
Mar 21, 2020
70460b5
Modified tests so that they now pass of windows
Mar 21, 2020
e060ee0
Fixed test mistake on windows
Mar 21, 2020
2450d0c
Linter fixes
Mar 21, 2020
7997436
Flake8 fix
Mar 21, 2020
8099f02
Code simplification for linter: `duration_ns` is now always present b…
Mar 21, 2020
c828c0b
Updated changelog
Mar 21, 2020
a94382f
mypy fix
Mar 21, 2020
15d0a22
Tiny fix: now `duration_ns` attribute is always available on `TestRep…
Mar 21, 2020
8455b07
Fixed typos in comments
Mar 22, 2020
4743e49
Merge branch 'master' of https://github.com/pytest-dev/pytest into fi…
Mar 22, 2020
22c2dbc
Added docstring for `CallInfo`
Mar 27, 2020
f527d84
Merge branch 'master' of https://github.com/pytest-dev/pytest into fi…
Mar 27, 2020
2f6ffde
Update src/_pytest/runner.py
smarie Mar 27, 2020
92017f6
Update src/_pytest/runner.py
smarie Mar 27, 2020
b8830d9
Update src/_pytest/runner.py
smarie Mar 27, 2020
37347e2
Merge branch 'fix_issue_4391' of https://github.com/smarie/pytest int…
Mar 27, 2020
514f055
Merge branch 'master' of https://github.com/pytest-dev/pytest into fi…
Mar 27, 2020
99b779e
Added type hints for `CallInfo`
Mar 27, 2020
a1e92e9
as per code review using sys.platform instead of the platform module
Mar 27, 2020
7f37a49
lint fix
Mar 27, 2020
9a33ef4
Improved changelog
Mar 27, 2020
51ebcba
As per code review: removed duration_ns from the picture
Mar 27, 2020
4b23061
Final changelog update
Mar 27, 2020
a2f682c
Added changelog for the terminal improvement
Mar 27, 2020
c86a42b
Minor docstring edits
Mar 27, 2020
8a27c97
Minor docstring edits
Mar 27, 2020
daa588c
docstring update as per code review
Mar 28, 2020
533e5ec
Update docstring as per request in code review
nicoddemus Mar 28, 2020
57f8a41
Fix linting
nicoddemus Mar 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ Stefano Taschini
Steffen Allner
Stephan Obermann
Sven-Hendrik Haase
Sylvain Marié
Tadek Teleżyński
Takafumi Arakaki
Tarcisio Fischer
Expand Down
1 change: 1 addition & 0 deletions changelog/4391.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +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()``).
1 change: 1 addition & 0 deletions changelog/6940.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When using the ``--duration`` option, the terminal message output is now more precise about the number and durations of hidden items.
2 changes: 1 addition & 1 deletion src/_pytest/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def from_item_and_call(cls, item, call) -> "TestReport":
Factory method to create and fill a TestReport with standard item and call info.
"""
when = call.when
duration = call.stop - call.start
duration = call.duration
keywords = {x: 1 for x in item.keywords}
excinfo = call.excinfo
sections = []
Expand Down
45 changes: 35 additions & 10 deletions src/_pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import bdb
import os
import sys
from time import perf_counter
from time import time
from typing import Callable
from typing import Dict
Expand Down Expand Up @@ -59,15 +60,18 @@ def pytest_terminal_summary(terminalreporter):
dlist.sort(key=lambda x: x.duration)
dlist.reverse()
if not durations:
tr.write_sep("=", "slowest test durations")
tr.write_sep("=", "slowest durations")
else:
tr.write_sep("=", "slowest %s test durations" % durations)
tr.write_sep("=", "slowest %s durations" % durations)
dlist = dlist[:durations]

for rep in dlist:
for i, rep in enumerate(dlist):
if verbose < 2 and rep.duration < 0.005:
tr.write_line("")
tr.write_line("(0.00 durations hidden. Use -vv to show these durations.)")
tr.write_line(
"(%s durations < 0.005s hidden. Use -vv to show these durations.)"
% (len(dlist) - i)
)
break
tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid))

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

@attr.s(repr=False)
class CallInfo:
""" Result/Exception info a function invocation. """
""" Result/Exception info a function invocation.

:param result: The return value of the call, if it didn't raise. Can only be accessed
if excinfo is None.
:param Optional[ExceptionInfo] excinfo: The captured exception of the call, if it raised.
:param float start: The system time when the call started, in seconds since the epoch.
:param float stop: The system time when the call ended, in seconds since the epoch.
:param float duration: The call duration, in seconds.
:param str when: The context of invocation: "setup", "call", "teardown", ...
"""

_result = attr.ib()
excinfo = attr.ib(type=Optional[ExceptionInfo])
start = attr.ib()
stop = attr.ib()
when = attr.ib()
start = attr.ib(type=float)
stop = attr.ib(type=float)
duration = attr.ib(type=float)
when = attr.ib(type=str)

@property
def result(self):
Expand All @@ -238,17 +252,28 @@ def result(self):
def from_call(cls, func, when, reraise=None) -> "CallInfo":
#: context of invocation: one of "setup", "call",
#: "teardown", "memocollect"
start = time()
excinfo = None
start = time()
precise_start = perf_counter()
try:
result = func()
except: # noqa
excinfo = ExceptionInfo.from_current()
if reraise is not None and excinfo.errisinstance(reraise):
raise
result = None
# use the perf counter
precise_stop = perf_counter()
duration = precise_stop - precise_start
stop = time()
return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo)
return cls(
start=start,
stop=stop,
duration=duration,
when=when,
result=result,
excinfo=excinfo,
)

def __repr__(self):
if self.excinfo is None:
Expand Down
44 changes: 32 additions & 12 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,26 +896,42 @@ def test_has_plugin(self, request):
class TestDurations:
source = """
import time
frag = 0.002
frag = 0.002 # 2 ms
def test_something():
pass
def test_2():
time.sleep(frag*5)
time.sleep(frag*5) # 10 ms: on windows might sleep < 0.005s
def test_1():
time.sleep(frag)
time.sleep(frag) # 2 ms: on macOS/windows might sleep > 0.005s
def test_3():
time.sleep(frag*10)
time.sleep(frag*10) # 20 ms
"""

def test_calls(self, testdir):
testdir.makepyfile(self.source)
result = testdir.runpytest("--durations=10")
assert result.ret == 0
result.stdout.fnmatch_lines_random(
["*durations*", "*call*test_3*", "*call*test_2*"]
)

# on Windows, test 2 (10ms) can actually sleep less than 5ms and become hidden
if sys.platform == "win32":
to_match = ["*durations*", "*call*test_3*"]
else:
to_match = ["*durations*", "*call*test_3*", "*call*test_2*"]
result.stdout.fnmatch_lines_random(to_match)

# The number of hidden should be 8, but on macOS and windows it sometimes is 7
# - on MacOS and Windows test 1 can last longer and appear in the list
# - on Windows test 2 can last less and disappear from the list
if sys.platform in ("win32", "darwin"):
nb_hidden = "*"
else:
nb_hidden = "8"

result.stdout.fnmatch_lines(
["(0.00 durations hidden. Use -vv to show these durations.)"]
[
"(%s durations < 0.005s hidden. Use -vv to show these durations.)"
% nb_hidden
]
)

def test_calls_show_2(self, testdir):
Expand All @@ -929,7 +945,10 @@ def test_calls_showall(self, testdir):
testdir.makepyfile(self.source)
result = testdir.runpytest("--durations=0")
assert result.ret == 0
for x in "23":

# on windows, test 2 (10ms) can actually sleep less than 5ms and become hidden
tested = "3" if sys.platform == "win32" else "23"
for x in tested:
for y in ("call",): # 'setup', 'call', 'teardown':
for line in result.stdout.lines:
if ("test_%s" % x) in line and y in line:
Expand All @@ -951,9 +970,10 @@ def test_calls_showall_verbose(self, testdir):

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

def test_with_failing_collection(self, testdir):
testdir.makepyfile(self.source)
Expand All @@ -975,7 +995,7 @@ class TestDurationWithFixture:
source = """
import pytest
import time
frag = 0.01
frag = 0.02 # as on windows sleep(0.01) might take < 0.005s

@pytest.fixture
def setup_fixt():
Expand Down