Skip to content

Commit

Permalink
Implement progress percentage reporting while running tests
Browse files Browse the repository at this point in the history
Fix #2657
  • Loading branch information
nicoddemus committed Nov 21, 2017
1 parent 7a7cb8c commit dab8893
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 27 deletions.
56 changes: 49 additions & 7 deletions _pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def pytest_addoption(parser):
choices=['yes', 'no', 'auto'],
help="color terminal output (yes/no/auto).")

parser.addini("console_output_style",
help="console output: classic or with additional progress information.",
default='progress')


def pytest_configure(config):
config.option.verbose -= config.option.quiet
Expand Down Expand Up @@ -135,16 +139,20 @@ def __init__(self, config, file=None):
self.showfspath = self.verbosity >= 0
self.showlongtestinfo = self.verbosity > 0
self._numcollected = 0
self._session = None

self.stats = {}
self.startdir = py.path.local()
if file is None:
file = sys.stdout
self._writer = _pytest.config.create_terminal_writer(config, file)
self._screen_width = self.writer.fullwidth
self.currentfspath = None
self.reportchars = getreportopt(config)
self.hasmarkup = self.writer.hasmarkup
self.isatty = file.isatty()
self._progress_items_reported = 0
self._show_progress_info = self.config.getini('console_output_style') == 'progress'

@property
def writer(self):
Expand All @@ -163,6 +171,8 @@ def hasopt(self, char):
def write_fspath_result(self, nodeid, res):
fspath = self.config.rootdir.join(nodeid.split("::")[0])
if fspath != self.currentfspath:
if self.currentfspath is not None:
self._write_progress_information_filling_space()
self.currentfspath = fspath
fspath = self.startdir.bestrelpath(fspath)
self.writer.line()
Expand All @@ -177,6 +187,7 @@ def write_ensure_prefix(self, prefix, extra="", **kwargs):
if extra:
self.writer.write(extra, **kwargs)
self.currentfspath = -2
self._write_progress_information_filling_space()

def ensure_newline(self):
if self.currentfspath:
Expand Down Expand Up @@ -256,20 +267,25 @@ def pytest_runtest_logreport(self, report):
rep = report
res = self.config.hook.pytest_report_teststatus(report=rep)
cat, letter, word = res
if isinstance(word, tuple):
word, markup = word
else:
markup = None
self.stats.setdefault(cat, []).append(rep)
self._tests_ran = True
if not letter and not word:
# probably passed setup/teardown
return
running_xdist = hasattr(rep, 'node')
self._progress_items_reported += 1
if self.verbosity <= 0:
if not hasattr(rep, 'node') and self.showfspath:
if not running_xdist and self.showfspath:
self.write_fspath_result(rep.nodeid, letter)
else:
self.writer.write(letter)
self._write_progress_if_past_edge()
else:
if isinstance(word, tuple):
word, markup = word
else:
if markup is None:
if rep.passed:
markup = {'green': True}
elif rep.failed:
Expand All @@ -279,17 +295,42 @@ def pytest_runtest_logreport(self, report):
else:
markup = {}
line = self._locationline(rep.nodeid, *rep.location)
if not hasattr(rep, 'node'):
if not running_xdist:
self.write_ensure_prefix(line, word, **markup)
# self.writer.write(word, **markup)
else:
self.ensure_newline()
if hasattr(rep, 'node'):
if running_xdist:
self.writer.write("[%s] " % rep.node.gateway.id)
self.writer.write(word, **markup)
self.writer.write(" " + line)
self.currentfspath = -2

def _write_progress_if_past_edge(self):
if not self._show_progress_info:
return
last_item = self._progress_items_reported == self._session.testscollected
if last_item:
self._write_progress_information_filling_space()
return

past_edge = self.writer.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width
if past_edge:
msg = self._get_progress_information_message()
self.writer.write(msg + '\n', cyan=True)

_PROGRESS_LENGTH = len(' [100%]')

def _get_progress_information_message(self):
progress = self._progress_items_reported * 100 // self._session.testscollected
return ' [{:3d}%]'.format(progress)

def _write_progress_information_filling_space(self):
if not self._show_progress_info:
return
msg = self._get_progress_information_message()
fill = ' ' * (self.writer.fullwidth - self.writer.chars_on_current_line - len(msg) - 1)
self.write(fill + msg, cyan=True)

def pytest_collection(self):
if not self.isatty and self.config.option.verbose >= 1:
self.write("collecting ... ", bold=True)
Expand Down Expand Up @@ -332,6 +373,7 @@ def pytest_collection_modifyitems(self):

@pytest.hookimpl(trylast=True)
def pytest_sessionstart(self, session):
self._session = session
self._sessionstarttime = time.time()
if not self.showheader:
return
Expand Down
10 changes: 5 additions & 5 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,18 +630,18 @@ def join_pythonpath(*dirs):
testdir.chdir()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*test_hello.py::test_hello*PASSED",
"*test_hello.py::test_other*PASSED",
"*test_world.py::test_world*PASSED",
"*test_world.py::test_other*PASSED",
"*test_hello.py::test_hello*PASSED*",
"*test_hello.py::test_other*PASSED*",
"*test_world.py::test_world*PASSED*",
"*test_world.py::test_other*PASSED*",
"*4 passed*"
])

