├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs ├── index.md └── usage.md ├── example ├── requirements.txt ├── test_dummy.py └── tox.ini ├── mkdocs.yml ├── pytest_timeouts.py ├── setup.cfg ├── setup.py ├── test_pytest_timeouts.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Configuration, settings, etc. 12 | 13 | **Logs** 14 | Pytest logs 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Details (please complete the following information):** 20 | - OS: [e.g. Linux] 21 | - Pytest version 22 | - Pytest plugin 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | .tox 4 | .pytest_cache 5 | *.egg-info 6 | site/ 7 | .coverage 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | stages: 4 | - name: test 5 | if: repo = Scony/pytest-timeouts AND tag IS NOT present 6 | - name: deploy 7 | if: repo = Scony/pytest-timeouts AND tag IS present 8 | python: '3.7' 9 | cache: false 10 | os: linux 11 | 12 | install: 13 | - python -m pip install --upgrade --pre tox 14 | 15 | jobs: 16 | include: 17 | - env: TOXENV=py3-codestyle 18 | - env: TOXENV=py3-coverage PYTEST_COVERAGE=1 19 | install: 20 | - python -m pip install --upgrade --pre tox codecov 21 | 22 | - env: TOXENV=py35 23 | python: '3.5' 24 | 25 | - env: TOXENV=py36 26 | python: '3.6' 27 | 28 | - env: TOXENV=py37 29 | python: '3.7' 30 | 31 | - env: TOXENV=py38 32 | python: '3.8' 33 | 34 | - stage: deploy 35 | python: '3.6' 36 | install: pip install -U setuptools setuptools_scm tox 37 | script: skip 38 | deploy: 39 | provider: pypi 40 | username: Scony 41 | password: 42 | secure: zesc8Ly0VeisTE1gT50Wq8oesX/5h3qXz/G5Zqv35sy9Hwj1AKcyAEBJOZGk7l16jlGgJc/SLaAHrmDmY+9kU9KCKstWnqA0z8Ap7sTVRpxEdNHt7XayK5rmoAPpb137F8GX6+wJoRLmpc8/8gaq985vqVKcxya6aUlrscdHp6VXPgMki+78BDGpXJw58QI7E0f0sGcN94YYxOP+tFV/MAqfYDZM+Vxfol/Vv3H20ZiesKAgNIRXAsK/cXNj4PYbxC6Ck+RCMc4+J223kKw0VAbuqJVc1lUBq2zgUdyIk57/GMHPbxM6eETU/BPFH8Ca/yJA6oz+dm+nmmHgszmevxEHY7K0LJEQnTmNfvlQM2EjgEh/YYiRPsS+QrUYUIWqbIme8jQT0l+xkMtLPFUg3eYQ9w9sjmR6fz+yO3yGHapdcbHfUxp3uGN6Z7y2/54pTL8LrmoXD24e7k6kHDKazw4TZrDpGGL38XAoX00NzRA0MAYsPbyGkr/oPk0M6O0P+MA0i1TC0jdVQerZM7eKcLOwbsEViDhEtobH7pIbz07lWusDsVC/KUEvc46+ZJOLLcYvQtN1V2FeBvfqI/8Oa11SIaDwpi2aMXlCVPH/aCqfDiJQR6LeNDNtSyyiHTxH4wMlDy4kS5W0ZvtXuwQrJ9ecSdgqf1RC1mp/dhOc4NE= 43 | on: 44 | tags: true 45 | branch: master 46 | repo: Scony/pytest-timeouts 47 | 48 | script: tox 49 | 50 | after_success: 51 | - | 52 | if [[ "$PYTEST_COVERAGE" = 1 ]]; then 53 | codecov 54 | fi 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ### Changed 3 | - Add tests for pytest 5 https://github.com/Scony/pytest-timeouts/issues/39 4 | - Add tests for python 3.5 3.6 3.7 3.8 https://github.com/Scony/pytest-timeouts/issues/40 5 | ## [1.2.1] 2019-09-21 6 | ### Fixed 7 | - PyTest get_marker warning fix https://github.com/Scony/pytest-timeouts/pull/36 8 | get_marker was incorrect selected on pytest 3.10.X 9 | ## [1.2.0] 2019-05-01 10 | ### Added 11 | - Add support for pytest 4 12 | - Add code of conduct 13 | - Add CHANGELOG.md 14 | ### Changed 15 | - Add validation of timeout order on startup 16 | - Move data from setup.py to setup.cfg 17 | ### Fixed 18 | - Correct README.md 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at pawel.lampe@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pawel Lampe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-timeouts 2 | [![Build Status](https://travis-ci.org/Scony/pytest-timeouts.svg?branch=master)](https://travis-ci.org/Scony/pytest-timeouts) 3 | [![codecov](https://codecov.io/gh/Scony/pytest-timeouts/branch/master/graph/badge.svg)](https://codecov.io/gh/Scony/pytest-timeouts) 4 | [![Documentation Status](https://readthedocs.org/projects/pytest-timeouts/badge/?version=latest)](https://pytest-timeouts.readthedocs.io/en/latest/?badge=latest) 5 | [![PyPI](https://img.shields.io/pypi/v/pytest-timeouts.svg)](https://pypi.org/project/pytest-timeouts/) 6 | ![pyversion](https://img.shields.io/pypi/pyversions/pytest-timeouts.svg) 7 | ![Supported pytest 3|4](https://img.shields.io/badge/pytest-3|4-blue.svg) 8 | [![Downloads](https://pepy.tech/badge/pytest-timeouts)](https://pepy.tech/project/pytest-timeouts) 9 | [![PyPI - License](https://img.shields.io/pypi/l/pytest-timeouts.svg)](https://github.com/Scony/pytest-timeouts/blob/master/LICENSE) 10 | ![GitHub Release Date](https://img.shields.io/github/release-date/Scony/pytest-timeouts.svg) 11 | 12 | Linux-only Pytest plugin to control durations of various test case execution phases. 13 | 14 | ## Documentation 15 | 16 | For documentation visit [pytest-timeouts.readthedocs.io](https://pytest-timeouts.readthedocs.io). 17 | 18 | ## About 19 | 20 | This plugin has been designed for specific use cases which are out of the scope of famous `pytest-timeout` plugin. 21 | It uses a `SIGALRM` signal to schedule a timer which breaks the test case. 22 | 23 | ## Features 24 | 25 | * `setup`, `execution` and `teardown` phase timeouts controllable by: 26 | * opts: `--setup-timeout`, `--execution-timeout` and `--teardown-timeout` 27 | * ini: `setup_timeout`, `execution_timeout` and `teardown_timeout` 28 | * mark: `setup_timeout`, `execution_timeout` and `teardown_timeout` 29 | * fixed order of timeout settings: **opts** > **markers** > **ini**, controlled by `--timeouts-order` 30 | * `--timeouts-order` allow change order of override timeout settings, and disable some settings, i.e. `--timeout-order i` disable markers and opts, any combination is allow 31 | * timeout disabled when debugging with PDB 32 | 33 | ## Installation 34 | 35 | ### Stable 36 | 37 | ```bash 38 | pip install pytest-timeouts 39 | ``` 40 | 41 | ### Master 42 | 43 | ```bash 44 | pip install git+https://github.com/Scony/pytest-timeouts.git 45 | ``` 46 | 47 | ## Usage 48 | 49 | ### Command line 50 | 51 | ```bash 52 | pytest --setup-timeout 2.5 --execution-timeout 2.01 --teardown-timeout 0 53 | ``` 54 | 55 | ### `pytest.ini` setting 56 | 57 | ```ini 58 | [pytest] 59 | setup_timeout = 2.5 60 | execution_timeout = 2.01 61 | teardown_timeout = 0 62 | ``` 63 | 64 | ### Mark 65 | 66 | ```python 67 | import time 68 | 69 | import pytest 70 | 71 | 72 | @pytest.mark.setup_timeout(0.3) 73 | @pytest.mark.execution_timeout(0.5) 74 | @pytest.mark.teardown_timeout(0.4) 75 | def test_timeout(): 76 | time.sleep(1) 77 | ``` 78 | 79 | ## Contributors 80 | 81 | * Pawel Lampe 82 | * Kamil Luczak 83 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Pytest test scenario can be split to three stage: 4 | 5 | - setup - control by **setup-timeout**: before test, when fixtures are setup 6 | - execution - control by **execution-timeout**: test execution 7 | - teardown - control by **teardown-timeout**: after test 8 | 9 | Timeout is provided in seconds. 10 | 11 | ## Command line 12 | 13 | *pytest-timeout* can be control from command line. 14 | 15 | List on available option: 16 | 17 | - **setup-timeout** 18 | - **execution-timeout** 19 | - **teardown-timeout** 20 | - **timeout-order**, control order of override timeout, more in [Timeout order](#timeout-order) 21 | 22 | ### Examples 23 | 24 | - Set setup timeout to 5s, execution timeout to 10s and teardown timeout to 7s 25 | ```bash 26 | pytest --setup-timeout 5 --execution-timeout 10 --teardown-timeout 7 27 | ``` 28 | 29 | - Set timeout order override **opt** > **ini**, **mark** disable 30 | ``` 31 | pytest --timeout-order oi 32 | ``` 33 | 34 | ## `pytest.ini` file 35 | 36 | In ini file you can set timeout for any stage. 37 | 38 | ### Example 39 | 40 | ```ini 41 | [pytest] 42 | setup_timeout = 2.5 43 | execution-timeout = 2.01 44 | teardown-timeout = 0 45 | ``` 46 | 47 | ## Timeout order 48 | 49 | Timeout order is a combination of three option: 50 | 51 | - **i** - ini 52 | - **o** - options set up from command line 53 | - **m** - markers 54 | 55 | Default order of override timeout is: **opts** > **markers** > **ini**. 56 | 57 | You can change order of override or disable it, i.e. **_i_** will disable **opts** and **markers**, **_mo_** change override order to **markers** > **opts** and **ini** will be disable 58 | 59 | ## Marks 60 | 61 | Marks can be use to change timeout for specific test, module, etc. 62 | Mark can be added in two way: 63 | 64 | - mark function 65 | 66 | Mark allow modify timeout specific element 67 | ```python 68 | import pytest 69 | 70 | @pytest.mark.setup_timeout(0.5) 71 | class TestClass(object): 72 | pass 73 | 74 | @pytest.mark.execution_timeout(0.5) 75 | @pytest.mark.teardown_timeout(0.4) 76 | def text_fixture(): 77 | pass 78 | ``` 79 | 80 | - mark file 81 | 82 | Mark define for file modify timeout for every element in module. 83 | ```python 84 | import python 85 | 86 | pytestmark = [ 87 | pytest.mark.execution_timeout(0.12), 88 | pytest.mark.setup_timeout(0.14), 89 | pytest.mark.teardown_timeout(0.13), 90 | ] 91 | ``` 92 | 93 | ### Marks with scope 94 | 95 | For file mark, we also can provide scope: `function`, `module`, `session`, `class` 96 | 97 | ```python 98 | import python 99 | 100 | pytestmark = [ 101 | pytest.mark.execution_timeout(0.12, 'session'), 102 | pytest.mark.setup_timeout(0.14, 'module'), 103 | pytest.mark.teardown_timeout(0.13, 'class'), 104 | ] 105 | ``` 106 | That solution allow us for example disable `teardown_timeout` for session fixture. 107 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | git+https://github.com/Scony/pytest-timeouts 3 | -------------------------------------------------------------------------------- /example/test_dummy.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope='function') 7 | def fx(): 8 | time.sleep(2) 9 | yield 10 | 11 | 12 | @pytest.fixture(scope='function') 13 | def fx2(): 14 | yield 15 | time.sleep(2) 16 | 17 | 18 | def test_dummy(fx): 19 | pass 20 | 21 | @pytest.mark.teardown_timeout(0.2) 22 | def test_dummy_2(fx2): 23 | pass 24 | -------------------------------------------------------------------------------- /example/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27 3 | skipsdist = True 4 | [testenv] 5 | deps = -rrequirements.txt 6 | commands = pytest -vv --setup-timeout 1 7 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pytest-timeouts 2 | theme: readthedocs 3 | -------------------------------------------------------------------------------- /pytest_timeouts.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import functools 4 | import signal 5 | 6 | import pytest 7 | 8 | SETUP_TIMEOUT_HELP = 'test case setup timeout in seconds' 9 | EXECUTION_TIMEOUT_HELP = 'test case execution timeout in seconds' 10 | TEARDOWN_TIMEOUT_HELP = 'test case teardown timeout in seconds' 11 | TIMEOUT_ORDER_HELP = """override order: i - ini, m - mark, o - opt 12 | example: "omi", "imo", "i" - ini only 13 | """ 14 | 15 | 16 | @staticmethod 17 | def get_markers_old_way(item, name): 18 | return item.get_marker(name=name) 19 | 20 | 21 | @staticmethod 22 | def get_markers_new_way(item, name): 23 | return item.iter_markers(name=name) 24 | 25 | 26 | @pytest.hookimpl 27 | def pytest_addoption(parser): 28 | group = parser.getgroup('timeouts') 29 | group.addoption( 30 | '--setup-timeout', 31 | type=float, 32 | help=SETUP_TIMEOUT_HELP, 33 | ) 34 | group.addoption( 35 | '--execution-timeout', 36 | type=float, 37 | help=EXECUTION_TIMEOUT_HELP, 38 | ) 39 | group.addoption( 40 | '--teardown-timeout', 41 | type=float, 42 | help=TEARDOWN_TIMEOUT_HELP, 43 | ) 44 | group.addoption( 45 | '--timeouts-order', 46 | type=str, 47 | help=TIMEOUT_ORDER_HELP, 48 | default='omi' 49 | ) 50 | parser.addini('setup_timeout', SETUP_TIMEOUT_HELP) 51 | parser.addini('execution_timeout', SETUP_TIMEOUT_HELP) 52 | parser.addini('teardown_timeout', SETUP_TIMEOUT_HELP) 53 | 54 | 55 | @pytest.hookimpl 56 | def pytest_configure(config): 57 | assert hasattr(signal, 'SIGALRM') 58 | TimeoutsPlugin.configure() 59 | config.pluginmanager.register(TimeoutsPlugin(config)) 60 | 61 | 62 | class TimeoutsPlugin(object): 63 | def __init__(self, config): 64 | config.addinivalue_line( 65 | 'markers', 66 | 'execution_timeout(seconds): ' 67 | 'time out test case after specified time\n' 68 | ) 69 | config.addinivalue_line( 70 | 'markers', 71 | 'setup_timeout(seconds): ' 72 | 'time out fixture setup after specific time\n' 73 | ) 74 | config.addinivalue_line( 75 | 'markers', 76 | 'teardown_timeout(seconds):' 77 | 'time out fixture teardown after specific time\n' 78 | ) 79 | self.order = self.fetch_timeout_order(config) 80 | self.timeout = { 81 | 'setup_timeout': self.fetch_timeout_from_config( 82 | 'setup_timeout', config), 83 | 'execution_timeout': self.fetch_timeout_from_config( 84 | 'execution_timeout', config), 85 | 'teardown_timeout': self.fetch_timeout_from_config( 86 | 'teardown_timeout', config), 87 | } 88 | 89 | @staticmethod 90 | def parse_timeout(timeout): 91 | timeout = ( 92 | 0.0 if (timeout is None) or (timeout == '') 93 | else float(timeout) 94 | ) 95 | timeout = 0.0 if timeout < 0.0 else timeout 96 | return timeout 97 | 98 | @staticmethod 99 | def configure(): 100 | ver = [int(v) for v in pytest.__version__.split('.')] 101 | if (ver[0] > 3) or ((ver[0] == 3) and (ver[1] >= 6)): 102 | TimeoutsPlugin.get_markers = get_markers_new_way 103 | else: 104 | TimeoutsPlugin.get_markers = get_markers_old_way 105 | 106 | @staticmethod 107 | def fetch_timeout_from_config(timeout_name, config): 108 | timeout_option = config.getvalue(timeout_name) 109 | timeout_ini = config.getini(timeout_name) 110 | return timeout_option, timeout_ini 111 | 112 | @staticmethod 113 | def fetch_timeout_order(config): 114 | order = list(config.getvalue('timeouts_order')) 115 | order_set = set(['i', 'm', 'o']) 116 | if len(order) == 0 or len(order) > 3: 117 | raise pytest.UsageError( 118 | 'Order should have at least 1 and less then or ' 119 | 'equal 3 elements' 120 | ) 121 | if not set(order).issubset(order_set): 122 | raise pytest.UsageError( 123 | 'Incorrect item \'{}\' in timeout order list'.format( 124 | list(set(order).difference(order_set))) 125 | ) 126 | return order 127 | 128 | def fetch_timeout(self, timeout_name, item): 129 | marker_timeout = ( 130 | self.fetch_marker_timeout(item, timeout_name) if item is not None 131 | else None 132 | ) 133 | timeout = None 134 | for order_item in self.order: 135 | if order_item == 'o' and self.timeout[timeout_name][0] is not None: 136 | timeout = self.timeout[timeout_name][0] 137 | break 138 | elif order_item == 'm' and marker_timeout is not None: 139 | timeout = marker_timeout 140 | break 141 | elif (order_item == 'i' and 142 | self.timeout[timeout_name][1] != ''): 143 | timeout = self.timeout[timeout_name][1] 144 | break 145 | return self.parse_timeout(timeout) 146 | 147 | @pytest.hookimpl(tryfirst=True) 148 | def pytest_report_header(self, config): 149 | timeout_prints = [ 150 | 'setup timeout: {}s'.format( 151 | self.fetch_timeout('setup_timeout', None)), 152 | 'execution timeout: {}s'.format( 153 | self.fetch_timeout('execution_timeout', None)), 154 | 'teardown timeout: {}s'.format( 155 | self.fetch_timeout('teardown_timeout', None)), 156 | ] 157 | return [', '.join(timeout_prints)] 158 | 159 | @pytest.hookimpl 160 | def pytest_enter_pdb(self): 161 | self.cancel_timer() 162 | 163 | @pytest.hookimpl(hookwrapper=True) 164 | def pytest_runtest_setup(self, item): 165 | self.setup_timer(self.fetch_timeout('setup_timeout', item)) 166 | yield 167 | self.cancel_timer() 168 | 169 | @pytest.hookimpl(hookwrapper=True) 170 | def pytest_runtest_call(self, item): 171 | self.setup_timer(self.fetch_timeout('execution_timeout', item)) 172 | yield 173 | self.cancel_timer() 174 | 175 | @staticmethod 176 | def fetch_marker_timeout(item, name): 177 | def get_fixture_scope(item): 178 | return item._fixtureinfo.name2fixturedefs[ 179 | item._fixtureinfo.names_closure[0]][0].scope 180 | markers = TimeoutsPlugin.get_markers(item, name) 181 | if markers: 182 | for marker in markers: 183 | if marker.args: 184 | if len(marker.args) == 2: 185 | if marker.args[1] == get_fixture_scope(item): 186 | return marker.args[0] 187 | else: 188 | continue 189 | else: 190 | return marker.args[0] 191 | else: 192 | raise TypeError('Timeout value is missing') 193 | return None 194 | 195 | @pytest.hookimpl(hookwrapper=True) 196 | def pytest_runtest_teardown(self, item): 197 | self.setup_timer(self.fetch_timeout('teardown_timeout', item)) 198 | yield 199 | self.cancel_timer() 200 | 201 | @staticmethod 202 | def setup_timer(timeout): 203 | handler = functools.partial(TimeoutsPlugin.timeout_handler, timeout) 204 | signal.signal(signal.SIGALRM, handler) 205 | signal.setitimer(signal.ITIMER_REAL, timeout) 206 | 207 | @staticmethod 208 | def cancel_timer(): 209 | signal.setitimer(signal.ITIMER_REAL, 0) 210 | signal.signal(signal.SIGALRM, signal.SIG_DFL) 211 | 212 | @staticmethod 213 | def timeout_handler(timeout, signum, frame): 214 | __tracebackhide__ = True 215 | pytest.fail('Timeout >%ss' % timeout) 216 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:multilint] 2 | paths = pytest_timeouts.py 3 | test_pytest_timeouts.py 4 | 5 | [metadata] 6 | name = pytest-timeouts 7 | description = Linux-only Pytest plugin to control durations of various test case execution phases 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | 11 | url = https://github.com/Scony/pytest-timeouts 12 | 13 | project_urls = 14 | Source=https://github.com/Scony/pytest-timeouts 15 | Documentation=https://pytest-timeouts.readthedocs.io 16 | Tracker=https://github.com/Scony/pytest-timeouts/issues 17 | 18 | author = Pawel Lampe 19 | author_email = pawel.lampe@gmail.com 20 | 21 | license = MIT license 22 | license_file = LICENSE 23 | classifiers = 24 | Development Status :: 5 - Production/Stable 25 | Environment :: Console 26 | Environment :: Plugins 27 | Intended Audience :: Developers 28 | License :: OSI Approved :: MIT License 29 | Operating System :: Unix 30 | Operating System :: POSIX 31 | Programming Language :: Python 32 | Programming Language :: Python :: 2.7 33 | Programming Language :: Python :: 3 34 | Topic :: Software Development :: Testing 35 | Topic :: Utilities 36 | platforms = unix, linux 37 | 38 | [options] 39 | py_modules = pytest_timeouts 40 | python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | version='1.2.1', 5 | install_requires=[ 6 | 'pytest>=3.1', 7 | ], 8 | entry_points={ 9 | 'pytest11': ['timeouts = pytest_timeouts'], 10 | }, 11 | ) 12 | -------------------------------------------------------------------------------- /test_pytest_timeouts.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = 'pytester' 2 | 3 | 4 | def test_arg_parse(testdir): 5 | testdir.makepyfile(""" 6 | def test_dummy(): pass 7 | """) 8 | result = testdir.runpytest( 9 | '--setup-timeout=1.5', 10 | '--execution-timeout=2.5', 11 | '--teardown-timeout=3.5', 12 | ) 13 | result.stdout.fnmatch_lines([ 14 | "setup timeout: 1.5s, execution timeout: 2.5s, teardown timeout: 3.5s" 15 | ]) 16 | 17 | 18 | def test_ini_parse(testdir): 19 | testdir.makepyfile(""" 20 | def test_dummy(): pass 21 | """) 22 | testdir.makeini(""" 23 | [pytest] 24 | setup_timeout = 1.5 25 | execution_timeout = 2.5 26 | teardown_timeout = 3.5 27 | """) 28 | result = testdir.runpytest() 29 | result.stdout.fnmatch_lines([ 30 | "setup timeout: 1.5s, execution timeout: 2.5s, teardown timeout: 3.5s" 31 | ]) 32 | 33 | 34 | def test_setup_timeout(testdir): 35 | testdir.makepyfile(""" 36 | import pytest 37 | import time 38 | 39 | 40 | @pytest.fixture(scope='function') 41 | def fx(): 42 | time.sleep(1) 43 | yield 44 | 45 | 46 | def test_dummy(fx): 47 | pass 48 | """) 49 | result = testdir.runpytest('--setup-timeout=0.5') 50 | result.stdout.fnmatch_lines([ 51 | '*Failed: Timeout >0.5s*' 52 | ]) 53 | 54 | 55 | def test_execution_timeout(testdir): 56 | testdir.makepyfile(""" 57 | import pytest 58 | import time 59 | 60 | 61 | @pytest.fixture(scope='function') 62 | def fx2(): 63 | time.sleep(1) 64 | yield 65 | 66 | 67 | def test_dummy(): 68 | time.sleep(1) 69 | """) 70 | result = testdir.runpytest('--execution-timeout=0.4') 71 | result.stdout.fnmatch_lines([ 72 | '*Failed: Timeout >0.4s*' 73 | ]) 74 | 75 | 76 | def test_teardown_timeout(testdir): 77 | testdir.makepyfile(""" 78 | import pytest 79 | import time 80 | 81 | 82 | @pytest.fixture(scope='function') 83 | def fx(): 84 | yield 85 | time.sleep(1) 86 | 87 | 88 | def test_dummy(fx): 89 | pass 90 | """) 91 | result = testdir.runpytest('--teardown-timeout=0.3') 92 | result.stdout.fnmatch_lines([ 93 | '*Failed: Timeout >0.3s*' 94 | ]) 95 | 96 | 97 | def test_execucution_marker_timeout(testdir): 98 | testdir.makepyfile(""" 99 | import pytest 100 | import time 101 | 102 | 103 | @pytest.mark.execution_timeout(0.2) 104 | def test_dummy(): 105 | time.sleep(1) 106 | """) 107 | result = testdir.runpytest('--strict') 108 | result.stdout.fnmatch_lines([ 109 | '*Failed: Timeout >0.2s*' 110 | ]) 111 | 112 | 113 | def test_setup_marker_timeout(testdir): 114 | testdir.makepyfile(""" 115 | import pytest 116 | import time 117 | 118 | 119 | @pytest.fixture(scope='function') 120 | def fx(): 121 | time.sleep(1) 122 | yield 123 | 124 | 125 | @pytest.mark.setup_timeout(0.2) 126 | def test_dummy(fx): 127 | time.sleep(1) 128 | """) 129 | result = testdir.runpytest('--strict') 130 | result.stdout.fnmatch_lines([ 131 | '*Failed: Timeout >0.2s*' 132 | ]) 133 | 134 | 135 | def test_teardown_marker_timeout(testdir): 136 | testdir.makepyfile(""" 137 | import pytest 138 | import time 139 | 140 | 141 | @pytest.fixture(scope='function') 142 | def fx(): 143 | yield 144 | time.sleep(1) 145 | 146 | 147 | @pytest.mark.teardown_timeout(0.2) 148 | def test_dummy(fx): 149 | pass 150 | """) 151 | result = testdir.runpytest('--strict') 152 | result.stdout.fnmatch_lines([ 153 | '*Failed: Timeout >0.2s*' 154 | ]) 155 | 156 | 157 | def test_timeout_setting_order(testdir): 158 | testdir.makepyfile(""" 159 | import pytest 160 | import time 161 | 162 | 163 | @pytest.fixture(scope='function') 164 | def fx(): 165 | yield 166 | time.sleep(1) 167 | 168 | 169 | @pytest.fixture(scope='function') 170 | def fx2(): 171 | time.sleep(1) 172 | yield 173 | 174 | 175 | @pytest.mark.teardown_timeout(0.2) 176 | def test_dummy(fx): 177 | pass 178 | 179 | 180 | @pytest.mark.setup_timeout(0.4) 181 | def test_dummy2(fx2): 182 | pass 183 | """) 184 | testdir.makeini(""" 185 | [pytest] 186 | setup_timeout = 0.3 187 | teardown_timeout = 0.3 188 | """) 189 | result = testdir.runpytest('--setup-timeout=0.1') 190 | result.stdout.fnmatch_lines([ 191 | '*Failed: Timeout >0.2s*', 192 | '*Failed: Timeout >0.1s*', 193 | ]) 194 | 195 | 196 | def test_timeout_override_order(testdir): 197 | testdir.makepyfile(""" 198 | import pytest 199 | import time 200 | 201 | 202 | @pytest.fixture(scope='function') 203 | def fx(): 204 | yield 205 | time.sleep(1) 206 | 207 | 208 | @pytest.fixture(scope='function') 209 | def fx2(): 210 | time.sleep(1) 211 | yield 212 | 213 | 214 | @pytest.mark.teardown_timeout(0.2) 215 | def test_dummy(fx): 216 | pass 217 | 218 | 219 | @pytest.mark.setup_timeout(0.4) 220 | def test_dummy_2(fx2): 221 | pass 222 | """) 223 | testdir.makeini(""" 224 | [pytest] 225 | setup_timeout = 0.1 226 | """) 227 | result = testdir.runpytest( 228 | '--setup-timeout=0.3', 229 | '--teardown-timeout=0.3', 230 | '--timeouts-order=imo', 231 | ) 232 | result.stdout.fnmatch_lines([ 233 | '*Failed: Timeout >0.2s*', 234 | '*Failed: Timeout >0.1s*', 235 | ]) 236 | 237 | 238 | def test_disable_args_and_markers(testdir): 239 | testdir.makepyfile(""" 240 | import pytest 241 | import time 242 | 243 | 244 | @pytest.fixture(scope='function') 245 | def fx(): 246 | yield 247 | time.sleep(1) 248 | 249 | 250 | @pytest.fixture(scope='function') 251 | def fx2(): 252 | time.sleep(1) 253 | yield 254 | 255 | 256 | @pytest.mark.teardown_timeout(0.2) 257 | def test_dummy(fx): 258 | pass 259 | 260 | 261 | @pytest.mark.setup_timeout(0.4) 262 | def test_dummy_2(fx2): 263 | pass 264 | """) 265 | testdir.makeini(""" 266 | [pytest] 267 | setup_timeout = 0.1 268 | teardown_timeout = 0.1 269 | """) 270 | result = testdir.runpytest( 271 | '--setup-timeout=0.3', 272 | '--teardown-timeout=0.3', 273 | '--timeouts-order=i', 274 | ) 275 | result.stdout.fnmatch_lines([ 276 | '*Failed: Timeout >0.1s*', 277 | '*Failed: Timeout >0.1s*', 278 | ]) 279 | 280 | 281 | def test_marker_value_missing(testdir): 282 | testdir.makepyfile(""" 283 | import pytest 284 | import time 285 | @pytest.mark.execution_timeout() 286 | def test_dummy(): 287 | time.sleep(1) 288 | """) 289 | result = testdir.runpytest() 290 | result.stdout.fnmatch_lines([ 291 | '*TypeError:*' 292 | ]) 293 | 294 | 295 | def test_marker_value_invalid(testdir): 296 | testdir.makepyfile(""" 297 | import pytest 298 | import time 299 | @pytest.mark.execution_timeout('asdf') 300 | def test_dummy(): 301 | time.sleep(1) 302 | """) 303 | result = testdir.runpytest() 304 | result.stdout.fnmatch_lines([ 305 | '*ValueError:*' 306 | ]) 307 | 308 | 309 | def test_timeout_scope_fixture(testdir): 310 | testdir.makepyfile(""" 311 | import pytest 312 | import time 313 | 314 | 315 | pytestmark = [ 316 | pytest.mark.teardown_timeout(0.12, 'function'), 317 | pytest.mark.teardown_timeout(0.14, 'module'), 318 | pytest.mark.teardown_timeout(0.13), 319 | ] 320 | 321 | 322 | @pytest.fixture(scope='function') 323 | def fx(): 324 | yield 325 | time.sleep(1) 326 | 327 | 328 | @pytest.fixture(scope='class') 329 | def fx2(): 330 | yield 331 | time.sleep(1) 332 | 333 | 334 | @pytest.fixture(scope='module') 335 | def fx3(): 336 | yield 337 | time.sleep(1) 338 | 339 | 340 | def test_dummy(fx): 341 | pass 342 | 343 | 344 | def test_dummy_2(fx2): 345 | pass 346 | 347 | 348 | @pytest.mark.teardown_timeout(0.11) 349 | def test_dummy_4(fx): 350 | pass 351 | 352 | 353 | def test_dummy_3(fx3): 354 | pass 355 | """) 356 | testdir.makeini(""" 357 | [pytest] 358 | teardown_timeout = 0.15 359 | """) 360 | result = testdir.runpytest() 361 | result.stdout.fnmatch_lines([ 362 | '*Failed: Timeout >0.12s*', 363 | '*Failed: Timeout >0.13s*', 364 | '*Failed: Timeout >0.11s*', 365 | '*Failed: Timeout >0.14s*', 366 | ]) 367 | 368 | 369 | def test_empty_timeout_order_should_show_error_on_startup(testdir): 370 | testdir.makepyfile(""" 371 | import pytest 372 | import time 373 | def test_dummy(): 374 | time.sleep(1) 375 | """) 376 | result = testdir.runpytest( 377 | '--timeouts-order=', 378 | ) 379 | result.stderr.fnmatch_lines([ 380 | 'ERROR: Order should have at least 1 and less then or equal 3 elements' 381 | ]) 382 | 383 | 384 | def test_4_timeout_order_item_should_show_error_on_startup(testdir): 385 | testdir.makepyfile(""" 386 | import pytest 387 | import time 388 | def test_dummy(): 389 | time.sleep(1) 390 | """) 391 | result = testdir.runpytest( 392 | '--timeouts-order=imoi', 393 | ) 394 | result.stderr.fnmatch_lines([ 395 | 'ERROR: Order should have at least 1 and less then or equal 3 elements' 396 | ]) 397 | 398 | 399 | def test_incorrect_timeout_order_item_should_show_error_on_startup(testdir): 400 | testdir.makepyfile(""" 401 | import pytest 402 | import time 403 | def test_dummy(): 404 | time.sleep(1) 405 | """) 406 | result = testdir.runpytest( 407 | '--timeouts-order=xa', 408 | ) 409 | result.stderr.fnmatch_lines([ 410 | 'ERROR: Incorrect item * in timeout order list' 411 | ]) 412 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -vv -ra 3 | 4 | [tox] 5 | envlist = py{35, 36, 37, 38}, 6 | py3-codestyle, 7 | py3-coverage 8 | 9 | [testenv] 10 | deps = 11 | pytest5: pytest>=5,<6 12 | 13 | commands = pytest test_pytest_timeouts.py {posargs} 14 | 15 | [testenv:py3-codestyle] 16 | deps = multilint 17 | flake8 18 | isort 19 | modernize 20 | commands = multilint 21 | 22 | [testenv:py3-coverage] 23 | deps = pytest 24 | pytest-cov 25 | 26 | commands = py.test --cov=pytest_timeouts test_pytest_timeouts.py 27 | --------------------------------------------------------------------------------