pytest-repeat-0.9.3/0000775000175000017500000000000014511050774014261 5ustar jriverojriveropytest-repeat-0.9.3/CHANGES.rst0000664000175000017500000000341514511050774016066 0ustar jriverojriveroRelease Notes ------------- **0.9.3 (2023-Oct-9)** * No externally visible changes. * Internal changes. * Add tox and CI testing on Python 3.12 * Add legacy pytest versions to CI. * Now testing pytest 4, 5, 6 * Non-legacy testing uses pytest 7 * setup.py -> pyproject.toml * using hatchling backend **0.9.2 (2023-Oct-1)** * Migrate CI to GitHub Actions * Officially Support Python 3.7+ **0.9.1 (2020-10-31)** * Using ``@pytest.mark.repeat(1)`` can now be used to disable repeating a test regardless of the ``--count`` parameter given in the command-line. * Python 3.4 is no longer officially supported. **0.9.0 (-)** * Not released do PyPI due to a deploy problem. **0.8.0 (2019-02-26)** * Fix mark deprecation warnings in new pytest versions. * ``pytest-repeat`` now requires pytest>=3.6. **0.7.0 (2018-08-23)** * Move step number to the end of the parametrisation ID * Thanks to `@gdyuldin `_ for suggesting this enhancement and providing a patch **0.6.0 (2018-08-01)** * Add option for controlling the scope of the repeat parameterisation * Thanks to `@gdyuldin `_ for suggesting this enhancement and providing a patch **0.5.0 (2018-07-19)** * Allow repeating a test using a decorator (`#16 `_) * Thanks to `@Peque `_ for suggesting this enhancement and providing a patch **0.4.0 (2016-08-25)** * No changes - testing deploy to PyPI from Travis CI **0.4.0 (2016-08-09)** * Fix deprecation warning present in pytest 3.0 for type argument in addoption **0.3.0 (2016-06-30)** * Added support for repeating parameterised tests **0.2 (2015-10-27)** * README updates **0.1 (2015-10-19)** * Initial release pytest-repeat-0.9.3/.gitignore0000664000175000017500000000012514511050774016247 0ustar jriverojrivero__pycache__ .cache .eggs .DS_Store .tox build dist *.egg-info *.pyc *.pyo .idea venv pytest-repeat-0.9.3/LICENSE0000664000175000017500000000031414511050774015264 0ustar jriverojriveroThis Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. pytest-repeat-0.9.3/.github/0000775000175000017500000000000014511050774015621 5ustar jriverojriveropytest-repeat-0.9.3/.github/workflows/0000775000175000017500000000000014511050774017656 5ustar jriverojriveropytest-repeat-0.9.3/.github/workflows/pypi-package.yml0000664000175000017500000000164714511050774022763 0ustar jriverojrivero--- name: Build & maybe upload PyPI package on: release: types: [published] workflow_dispatch: permissions: contents: read id-token: write jobs: build-package: name: Build & verify package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/build-and-inspect-python-package@v1 # Upload to real PyPI on GitHub Releases. release-pypi: name: Publish released package to pypi.org environment: release-pypi if: github.repository_owner == 'pytest-dev' && github.event.action == 'published' runs-on: ubuntu-latest needs: build-package steps: - name: Download packages built by build-and-inspect-python-package uses: actions/download-artifact@v3 with: name: Packages path: dist - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 pytest-repeat-0.9.3/.github/workflows/test.yml0000664000175000017500000000452214511050774021363 0ustar jriverojriveroname: test on: push: branches: - main - "test-me-*" pull_request: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: package: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build and Check Package uses: hynek/build-and-inspect-python-package@v1.5 test: needs: [package] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"] os: [ubuntu-latest, windows-latest] include: - python: "3.7" tox_env: "py37-pytest7" - python: "3.8" tox_env: "py38-pytest7" - python: "3.9" tox_env: "py39-pytest7" - python: "3.10" tox_env: "py310-pytest7" - python: "3.11" tox_env: "py311-pytest7" - python: "3.12" tox_env: "py312-pytest7" - python: "pypy3.10" tox_env: "pypy3-pytest7" steps: - uses: actions/checkout@v3 - name: Download Package uses: actions/download-artifact@v3 with: name: Packages path: dist - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Install tox run: | python -m pip install --upgrade pip pip install tox - name: Test shell: bash run: tox run -e ${{ matrix.tox_env }} test-legacy-pytest: needs: [package] runs-on: ubuntu-latest strategy: fail-fast: false matrix: pytest: ["4", "5", "6"] include: - pytest: "4" tox_env: "py37-pytest4" - pytest: "5" tox_env: "py37-pytest5" - pytest: "6" tox_env: "py37-pytest6" steps: - uses: actions/checkout@v3 - name: Download Package uses: actions/download-artifact@v3 with: name: Packages path: dist - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.7" - name: Install tox run: | python -m pip install --upgrade pip pip install tox - name: Test shell: bash run: tox run -e ${{ matrix.tox_env }} pytest-repeat-0.9.3/.github/RELEASING.md0000664000175000017500000000052614511050774017457 0ustar jriverojriveroHere are the steps on how to make a new release. 1. Create a `release-VERSION` branch from `upstream/main`. 2. Update `CHANGELOG.rst`. 3. Push the branch to `upstream`. 4. Once all tests pass, merge the PR. 5. Once the PR completes, create a Release with a new version tag on GitHub. Version should be in the form of "vx.y.z". ex: v0.9.2 pytest-repeat-0.9.3/README.rst0000664000175000017500000000665514511050774015764 0ustar jriverojriveropytest-repeat =================== pytest-repeat is a plugin for `pytest `_ that makes it easy to repeat a single test, or multiple tests, a specific number of times. |license| |python| |version| |anaconda| |ci| |issues| .. |license| image:: https://img.shields.io/badge/license-MPL%202.0-blue.svg :target: https://github.com/pytest-dev/pytest-repeat/blob/master/LICENSE .. |version| image:: http://img.shields.io/pypi/v/pytest-repeat.svg :target: https://pypi.python.org/pypi/pytest-repeat .. |anaconda| image:: https://img.shields.io/conda/vn/conda-forge/pytest-repeat.svg :target: https://anaconda.org/conda-forge/pytest-repeat .. |ci| image:: https://github.com/pytest-dev/pytest-repeat/workflows/test/badge.svg :target: https://github.com/pytest-dev/pytest-repeat/actions .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-repeat.svg :target: https://pypi.python.org/pypi/pytest-repeat/ .. |issues| image:: https://img.shields.io/github/issues-raw/pytest-dev/pytest-repeat.svg :target: https://github.com/pytest-dev/pytest-repeat/issues Requirements ------------ You will need the following prerequisites in order to use pytest-repeat: - Python 3.7+ or PyPy3 - pytest 4 or newer Installation ------------ To install pytest-repeat: .. code-block:: bash $ pip install pytest-repeat Repeating a test ---------------- Use the :code:`--count` command line option to specify how many times you want your test, or tests, to be run: .. code-block:: bash $ pytest --count=10 test_file.py Each test collected by pytest will be run :code:`count` times. If you want to mark a test in your code to be repeated a number of times, you can use the :code:`@pytest.mark.repeat(count)` decorator: .. code-block:: python import pytest @pytest.mark.repeat(3) 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 pytest's :code:`-x` option in conjunction with pytest-repeat to force the test runner to stop at the first failure. For example: .. code-block:: bash $ pytest --count=1000 -x test_file.py This will attempt to run test_file.py 1000 times, but will stop as soon as a failure occurs. UnitTest Style Tests -------------------- Unfortunately pytest-repeat is not able to work with unittest.TestCase test classes. These tests will simply always run once, regardless of :code:`--count`, and show a warning. Resources --------- - `Release Notes `_ - `Issue Tracker `_ - `Code `_ pytest-repeat-0.9.3/tox.ini0000664000175000017500000000064414511050774015600 0ustar jriverojrivero[tox] minversion = 4.0 isolated_build = true envlist = py{37,38,39,310,311,312,py3}-pytest7 py37-pytest{4,5,6} flake8 [testenv] commands = pytest {posargs} package = wheel wheel_build_env = .pkg deps = pytest4: pytest~=4.6 pytest5: pytest~=5.4 pytest6: pytest~=6.2 pytest7: pytest>=7 [testenv:flake8] basepython = py311 deps = flake8 commands = flake8 {posargs:pytest_repeat.py test_repeat.py} pytest-repeat-0.9.3/test_repeat.py0000664000175000017500000002537214511050774017163 0ustar jriverojrivero# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. import pytest pytest_plugins = "pytester", class TestRepeat: def test_no_repeat(self, testdir): testdir.makepyfile(""" def test_no_repeat(request): fixtures = request.fixturenames assert "__pytest_repeat_step_number" not in fixtures """) result = testdir.runpytest('-v', '--count', '1') result.stdout.fnmatch_lines([ '*test_no_repeat.py::test_no_repeat PASSED*', '*1 passed*', ]) assert result.ret == 0 def test_can_repeat(self, testdir): testdir.makepyfile(""" def test_repeat(): pass """) result = testdir.runpytest('--count', '2') result.stdout.fnmatch_lines(['*2 passed*']) assert result.ret == 0 def test_mark_repeat_decorator_is_registered(self, testdir): result = testdir.runpytest('--markers') result.stdout.fnmatch_lines([ '@pytest.mark.repeat(n): run the given test function `n` times.']) assert result.ret == 0 def test_mark_repeat_decorator(self, testdir): testdir.makepyfile(""" import pytest @pytest.mark.repeat(3) def test_mark_repeat_decorator(): pass """) result = testdir.runpytest() result.stdout.fnmatch_lines(['*3 passed*']) assert result.ret == 0 def test_mark_repeat_decorator_repeat_once(self, testdir): testdir.makepyfile(""" import pytest @pytest.mark.repeat(1) def test_mark_repeat_decorator_repeat_once(): pass """) result = testdir.runpytest('--count', '10') result.stdout.fnmatch_lines(['*1 passed*']) assert result.ret == 0 def test_parametrize(self, testdir): testdir.makepyfile(""" import pytest @pytest.mark.parametrize('x', ['a', 'b', 'c']) def test_repeat(x): pass """) result = testdir.runpytest('-v', '--count', '2') result.stdout.fnmatch_lines([ '*test_parametrize.py::test_repeat[[]a-1-2[]] PASSED*', '*test_parametrize.py::test_repeat[[]a-2-2[]] PASSED*', '*test_parametrize.py::test_repeat[[]b-1-2[]] PASSED*', '*test_parametrize.py::test_repeat[[]b-2-2[]] PASSED*', '*test_parametrize.py::test_repeat[[]c-1-2[]] PASSED*', '*test_parametrize.py::test_repeat[[]c-2-2[]] PASSED*', '*6 passed*', ]) assert result.ret == 0 def test_parametrized_fixture(self, testdir): testdir.makepyfile(""" import pytest @pytest.fixture(params=['a', 'b', 'c']) def parametrized_fixture(request): return request.param def test_repeat(parametrized_fixture): pass """) result = testdir.runpytest('--count', '2') result.stdout.fnmatch_lines(['*6 passed*']) assert result.ret == 0 def test_step_number(self, testdir): testdir.makepyfile(""" import pytest expected_steps = iter(range(5)) def test_repeat(__pytest_repeat_step_number): assert next(expected_steps) == __pytest_repeat_step_number if __pytest_repeat_step_number == 4: assert not list(expected_steps) """) result = testdir.runpytest('-v', '--count', '5') result.stdout.fnmatch_lines([ '*test_step_number.py::test_repeat[[]1-5[]] PASSED*', '*test_step_number.py::test_repeat[[]2-5[]] PASSED*', '*test_step_number.py::test_repeat[[]3-5[]] PASSED*', '*test_step_number.py::test_repeat[[]4-5[]] PASSED*', '*test_step_number.py::test_repeat[[]5-5[]] PASSED*', '*5 passed*', ]) assert result.ret == 0 def test_invalid_option(self, testdir): testdir.makepyfile(""" def test_repeat(): pass """) result = testdir.runpytest('--count', 'a') assert result.ret == 4 def test_unittest_test(self, testdir): testdir.makepyfile(""" from unittest import TestCase class ClassStyleTest(TestCase): def test_this(self): assert 1 """) result = testdir.runpytest('-v', '--count', '2') result.stdout.fnmatch_lines([ '*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 == 4 pytest-repeat-0.9.3/pytest_repeat.py0000664000175000017500000000412614511050774017526 0ustar jriverojrivero# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. import warnings from unittest import TestCase import pytest def pytest_addoption(parser): parser.addoption( '--count', action='store', default=1, 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( 'markers', 'repeat(n): run the given test function `n` times.') class UnexpectedError(Exception): pass @pytest.fixture() def __pytest_repeat_step_number(request): marker = request.node.get_closest_marker("repeat") count = marker and marker.args[0] or request.config.option.count if count > 1: try: return request.param except AttributeError: if issubclass(request.cls, TestCase): warnings.warn( "Repeating unittest class tests not supported") else: raise UnexpectedError( "This call couldn't work with pytest-repeat. " "Please consider raising an issue with your usage.") @pytest.hookimpl(trylast=True) def pytest_generate_tests(metafunc): count = metafunc.config.option.count m = metafunc.definition.get_closest_marker('repeat') if m is not None: count = int(m.args[0]) if count > 1: metafunc.fixturenames.append("__pytest_repeat_step_number") 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 ) pytest-repeat-0.9.3/pyproject.toml0000664000175000017500000000241614511050774017200 0ustar jriverojrivero[project] name = "pytest-repeat" description = "pytest plugin for repeating tests" readme = "README.rst" license = {file = "LICENSE"} authors = [{name = "Bob Silverberg"}] requires-python = ">=3.7" classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Pytest", "Intended Audience :: Developers", "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Quality Assurance", "Topic :: Software Development :: Testing", "Topic :: Utilities", ] dynamic = [ "version" ] dependencies = [ "pytest" ] [project.urls] Home = "https://github.com/pytest-dev/pytest-repeat" [project.entry-points.pytest11] repeat = "pytest_repeat" [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [tool.hatch.version] source = "vcs"