From 661055105c35039774c06be129fdf868b56dbdac Mon Sep 17 00:00:00 2001 From: Niclas Olofsson Date: Sat, 26 Sep 2015 14:30:16 +0200 Subject: [PATCH 01/16] Restructured project. --- pytest_stepwise/__init__.py | 1 + pytest_stepwise/compat.py | 4 + pytest_stepwise/plugin.py | 90 ++++++++++++++++++++++ tests/conftest.py | 1 + tests/test_pytest_stepwise.py | 136 ++++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+) create mode 100644 pytest_stepwise/__init__.py create mode 100644 pytest_stepwise/compat.py create mode 100644 pytest_stepwise/plugin.py create mode 100644 tests/conftest.py create mode 100644 tests/test_pytest_stepwise.py diff --git a/pytest_stepwise/__init__.py b/pytest_stepwise/__init__.py new file mode 100644 index 000000000..58d168b07 --- /dev/null +++ b/pytest_stepwise/__init__.py @@ -0,0 +1 @@ +__version__ = '0.4' diff --git a/pytest_stepwise/compat.py b/pytest_stepwise/compat.py new file mode 100644 index 000000000..31f132c70 --- /dev/null +++ b/pytest_stepwise/compat.py @@ -0,0 +1,4 @@ +try: + from _pytest.cacheprovider import Cache +except ImportError: + from pytest_cache import Cache diff --git a/pytest_stepwise/plugin.py b/pytest_stepwise/plugin.py new file mode 100644 index 000000000..949ca3a67 --- /dev/null +++ b/pytest_stepwise/plugin.py @@ -0,0 +1,90 @@ +import pytest +from .compat import Cache + + +def pytest_addoption(parser): + group = parser.getgroup('general') + group.addoption('--sw', action='store_true', dest='stepwise', + help='alias for --stepwise') + group.addoption('--stepwise', action='store_true', dest='stepwise', + help='exit on test fail and continue from last failing test next time') + group.addoption('--skip', action='store_true', dest='skip', + help='ignore the first failing test but stop on the next failing test') + + +@pytest.hookimpl(tryfirst=True) +def pytest_configure(config): + config.cache = Cache(config) + config.pluginmanager.register(StepwisePlugin(config), 'stepwiseplugin') + + +class StepwisePlugin: + def __init__(self, config): + self.config = config + self.active = config.getvalue('stepwise') + self.session = None + + if self.active: + self.lastfailed = config.cache.get('cache/stepwise', set()) + self.skip = config.getvalue('skip') + + def pytest_sessionstart(self, session): + self.session = session + + def pytest_collection_modifyitems(self, session, config, items): + if not self.active or not self.lastfailed: + return + + already_passed = [] + found = False + + # Make a list of all tests that has been runned before the last failing one. + for item in items: + if item.nodeid in self.lastfailed: + found = True + break + else: + already_passed.append(item) + + # If the previously failed test was not found among the test items, + # do not skip any tests. + if not found: + already_passed = [] + + for item in already_passed: + items.remove(item) + + config.hook.pytest_deselected(items=already_passed) + + def pytest_collectreport(self, report): + if self.active and report.failed: + self.session.shouldstop = 'Error when collecting test, stopping test execution.' + + def pytest_runtest_logreport(self, report): + # Skip this hook if plugin is not active or the test is xfailed. + if not self.active or 'xfail' in report.keywords: + return + + if report.failed: + if self.skip: + # Remove test from the failed ones (if it exists) and unset the skip option + # to make sure the following tests will not be skipped. + self.lastfailed.discard(report.nodeid) + self.skip = False + else: + # Mark test as the last failing and interrupt the test session. + self.lastfailed.add(report.nodeid) + self.session.shouldstop = 'Test failed, continuing from this test next run.' + + else: + # If the test was actually run and did pass. + if report.when == 'call': + # Remove test from the failed ones, if exists. + self.lastfailed.discard(report.nodeid) + + def pytest_sessionfinish(self, session): + if self.active: + self.config.cache.set('cache/stepwise', self.lastfailed) + else: + # Clear the list of failing tests if the plugin is not active. + self.config.cache.set('cache/stepwise', set()) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..bc711e55f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = 'pytester' diff --git a/tests/test_pytest_stepwise.py b/tests/test_pytest_stepwise.py new file mode 100644 index 000000000..96b376c3f --- /dev/null +++ b/tests/test_pytest_stepwise.py @@ -0,0 +1,136 @@ +import pytest + + +@pytest.fixture +def stepwise_testdir(testdir): + # Rather than having to modify our testfile between tests, we introduce + # a flag for wether or not the second test should fail. + testdir.makeconftest(''' +def pytest_addoption(parser): + group = parser.getgroup('general') + group.addoption('--fail', action='store_true', dest='fail') + group.addoption('--fail-last', action='store_true', dest='fail_last') +''') + + # Create a simple test suite. + testdir.makepyfile(test_stepwise=''' +def test_success_before_fail(): + assert 1 + +def test_fail_on_flag(request): + assert not request.config.getvalue('fail') + +def test_success_after_fail(): + assert 1 + +def test_fail_last_on_flag(request): + assert not request.config.getvalue('fail_last') + +def test_success_after_last_fail(): + assert 1 +''') + + testdir.makepyfile(testfile_b=''' +def test_success(): + assert 1 +''') + + return testdir + + +@pytest.fixture +def error_testdir(testdir): + testdir.makepyfile(test_stepwise=''' +def test_error(nonexisting_fixture): + assert 1 + +def test_success_after_fail(): + assert 1 +''') + + return testdir + + +@pytest.fixture +def broken_testdir(testdir): + testdir.makepyfile(working_testfile='def test_proper(): assert 1', broken_testfile='foobar') + return testdir + + +def test_run_without_stepwise(stepwise_testdir): + result = stepwise_testdir.runpytest('-v', '--strict', '--fail') + + assert not result.errlines + result.stdout.fnmatch_lines(['*test_success_before_fail PASSED*']) + result.stdout.fnmatch_lines(['*test_fail_on_flag FAILED*']) + result.stdout.fnmatch_lines(['*test_success_after_fail PASSED*']) + + +def test_fail_and_continue_with_stepwise(stepwise_testdir): + # Run the tests with a failing second test. + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail') + assert not result.errlines + + stdout = result.stdout.str() + # Make sure we stop after first failing test. + assert 'test_success_before_fail PASSED' in stdout + assert 'test_fail_on_flag FAILED' in stdout + assert 'test_success_after_fail' not in stdout + + # "Fix" the test that failed in the last run and run it again. + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise') + assert not result.errlines + + stdout = result.stdout.str() + # Make sure the latest failing test runs and then continues. + assert 'test_success_before_fail' not in stdout + assert 'test_fail_on_flag PASSED' in stdout + assert 'test_success_after_fail PASSED' in stdout + + +def test_run_with_skip_option(stepwise_testdir): + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--skip', + '--fail', '--fail-last') + assert not result.errlines + + stdout = result.stdout.str() + # Make sure first fail is ignore and second fail stops the test run. + assert 'test_fail_on_flag FAILED' in stdout + assert 'test_success_after_fail PASSED' in stdout + assert 'test_fail_last_on_flag FAILED' in stdout + assert 'test_success_after_last_fail' not in stdout + + +def test_fail_on_errors(error_testdir): + result = error_testdir.runpytest('-v', '--strict', '--stepwise') + + assert not result.errlines + stdout = result.stdout.str() + + assert 'test_error ERROR' in stdout + assert 'test_success_after_fail' not in stdout + + +def test_change_testfile(stepwise_testdir): + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail', + 'test_stepwise.py') + assert not result.errlines + + stdout = result.stdout.str() + assert 'test_fail_on_flag FAILED' in stdout + + # Make sure the second test run starts from the beginning, since the + # test to continue from does not exist in testfile_b. + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', + 'testfile_b.py') + assert not result.errlines + + stdout = result.stdout.str() + assert 'test_success PASSED' in stdout + + +def test_stop_on_collection_errors(broken_testdir): + result = broken_testdir.runpytest('-v', '--strict', '--stepwise', 'working_testfile.py', 'broken_testfile.py') + + stdout = result.stdout.str() + assert 'Error when collecting test' in stdout From 1d23bef3fb0bf3bf5efba933ccb7ac1bf5b65efa Mon Sep 17 00:00:00 2001 From: Niclas Olofsson Date: Sat, 26 Sep 2015 14:32:11 +0200 Subject: [PATCH 02/16] Use a single node ID rather than a set for failed tests. --- pytest_stepwise/plugin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pytest_stepwise/plugin.py b/pytest_stepwise/plugin.py index 949ca3a67..89cd5125b 100644 --- a/pytest_stepwise/plugin.py +++ b/pytest_stepwise/plugin.py @@ -25,7 +25,7 @@ class StepwisePlugin: self.session = None if self.active: - self.lastfailed = config.cache.get('cache/stepwise', set()) + self.lastfailed = config.cache.get('cache/stepwise', None) self.skip = config.getvalue('skip') def pytest_sessionstart(self, session): @@ -40,7 +40,7 @@ class StepwisePlugin: # Make a list of all tests that has been runned before the last failing one. for item in items: - if item.nodeid in self.lastfailed: + if item.nodeid == self.lastfailed: found = True break else: @@ -69,22 +69,25 @@ class StepwisePlugin: if self.skip: # Remove test from the failed ones (if it exists) and unset the skip option # to make sure the following tests will not be skipped. - self.lastfailed.discard(report.nodeid) + if report.nodeid == self.lastfailed: + self.lastfailed = None + self.skip = False else: # Mark test as the last failing and interrupt the test session. - self.lastfailed.add(report.nodeid) + self.lastfailed = report.nodeid self.session.shouldstop = 'Test failed, continuing from this test next run.' else: # If the test was actually run and did pass. if report.when == 'call': # Remove test from the failed ones, if exists. - self.lastfailed.discard(report.nodeid) + if report.nodeid == self.lastfailed: + self.lastfailed = None def pytest_sessionfinish(self, session): if self.active: self.config.cache.set('cache/stepwise', self.lastfailed) else: # Clear the list of failing tests if the plugin is not active. - self.config.cache.set('cache/stepwise', set()) + self.config.cache.set('cache/stepwise', []) From 33f1ff4e8cb49f05c0fe8df38765741e926dd12e Mon Sep 17 00:00:00 2001 From: Niclas Olofsson Date: Sat, 26 Sep 2015 14:59:28 +0200 Subject: [PATCH 03/16] Use result.stderr in tests since result.errlines has changed behaviour. --- tests/test_pytest_stepwise.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_pytest_stepwise.py b/tests/test_pytest_stepwise.py index 96b376c3f..1d0c4e8a8 100644 --- a/tests/test_pytest_stepwise.py +++ b/tests/test_pytest_stepwise.py @@ -60,7 +60,6 @@ def broken_testdir(testdir): def test_run_without_stepwise(stepwise_testdir): result = stepwise_testdir.runpytest('-v', '--strict', '--fail') - assert not result.errlines result.stdout.fnmatch_lines(['*test_success_before_fail PASSED*']) result.stdout.fnmatch_lines(['*test_fail_on_flag FAILED*']) result.stdout.fnmatch_lines(['*test_success_after_fail PASSED*']) @@ -69,7 +68,7 @@ def test_run_without_stepwise(stepwise_testdir): def test_fail_and_continue_with_stepwise(stepwise_testdir): # Run the tests with a failing second test. result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() # Make sure we stop after first failing test. @@ -79,7 +78,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): # "Fix" the test that failed in the last run and run it again. result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() # Make sure the latest failing test runs and then continues. @@ -91,7 +90,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): def test_run_with_skip_option(stepwise_testdir): result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--skip', '--fail', '--fail-last') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() # Make sure first fail is ignore and second fail stops the test run. @@ -104,7 +103,7 @@ def test_run_with_skip_option(stepwise_testdir): def test_fail_on_errors(error_testdir): result = error_testdir.runpytest('-v', '--strict', '--stepwise') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() assert 'test_error ERROR' in stdout @@ -114,7 +113,7 @@ def test_fail_on_errors(error_testdir): def test_change_testfile(stepwise_testdir): result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail', 'test_stepwise.py') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() assert 'test_fail_on_flag FAILED' in stdout @@ -123,7 +122,7 @@ def test_change_testfile(stepwise_testdir): # test to continue from does not exist in testfile_b. result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', 'testfile_b.py') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() assert 'test_success PASSED' in stdout From bd9495486b7c45a1aadd5fa94a95110ac450a143 Mon Sep 17 00:00:00 2001 From: Niclas Olofsson Date: Sat, 26 Sep 2015 15:23:11 +0200 Subject: [PATCH 04/16] pytest 2.7 compatibility. --- pytest_stepwise/compat.py | 8 ++++++++ pytest_stepwise/plugin.py | 5 ++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pytest_stepwise/compat.py b/pytest_stepwise/compat.py index 31f132c70..ce28f7474 100644 --- a/pytest_stepwise/compat.py +++ b/pytest_stepwise/compat.py @@ -1,4 +1,12 @@ +import pytest + try: from _pytest.cacheprovider import Cache except ImportError: from pytest_cache import Cache + + +if hasattr(pytest, 'hookimpl'): + tryfirst = pytest.hookimpl(tryfirst=True) +else: + tryfirst = pytest.mark.tryfirst diff --git a/pytest_stepwise/plugin.py b/pytest_stepwise/plugin.py index 89cd5125b..1f0137a46 100644 --- a/pytest_stepwise/plugin.py +++ b/pytest_stepwise/plugin.py @@ -1,5 +1,4 @@ -import pytest -from .compat import Cache +from .compat import Cache, tryfirst def pytest_addoption(parser): @@ -12,7 +11,7 @@ def pytest_addoption(parser): help='ignore the first failing test but stop on the next failing test') -@pytest.hookimpl(tryfirst=True) +@tryfirst def pytest_configure(config): config.cache = Cache(config) config.pluginmanager.register(StepwisePlugin(config), 'stepwiseplugin') From d9c428c1ded12bae5ac98c6780e20bb0d211c90a Mon Sep 17 00:00:00 2001 From: David Szotten Date: Wed, 1 Aug 2018 11:48:15 +0100 Subject: [PATCH 05/16] add compat for pytest 3.7 and tox config for (some of) the versions i could still get working --- pytest_stepwise/compat.py | 6 ++++++ tests/test_pytest_stepwise.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pytest_stepwise/compat.py b/pytest_stepwise/compat.py index ce28f7474..a1cc1e986 100644 --- a/pytest_stepwise/compat.py +++ b/pytest_stepwise/compat.py @@ -5,6 +5,12 @@ try: except ImportError: from pytest_cache import Cache +try: + # pytest 3.7+ + Cache = Cache.for_config +except AttributeError: + pass + if hasattr(pytest, 'hookimpl'): tryfirst = pytest.hookimpl(tryfirst=True) diff --git a/tests/test_pytest_stepwise.py b/tests/test_pytest_stepwise.py index 1d0c4e8a8..cb52e9ead 100644 --- a/tests/test_pytest_stepwise.py +++ b/tests/test_pytest_stepwise.py @@ -132,4 +132,7 @@ def test_stop_on_collection_errors(broken_testdir): result = broken_testdir.runpytest('-v', '--strict', '--stepwise', 'working_testfile.py', 'broken_testfile.py') stdout = result.stdout.str() - assert 'Error when collecting test' in stdout + if pytest.__version__ < '3.0.0': + assert 'Error when collecting test' in stdout + else: + assert 'errors during collection' in stdout From c56d7ac40e795494a0f6b445402dec5d36b9f5ed Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 09:23:21 +0100 Subject: [PATCH 06/16] move files into the pytest file structure --- pytest_stepwise/__init__.py | 1 - pytest_stepwise/compat.py | 18 ------------------ .../plugin.py => src/_pytest/stepwise.py | 0 .../test_stepwise.py | 0 tests/conftest.py | 1 - 5 files changed, 20 deletions(-) delete mode 100644 pytest_stepwise/__init__.py delete mode 100644 pytest_stepwise/compat.py rename pytest_stepwise/plugin.py => src/_pytest/stepwise.py (100%) rename tests/test_pytest_stepwise.py => testing/test_stepwise.py (100%) delete mode 100644 tests/conftest.py diff --git a/pytest_stepwise/__init__.py b/pytest_stepwise/__init__.py deleted file mode 100644 index 58d168b07..000000000 --- a/pytest_stepwise/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.4' diff --git a/pytest_stepwise/compat.py b/pytest_stepwise/compat.py deleted file mode 100644 index a1cc1e986..000000000 --- a/pytest_stepwise/compat.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -try: - from _pytest.cacheprovider import Cache -except ImportError: - from pytest_cache import Cache - -try: - # pytest 3.7+ - Cache = Cache.for_config -except AttributeError: - pass - - -if hasattr(pytest, 'hookimpl'): - tryfirst = pytest.hookimpl(tryfirst=True) -else: - tryfirst = pytest.mark.tryfirst diff --git a/pytest_stepwise/plugin.py b/src/_pytest/stepwise.py similarity index 100% rename from pytest_stepwise/plugin.py rename to src/_pytest/stepwise.py diff --git a/tests/test_pytest_stepwise.py b/testing/test_stepwise.py similarity index 100% rename from tests/test_pytest_stepwise.py rename to testing/test_stepwise.py diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index bc711e55f..000000000 --- a/tests/conftest.py +++ /dev/null @@ -1 +0,0 @@ -pytest_plugins = 'pytester' From 63c01d1541eda890cdb10909af09195711c8a36a Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 12:58:11 +0100 Subject: [PATCH 07/16] update for builtin plugin --- src/_pytest/config/__init__.py | 1 + src/_pytest/stepwise.py | 15 +++++++-------- testing/test_stepwise.py | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 88cbf14ba..29227cc6b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -126,6 +126,7 @@ default_plugins = ( "freeze_support", "setuponly", "setupplan", + "stepwise", "warnings", "logging", ) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 1f0137a46..f408e1fa9 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,19 +1,18 @@ -from .compat import Cache, tryfirst +from _pytest.cacheprovider import Cache +import pytest def pytest_addoption(parser): group = parser.getgroup('general') - group.addoption('--sw', action='store_true', dest='stepwise', - help='alias for --stepwise') - group.addoption('--stepwise', action='store_true', dest='stepwise', + group.addoption('--sw', '--stepwise', action='store_true', dest='stepwise', help='exit on test fail and continue from last failing test next time') - group.addoption('--skip', action='store_true', dest='skip', + group.addoption('--stepwise-skip', action='store_true', dest='stepwise_skip', help='ignore the first failing test but stop on the next failing test') -@tryfirst +@pytest.hookimpl(tryfirst=True) def pytest_configure(config): - config.cache = Cache(config) + config.cache = Cache.for_config(config) config.pluginmanager.register(StepwisePlugin(config), 'stepwiseplugin') @@ -25,7 +24,7 @@ class StepwisePlugin: if self.active: self.lastfailed = config.cache.get('cache/stepwise', None) - self.skip = config.getvalue('skip') + self.skip = config.getvalue('stepwise_skip') def pytest_sessionstart(self, session): self.session = session diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index cb52e9ead..0e1e53226 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -13,7 +13,7 @@ def pytest_addoption(parser): ''') # Create a simple test suite. - testdir.makepyfile(test_stepwise=''' + testdir.makepyfile(test_a=''' def test_success_before_fail(): assert 1 @@ -30,7 +30,7 @@ def test_success_after_last_fail(): assert 1 ''') - testdir.makepyfile(testfile_b=''' + testdir.makepyfile(test_b=''' def test_success(): assert 1 ''') @@ -40,7 +40,7 @@ def test_success(): @pytest.fixture def error_testdir(testdir): - testdir.makepyfile(test_stepwise=''' + testdir.makepyfile(test_a=''' def test_error(nonexisting_fixture): assert 1 @@ -88,7 +88,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): def test_run_with_skip_option(stepwise_testdir): - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--skip', + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--stepwise-skip', '--fail', '--fail-last') assert not result.stderr.str() @@ -112,7 +112,7 @@ def test_fail_on_errors(error_testdir): def test_change_testfile(stepwise_testdir): result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail', - 'test_stepwise.py') + 'test_a.py') assert not result.stderr.str() stdout = result.stdout.str() @@ -121,7 +121,7 @@ def test_change_testfile(stepwise_testdir): # Make sure the second test run starts from the beginning, since the # test to continue from does not exist in testfile_b. result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', - 'testfile_b.py') + 'test_b.py') assert not result.stderr.str() stdout = result.stdout.str() From fd66f69c1997fb9dc38a7891ec59210ecbb09558 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 18:50:06 +0100 Subject: [PATCH 08/16] draft doc --- doc/en/cache.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 08f204655..245edfc1b 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -260,3 +260,9 @@ by adding the ``--cache-clear`` option like this:: This is recommended for invocations from Continuous Integration servers where isolation and correctness is more important than speed. + + +Stepwise +-------- + +As an alternative to ``--lf -x``, especially for cases where you expect a large part of the test suite will fail, ``--sw``, ``--stepwise`` allows you to fix them one at a time. The test suite will run until the first failure and then stop. At the next invocation, tests will continue from the last failing test and then run until the next failing test. You may use the ``--stepwise-skip`` option to ignore one failing test and stop the test execution on the second failing test instead. This is useful if you get stuck on a failing test and just want to ignore it until later. From 8c059dbc48d201cbaf24a9fc6cc95f357c31abed Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 19:04:50 +0100 Subject: [PATCH 09/16] draft changelog --- changelog/xxx.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/xxx.feature.rst diff --git a/changelog/xxx.feature.rst b/changelog/xxx.feature.rst new file mode 100644 index 000000000..812898f90 --- /dev/null +++ b/changelog/xxx.feature.rst @@ -0,0 +1 @@ +Add ``-sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation `_ for more info. From 126bb0760e3f489562d7f7658f26362dcecddc32 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 19:04:55 +0100 Subject: [PATCH 10/16] authors --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 988d0e5fe..ae375228b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -59,6 +59,7 @@ Danielle Jenkins Dave Hunt David Díaz-Barquero David Mohr +David Szotten David Vierra Daw-Ran Liou Denis Kirisov @@ -161,6 +162,7 @@ Miro Hrončok Nathaniel Waisbrot Ned Batchelder Neven Mundar +Niclas Olofsson Nicolas Delaby Oleg Pidsadnyi Oleg Sushchenko From 4f652c9045ad9cbae3d7f67a1ffd319c95c7face Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 19:57:36 +0100 Subject: [PATCH 11/16] we have a pr number now --- changelog/{xxx.feature.rst => 4147.feature.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{xxx.feature.rst => 4147.feature.rst} (100%) diff --git a/changelog/xxx.feature.rst b/changelog/4147.feature.rst similarity index 100% rename from changelog/xxx.feature.rst rename to changelog/4147.feature.rst From e773c8ceda6ca576bca148f9018b6d287d709e3a Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 20:48:46 +0100 Subject: [PATCH 12/16] linting --- src/_pytest/stepwise.py | 43 ++++++++++++------- testing/test_stepwise.py | 93 +++++++++++++++++++++++----------------- 2 files changed, 81 insertions(+), 55 deletions(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index f408e1fa9..9af975ce1 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -3,28 +3,37 @@ import pytest def pytest_addoption(parser): - group = parser.getgroup('general') - group.addoption('--sw', '--stepwise', action='store_true', dest='stepwise', - help='exit on test fail and continue from last failing test next time') - group.addoption('--stepwise-skip', action='store_true', dest='stepwise_skip', - help='ignore the first failing test but stop on the next failing test') + group = parser.getgroup("general") + group.addoption( + "--sw", + "--stepwise", + action="store_true", + dest="stepwise", + help="exit on test fail and continue from last failing test next time", + ) + group.addoption( + "--stepwise-skip", + action="store_true", + dest="stepwise_skip", + help="ignore the first failing test but stop on the next failing test", + ) @pytest.hookimpl(tryfirst=True) def pytest_configure(config): config.cache = Cache.for_config(config) - config.pluginmanager.register(StepwisePlugin(config), 'stepwiseplugin') + config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") class StepwisePlugin: def __init__(self, config): self.config = config - self.active = config.getvalue('stepwise') + self.active = config.getvalue("stepwise") self.session = None if self.active: - self.lastfailed = config.cache.get('cache/stepwise', None) - self.skip = config.getvalue('stepwise_skip') + self.lastfailed = config.cache.get("cache/stepwise", None) + self.skip = config.getvalue("stepwise_skip") def pytest_sessionstart(self, session): self.session = session @@ -56,11 +65,13 @@ class StepwisePlugin: def pytest_collectreport(self, report): if self.active and report.failed: - self.session.shouldstop = 'Error when collecting test, stopping test execution.' + self.session.shouldstop = ( + "Error when collecting test, stopping test execution." + ) def pytest_runtest_logreport(self, report): # Skip this hook if plugin is not active or the test is xfailed. - if not self.active or 'xfail' in report.keywords: + if not self.active or "xfail" in report.keywords: return if report.failed: @@ -74,18 +85,20 @@ class StepwisePlugin: else: # Mark test as the last failing and interrupt the test session. self.lastfailed = report.nodeid - self.session.shouldstop = 'Test failed, continuing from this test next run.' + self.session.shouldstop = ( + "Test failed, continuing from this test next run." + ) else: # If the test was actually run and did pass. - if report.when == 'call': + if report.when == "call": # Remove test from the failed ones, if exists. if report.nodeid == self.lastfailed: self.lastfailed = None def pytest_sessionfinish(self, session): if self.active: - self.config.cache.set('cache/stepwise', self.lastfailed) + self.config.cache.set("cache/stepwise", self.lastfailed) else: # Clear the list of failing tests if the plugin is not active. - self.config.cache.set('cache/stepwise', []) + self.config.cache.set("cache/stepwise", []) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 0e1e53226..0e52911f4 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -5,15 +5,18 @@ import pytest def stepwise_testdir(testdir): # Rather than having to modify our testfile between tests, we introduce # a flag for wether or not the second test should fail. - testdir.makeconftest(''' + testdir.makeconftest( + """ def pytest_addoption(parser): group = parser.getgroup('general') group.addoption('--fail', action='store_true', dest='fail') group.addoption('--fail-last', action='store_true', dest='fail_last') -''') +""" + ) # Create a simple test suite. - testdir.makepyfile(test_a=''' + testdir.makepyfile( + test_a=""" def test_success_before_fail(): assert 1 @@ -28,111 +31,121 @@ def test_fail_last_on_flag(request): def test_success_after_last_fail(): assert 1 -''') +""" + ) - testdir.makepyfile(test_b=''' + testdir.makepyfile( + test_b=""" def test_success(): assert 1 -''') +""" + ) return testdir @pytest.fixture def error_testdir(testdir): - testdir.makepyfile(test_a=''' + testdir.makepyfile( + test_a=""" def test_error(nonexisting_fixture): assert 1 def test_success_after_fail(): assert 1 -''') +""" + ) return testdir @pytest.fixture def broken_testdir(testdir): - testdir.makepyfile(working_testfile='def test_proper(): assert 1', broken_testfile='foobar') + testdir.makepyfile( + working_testfile="def test_proper(): assert 1", broken_testfile="foobar" + ) return testdir def test_run_without_stepwise(stepwise_testdir): - result = stepwise_testdir.runpytest('-v', '--strict', '--fail') + result = stepwise_testdir.runpytest("-v", "--strict", "--fail") - result.stdout.fnmatch_lines(['*test_success_before_fail PASSED*']) - result.stdout.fnmatch_lines(['*test_fail_on_flag FAILED*']) - result.stdout.fnmatch_lines(['*test_success_after_fail PASSED*']) + result.stdout.fnmatch_lines(["*test_success_before_fail PASSED*"]) + result.stdout.fnmatch_lines(["*test_fail_on_flag FAILED*"]) + result.stdout.fnmatch_lines(["*test_success_after_fail PASSED*"]) def test_fail_and_continue_with_stepwise(stepwise_testdir): # Run the tests with a failing second test. - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail') + result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "--fail") assert not result.stderr.str() stdout = result.stdout.str() # Make sure we stop after first failing test. - assert 'test_success_before_fail PASSED' in stdout - assert 'test_fail_on_flag FAILED' in stdout - assert 'test_success_after_fail' not in stdout + assert "test_success_before_fail PASSED" in stdout + assert "test_fail_on_flag FAILED" in stdout + assert "test_success_after_fail" not in stdout # "Fix" the test that failed in the last run and run it again. - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise') + result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise") assert not result.stderr.str() stdout = result.stdout.str() # Make sure the latest failing test runs and then continues. - assert 'test_success_before_fail' not in stdout - assert 'test_fail_on_flag PASSED' in stdout - assert 'test_success_after_fail PASSED' in stdout + assert "test_success_before_fail" not in stdout + assert "test_fail_on_flag PASSED" in stdout + assert "test_success_after_fail PASSED" in stdout def test_run_with_skip_option(stepwise_testdir): - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--stepwise-skip', - '--fail', '--fail-last') + result = stepwise_testdir.runpytest( + "-v", "--strict", "--stepwise", "--stepwise-skip", "--fail", "--fail-last" + ) assert not result.stderr.str() stdout = result.stdout.str() # Make sure first fail is ignore and second fail stops the test run. - assert 'test_fail_on_flag FAILED' in stdout - assert 'test_success_after_fail PASSED' in stdout - assert 'test_fail_last_on_flag FAILED' in stdout - assert 'test_success_after_last_fail' not in stdout + assert "test_fail_on_flag FAILED" in stdout + assert "test_success_after_fail PASSED" in stdout + assert "test_fail_last_on_flag FAILED" in stdout + assert "test_success_after_last_fail" not in stdout def test_fail_on_errors(error_testdir): - result = error_testdir.runpytest('-v', '--strict', '--stepwise') + result = error_testdir.runpytest("-v", "--strict", "--stepwise") assert not result.stderr.str() stdout = result.stdout.str() - assert 'test_error ERROR' in stdout - assert 'test_success_after_fail' not in stdout + assert "test_error ERROR" in stdout + assert "test_success_after_fail" not in stdout def test_change_testfile(stepwise_testdir): - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail', - 'test_a.py') + result = stepwise_testdir.runpytest( + "-v", "--strict", "--stepwise", "--fail", "test_a.py" + ) assert not result.stderr.str() stdout = result.stdout.str() - assert 'test_fail_on_flag FAILED' in stdout + assert "test_fail_on_flag FAILED" in stdout # Make sure the second test run starts from the beginning, since the # test to continue from does not exist in testfile_b. - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', - 'test_b.py') + result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "test_b.py") assert not result.stderr.str() stdout = result.stdout.str() - assert 'test_success PASSED' in stdout + assert "test_success PASSED" in stdout def test_stop_on_collection_errors(broken_testdir): - result = broken_testdir.runpytest('-v', '--strict', '--stepwise', 'working_testfile.py', 'broken_testfile.py') + result = broken_testdir.runpytest( + "-v", "--strict", "--stepwise", "working_testfile.py", "broken_testfile.py" + ) stdout = result.stdout.str() - if pytest.__version__ < '3.0.0': - assert 'Error when collecting test' in stdout + if pytest.__version__ < "3.0.0": + assert "Error when collecting test" in stdout else: - assert 'errors during collection' in stdout + assert "errors during collection" in stdout From 8187c148d96de08bac2d1cdad34b825d3675fdef Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 21:58:30 +0100 Subject: [PATCH 13/16] now pinned to pytest version --- testing/test_stepwise.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 0e52911f4..ad9b77296 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -145,7 +145,4 @@ def test_stop_on_collection_errors(broken_testdir): ) stdout = result.stdout.str() - if pytest.__version__ < "3.0.0": - assert "Error when collecting test" in stdout - else: - assert "errors during collection" in stdout + assert "errors during collection" in stdout From d67d189d00c913218cdec3626460536ecae7d351 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 21:59:33 +0100 Subject: [PATCH 14/16] grammar --- src/_pytest/stepwise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 9af975ce1..3365af1b5 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -45,7 +45,7 @@ class StepwisePlugin: already_passed = [] found = False - # Make a list of all tests that has been runned before the last failing one. + # Make a list of all tests that have been run before the last failing one. for item in items: if item.nodeid == self.lastfailed: found = True From c25310d34f3ef454b7c3e363e0bd6802dab78e6e Mon Sep 17 00:00:00 2001 From: David Szotten Date: Mon, 15 Oct 2018 20:39:51 +0100 Subject: [PATCH 15/16] fix cacheprovider test --- testing/test_cacheprovider.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 2444d8bc1..114a63683 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -63,7 +63,8 @@ class TestNewAPI(object): ) result = testdir.runpytest("-rw") assert result.ret == 1 - result.stdout.fnmatch_lines(["*could not create cache path*", "*2 warnings*"]) + # warnings from nodeids, lastfailed, and stepwise + result.stdout.fnmatch_lines(["*could not create cache path*", "*3 warnings*"]) def test_config_cache(self, testdir): testdir.makeconftest( From e478f66d8b9d0e25af7aa4695192bf1adf063ba4 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Wed, 17 Oct 2018 09:08:40 +0100 Subject: [PATCH 16/16] cache is set by the cacheprovider --- src/_pytest/stepwise.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 3365af1b5..1efa2e7ca 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,4 +1,3 @@ -from _pytest.cacheprovider import Cache import pytest @@ -19,9 +18,8 @@ def pytest_addoption(parser): ) -@pytest.hookimpl(tryfirst=True) +@pytest.hookimpl def pytest_configure(config): - config.cache = Cache.for_config(config) config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")