# specify tests within a module
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*test_world.py::test_other*PASSED",
"*test_world.py::test_other*PASSED*",
"*1 passed*"
])

Expand Down
8 changes: 8 additions & 0 deletions testing/python/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -2119,6 +2119,10 @@ def test_2(arg):
assert values == [1, 1, 2, 2]

def test_module_parametrized_ordering(self, testdir):
testdir.makeini("""
[pytest]
console_output_style=classic
""")
testdir.makeconftest("""
import pytest
Expand Down Expand Up @@ -2165,6 +2169,10 @@ def test_func4(marg):
""")

def test_class_ordering(self, testdir):
testdir.makeini("""
[pytest]
console_output_style=classic
""")
testdir.makeconftest("""
import pytest
Expand Down
14 changes: 9 additions & 5 deletions testing/python/metafunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,10 @@ def test_func(arg2):
])

def test_parametrize_with_ids(self, testdir):
testdir.makeini("""
[pytest]
console_output_style=classic
""")
testdir.makepyfile("""
import pytest
def pytest_generate_tests(metafunc):
Expand Down Expand Up @@ -1005,9 +1009,9 @@ def test_function(a, b):
result = testdir.runpytest("-v")
assert result.ret == 1
result.stdout.fnmatch_lines_random([
"*test_function*basic*PASSED",
"*test_function*1-1*PASSED",
"*test_function*advanced*FAILED",
"*test_function*basic*PASSED*",
"*test_function*1-1*PASSED*",
"*test_function*advanced*FAILED*",
])

def test_fixture_parametrized_empty_ids(self, testdir):
Expand Down Expand Up @@ -1062,8 +1066,8 @@ def test_function(a, b):
result = testdir.runpytest("-v")
assert result.ret == 1
result.stdout.fnmatch_lines_random([
"*test_function*a0*PASSED",
"*test_function*a1*FAILED"
"*test_function*a0*PASSED*",
"*test_function*a1*FAILED*"
])

@pytest.mark.parametrize(("scope", "length"),
Expand Down
2 changes: 1 addition & 1 deletion testing/python/setup_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,6 @@ def test_arg(arg):

result.stdout.fnmatch_lines([
'*SETUP F arg*',
'*test_arg (fixtures used: arg)F',
'*test_arg (fixtures used: arg)F*',
'*TEARDOWN F arg*',
])
4 changes: 2 additions & 2 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,7 @@ def test_onefails():
""")
result = testdir.runpytest(p1, "--tb=long")
result.stdout.fnmatch_lines([
"*test_traceback_failure.py F",
"*test_traceback_failure.py F*",
"====* FAILURES *====",
"____*____",
"",
Expand All @@ -840,7 +840,7 @@ def test_onefails():

result = testdir.runpytest(p1) # "auto"
result.stdout.fnmatch_lines([
"*test_traceback_failure.py F",
"*test_traceback_failure.py F*",
"====* FAILURES *====",
"____*____",
"",
Expand Down
2 changes: 1 addition & 1 deletion testing/test_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def test_capturing_error():
""")
result = testdir.runpytest(p1)
result.stdout.fnmatch_lines([
"*test_capturing_outerr.py .F",
"*test_capturing_outerr.py .F*",
"====* FAILURES *====",
"____*____",
"*test_capturing_outerr.py:8: ValueError",
Expand Down
12 changes: 6 additions & 6 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_func():
])
else:
result.stdout.fnmatch_lines([
"*test_pass_skip_fail.py .sF"
"*test_pass_skip_fail.py .sF*"
])
result.stdout.fnmatch_lines([
" def test_func():",
Expand Down Expand Up @@ -142,12 +142,12 @@ class TestMore(BaseTests):
""")
result = testdir.runpytest(p2)
result.stdout.fnmatch_lines([
"*test_p2.py .",
"*test_p2.py .*",
"*1 passed*",
])
result = testdir.runpytest("-v", p2)
result.stdout.fnmatch_lines([
"*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED",
"*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED*",
])

def test_itemreport_directclasses_not_shown_as_subclasses(self, testdir):
Expand Down Expand Up @@ -431,7 +431,7 @@ def test_three():
)
result = testdir.runpytest("-k", "test_two:", testpath)
result.stdout.fnmatch_lines([
"*test_deselected.py ..",
"*test_deselected.py ..*",
"=* 1 test*deselected *=",
])
assert result.ret == 0
Expand Down Expand Up @@ -464,7 +464,7 @@ def test_method(self):
finally:
old.chdir()
result.stdout.fnmatch_lines([
"test_passes.py ..",
"test_passes.py ..*",
"* 2 pass*",
])
assert result.ret == 0
Expand All @@ -481,7 +481,7 @@ def test_passes():
"platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" % (
py.std.sys.platform, verinfo,
pytest.__version__, py.__version__, pluggy.__version__),
"*test_header_trailer_info.py .",
"*test_header_trailer_info.py .*",
"=* 1 passed*in *.[0-9][0-9] seconds *=",
])
if pytest.config.pluginmanager.list_plugin_distinfo():
Expand Down

0 comments on commit dab8893

Please sign in to comment.