├── .coveragerc ├── .gitignore ├── .readthedocs.yaml ├── .travis.yml ├── AUTHORS.rst ├── CHANGES.rst ├── DEPENDENCIES ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── api │ └── index.rst ├── conf.py └── index.rst ├── pyproject.toml ├── pytest_services ├── __init__.py ├── django_settings.py ├── folders.py ├── gui.py.orig ├── locks.py ├── log.py ├── memcached.py ├── mysql.py ├── plugin.py ├── process.py ├── service.py └── xvfb.py ├── requirements-testing.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py └── test_plugin.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = tests/* 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: "2.7" 4 | 5 | matrix: 6 | include: 7 | - env: TOXENV=linters 8 | - env: TOXENV=py27-pytest39 9 | - env: TOXENV=py27-pytest310 10 | - env: TOXENV=py27-pytestlatest 11 | - env: TOXENV=py27-pytestlatest-xdist 12 | - env: TOXENV=py34-pytestlatest 13 | python: "3.4" 14 | - env: TOXENV=py35-pytestlatest 15 | python: "3.5" 16 | - env: TOXENV=py36-pytestlatest 17 | python: "3.6" 18 | - env: TOXENV=py27-pytestlatest-coveralls 19 | install: pip install tox 20 | # mysql_install_db will not work if it can't find the my-default.cnf file 21 | before_script: sudo cp /etc/mysql/my.cnf /usr/share/mysql/my-default.cnf 22 | script: tox 23 | branches: 24 | except: 25 | - /^\d/ 26 | notifications: 27 | email: 28 | - bubenkoff@gmail.com 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 2.2.1 5 | ----- 6 | 7 | - #42: Retry on ``zc.lockfile.LockError`` in ``file_lock``, use existing timeout kwarg (mshriver) 8 | 9 | 2.2.0 10 | ----- 11 | 12 | - #38: Retry to lock resource if zc.lockfile.LockError is raised. Fix needed for pytest-xdist. (StabbarN) 13 | 14 | 2.1.0 15 | ----- 16 | 17 | - #34: Deprecated ``slave_id`` fixture in favor of ``worker_id``, 18 | for compatibility with ``pytest-xdist`` 2. 19 | 20 | 2.0.1 21 | ----- 22 | 23 | - #20: Added workaround for issue with SysLogHandler. 24 | 25 | 2.0.0 26 | ----- 27 | 28 | - #23: Rely on ``zc.lockfile`` for lockfile behavior. 29 | - #28: Fixtures now supports later versions of mysql and no longer 30 | support versions of mysql prior to ``mysql --initialize`` support. 31 | - #29: Fix issues with later versions of mysql where ``mysql_defaults_file`` 32 | fixture would prevent startup of mysql. 33 | - Fixed issue in test suite where mysql fixture was not tested. 34 | - Removed ``pytest_services.locks.lock_file``. 35 | 36 | 1.3.1 37 | ----- 38 | 39 | - Fix race condition causing when using port_getter/display_getter (youtux) 40 | 41 | 42 | 1.3.0 43 | ----- 44 | 45 | - Add `request` param to watcher_getter to have proper execution order 46 | of finalizers (youtux). 47 | 48 | 1.2.1 49 | ----- 50 | 51 | - Swap kill and terminate in watcher_getter finalization, allowing 52 | for a more polite SIGTERM for terminating child procs on Unix. See 53 | #15 for details (jaraco) 54 | 55 | 1.2.0 56 | ----- 57 | 58 | - Make pylibmc an optional dependency, available as an extra (jaraco) 59 | 60 | 1.1.15 61 | ------ 62 | 63 | - Fixed hang with updated netcat-openbsd>=1.130.3 (joepvandijken) 64 | 65 | 1.1.14 66 | ------ 67 | 68 | - Use a different strategy to determine whether xvfb supports (youtux ) 69 | 70 | 1.1.12 71 | ------ 72 | 73 | - use realpath for mysql base dir (bubenkoff) 74 | 75 | 1.1.11 76 | ------ 77 | 78 | - exclude locked displays for xvfb (bubenkoff) 79 | 80 | 1.1.7 81 | ----- 82 | 83 | - django settings fix (olegpidsadnyi) 84 | 85 | 1.1.3 86 | ----- 87 | 88 | - django 1.8 support (bubenkoff) 89 | 90 | 1.1.2 91 | ----- 92 | 93 | - old django support fix (olegpidsadnyi) 94 | 95 | 1.1.0 96 | ----- 97 | 98 | - django 1.7+ support (bubenkoff) 99 | 100 | 1.0.10 101 | ------ 102 | 103 | - removed auto artifacts cleanup (bubenkoff) 104 | 105 | 1.0.8 106 | ----- 107 | 108 | - fixed popen arguments (bubenkoff) 109 | 110 | 1.0.2 111 | ----- 112 | 113 | - added port and display getters (bubenkoff) 114 | 115 | 1.0.1 116 | ----- 117 | 118 | - Improved documentation (bubenkoff) 119 | 120 | 1.0.0 121 | ----- 122 | 123 | - Initial public release 124 | -------------------------------------------------------------------------------- /DEPENDENCIES: -------------------------------------------------------------------------------- 1 | # debian dependencies 2 | libmemcached-dev 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include setup.py 3 | include requirements-testing.txt 4 | include tox.ini 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # create virtual environment 2 | PATH := .env/bin:$(PATH) 3 | 4 | .env: 5 | virtualenv .env 6 | 7 | # install all needed for development 8 | develop: .env 9 | pip install -e . -r requirements-testing.txt tox coveralls 10 | 11 | coverage: develop 12 | coverage run --source=pytest_services .env/bin/py.test tests 13 | coverage report -m 14 | 15 | test: develop 16 | tox 17 | 18 | coveralls: coverage 19 | coveralls 20 | 21 | # clean the development envrironment 22 | clean: 23 | -rm -rf .env 24 | 25 | # debian dependencies 26 | dependencies: 27 | sudo apt-get install `grep -vh '#' DEPENDENCIES*` 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Services plugin for pytest testing framework 2 | ============================================ 3 | 4 | .. image:: https://img.shields.io/pypi/v/pytest-services.svg 5 | :target: https://pypi.python.org/pypi/pytest-services 6 | .. image:: https://img.shields.io/pypi/pyversions/pytest-services.svg 7 | :target: https://pypi.python.org/pypi/pytest-services 8 | .. image:: https://img.shields.io/coveralls/pytest-dev/pytest-services/master.svg 9 | :target: https://coveralls.io/r/pytest-dev/pytest-services 10 | .. image:: https://travis-ci.org/pytest-dev/pytest-services.svg?branch=master 11 | :target: https://travis-ci.org/pytest-dev/pytest-services 12 | .. image:: https://readthedocs.org/projects/pytest-services/badge/?version=latest 13 | :target: https://readthedocs.org/projects/pytest-services/?badge=latest 14 | :alt: Documentation Status 15 | 16 | Install pytest-services 17 | ----------------------- 18 | 19 | :: 20 | 21 | pip install pytest-services 22 | 23 | 24 | .. _pytest: http://pytest.org 25 | .. _pytest-xdist: https://pypi.python.org/pypi/pytest-xdist 26 | .. _pytest-splinter: https://pypi.python.org/pypi/pytest-splinter 27 | .. _pytest-bdd: https://pypi.python.org/pypi/pytest-bdd 28 | .. _pytest-django: https://pypi.python.org/pypi/pytest-django 29 | .. _memcached: http://memcached.org 30 | .. _xvfb: http://en.wikipedia.org/wiki/Xvfb 31 | .. _mysql-server: http://dev.mysql.com/ 32 | 33 | Features 34 | -------- 35 | 36 | The plugin provides a set of fixtures and utility functions to start service processes for your tests with 37 | pytest_ 38 | 39 | 40 | Fixtures 41 | -------- 42 | 43 | * run_services 44 | Determines whether services should be run or not. False by default if not in distributed environment 45 | (without pytest-xdist_). Can be manually set to True by overriding this fixture in your test config or 46 | just by using `--run-services` command line argument (see below). 47 | 48 | Infrastructure fixtures 49 | *********************** 50 | 51 | * worker_id 52 | Id of the worker if tests are run using pytest-xdist_. It is set to `local` if tests are not run using 53 | pytest-xdist_ (with `--dist` command line option set to `load`). 54 | Has a deprecated alias ``slave_id`` which will be removed in a future version. 55 | * session_id 56 | Test session id. Globally unique, and of course also guaranteed to be different for potentially multiple test 57 | sessions running on same test node via pytest-xdist_. 58 | * watcher_getter 59 | Function to instantiate test service watcher (popen object). Includes automatic finalization (exiting) of the 60 | service process, and testing the service before returning the watcher from the function. 61 | Example of usage for memcached service: 62 | 63 | .. code-block:: python 64 | 65 | @pytest.fixture(scope='session') 66 | def memcached(request, run_services, memcached_socket, watcher_getter): 67 | """The memcached instance which is ready to be used by the tests.""" 68 | if run_services: 69 | return watcher_getter( 70 | name='memcached', 71 | arguments=['-s', memcached_socket], 72 | checker=lambda: os.path.exists(memcached_socket), 73 | # Needed for the correct execution order of finalizers 74 | request=request, 75 | ) 76 | 77 | * services_log 78 | Logger used for debug logging when managing test services. 79 | * root_dir 80 | Parent directory for test service artifacts (disk based). Set to `/tmp` by default. 81 | * base_dir 82 | Base directory for test service artifacts (disk based), unique subdirectory of `root_dir`. 83 | Automatically removed recursively at the end of the test session. 84 | * temp_dir 85 | `Temporary` directory (disk based), subfolder of the `base_dir`. 86 | Used for strictly temporary artifacts (for example - folder where files are uploaded from the user input). 87 | * memory_root_dir 88 | Parent directory for test service artifacts (memory based). Main idea of having memory base directory is to 89 | store performance-critical files there. For example - mysql service will use it to store database file, it speeds up 90 | mysql server a lot, especially database management operations. 91 | Set to `/var/shm` by default, with a fallback to 'root_dir`. Note that if apparmor is running on your system, most 92 | likely it will prevent your test service to use it (for example - mysql has it's apparmor profile). You you'll need 93 | to disable such profile in apparmor configuration. 94 | Example of disabling apparmor for mysqld: 95 | 96 | .. code-block:: sh 97 | 98 | sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/ 99 | sudo /etc/init.d/apparmor restart 100 | 101 | * memory_base_dir 102 | Base directory for test service artifacts (memory based), unique subdirectory of `memory_root_dir`. 103 | Automatically removed recursively at the end of the test session. 104 | * memory_temp_dir 105 | `Temporary` directory (memory based), subfolder of the `base_dir`. 106 | * lock_dir 107 | Lock files directory for storing locks created for resource assignment (ports, display, etc). Subfolder of 108 | `memory_root_dir`. 109 | * run_dir 110 | Process id and socket files directory (like system-wide `/var/run` but local for test session). Subfolder of 111 | `memory_root_dir`. 112 | * port_getter 113 | Function to get unallocated port. 114 | Automatically ensures locking and un-locking of it on application level via flock. 115 | * display_getter 116 | Function to get unallocated display. 117 | Automatically ensures locking and un-locking of it on application level via flock. 118 | * lock_resource_timeout 119 | Used in function lock_resource. 120 | A maximum of total sleep between attempts to lock resource. 121 | 122 | Service fixtures 123 | **************** 124 | 125 | * memcached 126 | Start memcached_ instance. 127 | Requires `pylibmc` installed or `memcache` indicated as an extra (`pip install 'pytest-services[memcached]'`). 128 | * memcached_socket 129 | Memcached unix socket file name to be used for connection. 130 | * memcached_connection 131 | Memcached connection string. 132 | * do_memcached_clean 133 | Determine if memcached should be cleared before every test run. Equals to `run_services` fixture by default. 134 | Requires `pylibmc` installed or `memcache` indicated as an extra (`pip install 'pytest-services[memcached]'`). 135 | * memcached_client 136 | A pylibmc.Client instance bound to the service. 137 | Requires `pylibmc` installed or `memcache` indicated as an extra (`pip install 'pytest-services[memcached]'`). 138 | * mysql 139 | Start mysql-server_ instance. 140 | * mysql_database_name 141 | MySQL database name to be created after initialization of the mysql service `system` database. 142 | * mysql_database_getter 143 | Function with single parameter - database name. To create additional database(s) for tests. 144 | Used in `mysql_database` fixture which is used by `mysql` one. 145 | * mysql_connection 146 | MySQL connection string. 147 | * xvfb 148 | Start xvfb_ instance. 149 | * xvfb_display 150 | Xvfb display to use for connection. 151 | * xvfb_resolution 152 | Xvfb display resolution to use. Tuple in form `(1366, 768, 8)`. 153 | 154 | Utility functions 155 | ***************** 156 | 157 | Django settings 158 | ^^^^^^^^^^^^^^^ 159 | 160 | In some cases, there's a need of switching django settings during test run, because several django projects are tested 161 | whithin the single test suite. 162 | `pytest_services.django_settings` simplifies switching of django settings to a single function call: 163 | 164 | * setup_django_settings 165 | Override the enviroment variable and call the _setup method of the settings object to reload them. 166 | 167 | Example of usage: 168 | 169 | conftest.py: 170 | 171 | .. code-block:: python 172 | 173 | from pytest_services import django_settings 174 | 175 | django_settings.clean_django_settings() 176 | django_settings.setup_django_settings('your.project.settings') 177 | 178 | Note that the nice project pytest-django_ doesn't help with the situation, as it's single django project oriented, as 179 | well as standard django testing technique. Single project approach works fine, as long as there are no fixtures to share 180 | between them, but when there are fixtures to share, then you can get benefit of joining several django projects tests 181 | into a single test run, because all session-scoped fixtures will be instantiated only once for all projects tests. 182 | The benefit is only visible if you have big enough test suite and your fixtures are heavy enough. 183 | 184 | 185 | Command-line options 186 | -------------------- 187 | 188 | * `--run-services` 189 | Force services to be run even if tests are executed in a non-distributed way (without pytest-xdist_). 190 | * `--xvfb-display` 191 | Skip xvfb service to run and use provided display. Useful when you need to run all services except the xvfb_ 192 | to debug your browser tests, if, for example you use pytest-splinter_ with or without pytest-bdd_. 193 | 194 | Example 195 | ------- 196 | 197 | test_your_test.py: 198 | 199 | .. code-block:: python 200 | 201 | import MySQLdb 202 | 203 | 204 | def test_some_mysql_stuff(mysql): 205 | """Test using mysql server.""" 206 | conn = MySQLdb.connect(user='root') 207 | 208 | 209 | Contact 210 | ------- 211 | 212 | If you have questions, bug reports, suggestions, etc. please create an issue on 213 | the `GitHub project page `_. 214 | 215 | 216 | License 217 | ------- 218 | 219 | This software is licensed under the `MIT license `_ 220 | 221 | See `License file `_ 222 | 223 | 224 | © 2014 Anatoly Bubenkov, Paylogic International and others. 225 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pytest-BDD.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pytest-BDD.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Pytest-BDD" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pytest-BDD" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Sphinx config.""" 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pytest-services documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Apr 7 21:07:56 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | 19 | import sys 20 | import os 21 | 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | import pytest_services 25 | 26 | # -- General configuration ----------------------------------------------------- 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | #needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be extensions 32 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 33 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = '.rst' 40 | 41 | # The encoding of source files. 42 | #source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'pytest-services' 49 | AUTHOR = 'Anatoly Bubenkov, Paylogic International and others' 50 | copyright = u'2015, ' + AUTHOR 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = pytest_services.__version__ 58 | # The full version, including alpha/beta/rc tags. 59 | release = pytest_services.__version__ 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = ['_build'] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | #modindex_common_prefix = [] 94 | 95 | 96 | # -- Options for HTML output --------------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | html_theme = 'default' 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | #html_theme_options = {} 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | #html_theme_path = [] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | #html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = ['_static'] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | #html_last_updated_fmt = '%b %d, %Y' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | #html_use_smartypants = True 138 | 139 | # Custom sidebar templates, maps document names to template names. 140 | #html_sidebars = {} 141 | 142 | # Additional templates that should be rendered to pages, maps page names to 143 | # template names. 144 | #html_additional_pages = {} 145 | 146 | # If false, no module index is generated. 147 | #html_domain_indices = True 148 | 149 | # If false, no index is generated. 150 | #html_use_index = True 151 | 152 | # If true, the index is split into individual pages for each letter. 153 | #html_split_index = False 154 | 155 | # If true, links to the reST sources are added to the pages. 156 | #html_show_sourcelink = True 157 | 158 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 159 | #html_show_sphinx = True 160 | 161 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 162 | #html_show_copyright = True 163 | 164 | # If true, an OpenSearch description file will be output, and all pages will 165 | # contain a tag referring to it. The value of this option must be the 166 | # base URL from which the finished HTML is served. 167 | #html_use_opensearch = '' 168 | 169 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 170 | #html_file_suffix = None 171 | 172 | # Output file base name for HTML help builder. 173 | htmlhelp_basename = 'pytest-services-doc' 174 | 175 | 176 | # -- Options for LaTeX output -------------------------------------------------- 177 | 178 | latex_elements = { 179 | # The paper size ('letterpaper' or 'a4paper'). 180 | #'papersize': 'letterpaper', 181 | 182 | # The font size ('10pt', '11pt' or '12pt'). 183 | #'pointsize': '10pt', 184 | 185 | # Additional stuff for the LaTeX preamble. 186 | #'preamble': '', 187 | } 188 | 189 | # Grouping the document tree into LaTeX files. List of tuples 190 | # (source start file, target name, title, author, documentclass [howto/manual]). 191 | latex_documents = [ 192 | ('index', 'pytest-services.tex', u'pytest-services Documentation', AUTHOR, 'manual'), 193 | ] 194 | 195 | # The name of an image file (relative to this directory) to place at the top of 196 | # the title page. 197 | #latex_logo = None 198 | 199 | # For "manual" documents, if this is true, then toplevel headings are parts, 200 | # not chapters. 201 | #latex_use_parts = False 202 | 203 | # If true, show page references after internal links. 204 | #latex_show_pagerefs = False 205 | 206 | # If true, show URL addresses after external links. 207 | #latex_show_urls = False 208 | 209 | # Documents to append as an appendix to all manuals. 210 | #latex_appendices = [] 211 | 212 | # If false, no module index is generated. 213 | #latex_domain_indices = True 214 | 215 | 216 | # -- Options for manual page output -------------------------------------------- 217 | 218 | # One entry per manual page. List of tuples 219 | # (source start file, name, description, authors, manual section). 220 | man_pages = [ 221 | ('index', 'pytest-services', u'pytest-services Documentation', 222 | [AUTHOR], 1) 223 | ] 224 | 225 | # If true, show URL addresses after external links. 226 | #man_show_urls = False 227 | 228 | 229 | # -- Options for Texinfo output ------------------------------------------------ 230 | 231 | # Grouping the document tree into Texinfo files. List of tuples 232 | # (source start file, target name, title, author, 233 | # dir menu entry, description, category) 234 | texinfo_documents = [ 235 | ('index', 'pytest-services', u'pytest-services Documentation', 236 | AUTHOR, 'pytest-services', 'Services plugin for pytest testing framework.', 237 | 'Miscellaneous'), 238 | ] 239 | 240 | # Documents to append as an appendix to all manuals. 241 | #texinfo_appendices = [] 242 | 243 | # If false, no module index is generated. 244 | #texinfo_domain_indices = True 245 | 246 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 247 | #texinfo_show_urls = 'footnote' 248 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4.1"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /pytest_services/__init__.py: -------------------------------------------------------------------------------- 1 | """pytest-services package.""" 2 | __version__ = '2.2.1' 3 | -------------------------------------------------------------------------------- /pytest_services/django_settings.py: -------------------------------------------------------------------------------- 1 | """Django setttings helpers.""" 2 | import imp 3 | import os 4 | import sys 5 | 6 | from importlib import import_module 7 | from django.core.urlresolvers import clear_url_caches, set_urlconf 8 | from django.template import context, base, loader 9 | from django.utils import translation 10 | from django.utils.translation import trans_real 11 | 12 | 13 | def setup_django_settings(test_settings): 14 | """Override the enviroment variable and call the _setup method of the settings object to reload them.""" 15 | os.environ['DJANGO_SETTINGS_MODULE'] = test_settings 16 | 17 | from django.conf import settings as django_settings 18 | 19 | # (re)setup django settings 20 | django_settings._setup() 21 | 22 | # reload settings 23 | reload_settings(django_settings) 24 | 25 | 26 | def clean_django_settings(): 27 | """Clean current django settings.""" 28 | from django.conf import settings as django_settings 29 | 30 | del os.environ['DJANGO_SETTINGS_MODULE'] 31 | django_settings._wrapped = None 32 | 33 | 34 | def reload_settings(settings, databases=None): 35 | """Special routine to reload django settings. 36 | 37 | Including: 38 | urlconf module, context processor, templatetags settings, database settings. 39 | """ 40 | if databases: 41 | settings.DATABASES.update(databases) 42 | 43 | # check if there's settings to reload 44 | if hasattr(settings, 'ROOT_URLCONF'): 45 | if settings.ROOT_URLCONF in sys.modules: 46 | imp.reload(sys.modules[settings.ROOT_URLCONF]) 47 | import django 48 | if hasattr(django, 'setup'): 49 | django.setup() 50 | import_module(settings.ROOT_URLCONF) 51 | set_urlconf(settings.ROOT_URLCONF) 52 | settings.LANGUAGE_CODE = 'en' # all tests should be run with English by default 53 | 54 | # Make the ConnectionHandler use the new settings, otherwise the ConnectionHandler will have old configuraton. 55 | from django.db.utils import ConnectionHandler 56 | import django.db 57 | from django.db.utils import load_backend 58 | import django.db.transaction 59 | import django.db.models 60 | import django.db.models.sql.query 61 | import django.core.management.commands.syncdb 62 | import django.db.models.sql.compiler 63 | import django.db.backends 64 | import django.db.backends.mysql.base 65 | import django.core.management.commands.loaddata 66 | 67 | # all modules which imported django.db.connections should be changed to get new ConnectionHanlder 68 | django.db.models.sql.compiler.connections = django.db.models.connections = \ 69 | django.core.management.commands.loaddata.connections = \ 70 | django.db.backends.connections = django.db.backends.mysql.base.connections = \ 71 | django.core.management.commands.syncdb.connections = django.db.transaction.connections = \ 72 | django.db.connections = django.db.models.base.connections = django.db.models.sql.query.connections = \ 73 | ConnectionHandler(settings.DATABASES) 74 | 75 | # default django connection and backend should be also changed 76 | django.db.connection = django.db.connections[django.db.DEFAULT_DB_ALIAS] 77 | django.db.backend = load_backend(django.db.connection.settings_dict['ENGINE']) 78 | 79 | import django.core.cache 80 | django.core.cache.cache = django.core.cache._create_cache(django.core.cache.DEFAULT_CACHE_ALIAS) 81 | 82 | # clear django urls cache 83 | clear_url_caches() 84 | # clear django contextprocessors cache 85 | context._standard_context_processors = None 86 | # clear django templatetags cache 87 | base.templatetags_modules = None 88 | 89 | # reload translation files 90 | imp.reload(translation) 91 | imp.reload(trans_real) 92 | 93 | # clear django template loaders cache 94 | loader.template_source_loaders = None 95 | from django.template.loaders import app_directories 96 | imp.reload(app_directories) 97 | from django.template.base import get_templatetags_modules 98 | get_templatetags_modules.cache_clear() 99 | import django.apps 100 | import django 101 | import django.template 102 | django.template.engines.__dict__.pop('templates', None) 103 | django.template.engines._templates = None 104 | django.template.engines._engines = {} 105 | if django.apps.apps.ready: 106 | django.apps.apps.set_installed_apps(settings.INSTALLED_APPS) 107 | django.setup() 108 | -------------------------------------------------------------------------------- /pytest_services/folders.py: -------------------------------------------------------------------------------- 1 | """Fixtures for supporting a distributed test run.""" 2 | import os 3 | import shutil 4 | 5 | import psutil 6 | import pytest 7 | 8 | 9 | @pytest.fixture(scope='session') 10 | def root_dir(): 11 | """The parent directory of the test base artifact directory.""" 12 | return '/tmp' 13 | 14 | 15 | @pytest.yield_fixture(scope='session') 16 | def base_dir(request, session_id, root_dir, services_log): 17 | """The directory where test run artifacts should be stored. 18 | 19 | It is responsibility of fixtures and tests that depend on it to clean up 20 | after themselves. 21 | 22 | """ 23 | path = os.path.join(root_dir, 'sr-{0}'.format(session_id)) 24 | services_log.debug('creating base dir: {0}'.format(path)) 25 | if not os.path.exists(path): 26 | os.mkdir(path) 27 | 28 | yield path 29 | 30 | services_log.debug('finalizing base dir: {0}'.format(path)) 31 | shutil.rmtree(path, ignore_errors=True) 32 | 33 | 34 | @pytest.fixture(scope='session') 35 | def temp_dir(request, base_dir, services_log): 36 | """The temporary dir.""" 37 | path = os.path.join(base_dir, 'tmp') 38 | 39 | services_log.debug('creating temp dir: {0}'.format(path)) 40 | if not os.path.exists(path): 41 | os.mkdir(path) 42 | 43 | return path 44 | 45 | 46 | @pytest.fixture(scope='session') 47 | def memory_root_dir(root_dir): 48 | """The parent directory of the test artifact directory in memory.""" 49 | # check for a free space for at least 8 parallel processes 50 | if os.path.exists('/dev/shm') and psutil.disk_usage('/dev/shm').free > 1024 * 1024 * 64 * 10: 51 | return '/dev/shm' 52 | return root_dir 53 | 54 | 55 | @pytest.yield_fixture(scope='session') 56 | def memory_base_dir(request, session_id, memory_root_dir, services_log): 57 | """The directory where memory test run artifacts should be stored. 58 | 59 | It is responsibility of fixtures and tests that depend on it to clean up 60 | after themselves. 61 | 62 | """ 63 | path = os.path.join(memory_root_dir, 'sr-{0}'.format(session_id)) 64 | 65 | services_log.debug('creating memory base dir: {0}'.format(path)) 66 | if not os.path.exists(path): 67 | os.mkdir(path) 68 | 69 | yield path 70 | 71 | services_log.debug('finalizing memory base dir: {0}'.format(path)) 72 | shutil.rmtree(path, ignore_errors=True) 73 | 74 | 75 | @pytest.fixture(scope='session') 76 | def memory_temp_dir(request, memory_base_dir, services_log): 77 | """The memory temporary dir.""" 78 | path = os.path.join(memory_base_dir, 'tmp') 79 | 80 | services_log.debug('creating memory temp dir: {0}'.format(path)) 81 | if not os.path.exists(path): 82 | os.mkdir(path) 83 | 84 | return path 85 | 86 | 87 | @pytest.fixture(scope='session') 88 | def lock_dir(memory_root_dir, services_log): 89 | """The lock dir.""" 90 | path = os.path.join(memory_root_dir, 'service-locks') 91 | services_log.debug('ensuring lock dir: {0}'.format(path)) 92 | if not os.path.exists(path): 93 | try: 94 | os.mkdir(path, 0o777) 95 | except OSError: 96 | # concurrent already created this path 97 | pass 98 | try: 99 | os.chmod(path, 0o777) # permissions on previous line can be ignored 100 | except OSError: 101 | # could happen, but we'll continue as it's just a folder permissions 102 | pass 103 | 104 | return path 105 | 106 | 107 | @pytest.fixture(scope='session') 108 | def run_dir(memory_temp_dir, services_log): 109 | """The run dir (like local /var/run).""" 110 | path = os.path.join(memory_temp_dir, 'run') 111 | services_log.debug('creating run dir: {0}'.format(path)) 112 | os.mkdir(path) 113 | 114 | return path 115 | -------------------------------------------------------------------------------- /pytest_services/gui.py.orig: -------------------------------------------------------------------------------- 1 | """Fixtures for the GUI environment.""" 2 | import atexit 3 | import os 4 | import fcntl 5 | import time 6 | import socket 7 | import shlex 8 | import subprocess32 9 | 10 | from distutils.spawn import find_executable 11 | import pytest 12 | 13 | from tests.fixtures.services.util import ( 14 | file_lock, 15 | get_free_display, 16 | memory_lock, 17 | unlock_display, 18 | memoize, 19 | ) 20 | 21 | from testhelpers import pltest 22 | 23 | 24 | @pytest.fixture(scope='session') 25 | @memoize 26 | def display(request, pl_local_environment, lock_dir, log): 27 | """The DISPLAY environment variable used in this test run. 28 | 29 | In case it is not a local run, a random value will be picked up and set, 30 | otherwise it will be taken from the environment. 31 | 32 | """ 33 | if not pl_local_environment: 34 | if request.config.option.display: 35 | display = request.config.option.display 36 | else: 37 | display = get_free_display(lock_dir, log) 38 | atexit.register(lambda: unlock_display(display, lock_dir, log)) 39 | 40 | os.environ['DISPLAY'] = ':{0}'.format(display) if ':' not in str(display) else display 41 | 42 | return display 43 | 44 | 45 | @pytest.fixture(scope='session') 46 | @memoize 47 | def xvfb_min_free_memory(): 48 | """Minimum amount of memory required for xvfb in megabytes.""" 49 | return 10 50 | 51 | 52 | @pytest.fixture(scope='session') 53 | @memoize 54 | def xvfb_watcher( 55 | request, pl_local_environment, test_cleanup, display, lock_dir, memory_locks, xvfb_min_free_memory, 56 | timeout=600): 57 | """The Xvfb process. 58 | 59 | If the tests are run loccaly xvfb is not started. 60 | 61 | """ 62 | if ( 63 | request.config.option.display 64 | or pl_local_environment 65 | or request.config.option.splinter_webdriver == 'phantomjs' 66 | ): 67 | # display is passed, no action required 68 | return 69 | try: 70 | with file_lock( 71 | os.path.join(lock_dir, 'xvfb_{0}.lock'.format(display)), 72 | operation=fcntl.LOCK_EX | fcntl.LOCK_NB): 73 | xvfb = find_executable('Xvfb') 74 | assert xvfb, 'You have to have Xvfb installed' 75 | cmd = ( 76 | '{xvfb} ' 77 | ':{display} -screen 0 1366x768x8 -ac +extension RANDR' 78 | ).format(display=display, xvfb=xvfb) 79 | 80 | args = shlex.split(cmd) 81 | 82 | with memory_lock(lock_dir, xvfb_min_free_memory, lock=memory_locks): 83 | 84 | watcher = subprocess32.Popen( 85 | args, 86 | stdout=subprocess32.PIPE, 87 | stderr=subprocess32.PIPE, 88 | ) 89 | 90 | def finalize(): 91 | watcher.kill() 92 | try: 93 | watcher.communicate(timeout=timeout / 2) 94 | except subprocess32.TimeoutExpired: 95 | watcher.terminate() 96 | watcher.communicate(timeout=timeout / 2) 97 | 98 | atexit.register(finalize) 99 | 100 | times = 0 101 | while True: 102 | try: 103 | socket.create_connection(('127.0.0.1', 6000 + display)) 104 | except socket.error: 105 | if times > timeout: 106 | raise 107 | else: 108 | break 109 | 110 | time.sleep(1) 111 | times += 1 112 | 113 | return watcher 114 | except IOError: 115 | # no need in xvfb, it's already there 116 | return 117 | 118 | 119 | @pytest.fixture(scope='session', autouse=True) 120 | @memoize 121 | def browser_path(request, pl_local_environment, project_path): 122 | """Substitute the browser path.""" 123 | driver = request.config.option.splinter_webdriver 124 | paths = { 125 | 'firefox': [pltest.firefox.version, 'firefox'], 126 | 'chrome': [pltest.chrome.version], 127 | 'phantomjs': [pltest.phantomjs.version, 'phantomjs', 'bin']} 128 | path = os.path.join('.{0}'.format(driver), *paths[driver]) 129 | if not os.path.exists(path): 130 | path = os.path.join(project_path, path) 131 | if not os.path.exists(path): 132 | path = request.config.option.browser_path 133 | if not os.path.exists(path): 134 | raise Exception( 135 | 'browser path does not exist: {0}. Use "pltest " command to install it.' 136 | .format(path) 137 | ) 138 | os.environ['PATH'] = '{0}:'.format(path) + os.environ['PATH'] 139 | -------------------------------------------------------------------------------- /pytest_services/locks.py: -------------------------------------------------------------------------------- 1 | """Fixtures for supporting a distributed test run.""" 2 | import contextlib 3 | import json 4 | import os 5 | from random import random 6 | import socket 7 | import time 8 | 9 | import pytest 10 | import zc.lockfile 11 | 12 | marker = object() 13 | 14 | 15 | def try_remove(filename): 16 | try: 17 | os.unlink(filename) 18 | except OSError: 19 | pass 20 | 21 | 22 | @contextlib.contextmanager 23 | def file_lock(filename, remove=True, timeout=20): 24 | """A lock that is shared across processes. 25 | 26 | :param filename: the name of the file that will be locked. 27 | :param remove: whether or not to remove the file on context close 28 | :param timeout: Amount of time to retry the file lock if a :class:`zc.lockfile.LockError` is hit 29 | """ 30 | total_seconds_slept = 0 31 | while True: 32 | try: 33 | with contextlib.closing(zc.lockfile.SimpleLockFile(filename)) as lockfile: 34 | yield lockfile._fp 35 | break 36 | except zc.lockfile.LockError as err: 37 | if total_seconds_slept >= timeout: 38 | raise err 39 | seconds_to_sleep = random() * 0.1 + 0.05 40 | total_seconds_slept += seconds_to_sleep 41 | time.sleep(seconds_to_sleep) 42 | 43 | remove and try_remove(filename) 44 | 45 | 46 | def unlock_resource(name, resource, lock_dir, services_log): 47 | """Unlock previously locked resource. 48 | 49 | :param name: name to be used to separate various resources, eg. port, display 50 | :param resource: resource value which was previously locked 51 | :param lock_dir: directory for lockfiles to use. 52 | """ 53 | with locked_resources(name, lock_dir) as bound_resources: 54 | try: 55 | bound_resources.remove(resource) 56 | except ValueError: 57 | pass 58 | services_log.debug('resource freed {0}: {1}'.format(name, resource)) 59 | services_log.debug('bound resources {0}: {1}'.format(name, bound_resources)) 60 | 61 | 62 | @contextlib.contextmanager 63 | def locked_resources(name, lock_dir): 64 | """Contextmanager providing an access to locked shared resource list. 65 | 66 | :param name: name to be used to separate various resources, eg. port, display 67 | :param lock_dir: directory for lockfiles to use. 68 | """ 69 | with file_lock(os.path.join(lock_dir, name), remove=False) as fd: 70 | bound_resources = fd.read().strip() 71 | if bound_resources: 72 | try: 73 | bound_resources = json.loads(bound_resources) 74 | except ValueError: 75 | bound_resources = None 76 | if not isinstance(bound_resources, list): 77 | bound_resources = [] 78 | yield bound_resources 79 | 80 | fd.seek(0) 81 | fd.truncate() 82 | fd.write(json.dumps(bound_resources)) 83 | fd.flush() 84 | 85 | 86 | def unlock_port(port, lock_dir, services_log): 87 | """Unlock previously locked port.""" 88 | return unlock_resource('port', port, lock_dir, services_log) 89 | 90 | 91 | def unlock_display(display, lock_dir, services_log): 92 | """Unlock previously locked display.""" 93 | return unlock_resource('display', display, lock_dir, services_log) 94 | 95 | 96 | @pytest.fixture(scope='session') 97 | def lock_resource_timeout(): 98 | """Max number of seconds to obtain the lock.""" 99 | return 20 100 | 101 | 102 | def lock_resource(name, resource_getter, lock_dir, services_log, lock_resource_timeout): 103 | """Issue a lock for given resource.""" 104 | total_seconds_slept = 0 105 | while True: 106 | try: 107 | with locked_resources(name, lock_dir) as bound_resources: 108 | services_log.debug('bound_resources {0}: {1}'.format(name, bound_resources)) 109 | resource = resource_getter(bound_resources) 110 | while resource in bound_resources: 111 | # resource is already taken by someone, retry 112 | services_log.debug('bound resources {0}: {1}'.format(name, bound_resources)) 113 | resource = resource_getter(bound_resources) 114 | services_log.debug('free resource choosen {0}: {1}'.format(name, resource)) 115 | bound_resources.append(resource) 116 | services_log.debug('bound resources {0}: {1}'.format(name, bound_resources)) 117 | return resource 118 | except zc.lockfile.LockError as err: 119 | if total_seconds_slept >= lock_resource_timeout: 120 | raise err 121 | services_log.debug('lock resource failed: {0}'.format(err)) 122 | 123 | seconds_to_sleep = random() * 0.1 + 0.05 124 | total_seconds_slept += seconds_to_sleep 125 | time.sleep(seconds_to_sleep) 126 | 127 | 128 | def get_free_port(lock_dir, services_log, lock_resource_timeout): 129 | """Get free port to listen on.""" 130 | def get_port(bound_resources): 131 | if bound_resources: 132 | port = max(bound_resources) + 1 133 | else: 134 | port = 30000 135 | 136 | while True: 137 | s = socket.socket() 138 | try: 139 | s.bind(('127.0.0.1', port)) 140 | s.close() 141 | return port 142 | except socket.error: 143 | pass 144 | port += 1 145 | 146 | return lock_resource('port', get_port, lock_dir, services_log, lock_resource_timeout) 147 | 148 | 149 | def get_free_display(lock_dir, services_log, lock_resource_timeout): 150 | """Get free display to listen on.""" 151 | def get_display(bound_resources): 152 | display = 100 153 | while True: 154 | if bound_resources: 155 | display = max(bound_resources) + 1 156 | if os.path.exists('/tmp/.X{0}-lock'.format(display)): 157 | display += 1 158 | continue 159 | return display 160 | 161 | return lock_resource('display', get_display, lock_dir, services_log, lock_resource_timeout) 162 | 163 | 164 | @pytest.fixture(scope='session') 165 | def port_getter(request, lock_dir, services_log, lock_resource_timeout): 166 | """Lock getter function.""" 167 | def get_port(): 168 | """Lock a free port and unlock it on finalizer.""" 169 | port = get_free_port(lock_dir, services_log, lock_resource_timeout) 170 | 171 | def finalize(): 172 | unlock_port(port, lock_dir, services_log) 173 | request.addfinalizer(finalize) 174 | return port 175 | return get_port 176 | 177 | 178 | @pytest.fixture(scope='session') 179 | def display_getter(request, lock_dir, services_log, lock_resource_timeout): 180 | """Display getter function.""" 181 | def get_display(): 182 | """Lock a free display and unlock it on finalizer.""" 183 | display = get_free_display(lock_dir, services_log, lock_resource_timeout) 184 | request.addfinalizer(lambda: unlock_display(display, lock_dir, services_log)) 185 | return display 186 | return get_display 187 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/mysql.py: -------------------------------------------------------------------------------- 1 | """Fixtures for mysql.""" 2 | import os 3 | import shutil 4 | 5 | from distutils.spawn import find_executable # pylint: disable=E0611 6 | import pytest 7 | 8 | from .process import ( 9 | CalledProcessWithOutputError, 10 | check_output, 11 | ) 12 | 13 | 14 | @pytest.fixture(scope='session') 15 | def mysql_defaults_file( 16 | run_services, tmp_path_factory, memory_temp_dir, request): 17 | """MySQL defaults file.""" 18 | if run_services: 19 | cfg = tmp_path_factory.mktemp(request.session.name) 20 | defaults_path = str(cfg / 'defaults.cnf') 21 | 22 | with open(defaults_path, 'w+') as fd: 23 | fd.write(""" 24 | [mysqld] 25 | user = {user} 26 | tmpdir = {tmpdir} 27 | default-time-zone = SYSTEM 28 | """.format(user=os.environ['USER'], tmpdir=memory_temp_dir)) 29 | return defaults_path 30 | 31 | 32 | @pytest.fixture(scope='session') 33 | def mysql_base_dir(): 34 | my_print_defaults = find_executable('my_print_defaults') 35 | assert my_print_defaults, 'You have to install my_print_defaults script.' 36 | 37 | return os.path.dirname(os.path.dirname(os.path.realpath(my_print_defaults))) 38 | 39 | 40 | @pytest.fixture(scope='session') 41 | def mysql_system_database( 42 | run_services, 43 | mysql_data_dir, 44 | mysql_base_dir, 45 | mysql_defaults_file, 46 | memory_temp_dir, 47 | lock_dir, 48 | services_log, 49 | ): 50 | """Install database to given path.""" 51 | if run_services: 52 | mysqld = find_executable('mysqld') 53 | assert mysqld, 'You have to install mysqld script.' 54 | 55 | try: 56 | services_log.debug('Starting mysqld.') 57 | check_output([ 58 | mysqld, 59 | '--defaults-file={0}'.format(mysql_defaults_file), 60 | '--initialize-insecure', 61 | '--datadir={0}'.format(mysql_data_dir), 62 | '--basedir={0}'.format(mysql_base_dir), 63 | '--user={0}'.format(os.environ['USER']) 64 | ]) 65 | except CalledProcessWithOutputError as e: 66 | services_log.error( 67 | '{e.cmd} failed with output:\n{e.output}\nand erorr:\n{e.err}. ' 68 | 'Please ensure you disabled apparmor for /run/shm/** or for whole mysql'.format(e=e)) 69 | raise 70 | finally: 71 | services_log.debug('mysqld was executed.') 72 | 73 | 74 | @pytest.fixture(scope='session') 75 | def mysql_data_dir( 76 | request, memory_base_dir, memory_temp_dir, lock_dir, session_id, services_log, run_services): 77 | """The root directory for the mysql instance. 78 | 79 | `mysql_install_db` is run in that directory. 80 | 81 | """ 82 | if run_services: 83 | path = os.path.join(memory_base_dir, 'mysql') 84 | services_log.debug('Making mysql base dir in {path}'.format(path=path)) 85 | 86 | def finalizer(): 87 | shutil.rmtree(path, ignore_errors=True) 88 | 89 | finalizer() 90 | request.addfinalizer(finalizer) 91 | os.mkdir(path) 92 | return path 93 | 94 | 95 | @pytest.fixture(scope='session') 96 | def mysql_socket(run_dir): 97 | """The mysqld socket location.""" 98 | return os.path.join(run_dir, 'mysql.sock') 99 | 100 | 101 | @pytest.fixture(scope='session') 102 | def mysql_pid(run_dir): 103 | """The pid file of the mysqld.""" 104 | return os.path.join(run_dir, 'mysql.pid') 105 | 106 | 107 | @pytest.fixture(scope='session') 108 | def mysql_connection(run_services, mysql_socket): 109 | """The connection string to the local mysql instance.""" 110 | if run_services: 111 | return 'mysql://root@localhost/?unix_socket={0}&charset=utf8'.format(mysql_socket) 112 | 113 | 114 | @pytest.fixture(scope='session') 115 | def mysql_watcher( 116 | request, run_services, watcher_getter, mysql_system_database, mysql_pid, mysql_socket, mysql_data_dir, 117 | mysql_defaults_file): 118 | """The mysqld process watcher.""" 119 | if run_services: 120 | return watcher_getter( 121 | 'mysqld', 122 | [ 123 | '--defaults-file={0}'.format(mysql_defaults_file), 124 | '--datadir={mysql_data_dir}'.format(mysql_data_dir=mysql_data_dir), 125 | '--pid-file={mysql_pid}'.format(mysql_pid=mysql_pid), 126 | '--socket={mysql_socket}'.format(mysql_socket=mysql_socket), 127 | '--skip-networking', 128 | ], 129 | checker=lambda: os.path.exists(mysql_socket), 130 | request=request, 131 | ) 132 | 133 | 134 | @pytest.fixture(scope='session') 135 | def mysql_database_name(): 136 | """Name of test database to be created.""" 137 | return 'pytest_services_test' 138 | 139 | 140 | @pytest.fixture(scope='session') 141 | def mysql_database_getter(run_services, mysql_watcher, mysql_socket): 142 | """Prepare new test database creation function.""" 143 | if run_services: 144 | def getter(database_name): 145 | check_output( 146 | [ 147 | 'mysql', 148 | '--user=root', 149 | '--socket={0}'.format(mysql_socket), 150 | '--execute=create database {0};'.format(database_name), 151 | ], 152 | ) 153 | check_output( 154 | 'mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql --user=root --socket={0} mysql'.format(mysql_socket), 155 | shell=True 156 | ) 157 | return getter 158 | 159 | 160 | @pytest.fixture(scope='session') 161 | def mysql_database(run_services, mysql_database_getter, mysql_database_name): 162 | """Prepare new test database creation function.""" 163 | if run_services: 164 | return mysql_database_getter(mysql_database_name) 165 | 166 | 167 | @pytest.fixture(scope='session') 168 | def mysql(request, run_services, mysql_watcher, mysql_database): 169 | """The mysql instance which is ready to be used by the tests.""" 170 | if run_services: 171 | return mysql_watcher 172 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/service.py: -------------------------------------------------------------------------------- 1 | """Service fixtures.""" 2 | import time 3 | import re 4 | import warnings 5 | try: 6 | import subprocess32 as subprocess 7 | except ImportError: # pragma: no cover 8 | import subprocess 9 | import uuid # pylint: disable=C0411 10 | 11 | from distutils.spawn import find_executable # pylint: disable=E0611 12 | import pytest 13 | 14 | WRONG_FILE_NAME_CHARS_RE = re.compile(r'[^\w_-]') 15 | 16 | 17 | @pytest.fixture(scope='session') 18 | def run_services(request, worker_id): 19 | """Indicate whether the services should run or not.""" 20 | return worker_id != 'local' or request.config.option.run_services 21 | 22 | 23 | @pytest.fixture(scope='session') 24 | def worker_id(request): 25 | """ 26 | The id of the worker if tests are run using xdist. 27 | 28 | It is set to `'local'` if tests are not run using xdist. 29 | 30 | The id is unique for a test run. An id may clash if there are two workers 31 | that belong to different test sessions. 32 | 33 | """ 34 | return WRONG_FILE_NAME_CHARS_RE.sub('_', getattr(request.config, 'workerinput', {}).get('workerid', 'local')) 35 | 36 | 37 | @pytest.fixture(scope='session') 38 | def slave_id(request, worker_id): 39 | msg = "The `slave_id` fixture is deprecated; use `worker_id` instead." 40 | warnings.warn(msg, DeprecationWarning) 41 | return worker_id 42 | 43 | 44 | @pytest.fixture(scope='session') 45 | def session_id(request, worker_id, run_services): 46 | """The test session id. 47 | 48 | It is supposed to be globally unique. 49 | 50 | """ 51 | # UUID should be enough, other fields are added for the debugging purposes. 52 | session_id = '{random}-{worker_id}'.format( 53 | random=uuid.uuid4().hex, 54 | worker_id=worker_id, 55 | ) 56 | 57 | return session_id 58 | 59 | 60 | @pytest.fixture(scope='session') 61 | def watcher_getter(request, services_log): 62 | """Popen object of given executable name and it's arguments. 63 | 64 | Wait for the process to start. 65 | Add finalizer to properly stop it. 66 | """ 67 | orig_request = request 68 | 69 | def watcher_getter_function(name, arguments=None, kwargs=None, timeout=20, checker=None, request=None): 70 | if request is None: 71 | warnings.warn('Omitting the `request` parameter will result in an unstable order of finalizers.') 72 | request = orig_request 73 | executable = find_executable(name) 74 | assert executable, 'You have to install {0} executable.'.format(name) 75 | 76 | cmd = [name] + (arguments or []) 77 | 78 | services_log.debug('Starting {0}: {1}'.format(name, arguments)) 79 | 80 | watcher = subprocess.Popen( 81 | cmd, **(kwargs or {})) 82 | 83 | def finalize(): 84 | try: 85 | watcher.terminate() 86 | except OSError: 87 | pass 88 | if watcher.returncode is None: 89 | try: 90 | watcher.communicate(timeout=timeout / 2) 91 | except subprocess.TimeoutExpired: 92 | watcher.kill() 93 | watcher.communicate(timeout=timeout / 2) 94 | request.addfinalizer(finalize) 95 | 96 | # Wait for the service to start. 97 | times = 0 98 | while not checker(): 99 | if watcher.returncode is not None: 100 | raise Exception("Error running {0}".format(name)) 101 | 102 | if times > timeout: 103 | raise Exception('The {0} service checked did not succeed!'.format(name)) 104 | 105 | time.sleep(1) 106 | times += 1 107 | 108 | return watcher 109 | 110 | return watcher_getter_function 111 | -------------------------------------------------------------------------------- /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