diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 2b13fe1ea..eb7b03e35 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -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 @@ -135,16 +139,20 @@ class TerminalReporter: 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): @@ -163,6 +171,8 @@ class TerminalReporter: 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() @@ -177,6 +187,7 @@ class TerminalReporter: if extra: self.writer.write(extra, **kwargs) self.currentfspath = -2 + self._write_progress_information_filling_space() def ensure_newline(self): if self.currentfspath: @@ -256,20 +267,25 @@ class TerminalReporter: 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: @@ -279,17 +295,42 @@ class TerminalReporter: 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) @@ -332,6 +373,7 @@ class TerminalReporter: @pytest.hookimpl(trylast=True) def pytest_sessionstart(self, session): + self._session = session self._sessionstarttime = time.time() if not self.showheader: return diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 111be956b..48eb60a3f 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -630,10 +630,10 @@ class TestInvocationVariants(object): 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*" ]) @@ -641,7 +641,7 @@ class TestInvocationVariants(object): 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*" ]) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 1a0603a80..b159e8ebb 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2119,6 +2119,10 @@ class TestFixtureMarker(object): assert values == [1, 1, 2, 2] def test_module_parametrized_ordering(self, testdir): + testdir.makeini(""" + [pytest] + console_output_style=classic + """) testdir.makeconftest(""" import pytest @@ -2165,6 +2169,10 @@ class TestFixtureMarker(object): """) def test_class_ordering(self, testdir): + testdir.makeini(""" + [pytest] + console_output_style=classic + """) testdir.makeconftest(""" import pytest diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index a8b6d685b..2ffb7bb5d 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -960,6 +960,10 @@ class TestMetafuncFunctional(object): ]) def test_parametrize_with_ids(self, testdir): + testdir.makeini(""" + [pytest] + console_output_style=classic + """) testdir.makepyfile(""" import pytest def pytest_generate_tests(metafunc): @@ -1005,9 +1009,9 @@ class TestMetafuncFunctional(object): 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): @@ -1062,8 +1066,8 @@ class TestMetafuncFunctional(object): 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"), diff --git a/testing/python/setup_only.py b/testing/python/setup_only.py index 18af56477..ab34312fc 100644 --- a/testing/python/setup_only.py +++ b/testing/python/setup_only.py @@ -238,6 +238,6 @@ def test_show_fixtures_and_execute_test(testdir): result.stdout.fnmatch_lines([ '*SETUP F arg*', - '*test_arg (fixtures used: arg)F', + '*test_arg (fixtures used: arg)F*', '*TEARDOWN F arg*', ]) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 4cd050d8c..6c1443c30 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -820,7 +820,7 @@ def test_traceback_failure(testdir): """) result = testdir.runpytest(p1, "--tb=long") result.stdout.fnmatch_lines([ - "*test_traceback_failure.py F", + "*test_traceback_failure.py F*", "====* FAILURES *====", "____*____", "", @@ -840,7 +840,7 @@ def test_traceback_failure(testdir): result = testdir.runpytest(p1) # "auto" result.stdout.fnmatch_lines([ - "*test_traceback_failure.py F", + "*test_traceback_failure.py F*", "====* FAILURES *====", "____*____", "", diff --git a/testing/test_capture.py b/testing/test_capture.py index 3e8162f71..f769a725d 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -266,7 +266,7 @@ class TestPerTestCapturing(object): """) 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", diff --git a/testing/test_terminal.py b/testing/test_terminal.py index b7c880feb..122aba862 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -78,7 +78,7 @@ class TestTerminal(object): ]) else: result.stdout.fnmatch_lines([ - "*test_pass_skip_fail.py .sF" + "*test_pass_skip_fail.py .sF*" ]) result.stdout.fnmatch_lines([ " def test_func():", @@ -142,12 +142,12 @@ class TestTerminal(object): """) 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): @@ -431,7 +431,7 @@ class TestTerminalFunctional(object): ) result = testdir.runpytest("-k", "test_two:", testpath) result.stdout.fnmatch_lines([ - "*test_deselected.py ..", + "*test_deselected.py ..*", "=* 1 test*deselected *=", ]) assert result.ret == 0 @@ -464,7 +464,7 @@ class TestTerminalFunctional(object): finally: old.chdir() result.stdout.fnmatch_lines([ - "test_passes.py ..", + "test_passes.py ..*", "* 2 pass*", ]) assert result.ret == 0 @@ -481,7 +481,7 @@ class TestTerminalFunctional(object): "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():