├── .coveragerc ├── tests ├── __init__.py ├── conftest.py └── test_plugin.py ├── requirements-testing.txt ├── pytest_services ├── __init__.py ├── plugin.py ├── memcached.py ├── process.py ├── log.py ├── xvfb.py ├── service.py ├── folders.py ├── django_settings.py ├── gui.py.orig ├── mysql.py └── locks.py ├── pyproject.toml ├── .readthedocs.yaml ├── docs ├── index.rst ├── api │ └── index.rst ├── Makefile └── conf.py ├── tox.ini ├── RELEASING.rst ├── AUTHORS.rst ├── .gitignore ├── LICENSE.txt ├── .github └── workflows │ ├── test.yml │ └── deploy.yml ├── setup.py ├── CHANGES.rst └── README.rst /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = tests/* 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """pytest-services tests.""" 2 | -------------------------------------------------------------------------------- /requirements-testing.txt: -------------------------------------------------------------------------------- 1 | mysqlclient 2 | pylibmc 3 | astroid 4 | -------------------------------------------------------------------------------- /pytest_services/__init__.py: -------------------------------------------------------------------------------- 1 | """pytest-services package.""" 2 | __version__ = '2.2.1' 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/stable/config-file/v2.html 2 | version: 2 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | sphinx: 8 | configuration: docs/conf.py 9 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Configuration for pytest runner.""" 2 | 3 | import pytest 4 | 5 | pytest_plugins = 'pytester' 6 | 7 | 8 | @pytest.fixture(scope='session') 9 | def run_services(): 10 | """Run services for tests.""" 11 | return True 12 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pytest-services's documentation! 2 | =========================================== 3 | 4 | .. contents:: 5 | 6 | .. include:: ../README.rst 7 | 8 | .. include:: api/index.rst 9 | 10 | .. include:: ../AUTHORS.rst 11 | 12 | .. include:: ../CHANGES.rst 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{39,310,311,312,313,314} 4 | py-xdist 5 | 6 | [testenv] 7 | commands= 8 | pytest {posargs} 9 | xdist: pytest -n2 {posargs} 10 | deps = 11 | xdist: pytest-xdist 12 | -r{toxinidir}/requirements-testing.txt 13 | extras = memcache 14 | passenv = USER 15 | 16 | [pytest] 17 | addopts = -v -ra 18 | -------------------------------------------------------------------------------- /RELEASING.rst: -------------------------------------------------------------------------------- 1 | Here are the steps on how to make a new release. 2 | 3 | 1. Create a ``release-VERSION`` branch from ``upstream/main``. 4 | 2. Update ``CHANGELOG.rst``. 5 | 3. Push the branch to ``upstream``. 6 | 4. Once all tests pass, start the ``deploy`` workflow manually or via: 7 | 8 | ``` 9 | gh workflow run deploy.yml --repo pytest-dev/pytest-services --ref release-VERSION -f version=VERSION 10 | ``` 11 | 12 | 5. Merge the PR. -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | `Anatoly Bubenkov `_ 5 | idea and implementation 6 | 7 | These people have contributed to `pytest-services`, in alphabetical order: 8 | 9 | * `Alessio Bogon `_ 10 | * `Dmitrijs Milajevs `_ 11 | * `Jason R. Coombs `_ 12 | * `Joep van Dijken `_ 13 | * `Magnus Staberg `_ 14 | * `Michael Shriver `_ 15 | * `Oleg Pidsadnyi `_ 16 | * `Zac Hatfield-Dodds `_ 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | *.log 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | .cache 22 | MANIFEST 23 | *.tar.gz 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | nosetests.xml 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Mr Developer 37 | .mr.developer.cfg 38 | .project 39 | .pydevproject 40 | 41 | # Sublime 42 | /*.sublime-* 43 | 44 | # virtualenv 45 | /.Python 46 | /lib 47 | /include 48 | /src 49 | /.env 50 | /.eggs 51 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | Internal API 2 | ============ 3 | 4 | .. automodule:: pytest_services.plugin 5 | :members: 6 | 7 | .. automodule:: pytest_services.memcached 8 | :members: 9 | 10 | .. automodule:: pytest_services.mysql 11 | :members: 12 | 13 | .. automodule:: pytest_services.xvfb 14 | :members: 15 | 16 | .. automodule:: pytest_services.service 17 | :members: 18 | 19 | .. automodule:: pytest_services.cleanup 20 | :members: 21 | 22 | .. automodule:: pytest_services.folders 23 | :members: 24 | 25 | .. automodule:: pytest_services.locks 26 | :members: 27 | 28 | .. automodule:: pytest_services.log 29 | :members: 30 | 31 | .. automodule:: pytest_services.django_settings 32 | :members: 33 | -------------------------------------------------------------------------------- /pytest_services/plugin.py: -------------------------------------------------------------------------------- 1 | """Services plugin for pytest. 2 | 3 | Provides an easy way of running service processes for your tests. 4 | """ 5 | 6 | from .folders import * # NOQA 7 | from .log import * # NOQA 8 | from .locks import * # NOQA 9 | from .xvfb import * # NOQA 10 | from .memcached import * # NOQA 11 | from .mysql import * # NOQA 12 | from .service import * # NOQA 13 | 14 | 15 | def pytest_addoption(parser): 16 | """Add options for services plugin.""" 17 | group = parser.getgroup("services", "service processes for tests") 18 | group._addoption( 19 | '--run-services', 20 | action="store_true", dest="run_services", 21 | default=False, 22 | help="Run services automatically by pytest") 23 | group._addoption( 24 | '--xvfb-display', 25 | action="store", dest="display", 26 | default=None, 27 | help="X display to use") 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Anatoly Bubenkov, Paylogic International and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | """Tests for pytest-services plugin.""" 2 | import os.path 3 | import socket 4 | 5 | 6 | def test_memcached(request, memcached, memcached_socket): 7 | """Test memcached service.""" 8 | import pylibmc 9 | 10 | mc = pylibmc.Client([memcached_socket]) 11 | mc.set('some', 1) 12 | assert mc.get('some') == 1 13 | 14 | # check memcached cleaner 15 | request.getfixturevalue('memcached_clean') 16 | assert mc.get('some') is None 17 | 18 | 19 | def test_mysql(mysql, mysql_connection, mysql_socket): 20 | """Test mysql service.""" 21 | import MySQLdb 22 | 23 | conn = MySQLdb.connect(user='root', unix_socket=mysql_socket) 24 | assert conn 25 | 26 | 27 | def test_xvfb(xvfb, xvfb_display): 28 | """Test xvfb service.""" 29 | socket.create_connection(('127.0.0.1', 6000 + xvfb_display)) 30 | 31 | 32 | def test_port_getter(port_getter): 33 | """Test port getter utility.""" 34 | port1 = port_getter() 35 | sock1 = socket.socket(socket.AF_INET) 36 | sock1.bind(('127.0.0.1', port1)) 37 | assert port1 38 | port2 = port_getter() 39 | sock2 = socket.socket(socket.AF_INET) 40 | sock2.bind(('127.0.0.1', port2)) 41 | assert port2 42 | assert port1 != port2 43 | 44 | 45 | def test_display_getter(display_getter): 46 | """Test display getter utility.""" 47 | display1 = display_getter() 48 | assert display1 49 | display2 = display_getter() 50 | assert display2 51 | assert display1 != display2 52 | 53 | 54 | def test_temp_dir(temp_dir): 55 | """Test temp dir directory.""" 56 | assert os.path.isdir(temp_dir) 57 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - "test-me-*" 8 | 9 | pull_request: 10 | 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | 18 | package: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | permissions: 23 | id-token: write 24 | attestations: write 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Build and Check Package 29 | uses: hynek/build-and-inspect-python-package@v2.13.0 30 | 31 | test: 32 | needs: [package] 33 | 34 | runs-on: ubuntu-latest 35 | 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Download Package 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: Packages 48 | path: dist 49 | 50 | - name: Set up Python 51 | uses: actions/setup-python@v5 52 | with: 53 | python-version: ${{ matrix.python }} 54 | allow-prereleases: true 55 | 56 | - name: System dependencies 57 | run: | 58 | sudo apt-get update 59 | sudo apt-get install xvfb python3-dev libmemcached-dev libmysqlclient-dev memcached libmemcached-tools 60 | 61 | - name: Install tox 62 | run: | 63 | python -m pip install --upgrade pip 64 | pip install tox 65 | 66 | - name: Test 67 | shell: bash 68 | run: | 69 | tox run -e py --installpkg `find dist/*.tar.gz` 70 | -------------------------------------------------------------------------------- /pytest_services/memcached.py: -------------------------------------------------------------------------------- 1 | """Fixtures for memcache.""" 2 | import os 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope='session') 7 | def memcached_socket(run_dir, run_services): 8 | """The memcached socket location.""" 9 | if run_services: 10 | return os.path.join(run_dir, 'memcached.sock') 11 | 12 | 13 | @pytest.fixture(scope='session') 14 | def memcached(request, run_services, memcached_socket, watcher_getter): 15 | """The memcached instance which is ready to be used by the tests.""" 16 | if run_services: 17 | return watcher_getter( 18 | name='memcached', 19 | arguments=['-s', memcached_socket], 20 | checker=lambda: os.path.exists(memcached_socket), 21 | request=request, 22 | ) 23 | 24 | 25 | @pytest.fixture(scope='session') 26 | def memcached_connection(run_services, memcached_socket): 27 | """The connection string to the local memcached instance.""" 28 | if run_services: 29 | return 'unix:{0}'.format(memcached_socket) 30 | 31 | 32 | @pytest.fixture 33 | def do_memcached_clean(run_services): 34 | """Determine whether memcached should be clean on the start of every test.""" 35 | return run_services 36 | 37 | 38 | @pytest.fixture(scope='session') 39 | def memcached_client(memcached_socket, memcached): 40 | """Create client for memcached.""" 41 | mc = pytest.importorskip('pylibmc') 42 | return mc.Client([memcached_socket]) 43 | 44 | 45 | @pytest.fixture 46 | def memcached_clean(request, memcached_client, do_memcached_clean): 47 | """Clean memcached instance.""" 48 | if do_memcached_clean: 49 | memcached_client.flush_all() 50 | -------------------------------------------------------------------------------- /pytest_services/process.py: -------------------------------------------------------------------------------- 1 | """Subprocess related functions.""" 2 | try: 3 | import subprocess32 as subprocess 4 | except ImportError: 5 | import subprocess 6 | 7 | 8 | def check_output(*popenargs, **kwargs): 9 | """Run command with arguments and return its output (both stdout and stderr) as a byte string. 10 | 11 | If the exit code was non-zero it raises a CalledProcessWithOutputError. 12 | """ 13 | if 'stdout' in kwargs: 14 | raise ValueError('stdout argument not allowed, it will be overridden.') 15 | 16 | if 'stderr' in kwargs: 17 | raise ValueError('stderr argument not allowed, it will be overridden.') 18 | 19 | process = subprocess.Popen( 20 | stdout=subprocess.PIPE, 21 | stderr=subprocess.PIPE, 22 | *popenargs, **kwargs) 23 | output, err = process.communicate() 24 | retcode = process.poll() 25 | if retcode: 26 | cmd = kwargs.get("args") 27 | if cmd is None: 28 | cmd = popenargs[0] 29 | raise CalledProcessWithOutputError(retcode, cmd, output, err) 30 | 31 | return output, err 32 | 33 | 34 | class CalledProcessWithOutputError(subprocess.CalledProcessError): 35 | 36 | """An exception with the steout and stderr of a failed subprocess32.""" 37 | 38 | def __init__(self, returncode, cmd, output, err): 39 | """Assign output and error.""" 40 | super(CalledProcessWithOutputError, self).__init__(returncode, cmd) 41 | 42 | self.output = output 43 | self.err = err 44 | 45 | def __str__(self): 46 | """str representation.""" 47 | return super(CalledProcessWithOutputError, self).__str__() + ' with output: {0} and error: {1}'.format( 48 | self.output, self.err) 49 | -------------------------------------------------------------------------------- /pytest_services/log.py: -------------------------------------------------------------------------------- 1 | """Logging fixtures and functions.""" 2 | import contextlib 3 | import logging 4 | import logging.handlers 5 | import socket 6 | 7 | import pytest 8 | 9 | 10 | @pytest.fixture(scope='session') 11 | def services_log(worker_id): 12 | """A services_logger with the worker id.""" 13 | handler = None 14 | for kwargs in (dict(socktype=socket.SOCK_RAW), dict(socktype=socket.SOCK_STREAM), dict()): 15 | try: 16 | handler = logging.handlers.SysLogHandler( 17 | facility=logging.handlers.SysLogHandler.LOG_LOCAL7, address='/dev/log', **kwargs) 18 | break 19 | except (IOError, TypeError): 20 | pass 21 | logger = logging.getLogger('[{worker_id}] {name}'.format(name=__name__, worker_id=worker_id)) 22 | logger.setLevel(logging.DEBUG) 23 | if handler and workaround_issue_20(handler): 24 | logger.propagate = 0 25 | logger.addHandler(handler) 26 | return logger 27 | 28 | 29 | def workaround_issue_20(handler): 30 | """ 31 | Workaround for 32 | https://github.com/pytest-dev/pytest-services/issues/20, 33 | disabling installation of a broken handler. 34 | """ 35 | return hasattr(handler, 'socket') 36 | 37 | 38 | @contextlib.contextmanager 39 | def dont_capture(request): 40 | """Suspend capturing of stdout by pytest.""" 41 | capman = request.config.pluginmanager.getplugin("capturemanager") 42 | capman.suspendcapture() 43 | try: 44 | yield 45 | finally: 46 | capman.resumecapture() 47 | 48 | 49 | def remove_handlers(): 50 | """Remove root services_logging handlers.""" 51 | handlers = [] 52 | for handler in logging.root.handlers: 53 | if not isinstance(handler, logging.StreamHandler): 54 | handlers.append(handler) 55 | logging.root.handlers = handlers 56 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version' 8 | required: true 9 | default: '1.2.3' 10 | 11 | jobs: 12 | 13 | package: 14 | runs-on: ubuntu-latest 15 | # Required by attest-build-provenance-github. 16 | permissions: 17 | id-token: write 18 | attestations: write 19 | env: 20 | SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Build and Check Package 26 | uses: hynek/build-and-inspect-python-package@v2.13.0 27 | with: 28 | attest-build-provenance-github: 'true' 29 | 30 | 31 | deploy: 32 | needs: package 33 | runs-on: ubuntu-latest 34 | environment: PyPI 35 | permissions: 36 | id-token: write # For PyPI trusted publishers. 37 | contents: write # For tag and release notes. 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - name: Download Package 43 | uses: actions/download-artifact@v4 44 | with: 45 | name: Packages 46 | path: dist 47 | 48 | - name: Publish package to PyPI 49 | uses: pypa/gh-action-pypi-publish@v1.13.0 50 | with: 51 | attestations: true 52 | 53 | - name: Push tag 54 | run: | 55 | git config user.name "pytest bot" 56 | git config user.email "pytestbot@gmail.com" 57 | git tag --annotate --message=v${{ github.event.inputs.version }} v${{ github.event.inputs.version }} ${{ github.sha }} 58 | git push origin v${{ github.event.inputs.version }} 59 | 60 | - name: GitHub Release 61 | uses: softprops/action-gh-release@v2 62 | with: 63 | files: dist/* 64 | tag_name: v${{ github.event.inputs.version }} -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setuptools entry point.""" 2 | from setuptools import setup 3 | from pathlib import Path 4 | 5 | dirname = Path(__file__).parent 6 | 7 | long_description = ( 8 | dirname.joinpath('README.rst').read_text(encoding="UTF-8") + '\n' + 9 | dirname.joinpath('AUTHORS.rst').read_text(encoding="UTF-8") + '\n' + 10 | dirname.joinpath('CHANGES.rst').read_text(encoding="UTF-8") 11 | ) 12 | 13 | install_requires = [ 14 | 'requests', 15 | 'psutil', 16 | 'pytest', 17 | 'zc.lockfile >= 2.0', 18 | ] 19 | 20 | 21 | setup( 22 | name='pytest-services', 23 | description='Services plugin for pytest testing framework', 24 | long_description=long_description, 25 | long_description_content_type="text/x-rst", 26 | author='Anatoly Bubenkov, Paylogic International and others', 27 | license='MIT', 28 | author_email='bubenkoff@gmail.com', 29 | url='https://github.com/pytest-dev/pytest-services', 30 | extras={ 31 | 'memcached': ['pylibmc'], 32 | }, 33 | python_requires=">=3.9", 34 | install_requires=install_requires, 35 | classifiers=[ 36 | 'Development Status :: 6 - Mature', 37 | 'Intended Audience :: Developers', 38 | 'Operating System :: POSIX', 39 | 'Operating System :: Microsoft :: Windows', 40 | 'Operating System :: MacOS :: MacOS X', 41 | 'Topic :: Software Development :: Testing', 42 | 'Topic :: Software Development :: Libraries', 43 | 'Topic :: Utilities', 44 | 'Programming Language :: Python :: 2', 45 | 'Programming Language :: Python :: 3', 46 | 'Programming Language :: Python :: 3.9', 47 | 'Programming Language :: Python :: 3.10', 48 | 'Programming Language :: Python :: 3.11', 49 | 'Programming Language :: Python :: 3.12', 50 | 'Programming Language :: Python :: 3.13', 51 | 'Programming Language :: Python :: 3.14', 52 | ], 53 | tests_require=['tox'], 54 | entry_points={'pytest11': [ 55 | 'pytest-services=pytest_services.plugin', 56 | ]}, 57 | packages=['pytest_services'], 58 | ) 59 | -------------------------------------------------------------------------------- /pytest_services/xvfb.py: -------------------------------------------------------------------------------- 1 | """Fixtures for the GUI environment.""" 2 | import os 3 | import socket 4 | import re 5 | try: 6 | import subprocess32 as subprocess 7 | except ImportError: # pragma: no cover 8 | import subprocess 9 | 10 | import pytest 11 | 12 | from .locks import ( 13 | file_lock, 14 | ) 15 | 16 | 17 | def xvfb_supports_listen(): 18 | """Determine whether the '-listen' option is supported by Xvfb.""" 19 | p = subprocess.Popen( 20 | ['Xvfb', '-listen', 'TCP', '-__sentinel_parameter__'], 21 | stdout=subprocess.PIPE, 22 | stderr=subprocess.PIPE, 23 | ) 24 | p.wait() 25 | _, stderr = p.communicate() 26 | 27 | match = re.search( 28 | br'^Unrecognized option: (?P