pax_global_header00006660000000000000000000000064141001257520014507gustar00rootroot0000000000000052 comment=8ff49b634441e21688f04e2618fe1d77b13cc9b0 pytest-xprocess-0.18.1/000077500000000000000000000000001410012575200147725ustar00rootroot00000000000000pytest-xprocess-0.18.1/.github/000077500000000000000000000000001410012575200163325ustar00rootroot00000000000000pytest-xprocess-0.18.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001410012575200205155ustar00rootroot00000000000000pytest-xprocess-0.18.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000012751410012575200232140ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: 'bug' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - OS: [e.g. macOS Catalina] - Python Version [e.g. 3.8.5] - pytest-xprocess version [e.g. 0.15.0] **Additional context** Add any other context about the problem here. pytest-xprocess-0.18.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000000341410012575200225020ustar00rootroot00000000000000blank_issues_enabled: false pytest-xprocess-0.18.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011421410012575200242400ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: 'feature request' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. pytest-xprocess-0.18.1/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000005101410012575200227020ustar00rootroot00000000000000--- name: Question about: Any question related to pytest-xprocess title: '' labels: 'question' assignees: '' --- **What would you like to know?.** A clear, concise and well formulated question related to pytest-xprocess. **Additional context** Extra information that could help answer your question, any details are welcome. pytest-xprocess-0.18.1/.github/pull_request_template.md000066400000000000000000000016021410012575200232720ustar00rootroot00000000000000 - fixes # Checklist: - [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change. - [ ] Add or update relevant docs, in the docs folder and in code. - [ ] Add an entry in `CHANGELOG.rst`, summarizing the change and linking to the issue. - [ ] Run `pre-commit` hooks and fix any issues. - [ ] Run `pytest` and make sure no tests failed. pytest-xprocess-0.18.1/.github/workflows/000077500000000000000000000000001410012575200203675ustar00rootroot00000000000000pytest-xprocess-0.18.1/.github/workflows/main.yml000066400000000000000000000035361410012575200220450ustar00rootroot00000000000000name: build on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python: ["3.5", "3.6", "3.7", "3.8", "3.9"] os: [ubuntu-latest, windows-latest, macos-latest] include: - python: "3.5" tox_env: "py35" - python: "3.6" tox_env: "py36" - python: "3.7" tox_env: "py37" - python: "3.8" tox_env: "py38" - python: "3.9" tox_env: "py39" steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install tox run: | python -m pip install --upgrade pip setuptools pip install tox - name: Test run: | tox -e ${{ matrix.tox_env }} linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.8" - name: Install tox run: | python -m pip install --upgrade pip setuptools pip install tox - name: Lint run: | tox -e linting deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-latest needs: [build, linting] steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.7" - name: Install wheel run: | python -m pip install --upgrade pip setuptools pip install wheel - name: Build package run: | python setup.py sdist bdist_wheel - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_token }} pytest-xprocess-0.18.1/.gitignore000066400000000000000000000034551410012575200167710ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ .xprocess/ # editors .vscode *.swp pytest-xprocess-0.18.1/.pre-commit-config.yaml000066400000000000000000000016151410012575200212560ustar00rootroot00000000000000repos: - repo: https://github.com/asottile/pyupgrade rev: v2.7.2 hooks: - id: pyupgrade args: ["--py3-plus"] - repo: https://github.com/asottile/reorder_python_imports rev: v2.3.5 hooks: - id: reorder-python-imports - repo: https://github.com/python/black rev: 20.8b1 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.3 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - id: check-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - repo: local hooks: - id: rst name: rst entry: rst-lint --encoding utf-8 files: ^(README.rst)$ language: python additional_dependencies: [pygments, restructuredtext_lint] pytest-xprocess-0.18.1/CHANGELOG.rst000066400000000000000000000124061410012575200170160ustar00rootroot000000000000000.18.1 (2021-07-27) ------------------- - Fix bug with previous release where internal module was missing 0.18.0 (2021-07-21) ------------------- - :method:`ProcessInfo.terminate` will now terminate outer leaves in process tree first and work its way towards root process. For example, if a process has child and grandchild, xprocess will terminate first child and grandchild and only then will the root process receive a termination signal. - :class:`ProcessStarter` now has attr:`terminate_on_interrupt`. This flag will make xprocess attempt to terminate and clean up all started process resources upon interruptions during pytest runs (`CTRL+C`, `SIGINT` and internal errors) when set to `True`. It will default to `False`, so if the described behaviour is desired the flag must be explicitly set `True`. - Add a new `popen_kwargs` variable to `ProcessStarter`, this variable can be used for passing keyword values to the `subprocess.Popen` constructor, giving the user more control over how the process is initialized. 0.17.1 (2021-02-28) ------------------- - Fix `ResourceWarning` in :meth:`XProcess.ensure` caused by not properly waiting on process exit and leaked File handles 0.17.0 (2020-11-26) ------------------- - :class:`ProcessStarter` now has :meth:`startup_check`. This method can be optionaly overridden and will be called upon to check process responsiveness after :attr:`ProcessStarter.pattern` is matched. By default, :meth:`XProcess.ensure` will only attempt to match :attr:`ProcessStarter.pattern` when starting a process, if matched, xprocess will consider the process as ready to answer queries. If :meth:`startup_check` is provided though, its return value will also be considered to determine if the process has been successfully started. If :meth:`startup_check` returns `True` after :attr:`ProcessStarter.pattern` has been matched, :meth:`XProcess.ensure` will return sucessfully. In contrast, if :meth:`startup_check` does not return `True` before timing out, :meth:`XProcess.ensure` will raise a `TimeoutError` exception. - Remove deprecated :meth:`xprocess.CompatStarter` 0.16.0 (2020-10-29) ------------------- - :class:`ProcessStarter` now has a new `timeout` class variable optionaly overridden to define the maximum time :meth:`xprocess.ensure` should wait for process output when trying to match :attr:`ProcessStarter.pattern`. Defaults to 120 seconds. - The number of lines in the process logfile watched for :attr:`ProcessStarter.pattern` is now configurable and can be changed by setting :attr:`ProcessStarter.max_read_lines` to the desired value. Defaults to 50 lines. - Make :meth:`XProcessInfo.isrunning` ignore zombie processes by default. Pass ``ignore_zombies=False`` to get the previous behavior, which was to consider zombie processes as running. 0.15.0 (2020-10-03) ------------------- - pytest-xprocess now uses a sub-directory of `.pytest_cache` to store process related files. - Drop support for Python 2.7 - Fixed bug when non-ascii characters were written to stdout by external process - Removed deprecated :meth:`XProcessInfo.kill` 0.14.0 (2020-09-24) ------------------- - Now ``XProcessInfo.terminate`` will by default also terminate the entire process tree. This is safer as there's no risk of leaving lingering processes behind. If for some reason you need the previous behavior of only terminating the root process, pass ```kill_proc_tree=False`` to ``XProcessInfo.terminate``. 0.13.1 (2020-01-29) ------------------- - Drop support for Python 2.6 and 3.4. - Ignore empty lines in log files when looking for the pattern that indicates a process has started. 0.13.0 (UNRELEASED) ------------------- - Never released due to deploy issues. 0.12.1 (2017-06-07) ------------------- - Fixed example in README.md 0.12.0 (2017-06-06) ------------------- - #3: :meth:`XProcess.ensure` now accepts preferably a ProcessStarter subclass to define and customize the process startup behavior. Passing a simple function is deprecated and will be removed in a future release. 0.11.1 (2017-05-31) ------------------- - Restored :meth:`XProcessInfo.kill()` as alias for :meth:`XProcessInfo.terminate()` for API compatibility. 0.11 (2017-05-18) ----------------- - When tearing down processes (through ``--xkill``), the more polite SIGTERM is used before invoking SIGKILL, allowing the process to cleanly shutdown. See https://github.com/pytest-dev/pytest-xprocess/issues/1 for more details. - :meth:`XProcessInfo.kill()` is deprecated. 0.10 (2017-05-15) ----------------- - Project `now hosted on Github `_. 0.9.1 (2015-07-15) ------------------ - Don't use `__multicall__` in pytest hook 0.9 (2015-07-15) ---------------- - Fix issue Log calls without parameters now print the correct message instead of an empty tuple. See https://bitbucket.org/pytest-dev/pytest-xprocess/pull-request/3 for more info. - Use 3rd party `psutil` library for process handling 0.8.0 (2013-10-04) ------------------ - Support python3 (tested on linux/win32) - Split out pytest independent process support into `xprocess.py` - Add method:`xProcessInfo.kill` and some smaller refactoring - Fix various windows related Popen / killing details - Add tests 0.7.0 (2013-04-05) ------------------ - Initial release pytest-xprocess-0.18.1/CODE_OF_CONDUCT.md000066400000000000000000000064311410012575200175750ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge 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, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities 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. 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. ## Scope 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. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at pytest-dev@python.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and 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. 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. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq pytest-xprocess-0.18.1/CONTRIBUTING.rst000066400000000000000000000066351410012575200174450ustar00rootroot00000000000000How to contribute ================= All contributions are greatly appreciated! How to report issues ~~~~~~~~~~~~~~~~~~~~ Facilitating the work of potential contributors is recommended since it increases the likelihood of your issue being solved quickly. The few extra steps listed below will help clarify problems you might be facing: - Include a `minimal reproducible example`_ when possible. - Describe the expected behaviour and what actually happened including a full trace-back in case of exceptions. - Make sure to list details about your environment, such as your platform, versions of pytest, pytest-xprocess and python release. Also, it's important to check the current open issues for similar reports in order to avoid duplicates. .. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example Setting up your development environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Fork pytest-xprocess to your GitHub account by clicking the `Fork`_ button. - `Clone`_ the main repository (not your fork) to your local machine. .. code-block:: text $ git clone https://github.com/pytest-dev/pytest-xprocess $ cd pytest-xprocess - Add your fork as a remote to push your contributions.Replace ``{username}`` with your username. .. code-block:: text git remote add fork https://github.com/{username}/pytest-xprocess - Using `Tox`_, create a virtual environment and install xprocess in editable mode with development dependencies. .. code-block:: text $ tox -e dev $ source venv/bin/activate - Install pre-commit hooks .. code-block:: text $ pre-commit install .. _Fork: https://github.com/pytest-dev/pytest-xprocess/fork .. _Clone: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork .. _Tox: https://tox.readthedocs.io/en/latest/ Start Coding ~~~~~~~~~~~~ - Create a new branch to identify what feature you are working on. .. code-block:: text $ git fetch origin $ git checkout -b your-branch-name origin/master - Make your changes - Include tests that cover any code changes you make and run them as described below. - Push your changes to your fork. `create a pull request`_ describing your changes. .. code-block:: text $ git push --set-upstream fork your-branch-name .. _create a pull request: https://help.github.com/en/articles/creating-a-pull-request How to run tests ~~~~~~~~~~~~~~~~ You can run the test suite for the current environment with .. code-block:: text $ pytest To run the full test suite for all supported python versions .. code-block:: text $ tox Obs. CI will run tox when you submit your pull request, so this is optional. How to build docs ~~~~~~~~~~~~~~~~~ The docs can be built using ``tox`` with the following command .. code-block:: text $ tox -e docs This will generated the documentation in html format and save everything inside ``docs/build``. To open it, just open file ``docs/build/index.html`` using your browser. Checking Test Coverage ~~~~~~~~~~~~~~~~~~~~~~~ To get a complete report of code sections not being touched by the test suite run ``pytest`` using ``coverage``. .. code-block:: text $ coverage run -m pytest $ coverage html Open ``htmlcov/index.html`` in your browser. More about converage `here `__. pytest-xprocess-0.18.1/LICENSE000066400000000000000000000020361410012575200160000ustar00rootroot00000000000000 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest-xprocess-0.18.1/MANIFEST.in000066400000000000000000000001441410012575200165270ustar00rootroot00000000000000include CHANGELOG include README.rst include setup.py include tox.ini include LICENSE graft example pytest-xprocess-0.18.1/README.rst000077500000000000000000000057021410012575200164700ustar00rootroot00000000000000pytest-xprocess =============== A pytest plugin for managing external processes across test runs. .. image:: https://img.shields.io/maintenance/yes/2021?color=blue :target: https://github.com/pytest-dev/pytest-xprocess :alt: Maintenance .. image:: https://img.shields.io/github/last-commit/pytest-dev/pytest-xprocess?color=blue :target: https://github.com/pytest-dev/pytest-xprocess/commits/master :alt: GitHub last commit .. image:: https://img.shields.io/github/issues-pr-closed-raw/pytest-dev/pytest-xprocess?color=blue :target: https://github.com/pytest-dev/pytest-xprocess/pulls?q=is%3Apr+is%3Aclosed :alt: GitHub closed pull requests .. image:: https://img.shields.io/github/issues-closed/pytest-dev/pytest-xprocess?color=blue :target: https://github.com/pytest-dev/pytest-xprocess/issues?q=is%3Aissue+is%3Aclosed :alt: GitHub closed issues .. image:: https://img.shields.io/pypi/dm/pytest-xprocess?color=blue :target: https://pypi.org/project/pytest-xprocess/ :alt: PyPI - Downloads .. image:: https://img.shields.io/github/languages/code-size/pytest-dev/pytest-xprocess?color=blue :target: https://github.com/pytest-dev/pytest-xprocess :alt: Code size .. image:: https://img.shields.io/pypi/v/pytest-xprocess.svg?color=blue :target: https://github.com/pytest-dev/pytest-xprocess/releases :alt: Release .. image:: https://img.shields.io/badge/license-MIT-blue.svg?color=blue :target: https://github.com/pytest-dev/pytest-xprocess/blob/master/LICENSE :alt: License .. image:: https://img.shields.io/pypi/pyversions/pytest-xprocess.svg?color=blue :target: https://pypi.org/project/pytest-xprocess :alt: supported python versions .. image:: https://img.shields.io/github/issues-raw/pytest-dev/pytest-xprocess.svg?color=blue :target: https://github.com/pytest-dev/pytest-xprocess/issues :alt: Issues .. image:: https://github.com/pytest-dev/pytest-xprocess/workflows/build/badge.svg :target: https://github.com/pytest-dev/pytest-xprocess/actions :alt: build status .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black :alt: style .. image:: https://readthedocs.org/projects/pytest-xprocess/badge/?version=latest :target: https://pytest-xprocess.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status Installing ---------- Install using `pip`_: .. code-block:: text pip install pytest-xprocess .. _pip: https://pip.pypa.io/en/stable/quickstart/ Useful Links -------------- - Documentation: https://pytest-xprocess.readthedocs.io/ - Changelog: https://pytest-xprocess.readthedocs.io/en/latest/changes.html - PyPI Releases: https://pypi.org/project/pytest-xprocess/ - Source Code: https://github.com/pytest-dev/pytest-xprocess - Issue Tracker: https://github.com/pytest-dev/pytest-xprocess/issues/ - Pytest IRC channel: ircs://irc.libera.chat:6697/#pytest - Pytest Discord Channel: https://discord.gg/k7F2ZFvJV3 pytest-xprocess-0.18.1/RELEASING.rst000066400000000000000000000021541410012575200170370ustar00rootroot00000000000000========================= Releasing pytest-xprocess ========================= This document describes the steps to make a new ``pytest-xprocess`` release. Version ------- ``master`` should always be green and a potential release candidate. ``pytest-xprocess`` follows semantic versioning, so given that the current version is ``X.Y.Z``, to find the next version number one needs to look at the ``changelog`` file for the latest section marked as ``Unreleased`` Steps ----- #. Create a new branch named ``release-X.Y.Z`` from the latest ``master``. #. Create and activate a virtualenv:: $ python -m venv venv && source venv/bin/activate #. Install ``tox``:: $ pip install tox #. Update the necessary files with:: $ tox -e release -- X.Y.Z #. Commit and push the branch for review. #. Once PR is **green** and **approved**, create and push a tag:: $ export VERSION=X.Y.Z $ git tag $VERSION release-$VERSION $ git push git@github.com:pytest-dev/pytest-xprocess.git $VERSION That will build the package and publish it on ``PyPI`` automatically. #. Merge ``release-X.Y.Z`` branch into master. pytest-xprocess-0.18.1/docs/000077500000000000000000000000001410012575200157225ustar00rootroot00000000000000pytest-xprocess-0.18.1/docs/Makefile000066400000000000000000000011761410012575200173670ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pytest-xprocess-0.18.1/docs/make.bat000066400000000000000000000014371410012575200173340ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd pytest-xprocess-0.18.1/docs/source/000077500000000000000000000000001410012575200172225ustar00rootroot00000000000000pytest-xprocess-0.18.1/docs/source/changes.rst000066400000000000000000000116031410012575200213650ustar00rootroot00000000000000.. _changes: Changelog ========= 0.18.0 (UNRELEASED) ------------------- - :class:`ProcessStarter` now has attr:`terminate_on_interrupt`. This flag will make xprocess attempt to terminate and clean up all started process resources upon interruptions during pytest runs (CTRL+C, SIGINT and internal errors) when set to `True`. It will default to `False`, so if the described behaviour is desired the flag must be explicitly set `True`. - Add a new `popen_kwargs` variable to `ProcessStarter`, this variable can be used for passing keyword values to the `subprocess.Popen` constructor, giving the user more control over the initialized process. 0.17.1 (2020-02-28) ------------------- - Fix `ResourceWarning` in :meth:`XProcess.ensure` caused by not properly waiting on process exit and leaked File handles 0.17.0 (2020-11-26) ------------------- - :class:`ProcessStarter` now has :meth:`startup_check`. This method can be optionaly overridden and will be called upon to check process responsiveness after :attr:`ProcessStarter.pattern` is matched. By default, :meth:`XProcess.ensure` will only attempt to match :attr:`ProcessStarter.pattern` when starting a process, if matched, xprocess will consider the process as ready to answer queries. If :meth:`startup_check` is provided though, its return value will also be considered to determine if the process has been successfully started. If :meth:`startup_check` returns `True` after :attr:`ProcessStarter.pattern` has been matched, :meth:`XProcess.ensure` will return sucessfully. In contrast, if :meth:`startup_check` does not return `True` before timing out, :meth:`XProcess.ensure` will raise a `TimeoutError` exception. - Remove deprecated :meth:`xprocess.CompatStarter` 0.16.0 (2020-10-29) ------------------- - :class:`ProcessStarter` now has a new `timeout` class variable optionaly overridden to define the maximum time :meth:`xprocess.ensure` should wait for process output when trying to match :attr:`ProcessStarter.pattern`. Defaults to 120 seconds. - The number of lines in the process logfile watched for :attr:`ProcessStarter.pattern` is now configurable and can be changed by setting :attr:`ProcessStarter.max_read_lines` to the desired value. Defaults to 50 lines. - Make :meth:`XProcessInfo.isrunning` ignore zombie processes by default. Pass ``ignore_zombies=False`` to get the previous behavior, which was to consider zombie processes as running. 0.15.0 (2020-10-03) ------------------- - pytest-xprocess now uses a sub-directory of `.pytest_cache` to store process related files. - Drop support for Python 2.7 - Fixed bug when non-ascii characters were written to stdout by external process - Removed deprecated :meth:`XProcessInfo.kill` 0.14.0 (2020-09-24) ------------------- - Now ``XProcessInfo.terminate`` will by default also terminate the entire process tree. This is safer as there's no risk of leaving lingering processes behind. If for some reason you need the previous behavior of only terminating the root process, pass ```kill_proc_tree=False`` to ``XProcessInfo.terminate``. 0.13.1 (2020-01-29) ------------------- - Drop support for Python 2.6 and 3.4. - Ignore empty lines in log files when looking for the pattern that indicates a process has started. 0.13.0 (UNRELEASED) ------------------- - Never released due to deploy issues. 0.12.1 (2017-06-07) ------------------- - Fixed example in README.md 0.12.0 (2017-06-06) ------------------- - #3: :meth:`XProcess.ensure` now accepts preferably a ProcessStarter subclass to define and customize the process startup behavior. Passing a simple function is deprecated and will be removed in a future release. 0.11.1 (2017-05-31) ------------------- - Restored :meth:`XProcessInfo.kill()` as alias for :meth:`XProcessInfo.terminate()` for API compatibility. 0.11 (2017-05-18) ----------------- - When tearing down processes (through ``--xkill``), the more polite SIGTERM is used before invoking SIGKILL, allowing the process to cleanly shutdown. See https://github.com/pytest-dev/pytest-xprocess/issues/1 for more details. - :meth:`XProcessInfo.kill()` is deprecated. 0.10 (2017-05-15) ----------------- - Project `now hosted on Github `_. 0.9.1 (2015-07-15) ------------------ - Don't use `__multicall__` in pytest hook 0.9 (2015-07-15) ---------------- - Fix issue Log calls without parameters now print the correct message instead of an empty tuple. See https://bitbucket.org/pytest-dev/pytest-xprocess/pull-request/3 for more info. - Use 3rd party `psutil` library for process handling 0.8.0 (2013-10-04) ------------------ - Support python3 (tested on linux/win32) - Split out pytest independent process support into `xprocess.py` - Add method:`xProcessInfo.kill` and some smaller refactoring - Fix various windows related Popen / killing details - Add tests 0.7.0 (2013-04-05) ------------------ - Initial release pytest-xprocess-0.18.1/docs/source/command_line_options.rst000066400000000000000000000053511410012575200241600ustar00rootroot00000000000000.. _command_line_options: Command Line Options -------------------- Additionally to handling initialization, termination and logging of external process for you, ``pytest-xprocess`` also offers an easy way of manually managing long running processes that must persist across multiple test runs with ``--xshow`` and ``--xkill`` command line utilities. Listing Long-running Processes with ``--xshow`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can query pytest-xprocess for the state and information of all previously started processes by invoking pytest command and passing the ``--xshow`` option:: $ pytest --xshow 10598 redis-server LIVE /.pytest_cache/d/.xprocess/redis-server/xprocess.log 10599 memcached DEAD /.pytest_cache/d/.xprocess/memcached/xprocess.log 10600 db-service LIVE /pytest-xprocess/.pytest_cache/d/.xprocess/db-service/xprocess.log As we can see, xprocess will list the state of all invoked processes along with some relevant information, namely: PID, process name, state (`ALIVE` or `DEAD`) and path to the process log file. Terminating Long-running Processes with ``--xkill`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Besides quering for information on long-running processes, it's also possible to terminate any of these processes started by pytest-xprocess by invoking pytest command and passing ``--xkill`` option. Following is a simple example. Let's start by listing all processes:: $ pytest --xshow 10598 redis-server LIVE /.pytest_cache/d/.xprocess/redis-server/xprocess.log 10599 memcached LIVE /.pytest_cache/d/.xprocess/memcached/xprocess.log 10600 db-service LIVE /pytest-xprocess/.pytest_cache/d/.xprocess/db-service/xprocess.log Now, let's terminate the first one of PID 10598, redis-server:: $ pytest --xkill redis-server 10598 redis-server TERMINATED /.pytest_cache/d/.xprocess/redis-server/xprocess.log If we check the processes states again, we can see that redis-server is now ``DEAD``:: $ pytest --xshow 10598 redis-server DEAD /.pytest_cache/d/.xprocess/redis-server/xprocess.log 10599 memcached LIVE /.pytest_cache/d/.xprocess/memcached/xprocess.log 10600 db-service LIVE /pytest-xprocess/.pytest_cache/d/.xprocess/db-service/xprocess.log We call also kill all processes started by pytest-xprocess by only passing ``--xkill`` without a name:: $ pytest --xkill # this will kill all processes ... $ pytest --xshow 10598 redis-server DEAD /.pytest_cache/d/.xprocess/redis-server/xprocess.log 10599 memcached DEAD /.pytest_cache/d/.xprocess/memcached/xprocess.log 10600 db-service DEAD /pytest-xprocess/.pytest_cache/d/.xprocess/db-service/xprocess.log pytest-xprocess-0.18.1/docs/source/conf.py000066400000000000000000000035511410012575200205250ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = "pytest-xprocess" copyright = "2021, Holger Krekel" author = "Holger Krekel" # The full version, including alpha/beta/rc tags release = "0.17.1" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] pytest-xprocess-0.18.1/docs/source/contact.rst000066400000000000000000000004001410012575200214010ustar00rootroot00000000000000.. _contact: Contact Channels ---------------- You can reach out for question or discussions through one of Pytest channels on: * `IRC on irc.libera.chat `_ * `Discord - pytest-help `_ pytest-xprocess-0.18.1/docs/source/contributing.rst000066400000000000000000000061571410012575200224740ustar00rootroot00000000000000.. _contributring: How to contribute ================= All contributions are greatly appreciated! How to report issues ~~~~~~~~~~~~~~~~~~~~ Facilitating the work of potential contributors is recommended since it increases the likelihood of your issue being solved quickly. The few extra steps listed below will help clarify problems you might be facing: - Include a `minimal reproducible example`_ when possible. - Describe the expected behaviour and what actually happened including a full trace-back in case of exceptions. - Make sure to list details about your environment, such as your platform, versions of pytest, pytest-xprocess and python release. Also, it's important to check the current open issues for similar reports in order to avoid duplicates. .. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example Setting up your development environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Fork pytest-xprocess to your GitHub account by clicking the `Fork`_ button. - `Clone`_ the main repository (not your fork) to your local machine. .. code-block:: text $ git clone https://github.com/pytest-dev/pytest-xprocess $ cd pytest-xprocess - Add your fork as a remote to push your contributions.Replace ``{username}`` with your username. .. code-block:: text git remote add fork https://github.com/{username}/pytest-xprocess - Using `Tox`_, create a virtual environment and install xprocess in editable mode with development dependencies. .. code-block:: text $ tox -e dev $ source venv/bin/activate - Install pre-commit hooks .. code-block:: text $ pre-commit install .. _Fork: https://github.com/pytest-dev/pytest-xprocess/fork .. _Clone: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork .. _Tox: https://tox.readthedocs.io/en/latest/ Start Coding ~~~~~~~~~~~~ - Create a new branch to identify what feature you are working on. .. code-block:: text $ git fetch origin $ git checkout -b your-branch-name origin/master - Make your changes - Include tests that cover any code changes you make and run them as described below. - Push your changes to your fork. `create a pull request`_ describing your changes. .. code-block:: text $ git push --set-upstream fork your-branch-name .. _create a pull request: https://help.github.com/en/articles/creating-a-pull-request How to run tests ~~~~~~~~~~~~~~~~ You can run the test suite for the current environment with .. code-block:: text $ pytest To run the full test suite for all supported python versions .. code-block:: text $ tox Obs. CI will run tox when you submit your pull request, so this is optional. Checking Test Coverage ~~~~~~~~~~~~~~~~~~~~~~~ To get a complete report of code sections not being touched by the test suite run ``pytest`` using ``coverage``. .. code-block:: text $ coverage run -m pytest $ coverage html Open ``htmlcov/index.html`` in your browser. More about converage `here `__. pytest-xprocess-0.18.1/docs/source/index.rst000066400000000000000000000030101410012575200210550ustar00rootroot00000000000000.. pytest-xprocess documentation master file, created by sphinx-quickstart on Sun Jun 6 00:00:19 2021. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to pytest-xprocess's documentation! =========================================== A `pytest `_ plugin for managing processes. It will make sure external processes on which your application depends are up during every pytest run without the need of manual start-up. Quickstart ---------- Install plugin via ``pip``:: $ pip install pytest-xprocess Define your process fixture in ``conftest.py``: .. code-block:: python # content of conftest.py import pytest from xprocess import ProcessStarter @pytest.fixture def myserver(xprocess): class Starter(ProcessStarter): # startup pattern pattern = "PATTERN" # command to start process args = ['command', 'arg1', 'arg2'] # ensure process is running and return its logfile logfile = xprocess.ensure("myserver", Starter) conn = # create a connection or url/port info to the server yield conn # clean up whole process tree afterwards xprocess.getinfo("myserver").terminate() Now you can use this fixture in any test functions where ``myserver`` needs to be up and ``xprocess`` will take care of it for you! .. toctree:: :maxdepth: 2 starter command_line_options contributing changes contact pytest-xprocess-0.18.1/docs/source/starter.rst000066400000000000000000000166561410012575200214560ustar00rootroot00000000000000.. _starter: Starter Class ------------- Your ``Starter`` will be used to customize how xprocess behaves. It must be a subclass of ``ProcessStarter`` where the required information to start a process instance will be provided. Matching process output with ``pattern`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to detect that your process is ready to answer queries, ``pytest-xprocess`` allows the user to provide a string pattern by setting the class variable ``pattern`` in the Starter class. ``pattern`` will be waited for in the process logfile for a maximum time defined by ``timeout`` before timing out in case the provided pattern is not matched. It's important to note that ``pattern`` is a regular expression and will be matched using python `re.search `_, so usual regex syntax (e.g. ``"eggs\s+([a-zA-Z_][a-zA-Z_0-9]*"``) can be used freely. .. code-block:: python @pytest.fixture def myserver(xprocess): class Starter(ProcessStarter): # Here, we assume that our hypothetical process # will print the message "server has started" # once initialization is done pattern = "[Ss]erver has started!" # ... Controlling Startup Wait Time with ``timeout`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Some processes naturally take longer to start than others. By default, ``pytest-xprocess`` will wait for a maxium of 120 seconds for a given process to start before raising a ``TimeoutError``. Changing this value may be useful, for example, when the user knows that a given process would never take longer than a known amount of time to start under normal circunstances, so if it does go over this known upper boundary, that means something is wrong and the waiting process must be interrupted. The maxium wait time can be controlled thourgh the class variable ``timeout``. .. code-block:: python @pytest.fixture def myserver(xprocess): class Starter(ProcessStarter): # will wait for 10 seconds before timing out timeout = 10 # ... Telling pytest-xprocess how to start a process with ``args`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to start a process, pytest-xprocess must be given a command to be passed into the `subprocess.Popen constructor `_. Any arguments passed to the process command can also be passed using ``args``. As an example, if I usually use the following command to start a given process: ``$> myproc -name "bacon" -cores 4 `` That would look like: ``args = ['myproc', '-name', '"bacon"', '-cores', 4, '']`` when using ``args`` in ``pytest-xprocess`` to start the same process. .. code-block:: python @pytest.fixture def myserver(xprocess): class Starter(ProcessStarter): # will pass "$> myproc -name "bacon" -cores 4 " to the # subprocess.Popen constructor so the process can be started with # the given arguments args = ['myproc', '-name', '"bacon"', '-cores', 4, ''] # ... Limiting number of lines searched for pattern with ``max_read_lines`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the specified string ``patern`` can be found within the first ``n`` outputted lines, there's no reason to search all the remaining output (possibly hundreds of lines or more depending on the process). For that reason, ``pytest-xprocess`` allows the user to limit the maxium number of lines outputted by the process that will be searched for the given pattern with ``max_read_lines``. If ``max_read_lines`` lines have been searched and ``patern`` has not been found, a ``RuntimeError`` will be raised to let the user know that startup has failed. When not specified, ``max_read_lines`` will default to 50 lines. .. code-block:: python @pytest.fixture def myserver(xprocess): class Starter(ProcessStarter): # search the first 12 lines for pattern, if not found # a RuntimeError will be raised informing the user max_read_lines = 12 # ... Making sure your process is ready with ``startup_check`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Some processes don't have that much console output, so ``pytest-xprocess`` offers a way to double-check that the initialized process is in a query-ready state by allowing the user to define a callback function ``startup_check``. When provided, this function will be called upon to check process responsiveness after ``ProcessStarter.pattern`` has been matched. By default, ``XProcess.ensure`` will attempt to match ``ProcessStarter.pattern`` when starting a process, if matched, xprocess will consider the process as ready to answer queries. If ``startup_check`` is provided though, its return value will also be considered to determine if the process has been properly started. If ``startup_check`` returns True after ``ProcessStarter.pattern`` has been matched, ``XProcess.ensure`` will return sucessfully. In contrast, if ``startup_check`` does not return ``True`` before timing out, ``XProcess.ensure`` will raise a ``TimeoutError`` exception. ``startup_check`` must return a boolean value (``True`` or ``False``) .. code-block:: python @pytest.fixture def myserver(xprocess): class Starter(ProcessStarter): # checks if our server is ready with a ping def startup_check(self): sock = socket.socket() sock.connect(("myhostname", 6777)) sock.sendall(b"ping\n") return sock.recv(1) == "pong!" # ... Customizing process execution environment with ``env`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, the execution environment of the main test process will be inherited by the invoked process. But, if desired, it's possible to customize the environment in which the new process will be invoked by providing a mapping containg the desired environment variables and their respective values with ``env``. .. code-block:: python @pytest.fixture def myserver(xprocess): class Starter(ProcessStarter): # checks if our server is ready with a ping env = {"PYTHONPATH": str(some_path), "PYTHONUNBUFFERED": "1"} # ... Overriding Wait Behavior ~~~~~~~~~~~~~~~~~~~~~~~~ To override the wait behavior, override ``ProcessStarter.wait``. See the ``xprocess.ProcessStarter`` interface for more details. Note that the plugin uses a subdirectory in ``.pytest_cache`` to persist the process ID and logfile information. An Important Note Regarding Stream Buffering ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There have been reports of issues with test suites hanging when users attempt to start external **python** processes with ``xprocess.ensure`` method. The reason for this is that ``pytest-xprocess`` relies on matching predefined string patterns written to your environment standard output streams to detect when processes start and python's `sys.stdout/sys.stderr`_ buffering ends up getting in the way of that. A possible solution for this problem is making both streams unbuffered by passing the ``-u`` command-line option to your process start command or setting the ``PYTHONUNBUFFERED`` environment variable. .. _sys.stdout/sys.stderr: https://docs.python.org/3/library/sys.html#sys.stderr pytest-xprocess-0.18.1/pytest_xprocess.py000066400000000000000000000061441410012575200206270ustar00rootroot00000000000000import py import pytest from xprocess import XProcess def getrootdir(config): return config.cache.makedir(".xprocess") def pytest_addoption(parser): group = parser.getgroup( "xprocess", "managing external processes across test-runs [xprocess]" ) group.addoption("--xkill", action="store_true", help="kill all external processes") group.addoption( "--xshow", action="store_true", help="show status of external process" ) def pytest_cmdline_main(config): xkill = config.option.xkill xshow = config.option.xshow if xkill or xshow: config._do_configure() tw = py.io.TerminalWriter() rootdir = getrootdir(config) xprocess = XProcess(config, rootdir) if xkill: return xprocess._xkill(tw) if xshow: return xprocess._xshow(tw) @pytest.fixture(scope="session") def xprocess(request): """yield session-scoped XProcess helper to manage long-running processes required for testing.""" rootdir = getrootdir(request.config) with XProcess(request.config, rootdir) as xproc: # pass in xprocess object into pytest_unconfigure # through config for proper cleanup during teardown request.config._xprocess = xproc yield xproc @pytest.mark.hookwrapper def pytest_runtest_makereport(item, call): logfiles = getattr(item.config, "_extlogfiles", None) report = yield if logfiles is None: return for name in sorted(logfiles): content = logfiles[name].read() if content: longrepr = getattr(report, "longrepr", None) if hasattr(longrepr, "addsection"): # pragma: no cover longrepr.addsection("%s log" % name, content) def pytest_unconfigure(config): try: xprocess = config._xprocess except AttributeError: # xprocess fixture was not used pass else: xprocess._clean_up_resources() print( "pytest-xprocess reminder::Be sure to terminate the started process by running " "'pytest --xkill' if you have not explicitly done so in your fixture with " "'xprocess.getinfo().terminate()'." ) def pytest_configure(config): config.pluginmanager.register(InterruptionHandler()) class InterruptionHandler: """The purpose of this class is exposing the config object containing references necessary to properly clean-up in the event of an exception during test runs""" def pytest_configure(self, config): self.config = config def info_objects(self): return self.config._xprocess._info_objects def interruption_clean_up(self): try: xprocess = self.config._xprocess except AttributeError: pass else: for info, terminate_on_interrupt in self.info_objects(): if terminate_on_interrupt: info.terminate() xprocess._clean_up_resources() def pytest_keyboard_interrupt(self, excinfo): self.interruption_clean_up() def pytest_internalerror(self, excrepr, excinfo): self.interruption_clean_up() pytest-xprocess-0.18.1/setup.cfg000066400000000000000000000026511410012575200166170ustar00rootroot00000000000000[metadata] name = pytest-xprocess author = Holger Krekel author_email = holger@merlinux.eu license = MIT license_files = LICENSE url = https://github.com/pytest-dev/pytest-xprocess/ long_description = file: README.rst description = A pytest plugin for managing processes across test runs. classifiers= Framework :: Pytest Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Topic :: Software Development :: Testing Topic :: Software Development :: Libraries Topic :: Utilities [options] setup_requires= setuptools_scm py_modules= pytest_xprocess xprocess python_requires = >= 3.5 [options.entry_points] pytest11 = xprocess = pytest_xprocess [coverage:run] branch = true include = xprocess.py pytest_xprocess.py [flake8] # B = bugbear # E = pycodestyle errors # F = flake8 pyflakes # W = pycodestyle warnings # B9 = bugbear opinions # E203 = slice notation whitespace, invalid # E501 = line length, handled by bugbear B950 # E722 = bare except, handled by bugbear B001 # W503 = bin op line break, invalid select = B, E, F, W, B9 ignore = E203 E501 E722 W503 max-line-length = 80 pytest-xprocess-0.18.1/setup.py000077500000000000000000000003541410012575200165110ustar00rootroot00000000000000from setuptools import setup if __name__ == "__main__": setup( name="pytest-xprocess", use_scm_version=True, # this is for GitHub's dependency graph install_requires=["pytest>=2.8", "psutil"], ) pytest-xprocess-0.18.1/tests/000077500000000000000000000000001410012575200161345ustar00rootroot00000000000000pytest-xprocess-0.18.1/tests/conftest.py000066400000000000000000000012721410012575200203350ustar00rootroot00000000000000import socket from contextlib import closing import pytest from xprocess import ProcessStarter pytest_plugins = "pytester" @pytest.fixture def example(xprocess): """fixture for testing ResourceWarnings in test_resource_cleanup.py module""" class Starter(ProcessStarter): pattern = "foo" args = ("sh", "-c", "echo foo; sleep 10; echo bar") xprocess.ensure("example", Starter) yield xprocess.getinfo("example").terminate() @pytest.fixture def tcp_port(): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: s.bind(("", 0)) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return s.getsockname()[1] pytest-xprocess-0.18.1/tests/server.py000066400000000000000000000047601410012575200200230ustar00rootroot00000000000000import signal import socketserver import sys from multiprocessing import Process from time import sleep class TestHandler(socketserver.StreamRequestHandler): """The request handler class for the test server.""" def handle(self): while True: line = self.rfile.readline() if not line: break if line == bytes("exit\n", "utf-8"): # terminate itself self.server._BaseServer__shutdown_request = True else: response = line.upper() self.request.sendall(response) class TestServer(socketserver.TCPServer): """ This server class is used for testing only""" allow_reuse_address = True def write_test_patterns(self): self.write_blank_lines() self.write_complex_strings() self.write_non_ascii() sys.stderr.write("started\n") self.write_long_output() sys.stderr.write("finally started\n") sys.stderr.flush() def write_long_output(self): """write several lines to test pattern matching with process with a lot of output""" for _ in range(50): sys.stderr.write("spam, bacon, eggs\n") def write_non_ascii(self): """non-ascii characters must be supported""" for _ in range(5): sys.stderr.write("Ê�æ�pP��çîöē�P��adåráøū\n") def write_complex_strings(self): """Special/control characters should not cause problems""" for i in range(5): sys.stderr.write("{} , % /.%,@%@._%%# #/%/ %\n".format(i)) def write_blank_lines(self): """Blank lines should be igored by xprocess""" for _ in range(100): sys.stderr.write("\n") def fork_children(self, target, amount): """forks multiple children for testing process tree termination""" for _ in range(amount): p = Process(target=target) p.start() if __name__ == "__main__": def do_nothing(): while True: sleep(1) HOST, PORT = "localhost", int(sys.argv[1]) server = TestServer((HOST, PORT), TestHandler) if "--ignore-sigterm" in sys.argv and sys.platform != "win32": # ignore sigterm for testing XProcessInfo.terminate # when processes fail to exit signal.signal(signal.SIGTERM, signal.SIG_IGN) if "--no-children" not in sys.argv: server.fork_children(do_nothing, 3) server.write_test_patterns() server.serve_forever() pytest-xprocess-0.18.1/tests/test_callback.py000066400000000000000000000031241410012575200213010ustar00rootroot00000000000000import socket import sys from pathlib import Path import pytest from xprocess import ProcessStarter server_path = Path(__file__).parent.joinpath("server.py").absolute() @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_callback_success(xprocess, tcp_port, proc_name): class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port, "--no-children"] def startup_check(self): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect(("localhost", tcp_port)) sock.sendall(bytes("bacon\n", "utf-8")) received = str(sock.recv(1024), "utf-8") return received == "bacon\n".upper() xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) assert info.isrunning() info.terminate() @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_callback_fail(xprocess, tcp_port, proc_name): class Starter(ProcessStarter): timeout = 5 pattern = "started" args = [sys.executable, server_path, tcp_port, "--no-children"] def startup_check(self): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect(("localhost", tcp_port)) sock.sendall(bytes("bacon\n", "utf-8")) received = str(sock.recv(1024), "utf-8") return received == "wrong" # this wont match with pytest.raises(TimeoutError): xprocess.ensure(proc_name, Starter) xprocess.getinfo(proc_name).terminate() pytest-xprocess-0.18.1/tests/test_functional_workflow.py000066400000000000000000000024631410012575200236460ustar00rootroot00000000000000from pathlib import Path def test_functional_work_flow(testdir, tcp_port): server_path = Path(__file__).parent.joinpath("server.py").absolute() testdir.makepyfile( """ import sys import socket from xprocess import ProcessStarter def test_server(request, xprocess): port = %r data = "spam\\n" server_path = %r class Starter(ProcessStarter): pattern = "started" max_read_lines = 200 args = [sys.executable, server_path, port] # required so test won't hang on pytest_unconfigure xprocess.proc_wait_timeout = 1 xprocess.ensure("server_workflow_test", Starter) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect(("localhost", port)) sock.sendall(bytes(data, "utf-8")) received = str(sock.recv(1024), "utf-8") assert received == data.upper() """ % (tcp_port, str(server_path)) ) result = testdir.runpytest() result.stdout.fnmatch_lines("*1 passed*") result = testdir.runpytest("--xshow") result.stdout.fnmatch_lines("*LIVE*") result = testdir.runpytest("--xkill") result.stdout.fnmatch_lines("*TERMINATED*") pytest-xprocess-0.18.1/tests/test_interruption_clean_up.py000066400000000000000000000034611410012575200241610ustar00rootroot00000000000000from pathlib import Path def test_interruption_cleanup(testdir, tcp_port): server_path = Path(__file__).parent.joinpath("server.py").absolute() testdir.makepyfile( """ import sys import socket from xprocess import ProcessStarter def test_servers_start(request, xprocess): port = %r server_path = %r class Starter(ProcessStarter): terminate_on_interrupt = True pattern = "started" args = [sys.executable, server_path, port] xprocess.ensure("server_test_interrupt", Starter) raise KeyboardInterrupt """ % (tcp_port, str(server_path)) ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines("*KeyboardInterrupt*") result = testdir.runpytest("--xshow") result.stdout.no_fnmatch_line("*LIVE*") def test_interruption_does_not_cleanup(testdir, tcp_port): server_path = Path(__file__).parent.joinpath("server.py").absolute() testdir.makepyfile( """ import sys import socket from xprocess import ProcessStarter def test_servers_start(request, xprocess): port = %r server_path = %r class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, port] xprocess.ensure("server_test_interrupt_no_terminate", Starter) raise KeyboardInterrupt """ % (tcp_port, str(server_path)) ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines("*KeyboardInterrupt*") result = testdir.runpytest("--xshow") result.stdout.fnmatch_lines("*LIVE*") result = testdir.runpytest("--xkill") result.stdout.fnmatch_lines("*TERMINATED*") pytest-xprocess-0.18.1/tests/test_process_initialization.py000066400000000000000000000070531410012575200243370ustar00rootroot00000000000000import socket import sys from pathlib import Path import pytest from xprocess import ProcessStarter server_path = Path(__file__).parent.joinpath("server.py").absolute() def request_response_cycle(tcp_port, data): """test started server instance by sending request and checking response""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect(("localhost", tcp_port)) sock.sendall(bytes(data, "utf-8")) received = str(sock.recv(1024), "utf-8") return received == data.upper() @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_servers_start(tcp_port, proc_name, xprocess): data = "bacon\n" class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port, "--no-children"] xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) assert request_response_cycle(tcp_port, data) assert info.isrunning() info.terminate() @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_ensure_not_restart(tcp_port, proc_name, xprocess): class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port, "--no-children"] proc_id = xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) assert xprocess.ensure(proc_name, Starter) == proc_id assert info.isrunning() info.terminate() @pytest.mark.parametrize( "proc_name,proc_pttrn,lines", [ ("s1", "started", 20), ("s2", "spam, bacon, eggs", 30), ("s3", "finally started", 62), ], ) def test_startup_detection_max_read_lines( tcp_port, proc_name, proc_pttrn, lines, xprocess ): data = "bacon\n" class Starter(ProcessStarter): pattern = proc_pttrn max_read_lines = lines args = [sys.executable, server_path, tcp_port, "--no-children"] xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) assert info.isrunning() assert request_response_cycle(tcp_port, data) info.terminate() @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_runtime_error_on_start_fail(tcp_port, proc_name, xprocess): restart = False class Starter(ProcessStarter): pattern = "I will not be matched!" args = [ sys.executable, server_path, tcp_port, "--no-children", "--ignore-sigterm", ] with pytest.raises(RuntimeError): xprocess.ensure(proc_name, Starter, restart) # since we made xprocess fail to start the server on purpose, we cannot # terminate it using XProcessInfo.terminate method once it does not # know the PID, process name or even that it is running, so we tell the # server to terminate itself. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect(("localhost", tcp_port)) sock.sendall(bytes("exit\n", "utf-8")) @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_popen_kwargs(tcp_port, proc_name, xprocess): class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port, "--no-children"] popen_kwargs = {"universal_newlines": True} xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) proc = xprocess._popen_instances[-1] if sys.version_info < (3, 7): text_mode = proc.universal_newlines else: text_mode = proc.text_mode assert info.isrunning() assert text_mode info.terminate() pytest-xprocess-0.18.1/tests/test_process_termination.py000066400000000000000000000060531410012575200236400ustar00rootroot00000000000000import sys from pathlib import Path import psutil import pytest from xprocess import ProcessStarter server_path = Path(__file__).parent.joinpath("server.py").absolute() @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_clean_shutdown(tcp_port, proc_name, xprocess): class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port] xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) assert info.isrunning() children = psutil.Process(info.pid).children() assert info.terminate() == 1 for child in children: assert not child.is_running() or child.status() == psutil.STATUS_ZOMBIE @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_terminate_no_pid(tcp_port, proc_name, xprocess): class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port] xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) pid, info.pid = info.pid, None # call terminate through XProcessInfo instance # with pid=None to test edge case assert info.terminate() == 0 info.pid = pid info.terminate() @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_terminate_only_parent(tcp_port, proc_name, xprocess): class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port] xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) children = psutil.Process(info.pid).children() assert info.terminate(kill_proc_tree=False) == 1 assert not info.isrunning() for p in children: try: p.terminate() except Exception: pass @pytest.mark.skipif( sys.platform == "win32", reason="on windows SIGTERM is treated as an alias for kill()", ) @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_sigkill_after_failed_sigterm(tcp_port, proc_name, xprocess): # explicitly tell xprocess_starter fixture to make # server instance ignore SIGTERM class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port, "--ignore-sigterm"] xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) # since terminate with sigterm will fail, set a lower # timeout before sending sigkill so tests won't take too long assert ( info.terminate(timeout=2) == 1 or psutil.Process(info.pid).status() == psutil.STATUS_ZOMBIE ) @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_return_value_on_failure(tcp_port, proc_name, xprocess): class Starter(ProcessStarter): pattern = "started" args = [sys.executable, server_path, tcp_port] xprocess.ensure(proc_name, Starter) info = xprocess.getinfo(proc_name) assert info.terminate(timeout=-1) == -1 try: # make sure hanging processes are not left behind psutil.Process(info.pid).terminate() except psutil.NoSuchProcess: pass pytest-xprocess-0.18.1/tests/test_resource_cleanup.py000066400000000000000000000001501410012575200230770ustar00rootroot00000000000000def test_0(example): pass def test_1(example): pass def test_2(example): pass pytest-xprocess-0.18.1/tests/test_startup_timeout.py000066400000000000000000000020031410012575200230100ustar00rootroot00000000000000import socket import sys import time from pathlib import Path import pytest from xprocess import ProcessStarter server_path = Path(__file__).parent.joinpath("server.py").absolute() def cleanup_server_instance(tcp_port): sock = socket.socket() sock.connect(("localhost", tcp_port)) try: for _ in range(10): sock.sendall(b"exit\n") sock.recv(1) time.sleep(0.1) except ( BrokenPipeError, ConnectionAbortedError, ConnectionResetError, ): # Server is terminated pass sock.close() @pytest.mark.parametrize("proc_name", ["s1", "s2", "s3"]) def test_timeout_raise_exception(tcp_port, proc_name, xprocess, request): class Starter(ProcessStarter): timeout = 2 max_read_lines = 500 pattern = "will not match" args = [sys.executable, server_path, tcp_port, "--no-children"] with pytest.raises(TimeoutError): xprocess.ensure(proc_name, Starter) cleanup_server_instance(tcp_port) pytest-xprocess-0.18.1/tox.ini000066400000000000000000000015271410012575200163120ustar00rootroot00000000000000[tox] envlist=begin,py{35,36,37,38,39} [testenv:dev] commands = envdir = venv deps= pre-commit>=1.11.0 coverage tox basepython = python3.8 usedevelop = True [testenv:begin] commands = coverage erase [testenv] changedir=tests/ deps= coverage commands= coverage run -m pytest -v coverage report --omit="*/.tox/*,*/test_functional_workflow.py" --fail-under=90 usedevelop = True [testenv:linting] skip_install = True basepython = python3 deps = pre-commit>=1.11.0 commands = pre-commit run --all-files --show-diff-on-failure {posargs:} [testenv:release] changedir= decription = new release basepython = python3.8 skipsdist = True usedevelop = True passenv = * [testenv:docs] changedir = docs skipsdist = True usedevelop = True deps = sphinx commands = sphinx-build -b html source/ build/ [pytest] filterwarnings = error pytest-xprocess-0.18.1/xprocess.py000066400000000000000000000324041410012575200172150ustar00rootroot00000000000000import itertools import os import signal import sys import traceback from abc import ABC from abc import abstractmethod from datetime import datetime from datetime import timedelta from time import sleep import psutil from py import std class XProcessInfo: """Holds information of an active process instance represented by a XProcess Object and offers recursive termination functionality of said process tree.""" def __init__(self, path, name): self.name = name self._termination_signal = False self.controldir = path.ensure(name, dir=1) self.logpath = self.controldir.join("xprocess.log") self.pidpath = self.controldir.join("xprocess.PID") self.pid = int(self.pidpath.read()) if self.pidpath.check() else None def _signal_process(self, p, sig): try: p.send_signal(sig) except psutil.NoSuchProcess: pass def terminate(self, *, kill_proc_tree=True, timeout=20): """Recursively terminates process tree. Attempt graceful termination starting by leaves of process tree. A ─┐ │ ├─ B (child) ─┐ │ └─ X (grandchild) ─┐ │ └─ Y (great grandchild) ├─ C (child) └─ D (child) 1. kill_list = [A, B, X, Y, C, D] 2. reversed(kill_list) = [D, C, Y, X, B, A] 3. terminated reversed kill_list This is the default behavior unless explicitly disabled by setting kill_proc_tree keyword-only parameter to false when calling ``XProcessInfo.terminate``. :param kill_proc_tree: Enable/disable recursive process tree termination. Defaults to True. :param timeout: Maximum time in seconds to wait on process termination. When timeout is reached after sending SIGTERM, this method will attempt to SIGKILL the process and return ``-1`` in case the operation times out again. return codes: 0 no work to do 1 terminated -1 failed to terminate""" if not self.pid: return 0 try: parent = psutil.Process(self.pid) except psutil.NoSuchProcess: return 0 try: kill_list = [parent] if kill_proc_tree: kill_list += parent.children(recursive=True) # attempt graceful termination first for p in reversed(kill_list): self._signal_process(p, signal.SIGTERM) _, alive = psutil.wait_procs(kill_list, timeout=timeout) # forcefuly terminate procs still running for p in alive: self._signal_process(p, signal.SIGKILL) _, alive = psutil.wait_procs(kill_list, timeout=timeout) # even if termination itself fails, # the signal has been sent to the process self._termination_signal = True if alive: # pragma: no cover print("could not terminated process {}".format(alive)) return -1 except (psutil.Error, ValueError) as err: print("Error while terminating process {}".format(err)) return -1 else: return 1 def isrunning(self, ignore_zombies=True): """Returns whether the process is running or not. @param ignore_zombies: Treat zombie processes as terminated. Sometimes a process that terminates itself during test execution will become a zombie process during pytest's lifetime. @return: ``True`` if the process is running, ``False`` if it is not.""" if self.pid is None: return False try: proc = psutil.Process(self.pid) except psutil.NoSuchProcess: return False return proc.is_running() and ( not ignore_zombies or proc.status() != psutil.STATUS_ZOMBIE ) class XProcess: """Main xprocess class. Represents a running process instance for which a set of actions is offered, such as process startup, command line actions and information fetching.""" def __init__(self, config, rootdir, log=None, proc_wait_timeout=60): self.config = config self.rootdir = rootdir self.proc_wait_timeout = proc_wait_timeout # these will be used to keep all necessary # references for proper cleanup before exiting self._info_objects = [] self._file_handles = [] self._popen_instances = [] class Log: def debug(self, msg, *args): if args: print(msg % args) else: print(msg) self.log = log or Log() def __enter__(self): return self def __exit__(self, exc_type, exc_value, tb): if exc_type is not None: traceback.print_exception(exc_type, exc_value, tb) def getinfo(self, name): """Return Process Info for the given external process.""" return XProcessInfo(self.rootdir, name) def ensure(self, name, preparefunc, restart=False): """Returns (PID, logfile) from a newly started or already running process. @param name: name of the external process, used for caching info across test runs. @param preparefunc: A subclass of ProcessStarter. @param restart: force restarting the process if it is running. @return: (PID, logfile) logfile will be seeked to the end if the server was running, otherwise seeked to the line after where the waitpattern matched.""" from subprocess import Popen, STDOUT info = self.getinfo(name) if not restart and not info.isrunning(): restart = True if restart: # ensure the process is terminated first if info.pid is not None: info.terminate() controldir = info.controldir.ensure(dir=1) starter = preparefunc(controldir, self) args = [str(x) for x in starter.args] self.log.debug("%s$ %s", controldir, " ".join(args)) stdout = open(str(info.logpath), "wb", 0) # is env still necessary? we could pass all in popen_kwargs kwargs = {"env": starter.env} popen_kwargs = { "cwd": str(controldir), "stdout": stdout, "stderr": STDOUT, # this gives the user the ability to # override the previous keywords if # desired **starter.popen_kwargs, } if sys.platform == "win32": # pragma: no cover kwargs["startupinfo"] = sinfo = std.subprocess.STARTUPINFO() sinfo.dwFlags |= std.subprocess.STARTF_USESHOWWINDOW sinfo.wShowWindow |= std.subprocess.SW_HIDE else: kwargs["close_fds"] = True kwargs["preexec_fn"] = os.setpgrp # no CONTROL-C # keep references of all popen # and info objects for cleanup self._info_objects.append((info, starter.terminate_on_interrupt)) self._popen_instances.append(Popen(args, **popen_kwargs, **kwargs)) info.pid = pid = self._popen_instances[-1].pid info.pidpath.write(str(pid)) self.log.debug("process %r started pid=%s", name, pid) stdout.close() # keep track of all file handles so we can # cleanup later during teardown phase self._file_handles.append(info.logpath.open()) if not restart: self._file_handles[-1].seek(0, 2) else: if not starter.wait(self._file_handles[-1]): raise RuntimeError( "Could not start process {}, the specified " "log pattern was not found within {} lines.".format( name, starter.max_read_lines ) ) self.log.debug("%s process startup detected", name) pytest_extlogfiles = self.config.__dict__.setdefault("_extlogfiles", {}) pytest_extlogfiles[name] = self._file_handles[-1] self.getinfo(name) return info.pid, info.logpath def _infos(self): return (self.getinfo(p.basename) for p in self.rootdir.listdir()) def _xkill(self, tw): ret = 0 for info in self._infos(): termret = info.terminate() ret = ret or (termret == 1) status = { 1: "TERMINATED", -1: "FAILED TO TERMINATE", 0: "NO PROCESS FOUND", }[termret] tmpl = "{info.pid} {info.name} {status}" tw.line(tmpl.format(**locals())) return ret def _xshow(self, tw): for info in self._infos(): running = "LIVE" if info.isrunning() else "DEAD" tmpl = "{info.pid} {info.name} {running} {info.logpath}" tw.line(tmpl.format(**locals())) return 0 def _clean_up_resources(self): # file handles should always be closed # in order to avoid ResourceWarnings for f in self._file_handles: f.close() # XProcessInfo objects and Popen objects have # a one to one relation, so we should wait on # procs exit status if termination signal has # been isued for that particular XProcessInfo # Object (subprocess requirement) for (info, _), proc in zip(self._info_objects, self._popen_instances): if info._termination_signal: proc.wait(self.proc_wait_timeout) class ProcessStarter(ABC): """Describes the characteristics of a process to start and, waiting for a process to achieve a started state. @cvar env: The environment in which to invoke the process. @cvar env: A dictionary containing keyword arguments to be passed to the Popen constructor. @cvar timeout: The maximum time ProcessStarter.wait will hang waiting for a new line when trying to match pattern before raising TimeoutError. @cvar max_read_lines: The maximum amount of lines of the log that will be read before presuming the attached process dead. @cvar terminate_on_interrupt: When set to True, xprocess will attempt to terminate and clean-up the resources of started processes upon interruption during the test run (e.g. SIGINT, CTRL+C or internal errors).""" env = None timeout = 120 popen_kwargs = {} max_read_lines = 50 terminate_on_interrupt = False def __init__(self, control_dir, process): self.control_dir = control_dir self.process = process @property @abstractmethod def args(self): "The args to start the process." @property @abstractmethod def pattern(self): "The pattern to match when the process has started." def startup_check(self): """Used to assert process responsiveness after pattern match""" return True def wait_callback(self): """Assert that process is ready to answer queries using provided callback funtion. Will raise TimeoutError if self.callback does not return True before self.timeout seconds""" while True: sleep(0.1) if self.startup_check(): return True if datetime.now() > self._max_time: raise TimeoutError( "The provided startup callback could not assert process\ responsiveness within the specified time interval of {} \ seconds".format( self.timeout ) ) def wait(self, log_file): """Wait until the pattern is mached and callback returns successful.""" self._max_time = datetime.now() + timedelta(seconds=self.timeout) lines = map(self.log_line, self.filter_lines(self.get_lines(log_file))) has_match = any(std.re.search(self.pattern, line) for line in lines) process_ready = self.wait_callback() return has_match and process_ready def filter_lines(self, lines): """fetch first , ignoring blank lines.""" non_empty_lines = (x for x in lines if x.strip()) return itertools.islice(non_empty_lines, self.max_read_lines) def log_line(self, line): """Write line to process log file.""" self.process.log.debug(line) return line def get_lines(self, log_file): """Read and yield one line at a time from log_file. Will raise TimeoutError if pattern is not matched before self.timeout seconds.""" while True: line = log_file.readline() if not line: std.time.sleep(0.1) if datetime.now() > self._max_time: raise TimeoutError( "The provided start pattern {} could not be matched \ within the specified time interval of {} seconds".format( self.pattern, self.timeout ) ) yield line