Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,19 @@ can use the :code:`@pytest.mark.repeat(count)` decorator:
def test_repeat_decorator():
pass

If you want to override default tests executions order, you can use :code:`--repeat-scope`
command line option with one of the next values: :code:`session`, :code:`module`, :code:`class` or :code:`function` (default).
It behaves like a scope of the pytest fixture.

:code:`function` (default) scope repeats each test :code:`count` or :code:`repeat` times before executing next test.
:code:`session` scope repeats whole tests session, i.e. all collected tests executed once, then all such tests executed again and etc.
:code:`class` and :code:`module` behaves similar :code:`session` , but repeating set of tests is a tests from class or module, not all collected tests.

Repeating a test until failure
------------------------------

If you are trying to diagnose an intermittent failure, it can be useful to run the same
test over and over again until it fails. You can use py.test's :code:`-x` option in
test over and over again until it fails. You can use pytest's :code:`-x` option in
conjunction with pytest-repeat to force the test runner to stop at the first failure.
For example:

Expand Down
10 changes: 10 additions & 0 deletions pytest_repeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ def pytest_addoption(parser):
type=int,
help='Number of times to repeat each test')

parser.addoption(
'--repeat-scope',
action='store',
default='function',
type=str,
choices=('function', 'class', 'module', 'session'),
help='Scope for repeating tests')


def pytest_configure(config):
config.addinivalue_line(
Expand Down Expand Up @@ -50,9 +58,11 @@ def pytest_generate_tests(metafunc):
def make_progress_id(i, n=count):
return '{0}/{1}'.format(i + 1, n)

scope = metafunc.config.option.repeat_scope
metafunc.parametrize(
'__pytest_repeat_step_number',
range(count),
indirect=True,
ids=make_progress_id,
scope=scope
)
138 changes: 138 additions & 0 deletions test_repeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import pytest

pytest_plugins = "pytester",


Expand Down Expand Up @@ -111,3 +113,139 @@ def test_this(self):
'*test_unittest_test.py::ClassStyleTest::test_this PASSED*',
'*1 passed*',
])

@pytest.mark.parametrize(['scope', 'lines'], [
('session', [
'*test_1.py::test_repeat1[[]1/2[]] PASSED*',
'*test_1.py::test_repeat2[[]1/2[]] PASSED*',
'*test_2.py::test_repeat3[[]1/2[]] PASSED*',
'*test_2.py::test_repeat4[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat5[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat6[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat7[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat8[[]1/2[]] PASSED*',
'*test_1.py::test_repeat1[[]2/2[]] PASSED*',
'*test_1.py::test_repeat2[[]2/2[]] PASSED*',
'*test_2.py::test_repeat3[[]2/2[]] PASSED*',
'*test_2.py::test_repeat4[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat5[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat6[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat7[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat8[[]2/2[]] PASSED*',
'*16 passed*',
]),
('module', [
'*test_1.py::test_repeat1[[]1/2[]] PASSED*',
'*test_1.py::test_repeat2[[]1/2[]] PASSED*',
'*test_1.py::test_repeat1[[]2/2[]] PASSED*',
'*test_1.py::test_repeat2[[]2/2[]] PASSED*',
'*test_2.py::test_repeat3[[]1/2[]] PASSED*',
'*test_2.py::test_repeat4[[]1/2[]] PASSED*',
'*test_2.py::test_repeat3[[]2/2[]] PASSED*',
'*test_2.py::test_repeat4[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat5[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat6[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat7[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat8[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat5[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat6[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat7[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat8[[]2/2[]] PASSED*',
'*16 passed*',
]),
('class', [
'*test_1.py::test_repeat1[[]1/2[]] PASSED*',
'*test_1.py::test_repeat2[[]1/2[]] PASSED*',
'*test_1.py::test_repeat1[[]2/2[]] PASSED*',
'*test_1.py::test_repeat2[[]2/2[]] PASSED*',
'*test_2.py::test_repeat3[[]1/2[]] PASSED*',
'*test_2.py::test_repeat4[[]1/2[]] PASSED*',
'*test_2.py::test_repeat3[[]2/2[]] PASSED*',
'*test_2.py::test_repeat4[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat5[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat6[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat5[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat6[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat7[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat8[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat7[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat8[[]2/2[]] PASSED*',
'*16 passed*',
]),
('function', [
'*test_1.py::test_repeat1[[]1/2[]] PASSED*',
'*test_1.py::test_repeat1[[]2/2[]] PASSED*',
'*test_1.py::test_repeat2[[]1/2[]] PASSED*',
'*test_1.py::test_repeat2[[]2/2[]] PASSED*',
'*test_2.py::test_repeat3[[]1/2[]] PASSED*',
'*test_2.py::test_repeat3[[]2/2[]] PASSED*',
'*test_2.py::test_repeat4[[]1/2[]] PASSED*',
'*test_2.py::test_repeat4[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat5[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat5[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat6[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat1::test_repeat6[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat7[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat7[[]2/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat8[[]1/2[]] PASSED*',
'*test_3.py::TestRepeat2::test_repeat8[[]2/2[]] PASSED*',
'*16 passed*',
]),
])
def test_scope(self, testdir, scope, lines):
testdir.makepyfile(test_1="""
def test_repeat1():
pass

def test_repeat2():
pass
""")
testdir.makepyfile(test_2="""
def test_repeat3():
pass

def test_repeat4():
pass
""")
testdir.makepyfile(test_3="""
class TestRepeat1(object):
def test_repeat5(self):
pass
def test_repeat6(self):
pass
class TestRepeat2(object):
def test_repeat7(self):
pass
def test_repeat8(self):
pass
""")
result = testdir.runpytest('-v', '--count', '2', '--repeat-scope',
scope)
result.stdout.fnmatch_lines(lines)
assert result.ret == 0

def test_omitted_scope(self, testdir):
testdir.makepyfile("""
def test_repeat1():
pass

def test_repeat2():
pass
""")
result = testdir.runpytest('-v', '--count', '2')
result.stdout.fnmatch_lines([
'*test_omitted_scope.py::test_repeat1[[]1/2[]] PASSED*',
'*test_omitted_scope.py::test_repeat1[[]2/2[]] PASSED*',
'*test_omitted_scope.py::test_repeat2[[]1/2[]] PASSED*',
'*test_omitted_scope.py::test_repeat2[[]2/2[]] PASSED*',
'*4 passed*',
])
assert result.ret == 0

def test_invalid_scope(self, testdir):
testdir.makepyfile("""
def test_repeat():
pass
""")
result = testdir.runpytest('--count', '2', '--repeat-scope', 'a')
assert result.ret == 2