├── .circleci └── config.yml ├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── VERSION ├── Vagrantfile ├── common_setup.py ├── foreach.sh ├── img ├── linux.png └── windows.png ├── install.sh ├── pytest-devpi-server ├── README.md ├── _pytest_devpi_server │ └── __init__.py ├── setup.cfg ├── setup.py └── tests │ └── integration │ └── test_devpi_server.py ├── pytest-fixture-config ├── README.md ├── pytest_fixture_config.py ├── setup.cfg ├── setup.py └── tests │ └── unit │ └── test_fixture_config.py ├── pytest-git ├── README.md ├── pytest_git.py ├── setup.cfg ├── setup.py └── tests │ └── integration │ └── test_git.py ├── pytest-listener ├── README.md ├── pytest_listener.py ├── setup.cfg ├── setup.py └── tests │ └── integration │ └── test_listener.py ├── pytest-profiling ├── README.md ├── docs │ └── static │ │ └── profile_combined.svg ├── pytest_profiling.py ├── setup.cfg ├── setup.py └── tests │ ├── integration │ ├── profile │ │ └── tests │ │ │ └── unit │ │ │ ├── test_chdir.py │ │ │ ├── test_example.py │ │ │ └── test_long_name.py │ └── test_profile_integration.py │ └── unit │ └── test_profile.py ├── pytest-pyramid-server ├── .gitignore ├── README.md ├── pyramid_server_test.py ├── pytest_pyramid_server.py ├── setup.cfg ├── setup.py └── tests │ └── integration │ ├── config │ └── testing.ini │ └── test_pyramid_server.py ├── pytest-qt-app ├── README.md ├── pytest_qt_app.py ├── setup.cfg ├── setup.py └── tests │ └── integration │ └── test_q_application.py ├── pytest-server-fixtures ├── README.md ├── pytest_server_fixtures │ ├── __init__.py │ ├── base.py │ ├── base2.py │ ├── http.py │ ├── httpd.py │ ├── jenkins.py │ ├── mongo.py │ ├── postgres.py │ ├── redis.py │ ├── s3.py │ ├── serverclass │ │ ├── __init__.py │ │ ├── common.py │ │ ├── docker.py │ │ ├── kubernetes.py │ │ └── thread.py │ ├── util.py │ └── xvfb.py ├── setup.cfg ├── setup.py └── tests │ ├── integration │ ├── jenkins_plugins │ │ ├── jython.hpi │ │ └── notification.hpi │ ├── test_httpd_proxy_server.py │ ├── test_jenkins_server.py │ ├── test_mongo_server.py │ ├── test_postgres.py │ ├── test_redis_server.py │ ├── test_s3_server.py │ └── test_xvfb_server.py │ └── unit │ ├── serverclass │ ├── test_docker_unit.py │ ├── test_kubernetes_unit.py │ └── test_thread_unit.py │ ├── test_server_unit.py │ └── test_server_v2_unit.py ├── pytest-shutil ├── README.md ├── pytest_shutil │ ├── __init__.py │ ├── cmdline.py │ ├── env.py │ ├── run.py │ └── workspace.py ├── setup.cfg ├── setup.py └── tests │ ├── integration │ ├── test_cmdline_integration.py │ ├── test_env_integration.py │ ├── test_run_integration.py │ └── test_workspace_integration.py │ └── unit │ ├── test_cmdline.py │ ├── test_env.py │ └── test_run.py ├── pytest-svn ├── README.md ├── pytest_svn.py ├── setup.cfg ├── setup.py └── tests │ └── integration │ └── test_svn.py ├── pytest-verbose-parametrize ├── README.md ├── pytest_verbose_parametrize.py ├── setup.cfg ├── setup.py └── tests │ ├── __init__.py │ ├── integration │ ├── __init__.py │ ├── parametrize_ids │ │ └── tests │ │ │ └── unit │ │ │ ├── test_duplicates.py │ │ │ ├── test_example.py │ │ │ ├── test_long_ids.py │ │ │ ├── test_non_parametrized.py │ │ │ └── test_parametrized.py │ └── test_verbose_parametrize.py │ └── unit │ ├── __init__.py │ └── test_verbose_parametrize.py ├── pytest-virtualenv ├── README.md ├── pytest_virtualenv.py ├── setup.cfg ├── setup.py └── tests │ ├── integration │ └── test_tmpvirtualenv.py │ └── unit │ ├── test_package_entry.py │ └── test_venv.py └── pytest-webdriver ├── README.md ├── pytest_webdriver.py ├── setup.cfg ├── setup.py └── tests ├── integration └── test_integration.py └── unit └── test_webdriver.py /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: circle-ci 2 | parallel: true 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | trim_trailing_whitespace = true 5 | 6 | [*.py] 7 | indent_style = space 8 | indent_size = 4 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .cache 3 | .mypy_cache 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | lib 17 | lib64 18 | .eggs 19 | 20 | # Unit test / coverage reports 21 | .coverage 22 | .coverage.* 23 | junit.xml 24 | coverage.xml 25 | htmlcov 26 | FAILED-* 27 | *.log 28 | 29 | # IDEs 30 | .project 31 | .pydevproject 32 | .settings 33 | TAGS 34 | /venv/ 35 | /pip-log.txt 36 | .idea 37 | *.swp 38 | .vscode 39 | 40 | # Copied files 41 | */MANIFEST.in 42 | */CHANGES.md 43 | */VERSION 44 | */LICENSE 45 | */common_setup.py 46 | /pyqt 47 | /sip 48 | 49 | # macOS 50 | .DS_Store 51 | 52 | # vagrant 53 | .vagrant 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Man AHL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include common_setup.py *.md VERSION LICENSE 2 | recursive-include tests * 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Package list, in order of ancestry 2 | # removed pytest-qt-app 3 | EXTRA_DEPS = setuptools-git \ 4 | pytest-timeout \ 5 | pypandoc \ 6 | wheel \ 7 | coverage \ 8 | python-jenkins \ 9 | redis \ 10 | pymongo \ 11 | psycopg2-binary\ 12 | boto3 \ 13 | docker \ 14 | kubernetes 15 | 16 | 17 | COPY_FILES = VERSION CHANGES.md common_setup.py MANIFEST.in LICENSE 18 | UPLOAD_OPTS = 19 | PYPI_INDEX = 20 | 21 | PIP_INSTALL_ARGS := $(shell [ ! -z "${PYPI_INDEX}" ] && echo --index ${PYPI_INDEX} ) 22 | 23 | # removed from PHONY: circleci_sip circleci_pyqt 24 | .PHONY: extras copyfiles wheels eggs sdists install develop test upload clean 25 | 26 | extras: 27 | pip install ${PIP_INSTALL_ARGS} ${EXTRA_DEPS} 28 | 29 | copyfiles: 30 | ./foreach.sh 'for file in ${COPY_FILES}; do cp ../$$file .; done' 31 | 32 | wheels: copyfiles 33 | pip install ${PIP_INSTALL_ARGS} -U wheel 34 | ./foreach.sh --changed 'python setup.py bdist_wheel' 35 | 36 | eggs: copyfiles 37 | ./foreach.sh --changed 'python setup.py bdist_egg' 38 | 39 | sdists: copyfiles 40 | ./foreach.sh --changed 'python setup.py sdist' 41 | 42 | install: copyfiles 43 | pip install ${PIP_INSTALL_ARGS} -U wheel 44 | ./foreach.sh 'python setup.py bdist_wheel' 45 | ./foreach.sh 'pip install ${PIP_INSTALL_ARGS} dist/*.whl' 46 | 47 | develop: copyfiles extras 48 | ./foreach.sh 'pip install ${PIP_INSTALL_ARGS} -e.[tests]' 49 | 50 | test: 51 | rm -f FAILED-* 52 | ./foreach.sh 'DEBUG=1 python setup.py test -sv -ra || touch ../FAILED-$$PKG' 53 | bash -c "! compgen -G 'FAILED-*'" 54 | 55 | test-ci: 56 | rm -f FAILED-* 57 | mkdir junit 58 | ./foreach.sh 'cat *.egg-info/top_level.txt | xargs -Ipysrc coverage run -p --source=pysrc -m pytest --junitxml junit.xml -svvvv -ra || touch ../FAILED-$$PKG' 59 | ./foreach.sh 'cp junit.xml ../junit/junit-$PKG.xml || true' 60 | 61 | list-test-failures: 62 | @if compgen -G 'FAILED-*' > /dev/null; then \ 63 | echo "Error: Found failure artifacts:"; \ 64 | compgen -G 'FAILED-*'; \ 65 | exit 1; \ 66 | else \ 67 | echo "No failure artifacts found."; \ 68 | fi 69 | 70 | upload: 71 | pip install twine 72 | ./foreach.sh --changed '[ -f common_setup.py ] && twine upload $(UPLOAD_OPTS) dist/*' 73 | 74 | clean: 75 | ./foreach.sh 'rm -rf build dist *.xml *.egg-info .eggs htmlcov .cache $(COPY_FILES)' 76 | rm -rf pytest-pyramid-server/vx pip-log.txt 77 | find . -name *.pyc -delete 78 | find . -name .coverage -delete 79 | find . -name .coverage.* -delete 80 | rm -f FAILED-* 81 | 82 | all: extras develop test 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A goody-bag of nifty plugins for [pytest](https://pytest.org) 2 | 3 | OS | Build | Coverage | 4 | ------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------| -------- | 5 | ![Linux](img/linux.png) | [![CircleCI (Linux)](https://circleci.com/gh/man-group/pytest-plugins/tree/master.svg?style=svg)](https://circleci.com/gh/man-group/pytest-plugins/tree/master) | [![Coverage Status](https://coveralls.io/repos/github/manahl/pytest-plugins/badge.svg?branch=master)](https://coveralls.io/github/manahl/pytest-plugins?branch=master) 6 | ![Windows](img/windows.png) | [![CircleCI (Linux)](https://circleci.com/gh/man-group/pytest-plugins/tree/master.svg?style=svg)](https://circleci.com/gh/man-group/pytest-plugins/tree/master) | 7 | 8 | Plugin | Description | Supported OS | 9 | ------ | ----------- | ------------ | 10 | | [pytest-server-fixtures](pytest-server-fixtures) | Extensible server-running framework with a suite of well-known databases and webservices included | ![Linux](img/linux.png) 11 | | [pytest-shutil](pytest-shutil) | Unix shell and environment management tools |![Linux](img/linux.png) 12 | | [pytest-profiling](pytest-profiling) | Profiling plugin with tabular heat graph output and gprof support for C-Extensions |![Linux](img/linux.png) 13 | | [pytest-devpi-server](pytest-devpi-server) | DevPI server fixture |![Linux](img/linux.png) 14 | | [pytest-pyramid-server](pytest-pyramid-server) | Pyramid server fixture |![Linux](img/linux.png) 15 | | [pytest-webdriver](pytest-webdriver) | Selenium webdriver fixture |![Linux](img/linux.png) 16 | | [pytest-virtualenv](pytest-virtualenv) | Virtualenv fixture |![Linux](img/linux.png) ![Windows](img/windows.png) 17 | | [pytest-qt-app](pytest-qt-app) | PyQT application fixture |![Linux](img/linux.png) 18 | | [pytest-listener](pytest-listener) | TCP Listener/Reciever for testing remote systems |![Linux](img/linux.png) ![Windows](img/windows.png) 19 | | [pytest-git](pytest-git) | Git repository fixture |![Linux](img/linux.png) ![Windows](img/windows.png) 20 | | [pytest-svn](pytest-svn) | SVN repository fixture |![Linux](img/linux.png) 21 | | [pytest-fixture-config](pytest-fixture-config) | Configuration tools for Py.test fixtures |![Linux](img/linux.png) ![Windows](img/windows.png) 22 | | [pytest-verbose-parametrize](pytest-verbose-parametrize) | Makes py.test's parametrize output a little more verbose |![Linux](img/linux.png) 23 | 24 | 25 | ## Developing these plugins 26 | 27 | All of these plugins share setup code and configuration so there is a top-level Makefile to 28 | automate process of setting them up for test and development. 29 | 30 | ### Pre-requisites 31 | 32 | You have `python` installed on your path, preferably using a `virtualenv` 33 | 34 | ### Makefile targets 35 | 36 | To install all dependencies and set up all of the packages for development simply run: 37 | 38 | ```bash 39 | make develop 40 | ``` 41 | 42 | To install all the packages as wheel distributions: 43 | 44 | ```bash 45 | make install 46 | ``` 47 | 48 | To run all the tests: 49 | 50 | ```bash 51 | make test 52 | ``` 53 | 54 | ## Vagrant 55 | 56 | Some of the plugins have complex dependencies, particularly `pytest-server-fixtures`. 57 | To make it easier to develop, there is a `Vagrantfile` which will setup a virtual machine 58 | with all the dependencies installed to run the tests. 59 | 60 | To set up the environment in Vagrant (requires virtualbox) and run the tests: 61 | 62 | ```bash 63 | $ vagrant up 64 | $ vagrant ssh 65 | 66 | # ..... inside vagrant .... 67 | . venv/bin/activate 68 | cd src 69 | make develop 70 | make test 71 | ``` 72 | 73 | ## `foreach.sh` 74 | 75 | To run a command in each of the package directories, use the `foreach.sh` script. 76 | This example will build all the wheel distributions: 77 | 78 | ```bash 79 | ./foreach.sh python setup.py bdist_wheel 80 | ``` 81 | 82 | ### Only-Changed mode 83 | 84 | To run a command only on packages that have changed since the last tagged release, use `--changed`. 85 | This example will only upload packages that need releasing: 86 | 87 | ```bash 88 | ./foreach.sh python setup.py bdist_wheel upload 89 | ``` 90 | 91 | ### Quiet mode 92 | 93 | To run a command with no extra output other than from what you run, use `--quiet` 94 | ```bash 95 | ./foreach.sh --quiet grep PY3 96 | ``` 97 | 98 | 99 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.8.1 2 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | ############# Start vagrant config ############### 2 | Vagrant.configure("2") do |config| 3 | # Specify your hostname if you like 4 | config.vm.provider "virtualbox" do |v| 5 | v.memory = 4096 6 | v.cpus = 4 7 | end 8 | 9 | if Vagrant.has_plugin?("vagrant-proxyconf") 10 | config.proxy.http = "#{ENV['http_proxy']}" 11 | config.proxy.https = "#{ENV['https_proxy']}" 12 | config.proxy.no_proxy = "localhost,127.0.0.0/8,10.0.0.0/8,172.0.0.0/8" 13 | end 14 | 15 | config.vm.hostname = "pytest-plugins-dev" 16 | config.vm.box = "bento/ubuntu-20.04" 17 | config.vm.network "private_network", type: "dhcp" 18 | config.vm.provision "docker" 19 | config.vm.provision "file", source: "install.sh", destination: "/tmp/install.sh" 20 | config.vm.provision "shell", inline: ". /tmp/install.sh && install_all" 21 | config.vm.provision "shell", inline: ". /tmp/install.sh && init_venv python3.7", privileged: false 22 | config.vm.synced_folder ".", "/home/vagrant/src" 23 | end 24 | -------------------------------------------------------------------------------- /common_setup.py: -------------------------------------------------------------------------------- 1 | # Common setup.py code shared between all the projects in this repository 2 | import os 3 | 4 | 5 | def common_setup(src_dir): 6 | this_dir = os.path.dirname(__file__) 7 | readme_file = os.path.join(this_dir, 'README.md') 8 | changelog_file = os.path.join(this_dir, 'CHANGES.md') 9 | version_file = os.path.join(this_dir, 'VERSION') 10 | 11 | long_description = open(readme_file).read() 12 | changelog = open(changelog_file).read() 13 | 14 | return dict( 15 | # Version is shared between all the projects in this repo 16 | version=open(version_file).read().strip(), 17 | long_description='\n'.join((long_description, changelog)), 18 | long_description_content_type='text/markdown', 19 | url='https://github.com/man-group/pytest-plugins', 20 | license='MIT license', 21 | platforms=['unix', 'linux'], 22 | include_package_data=True, 23 | python_requires='>=3.6', 24 | ) 25 | -------------------------------------------------------------------------------- /foreach.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run a command for each of our packages 3 | set -ef 4 | 5 | QUIET=0 6 | if [ "$1" = '--quiet' ]; then 7 | QUIET=1 8 | shift 9 | fi 10 | 11 | if [ "$1" = '--changed' ]; then 12 | shift 13 | # Package list, filtered to ones changed since last tag 14 | LAST_TAG=$(git tag -l v\* | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1) 15 | PACKAGES=$(git diff --name-only ${LAST_TAG} | grep pytest- | cut -d'/' -f1 | sort | uniq) 16 | else 17 | # Package list, in order of ancestry 18 | # removed pytest-qt-app 19 | DEFAULT_PACKAGES="pytest-fixture-config \ 20 | pytest-shutil \ 21 | pytest-server-fixtures \ 22 | pytest-pyramid-server \ 23 | pytest-devpi-server \ 24 | pytest-listener \ 25 | pytest-svn \ 26 | pytest-git \ 27 | pytest-virtualenv \ 28 | pytest-webdriver \ 29 | pytest-profiling \ 30 | pytest-verbose-parametrize" 31 | PACKAGES="${PACKAGES:-$DEFAULT_PACKAGES}" 32 | fi 33 | 34 | for pkg in $PACKAGES; do 35 | export PKG=$pkg 36 | (cd $pkg 37 | if [ $QUIET -eq 1 ]; then 38 | bash -c "$*" 39 | else 40 | echo "-----------------------------------------------------" 41 | echo " $pkg" 42 | echo "-----------------------------------------------------" 43 | echo 44 | bash -x -c "$*" 45 | echo 46 | fi 47 | ) 48 | done 49 | -------------------------------------------------------------------------------- /img/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/man-group/pytest-plugins/0018cc543229732b9c04fad909d2b7ee6167fa40/img/linux.png -------------------------------------------------------------------------------- /img/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/man-group/pytest-plugins/0018cc543229732b9c04fad909d2b7ee6167fa40/img/windows.png -------------------------------------------------------------------------------- /pytest-devpi-server/README.md: -------------------------------------------------------------------------------- 1 | # Py.test DevPi Server Fixture 2 | 3 | DevPi server fixture for ``py.test``. The server is session-scoped by default 4 | and run in a subprocess and temp dir to cleanup when it's done. 5 | 6 | After the server has started up it will create a single user with a password, 7 | and an index for that user. It then activates that index and provides a 8 | handle to the ``devpi-client`` API so you can manipulate the server in your tests. 9 | 10 | ## Installation 11 | 12 | Install using your favourite package manager: 13 | 14 | ```bash 15 | pip install pytest-devpi-server 16 | # or.. 17 | easy_install pytest-devpi-server 18 | ``` 19 | 20 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 21 | 22 | ```python 23 | pytest_plugins = ['pytest_devpi_server'] 24 | ``` 25 | 26 | ## Example 27 | 28 | Here's a noddy test case showing the main functionality: 29 | 30 | ```python 31 | def test_devpi_server(devpi_server): 32 | # This is the client API for the server that's bound directly to the 'devpi' command-line tool. 33 | # Here we list the available indexes 34 | print(devpi_server.api('use', '-l')) 35 | 36 | # Create and use another index 37 | devpi_server.api('index', '-c', 'myindex') 38 | devpi_server.api('index', 'use', 'myindex') 39 | 40 | # Upload a package 41 | import os 42 | os.chdir('/path/to/my/setup/dot/py') 43 | devpi_server.api('upload') 44 | 45 | # Get some json data 46 | import json 47 | res = devpi_server.api('getjson', '/user/myindex') 48 | assert json.loads(res)['result']['projects'] == ['my-package-name'] 49 | 50 | ``` 51 | 52 | ## `DevpiServer` class 53 | 54 | Using this with the default `devpi_server` py.test fixture is good enough for a lot of 55 | use-cases however you may wish to have more fine-grained control about the server configuration. 56 | 57 | To do this you can use the underlying server class directly - this is an implenentation of the 58 | `pytest-server-fixture` framework and as such acts as a context manager: 59 | 60 | ```python 61 | import json 62 | from pytest_devpi_server import DevpiServer 63 | 64 | def test_custom_server(): 65 | with DevPiServer( 66 | # You can specify you own initial user and index 67 | user='bob', 68 | password='secret', 69 | index='myindex', 70 | 71 | # You can provide a zip file that contains the initial server database, 72 | # this is useful to pre-load any required packages for a test run 73 | data='/path/to/data.zip' 74 | ) as server: 75 | 76 | assert not server.dead 77 | res = server.api('getjson', '/bob/myindex') 78 | assert 'pre-loaded-package' in json.loads(res)['result']['projects'] 79 | 80 | # Server should now be dead 81 | assert server.dead 82 | ``` 83 | -------------------------------------------------------------------------------- /pytest-devpi-server/_pytest_devpi_server/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 25 Apr 2012 3 | 4 | @author: eeaston 5 | ''' 6 | import io 7 | import os 8 | import sys 9 | import zipfile 10 | import logging 11 | 12 | from pytest import yield_fixture, fixture 13 | import devpi_server as _devpi_server 14 | from devpi.main import main as devpi_client 15 | from pytest_server_fixtures.http import HTTPTestServer 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | @yield_fixture(scope='session') 21 | def devpi_server(request): 22 | """ Session-scoped Devpi server run in a subprocess, out of a temp dir. 23 | Out-of-the-box it creates a single user an index for that user, then 24 | uses that index. 25 | 26 | Methods 27 | ------- 28 | api(): Client API method, directly bound to the devpi-client command-line tool. Examples: 29 | ... api('index', '-c', 'myindex') to create an index called 'myindex' 30 | ... api('getjson', '/user/myindex') to return the json string describing this index 31 | 32 | Attributes 33 | ---------- 34 | uri: Server URI 35 | user: Initially created username 36 | password: Initially created password 37 | index: Initially created index name 38 | server_dir: Path to server database 39 | client_dir: Path to client directory 40 | 41 | .. also inherits all attributes from the `workspace` fixture 42 | 43 | For more fine-grained control over these attributes, use the class directly and pass in 44 | constructor arguments. 45 | """ 46 | with DevpiServer() as server: 47 | server.start() 48 | yield server 49 | 50 | 51 | @fixture 52 | def devpi_function_index(request, devpi_server): 53 | """ Creates and activates an index for your current test function. 54 | """ 55 | index_name = '/'.join((devpi_server.user, request.function.__name__)) 56 | devpi_server.api('index', '-c', index_name) 57 | devpi_server.api('use', index_name) 58 | return index_name 59 | 60 | 61 | class DevpiServer(HTTPTestServer): 62 | 63 | def __init__(self, offline=True, debug=False, data=None, user="testuser", password="", index='dev', **kwargs): 64 | """ Devpi Server instance. 65 | 66 | Parameters 67 | ---------- 68 | offline : `bool` 69 | Run in offline mode. Defaults to True 70 | data: `str` 71 | Filesystem path to a zipfile archive of the initial server data directory. 72 | If not set and in offline mode, it uses a pre-canned snapshot of a 73 | newly-created empty server. 74 | """ 75 | self.debug = debug 76 | if os.getenv('DEBUG') in (True, '1', 'Y', 'y'): 77 | self.debug = True 78 | super(DevpiServer, self).__init__(preserve_sys_path=True, **kwargs) 79 | 80 | self.offline = offline 81 | self.data = data 82 | self.server_dir = self.workspace / 'server' 83 | self.client_dir = self.workspace / 'client' 84 | self.user = user 85 | self.password = password 86 | self.index = index 87 | 88 | @property 89 | def run_cmd(self): 90 | res = [sys.executable, '-c', 'import sys; from devpi_server.main import main; sys.exit(main())', 91 | '--serverdir', str(self.server_dir), 92 | '--host', self.hostname, 93 | '--port', str(self.port) 94 | ] 95 | if self.offline: 96 | res.append('--offline-mode') 97 | if self.debug: 98 | res.append('--debug') 99 | return res 100 | 101 | def api(self, *args): 102 | """ Client API. 103 | """ 104 | client_args = ['devpi'] 105 | client_args.extend(args) 106 | client_args.extend(['--clientdir', str(self.client_dir)]) 107 | log.info(' '.join(client_args)) 108 | captured = io.StringIO() 109 | stdout = sys.stdout 110 | sys.stdout = captured 111 | try: 112 | devpi_client(client_args) 113 | return captured.getvalue() 114 | finally: 115 | sys.stdout = stdout 116 | 117 | 118 | def pre_setup(self): 119 | if self.data: 120 | log.info("Extracting initial server data from {}".format(self.data)) 121 | zipfile.ZipFile(self.data, 'r').extractall(str(self.server_dir)) 122 | else: 123 | self.run([os.path.join(sys.exec_prefix, "bin", "devpi-init"), 124 | '--serverdir', str(self.server_dir), 125 | ]) 126 | 127 | 128 | def post_setup(self): 129 | # Connect to our server 130 | self.api('use', self.uri) 131 | # Create and log in initial user 132 | self.api('user', '-c', self.user, 'password={}'.format(self.password)) 133 | self.api('login', self.user, '--password={}'.format(self.password)) 134 | # Create and use stand-alone index 135 | self.api('index', '-c', self.index, 'bases=') 136 | self.api('use', self.index) 137 | log.info("=" * 60) 138 | log.info(" Started DevPI server at {}".format(self.uri)) 139 | log.info(" Created initial index at {}/{}".format(self.user, self.index)) 140 | log.info("=" * 60) 141 | -------------------------------------------------------------------------------- /pytest-devpi-server/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-devpi-server/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup, find_packages 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Framework :: Pyramid', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | ] 20 | 21 | install_requires = ['pytest-server-fixtures', 22 | 'pytest', 23 | 'devpi-server>=3.0.1', 24 | 'devpi-client', 25 | 'ruamel.yaml>=0.15', 26 | ] 27 | 28 | entry_points = { 29 | 'pytest11': [ 30 | 'devpi_server = _pytest_devpi_server', 31 | ], 32 | } 33 | 34 | if __name__ == '__main__': 35 | kwargs = common_setup('_pytest_devpi_server') 36 | kwargs.update(dict( 37 | name='pytest-devpi-server', 38 | description='DevPI server fixture for py.test', 39 | author='Edward Easton', 40 | author_email='eeaston@gmail.com', 41 | classifiers=classifiers, 42 | install_requires=install_requires, 43 | packages=find_packages(exclude='tests'), 44 | entry_points=entry_points, 45 | )) 46 | setup(**kwargs) 47 | -------------------------------------------------------------------------------- /pytest-devpi-server/tests/integration/test_devpi_server.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | NEW_INDEX = { 6 | u"result": { 7 | u"acl_toxresult_upload": [u":ANONYMOUS:"], 8 | u"acl_upload": [u"testuser"], 9 | u"bases": [], 10 | u"mirror_whitelist": [], 11 | u"mirror_whitelist_inheritance": u"intersection", 12 | u"projects": [], 13 | u"type": u"stage", 14 | u"volatile": True, 15 | }, 16 | u"type": u"indexconfig", 17 | } 18 | 19 | 20 | def test_server(devpi_server): 21 | res = devpi_server.api( 22 | "getjson", "/{}/{}".format(devpi_server.user, devpi_server.index) 23 | ) 24 | assert json.loads(res) == NEW_INDEX 25 | 26 | 27 | def test_upload(devpi_server): 28 | pkg_dir: Path = devpi_server.workspace / "pkg" 29 | pkg_dir.mkdir(parents=True, exist_ok=True) 30 | setup_py = pkg_dir / "setup.py" 31 | setup_py.write_text( 32 | """ 33 | from setuptools import setup 34 | setup(name='test-foo', 35 | version='1.2.3') 36 | """ 37 | ) 38 | orig_dir = os.getcwd() 39 | try: 40 | os.chdir(pkg_dir) 41 | devpi_server.api("upload") 42 | res = devpi_server.api( 43 | "getjson", "/{}/{}".format(devpi_server.user, devpi_server.index) 44 | ) 45 | assert json.loads(res)["result"]["projects"] == ["test-foo"] 46 | finally: 47 | os.chdir(orig_dir) 48 | 49 | 50 | def test_function_index(devpi_server, devpi_function_index): 51 | res = devpi_server.api( 52 | "getjson", "/{}/test_function_index".format(devpi_server.user) 53 | ) 54 | assert json.loads(res) == NEW_INDEX 55 | -------------------------------------------------------------------------------- /pytest-fixture-config/README.md: -------------------------------------------------------------------------------- 1 | # Py.test Fixture Configuration 2 | 3 | Simple configuration objects for Py.test fixtures. 4 | Allows you to skip tests when their required config variables aren't set. 5 | 6 | ## Installation 7 | 8 | Install using your favourite package manager: 9 | 10 | ```bash 11 | pip install pytest-fixture-config 12 | # or.. 13 | easy_install pytest-fixture-config 14 | ``` 15 | 16 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 17 | 18 | ```python 19 | pytest_plugins = ['pytest_fixture_config'] 20 | ``` 21 | 22 | 23 | ## Specifying Configuration 24 | 25 | To specify your variables you create a class somewhere in your plugin module, 26 | and a singleton instance of the class which reads the variables from wherever 27 | you want. In this example we read them from the shell environment: 28 | 29 | ```python 30 | import os 31 | from pytest_fixture_config import Config 32 | 33 | class FixtureConfig(Config): 34 | __slots__ = ('log_dir', 'log_watcher') 35 | 36 | CONFIG=FixtureConfig( 37 | log_dir=os.getenv('LOG_DIR', '/var/log'), # This has a default 38 | log_watcher=os.getenv('LOG_WATCHER'), # This does not 39 | ) 40 | ``` 41 | 42 | ## Using Configuration 43 | 44 | Simply reference the singleton at run-time in your fixtures: 45 | 46 | ```python 47 | import pytest 48 | 49 | @pytest.fixture 50 | def log_watcher(): 51 | return subprocess.popen([CONFIG.log_watcher, '--log-dir', CONFIG.log_dir]) 52 | 53 | def test_log_watcher(watcher): 54 | watcher.communicate() 55 | ``` 56 | 57 | ## Skipping tests when things are missing 58 | 59 | There are some decorators that allow you to skip tests when settings aren't set. 60 | This is useful when you're testing something you might not have installed 61 | but don't want your tests suite to fail: 62 | 63 | ```python 64 | from pytest_fixture_config import requires_config 65 | 66 | @pytest.fixture 67 | @requires_config(CONFIG, ['log_watcher', 'log_dir']) 68 | def log_watcher(): 69 | return subprocess.popen([CONFIG.log_watcher, '--log-dir', CONFIG.log_dir]) 70 | ``` 71 | 72 | There is also a version for yield_fixtures: 73 | 74 | ```python 75 | from pytest_fixture_config import yield_requires_config 76 | 77 | @pytest.fixture 78 | @yield_requires_config(CONFIG, ['log_watcher', 'log_dir']) 79 | def log_watcher(): 80 | watcher = subprocess.popen([CONFIG.log_watcher, '--log-dir', CONFIG.log_dir]) 81 | yield watcher 82 | watcher.kill() 83 | ``` 84 | -------------------------------------------------------------------------------- /pytest-fixture-config/pytest_fixture_config.py: -------------------------------------------------------------------------------- 1 | """ Fixture configuration 2 | """ 3 | import functools 4 | 5 | import pytest 6 | 7 | 8 | class Config(object): 9 | __slots__ = () 10 | 11 | def __init__(self, **kwargs): 12 | [setattr(self, k, v) for (k, v) in kwargs.items()] 13 | 14 | def update(self, cfg): 15 | for k in cfg: 16 | if k not in self.__slots__: 17 | raise ValueError("Unknown config option: {0}".format(k)) 18 | setattr(self, k, cfg[k]) 19 | 20 | 21 | def requires_config(cfg, vars_): 22 | """ Decorator for fixtures that will skip tests if the required config variables 23 | are missing or undefined in the configuration 24 | """ 25 | def decorator(f): 26 | # We need to specify 'request' in the args here to satisfy pytest's fixture logic 27 | @functools.wraps(f) 28 | def wrapper(request, *args, **kwargs): 29 | for var in vars_: 30 | if not getattr(cfg, var): 31 | pytest.skip('config variable {0} missing, skipping test'.format(var)) 32 | return f(request, *args, **kwargs) 33 | return wrapper 34 | return decorator 35 | 36 | 37 | def yield_requires_config(cfg, vars_): 38 | """ As above but for py.test yield_fixtures 39 | """ 40 | def decorator(f): 41 | @functools.wraps(f) 42 | def wrapper(*args, **kwargs): 43 | for var in vars_: 44 | if not getattr(cfg, var): 45 | pytest.skip('config variable {0} missing, skipping test'.format(var)) 46 | gen = f(*args, **kwargs) 47 | yield next(gen) 48 | return wrapper 49 | return decorator 50 | -------------------------------------------------------------------------------- /pytest-fixture-config/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-fixture-config/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Operating System :: Microsoft :: Windows', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | ] 20 | 21 | install_requires = ['pytest'] 22 | 23 | tests_require = [] 24 | 25 | if __name__ == '__main__': 26 | kwargs = common_setup('pytest_fixture_config') 27 | kwargs.update(dict( 28 | name='pytest-fixture-config', 29 | description='Fixture configuration utils for py.test', 30 | author='Edward Easton', 31 | author_email='eeaston@gmail.com', 32 | classifiers=classifiers, 33 | install_requires=install_requires, 34 | tests_require=tests_require, 35 | py_modules=['pytest_fixture_config'], 36 | )) 37 | setup(**kwargs) 38 | -------------------------------------------------------------------------------- /pytest-fixture-config/tests/unit/test_fixture_config.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | import pytest 4 | 5 | # HACK: if the plugin is imported before the coverage plugin then all 6 | # the top-level code will be omitted from coverage, so force it to be 7 | # reloaded within this unit test under coverage 8 | import pytest_fixture_config 9 | importlib.reload(pytest_fixture_config) 10 | 11 | from pytest_fixture_config import Config, requires_config, yield_requires_config 12 | 13 | class DummyConfig(Config): 14 | __slots__ = ('foo', 'bar') 15 | 16 | 17 | def test_config_update(): 18 | cfg = DummyConfig(foo=1, 19 | bar=2 20 | ) 21 | cfg.update({"foo": 10, "bar":20}) 22 | assert cfg.foo == 10 23 | assert cfg.bar == 20 24 | with pytest.raises(ValueError): 25 | cfg.update({"baz": 30}) 26 | 27 | 28 | CONFIG1 = DummyConfig(foo=None, bar=1) 29 | 30 | @pytest.fixture 31 | @requires_config(CONFIG1, ('foo', 'bar')) 32 | def a_fixture(request): 33 | raise ValueError('Should not run') 34 | 35 | 36 | def test_requires_config_skips(a_fixture): 37 | raise ValueError('Should not run') 38 | 39 | 40 | @pytest.fixture 41 | @requires_config(CONFIG1, ('bar',)) 42 | def another_fixture(request): 43 | return 'xxxx' 44 | 45 | 46 | def test_requires_config_doesnt_skip(another_fixture): 47 | assert another_fixture == 'xxxx' 48 | 49 | 50 | 51 | @pytest.yield_fixture 52 | @yield_requires_config(CONFIG1, ('foo', 'bar')) 53 | def yet_another_fixture(): 54 | raise ValueError('Should also not run') 55 | yield 'yyyy' 56 | 57 | 58 | def test_yield_requires_config_skips(yet_another_fixture): 59 | raise ValueError('Should also not run') 60 | 61 | 62 | @pytest.yield_fixture 63 | @yield_requires_config(CONFIG1, ('bar',)) 64 | def yet_some_other_fixture(): 65 | yield 'yyyy' 66 | 67 | 68 | def test_yield_requires_config_doesnt_skip(yet_some_other_fixture): 69 | assert yet_some_other_fixture == 'yyyy' 70 | -------------------------------------------------------------------------------- /pytest-git/README.md: -------------------------------------------------------------------------------- 1 | # Pytest GIT Fixture 2 | 3 | Creates an empty Git repository for testing that cleans up after itself on teardown. 4 | 5 | ## Installation 6 | 7 | Install using your favourite package installer: 8 | ```bash 9 | pip install pytest-git 10 | # or 11 | easy_install pytest-git 12 | ``` 13 | 14 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 15 | 16 | ```python 17 | pytest_plugins = ['pytest_git'] 18 | ``` 19 | 20 | ## Usage 21 | 22 | This plugin is a thin wrapper around the excellent GitPython library (see http://gitpython.readthedocs.org/en/stable/). 23 | Here's a noddy test case that shows it working: 24 | 25 | ```python 26 | def test_git_repo(git_repo): 27 | # The fixture derives from `workspace` in `pytest-shutil`, so they contain 28 | # a handle to the path `path` object (see https://path.readthedocs.io/) 29 | path = git_repo.workspace 30 | file = path / 'hello.txt' 31 | file.write_text('hello world!') 32 | 33 | # We can run commands relative to the working directory 34 | git_repo.run('git add hello.txt') 35 | 36 | # It's better to use the GitPython api directly - the 'api' attribute is 37 | # a handle to the repository object. 38 | git_repo.api.index.commit("Initial commit") 39 | 40 | # The fixture has a URI property you can use in downstream systems 41 | assert git_repo.uri.startswith('file://') 42 | ``` -------------------------------------------------------------------------------- /pytest-git/pytest_git.py: -------------------------------------------------------------------------------- 1 | """ Repository fixtures 2 | """ 3 | import pytest 4 | from pytest_shutil.workspace import Workspace 5 | from git import Repo 6 | 7 | 8 | @pytest.yield_fixture 9 | def git_repo(request): 10 | """ Function-scoped fixture to create a new git repo in a temporary workspace. 11 | 12 | Attributes 13 | ---------- 14 | uri (str) : Repository URI 15 | api (`git.Repo`) : Git Repo object for this repository 16 | .. also inherits all attributes from the `workspace` fixture 17 | 18 | """ 19 | with GitRepo() as repo: 20 | yield repo 21 | 22 | 23 | class GitRepo(Workspace): 24 | """ 25 | Creates an empty Git repository in a temporary workspace. 26 | Cleans up on exit. 27 | 28 | Attributes 29 | ---------- 30 | uri : `str` 31 | repository base uri 32 | api : `git.Repo` handle to the repository 33 | """ 34 | def __init__(self): 35 | super(GitRepo, self).__init__() 36 | self.api = Repo.init(self.workspace) 37 | self.uri = "file://%s" % self.workspace 38 | -------------------------------------------------------------------------------- /pytest-git/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-git/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Operating System :: Microsoft :: Windows', 17 | 'Programming Language :: Python :: 2', 18 | 'Programming Language :: Python :: 2.7', 19 | 'Programming Language :: Python :: 3', 20 | 'Programming Language :: Python :: 3.5', 21 | 'Programming Language :: Python :: 3.6', 22 | 'Programming Language :: Python :: 3.7', 23 | ] 24 | 25 | install_requires = ['pytest', 26 | 'pytest-shutil', 27 | 'gitpython', 28 | ] 29 | 30 | tests_require = [ 31 | ] 32 | 33 | entry_points = { 34 | 'pytest11': [ 35 | 'git_repo = pytest_git', 36 | ] 37 | } 38 | 39 | if __name__ == '__main__': 40 | kwargs = common_setup('pytest_git') 41 | kwargs.update(dict( 42 | name='pytest-git', 43 | description='Git repository fixture for py.test', 44 | platforms=['unix', 'linux'], 45 | author='Edward Easton', 46 | author_email='eeaston@gmail.com', 47 | classifiers=classifiers, 48 | install_requires=install_requires, 49 | tests_require=tests_require, 50 | py_modules=['pytest_git'], 51 | entry_points=entry_points, 52 | )) 53 | setup(**kwargs) 54 | -------------------------------------------------------------------------------- /pytest-git/tests/integration/test_git.py: -------------------------------------------------------------------------------- 1 | def test_basic_usage(git_repo): 2 | hello = git_repo.workspace / 'hello.txt' 3 | hello.write_text('hello world!') 4 | git_repo.run('git add hello.txt') 5 | git_repo.api.index.commit("Initial commit") 6 | assert "Initial commit" in git_repo.api.git.log() 7 | # The fixture has a URI property you can use in downstream systems 8 | assert git_repo.uri.startswith('file://') 9 | -------------------------------------------------------------------------------- /pytest-listener/README.md: -------------------------------------------------------------------------------- 1 | # pytest-listener 2 | 3 | Simple JSON listener using TCP that listens for data and stores it in a queue for later retrieval. 4 | 5 | ## Installation 6 | 7 | Install using your favourite package manager: 8 | 9 | ```bash 10 | pip install pytest-listener 11 | # or.. 12 | easy_install pytest-listener 13 | ``` 14 | 15 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 16 | 17 | ```python 18 | pytest_plugins = ['pytest_listener'] 19 | ``` 20 | 21 | ## Basic Test Usage 22 | 23 | Here's a test showing the basic functionality: 24 | 25 | ```python 26 | def test_listener(listener): 27 | data1 = {'foo': 1} 28 | listener.send(some_data) 29 | 30 | data2 = {'bar': 2} 31 | listener.send(some_data) 32 | 33 | assert listener.receive() == data1 34 | assert listener.receive() == data2 35 | 36 | data3 = {'baz': 3} 37 | listener.send(some_data) 38 | 39 | # Clear the listening queue - this deletes data3 40 | listener.clear_queue() 41 | 42 | data2 = {'qux': 4} 43 | listener.send(some_data) 44 | assert listener.recieve() == data3 45 | ``` -------------------------------------------------------------------------------- /pytest-listener/pytest_listener.py: -------------------------------------------------------------------------------- 1 | """pytest: avoid already-imported warning: PYTEST_DONT_REWRITE.""" 2 | 3 | import collections 4 | import json 5 | import logging 6 | import pickle 7 | import socket 8 | import time 9 | from threading import Thread, Event 10 | from time import sleep 11 | 12 | import pytest 13 | from pytest_server_fixtures.base import get_ephemeral_port, get_ephemeral_host 14 | 15 | TERMINATOR = json.dumps(['STOP']).encode('utf-8') 16 | CLEAR = json.dumps(['CLEAR']).encode('utf-8') 17 | TIMEOUT_DEFAULT = 10 18 | DEBUG = False 19 | logger = logging.getLogger('pytest-listener') 20 | logging.basicConfig( 21 | level=logging.INFO, 22 | format='%(asctime)s | %(levelname)-8s | %(name)s | %(message)s', 23 | datefmt='%Y-%m-%d %H:%M:%S', 24 | ) 25 | 26 | 27 | @pytest.yield_fixture(scope='module') 28 | def listener(request): 29 | """ Simple module-scoped network listener. 30 | 31 | Methods 32 | ------- 33 | send(data, timeout): Send data to the listener 34 | recieve(timeout): Recieve data from the listener 35 | clear_queue(): Clear the listener queue 36 | """ 37 | res = Listener() 38 | res.start() 39 | # Wait for socket to become available 40 | time.sleep(1) 41 | yield res 42 | stop_listener(res) 43 | 44 | 45 | def stop_listener(listener): 46 | # the listener is most likely to be blocked on waiting for an accept, 47 | # so send it the STOP message: 48 | s = socket.socket() 49 | s.settimeout(2) 50 | try: 51 | s.connect((listener.host, listener.port)) 52 | s.send(TERMINATOR) 53 | except socket.error: 54 | s.close() 55 | 56 | 57 | class TimedMsg(object): 58 | def __init__(self, value): 59 | self.value = value 60 | self.time = time.time() 61 | 62 | def __str__(self): 63 | return 'TimedMsg: %s (@ %s)' % (str(self.value), self.time) 64 | 65 | def pickled(self): 66 | return pickle.dumps(self) 67 | 68 | 69 | class Listener(Thread): 70 | 71 | def __init__(self, host=None): 72 | super(Listener, self).__init__() 73 | self.host = host or get_ephemeral_host() 74 | self.port = get_ephemeral_port(host=self.host) 75 | self._stop_event = Event() 76 | self.clear_time = None 77 | 78 | self.s = socket.socket() 79 | self.queue = collections.deque() 80 | self.s.bind((self.host, self.port)) 81 | 82 | def run(self): 83 | if DEBUG: 84 | logger.info('listening on %s:%s' % (self.host, self.port)) 85 | self.s.listen(5) 86 | while True: 87 | if self.stopped: 88 | return 89 | c, addr = self.s.accept() 90 | if DEBUG: 91 | logger.info('got connection %s' % str(addr)) 92 | data = c.recv(1024) 93 | if DEBUG: 94 | logger.info('got data: %s' % str(data)) 95 | if data == TERMINATOR: 96 | self.stop() 97 | return 98 | elif data == CLEAR: 99 | if DEBUG: 100 | logger.info('clearing') 101 | self.clear_time = time.time() 102 | else: 103 | self.queue.appendleft(data) 104 | c.close() 105 | 106 | def put_data(self, data): 107 | s = socket.socket() 108 | s.connect((self.host, self.port)) 109 | s.send(data) 110 | s.close() 111 | 112 | def get_data(self): 113 | """ pops the latest off the queue, or None is there is none 114 | """ 115 | try: 116 | data = self.queue.pop() 117 | except IndexError: 118 | return None, None 119 | 120 | try: 121 | data = pickle.loads(data) 122 | except: 123 | try: 124 | data = data.decode('utf-8') 125 | except: 126 | pass 127 | 128 | if DEBUG: 129 | logger.info('got %s' % str(data)) 130 | 131 | t = None 132 | if isinstance(data, TimedMsg): 133 | d = data.value 134 | t = data.time 135 | elif isinstance(data, str): 136 | try: 137 | d = json.loads(data) 138 | except: 139 | d = data 140 | else: 141 | d = data 142 | 143 | return d, t 144 | 145 | def _process_chunk(self, d, t): 146 | if t is not None: 147 | if DEBUG: 148 | logger.info('diff %s' % (t - self.clear_time)) 149 | if t <= self.clear_time: 150 | if DEBUG: 151 | logger.info('%s < %s' % (t, self.clear_time)) 152 | logger.info('discarding cleared %s' % d) 153 | return True 154 | else: 155 | if DEBUG: 156 | logger.info('removed clearing') 157 | self.clear_time = None # unset as we've got one after last clear 158 | else: 159 | if DEBUG: 160 | logger.info('removed clearing (nmsg with no time)') 161 | self.clear_time = None 162 | 163 | return False 164 | 165 | def receive(self, timeout=TIMEOUT_DEFAULT): 166 | if timeout is None: 167 | raise ValueError("timeout cannot be None") 168 | max_count = int(timeout) * 10 169 | d = None 170 | count = 0 171 | while d is None and count < max_count: 172 | 173 | d, t = self.get_data() 174 | if d is None: 175 | sleep(.1) 176 | if timeout is not None: 177 | count += 1 178 | elif self.clear_time is not None and self._process_chunk(d, t): 179 | count = 0 180 | d = None 181 | 182 | return d 183 | 184 | def send(self, data, timeout=TIMEOUT_DEFAULT): # @UnusedVariable 185 | payload = TimedMsg(data).pickled() 186 | if DEBUG: 187 | logger.info('sending %s' % str(data)) 188 | self.put_data(payload) 189 | 190 | def clear_queue(self): 191 | self.put_data(CLEAR) 192 | time.sleep(.05) 193 | 194 | def stop(self): 195 | try: 196 | self.s.shutdown(socket.SHUT_WR) 197 | except OSError: 198 | pass 199 | self.s.close() 200 | self._stop_event.set() 201 | 202 | @property 203 | def stopped(self): 204 | return self._stop_event.isSet() 205 | 206 | 207 | if __name__ == '__main__': 208 | import sys 209 | DEBUG = True 210 | listener = Listener('localhost') 211 | 212 | listener.start() 213 | while not listener.stopped: 214 | try: 215 | sleep(.1) 216 | except KeyboardInterrupt: 217 | sys.exit(1) 218 | -------------------------------------------------------------------------------- /pytest-listener/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-listener/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup, find_packages 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Operating System :: Microsoft :: Windows', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | ] 20 | 21 | install_requires = ['pytest', 22 | 'pytest-server-fixtures' 23 | ] 24 | 25 | tests_require = [] 26 | 27 | entry_points = { 28 | 'pytest11': [ 29 | 'listener = pytest_listener', 30 | ] 31 | } 32 | 33 | if __name__ == '__main__': 34 | kwargs = common_setup('pytest_listener') 35 | kwargs.update(dict( 36 | name='pytest-listener', 37 | description='A simple network listener', 38 | author='Tim Couper', 39 | author_email='drtimcouper@gmail.com', 40 | classifiers=classifiers, 41 | install_requires=install_requires, 42 | tests_require=tests_require, 43 | py_modules=['pytest_listener'], 44 | entry_points=entry_points, 45 | )) 46 | setup(**kwargs) 47 | -------------------------------------------------------------------------------- /pytest-listener/tests/integration/test_listener.py: -------------------------------------------------------------------------------- 1 | import pytest_listener as li 2 | 3 | RECEIVE_TIMEOUT = 10 4 | 5 | 6 | def test_send_method(listener): 7 | obj = ['hello', 'world'] 8 | listener.send(obj) 9 | d = listener.receive(RECEIVE_TIMEOUT) 10 | assert d == obj 11 | 12 | 13 | def test_send_method_again(listener): 14 | test_send_method(listener) 15 | 16 | 17 | def test_multiple_data(listener): 18 | obj1 = ['hello', 'world'] 19 | listener.send(obj1) 20 | obj2 = 'fred' 21 | listener.send(obj2) 22 | 23 | d = listener.receive(RECEIVE_TIMEOUT) 24 | assert d == obj1 25 | d = listener.receive(RECEIVE_TIMEOUT) 26 | assert d == obj2 27 | 28 | 29 | def test_send_halfway_data(listener): 30 | li.DEBUG = True 31 | try: 32 | obj1 = ['first', 'message'] 33 | listener.send(obj1) 34 | 35 | obj2 = 'second' 36 | listener.send(obj2) 37 | 38 | d = listener.receive(RECEIVE_TIMEOUT) 39 | assert d == obj1 40 | 41 | listener.clear_queue() 42 | 43 | obj3 = 'third' 44 | listener.send(obj3) 45 | d = listener.receive(RECEIVE_TIMEOUT) 46 | assert d == obj3 # demonstrating that obj2 has been "removed" 47 | 48 | finally: 49 | li.DEBUG = False 50 | -------------------------------------------------------------------------------- /pytest-profiling/README.md: -------------------------------------------------------------------------------- 1 | # Pytest Profiling Plugin 2 | 3 | Profiling plugin for pytest, with tabular and heat graph output. 4 | 5 | Tests are profiled with [cProfile](http://docs.python.org/library/profile.html#module-cProfile) and analysed with [pstats](http://docs.python.org/library/profile.html#pstats.Stats); heat graphs are 6 | generated using [gprof2dot](https://github.com/jrfonseca/gprof2dot) and [dot](http://www.graphviz.org/). 7 | 8 | ![](https://cdn.rawgit.com/manahl/pytest-plugins/master/pytest-profiling/docs/static/profile_combined.svg) 9 | 10 | 11 | ## Installation 12 | 13 | Install using your favourite package installer: 14 | ```bash 15 | pip install pytest-profiling 16 | # or 17 | easy_install pytest-profiling 18 | ``` 19 | 20 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 21 | 22 | ```python 23 | pytest_plugins = ['pytest_profiling'] 24 | ``` 25 | 26 | ## Usage 27 | 28 | Once installed, the plugin provides extra options to pytest: 29 | 30 | ```bash 31 | $ py.test --help 32 | ... 33 | Profiling: 34 | --profile generate profiling information 35 | --profile-svg generate profiling graph (using gprof2dot and dot 36 | -Tsvg) 37 | ``` 38 | 39 | The ``--profile`` and ``profile-svg`` options can be combined with any other option: 40 | 41 | 42 | ```bash 43 | $ py.test tests/unit/test_logging.py --profile 44 | ============================= test session starts ============================== 45 | platform linux2 -- Python 2.6.2 -- pytest-2.2.3 46 | collected 3 items 47 | 48 | tests/unit/test_logging.py ... 49 | Profiling (from prof/combined.prof): 50 | Fri Oct 26 11:05:00 2012 prof/combined.prof 51 | 52 | 289 function calls (278 primitive calls) in 0.001 CPU seconds 53 | 54 | Ordered by: cumulative time 55 | List reduced from 61 to 20 due to restriction <20> 56 | 57 | ncalls tottime percall cumtime percall filename:lineno(function) 58 | 3 0.000 0.000 0.001 0.000 :1() 59 | 6/3 0.000 0.000 0.001 0.000 core.py:344(execute) 60 | 3 0.000 0.000 0.001 0.000 python.py:63(pytest_pyfunc_call) 61 | 1 0.000 0.000 0.001 0.001 test_logging.py:34(test_flushing) 62 | 1 0.000 0.000 0.000 0.000 _startup.py:23(_flush) 63 | 2 0.000 0.000 0.000 0.000 mock.py:979(__call__) 64 | 2 0.000 0.000 0.000 0.000 mock.py:986(_mock_call) 65 | 4 0.000 0.000 0.000 0.000 mock.py:923(_get_child_mock) 66 | 6 0.000 0.000 0.000 0.000 mock.py:512(__new__) 67 | 2 0.000 0.000 0.000 0.000 mock.py:601(__get_return_value) 68 | 4 0.000 0.000 0.000 0.000 mock.py:695(__getattr__) 69 | 6 0.000 0.000 0.000 0.000 mock.py:961(__init__) 70 | 22/14 0.000 0.000 0.000 0.000 mock.py:794(__setattr__) 71 | 6 0.000 0.000 0.000 0.000 core.py:356(getkwargs) 72 | 6 0.000 0.000 0.000 0.000 mock.py:521(__init__) 73 | 3 0.000 0.000 0.000 0.000 skipping.py:122(pytest_pyfunc_call) 74 | 6 0.000 0.000 0.000 0.000 core.py:366(varnames) 75 | 3 0.000 0.000 0.000 0.000 skipping.py:125(check_xfail_no_run) 76 | 2 0.000 0.000 0.000 0.000 mock.py:866(assert_called_once_with) 77 | 6 0.000 0.000 0.000 0.000 mock.py:645(__set_side_effect) 78 | 79 | 80 | =========================== 3 passed in 0.13 seconds =========================== 81 | ``` 82 | 83 | `pstats` files (one per test item) are retained for later analysis in `prof` directory, along with a `combined.prof` file: 84 | 85 | ```bash 86 | $ ls -1 prof/ 87 | combined.prof 88 | test_app.prof 89 | test_flushing.prof 90 | test_import.prof 91 | ``` 92 | 93 | By default the `pstats` files are named after their corresponding test name, with illegal filesystem characters replaced by underscores. 94 | If the full path is longer that operating system allows then it will be renamed to first 4 bytes of an md5 hash of the test name: 95 | 96 | ```bash 97 | $ ls -1 prof/ 98 | combined.prof 99 | test_not_longer_than_max_allowed.prof 100 | 68b329da.prof 101 | ``` 102 | 103 | If the ``--profile-svg`` option is given, along with the prof files and tabular output a svg file will be generated: 104 | 105 | ```bash 106 | $ py.test tests/unit/test_logging.py --profile-svg 107 | ... 108 | SVG profile in prof/combined.svg. 109 | ``` 110 | 111 | This is best viewed with a good svg viewer e.g. Chrome. 112 | -------------------------------------------------------------------------------- /pytest-profiling/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | tests/integration/profile/tests 10 | 11 | [bdist_wheel] 12 | universal = 0 13 | -------------------------------------------------------------------------------- /pytest-profiling/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 5 | 6 | from setuptools import setup 7 | 8 | from common_setup import common_setup 9 | 10 | classifiers = [ 11 | 'License :: OSI Approved :: MIT License', 12 | 'Development Status :: 5 - Production/Stable', 13 | 'Topic :: Software Development :: Libraries', 14 | 'Topic :: Software Development :: Testing', 15 | 'Topic :: Utilities', 16 | 'Intended Audience :: Developers', 17 | 'Operating System :: POSIX', 18 | 'Programming Language :: Python :: 3.6', 19 | 'Programming Language :: Python :: 3.7', 20 | 'Programming Language :: Python :: 3.8', 21 | 'Programming Language :: Python :: 3.9', 22 | 'Programming Language :: Python :: 3.10', 23 | 'Programming Language :: Python :: 3.11', 24 | 'Programming Language :: Python :: 3.12', 25 | ] 26 | 27 | install_requires = ['pytest', 28 | 'gprof2dot', 29 | ] 30 | 31 | tests_require = [ 32 | 'pytest-virtualenv', 33 | ] 34 | 35 | entry_points = { 36 | 'pytest11': [ 37 | 'profiling = pytest_profiling', 38 | ] 39 | } 40 | 41 | if __name__ == '__main__': 42 | kwargs = common_setup('pytest_profiling') 43 | kwargs.update(dict( 44 | name='pytest-profiling', 45 | description='Profiling plugin for py.test', 46 | author='Ed Catmur', 47 | author_email='ed@catmur.co.uk', 48 | classifiers=classifiers, 49 | install_requires=install_requires, 50 | tests_require=tests_require, 51 | py_modules=['pytest_profiling'], 52 | entry_points=entry_points, 53 | )) 54 | setup(**kwargs) 55 | -------------------------------------------------------------------------------- /pytest-profiling/tests/integration/profile/tests/unit/test_chdir.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def test_chdir(): 4 | os.mkdir('foo') 5 | os.chdir('foo') 6 | -------------------------------------------------------------------------------- /pytest-profiling/tests/integration/profile/tests/unit/test_example.py: -------------------------------------------------------------------------------- 1 | def test_foo(): 2 | from time import sleep 3 | sleep(0.1) # make sure we register in profiling 4 | -------------------------------------------------------------------------------- /pytest-profiling/tests/integration/profile/tests/unit/test_long_name.py: -------------------------------------------------------------------------------- 1 | def test_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_long_test(): 2 | pass 3 | -------------------------------------------------------------------------------- /pytest-profiling/tests/integration/test_profile_integration.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import sys 3 | 4 | from pkg_resources import resource_filename, get_distribution 5 | import pytest 6 | 7 | from pytest_virtualenv import VirtualEnv 8 | 9 | 10 | @pytest.fixture(scope="session") 11 | def virtualenv(): 12 | with VirtualEnv() as venv: 13 | test_dir = resource_filename("pytest_profiling", "tests/integration/profile") 14 | venv.install_package("more-itertools") 15 | venv.install_package("pytest=={}".format(get_distribution("pytest").version)) 16 | venv.install_package("pytest-cov") 17 | venv.install_package(resource_filename("pytest_profiling", ".")) 18 | 19 | pyversion = sys.version_info 20 | if (pyversion.major, pyversion.minor) < (3, 8): 21 | import distutils.dir_util 22 | distutils.dir_util.copy_tree(str(test_dir), str(venv.workspace)) 23 | else: 24 | shutil.copytree(str(test_dir), str(venv.workspace), dirs_exist_ok=True) 25 | shutil.rmtree( 26 | venv.workspace / "tests" / "unit" / "__pycache__", ignore_errors=True 27 | ) 28 | yield venv 29 | 30 | 31 | def test_profile_profiles_tests(pytestconfig, virtualenv): 32 | output = virtualenv.run_with_coverage( 33 | ["-m", "pytest", "--profile", "tests/unit/test_example.py"], 34 | pytestconfig, 35 | cd=virtualenv.workspace, 36 | ) 37 | assert "test_example.py:1(test_foo)" in output 38 | 39 | 40 | def test_profile_generates_svg(pytestconfig, virtualenv): 41 | output = virtualenv.run_with_coverage( 42 | ["-m", "pytest", "--profile-svg", "tests/unit/test_example.py"], 43 | pytestconfig, 44 | cd=virtualenv.workspace, 45 | ) 46 | assert any( 47 | [ 48 | "test_example:1:test_foo" in i 49 | for i in (virtualenv.workspace / "prof/combined.svg").open().readlines() 50 | ] 51 | ) 52 | 53 | assert "test_example.py:1(test_foo)" in output 54 | assert "SVG" in output 55 | 56 | 57 | def test_profile_long_name(pytestconfig, virtualenv): 58 | output = virtualenv.run_with_coverage( 59 | ["-m", "pytest", "--profile", "tests/unit/test_long_name.py"], 60 | pytestconfig, 61 | cd=virtualenv.workspace, 62 | ) 63 | assert (virtualenv.workspace / "prof/fbf7dc37.prof").is_file() 64 | 65 | 66 | def test_profile_chdir(pytestconfig, virtualenv): 67 | output = virtualenv.run_with_coverage( 68 | ["-m", "pytest", "--profile", "tests/unit/test_chdir.py"], 69 | pytestconfig, 70 | cd=virtualenv.workspace, 71 | ) 72 | -------------------------------------------------------------------------------- /pytest-profiling/tests/unit/test_profile.py: -------------------------------------------------------------------------------- 1 | # HACK: if the profile plugin is imported before the coverage plugin then all 2 | # the top-level code in pytest_profiling will be omitted from 3 | # coverage, so force it to be reloaded within this test unit under coverage 4 | 5 | import importlib 6 | import os.path 7 | 8 | import pytest_profiling 9 | 10 | importlib.reload(pytest_profiling) 11 | 12 | import os 13 | import subprocess 14 | 15 | from pytest_profiling import Profiling, pytest_addoption, pytest_configure 16 | 17 | try: 18 | from unittest.mock import Mock, ANY, patch, sentinel, call 19 | except ImportError: 20 | # python 2 21 | from mock import Mock, ANY, patch, sentinel 22 | 23 | 24 | def test_creates_prof_dir(): 25 | with patch("os.makedirs", side_effect=OSError) as makedirs: 26 | Profiling(False).pytest_sessionstart(Mock()) 27 | makedirs.assert_called_with("prof") 28 | 29 | 30 | def test_combines_profs(): 31 | plugin = Profiling(False) 32 | plugin.profs = [sentinel.prof0, sentinel.prof1] 33 | with patch("pstats.Stats") as Stats: 34 | plugin.pytest_sessionfinish(Mock(), Mock()) 35 | Stats.assert_called_once_with(sentinel.prof0) 36 | Stats.return_value.add.assert_called_once_with(sentinel.prof1) 37 | assert Stats.return_value.dump_stats.called 38 | 39 | 40 | def test_generates_svg(): 41 | plugin = Profiling(True) 42 | plugin.gprof2dot = "/somewhere/gprof2dot" 43 | plugin.profs = [sentinel.prof] 44 | popen1 = Mock( 45 | communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0), returncode=0 46 | ) 47 | popen2 = Mock( 48 | communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0), returncode=0 49 | ) 50 | with patch("pstats.Stats"): 51 | with patch("subprocess.Popen") as popen: 52 | popen.return_value.__enter__.side_effect = [popen1, popen2] 53 | plugin.pytest_sessionfinish(Mock(), Mock()) 54 | popen.assert_any_call( 55 | ["dot", "-Tsvg", "-o", f"{os.getcwd()}/prof/combined.svg"], 56 | stdin=popen1.stdout, 57 | stdout=subprocess.PIPE, 58 | stderr=subprocess.PIPE, 59 | ) 60 | popen.assert_any_call( 61 | ["/somewhere/gprof2dot", "-f", "pstats", f"{os.getcwd()}/prof/combined.prof"], 62 | stdout=subprocess.PIPE, 63 | ) 64 | 65 | 66 | def test_writes_summary(): 67 | plugin = Profiling(False) 68 | plugin.profs = [sentinel.prof] 69 | terminalreporter, stats = Mock(), Mock() 70 | with patch("pstats.Stats", return_value=stats) as Stats: 71 | plugin.pytest_sessionfinish(Mock(), Mock()) 72 | plugin.pytest_terminal_summary(terminalreporter) 73 | combined = os.path.abspath( 74 | os.path.join(os.path.curdir, "prof", "combined.prof")) 75 | assert "Profiling" in terminalreporter.write.call_args[0][0] 76 | Stats.assert_called_with(combined, stream=terminalreporter) 77 | 78 | 79 | def test_writes_summary_svg(): 80 | plugin = Profiling(True) 81 | plugin.profs = [sentinel.prof] 82 | terminalreporter = Mock() 83 | popen1 = Mock( 84 | communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0), returncode=0 85 | ) 86 | popen2 = Mock( 87 | communicate=Mock(return_value=[None, None]), poll=Mock(return_value=0), returncode=0 88 | ) 89 | with patch("pstats.Stats"): 90 | with patch("subprocess.Popen") as popen: 91 | popen.return_value.__enter__.side_effect = [popen1, popen2] 92 | plugin.pytest_sessionfinish(Mock(), Mock()) 93 | plugin.pytest_terminal_summary(terminalreporter) 94 | assert "SVG" in terminalreporter.write.call_args[0][0] 95 | 96 | 97 | def test_adds_options(): 98 | parser = Mock() 99 | pytest_addoption(parser) 100 | parser.getgroup.assert_called_with("Profiling") 101 | group = parser.getgroup.return_value 102 | group.addoption.assert_any_call("--profile", action="store_true", help=ANY) 103 | group.addoption.assert_any_call("--profile-svg", action="store_true", help=ANY) 104 | 105 | 106 | def test_configures(): 107 | config = Mock(getvalue=lambda x: x == "profile") 108 | with patch("pytest_profiling.Profiling") as Profiling: 109 | pytest_configure(config) 110 | config.pluginmanager.register.assert_called_with(Profiling.return_value) 111 | 112 | 113 | def test_clean_filename(): 114 | assert pytest_profiling.clean_filename("a:b/c\256d") == "a_b_c_d" 115 | -------------------------------------------------------------------------------- /pytest-pyramid-server/.gitignore: -------------------------------------------------------------------------------- 1 | /vx/ 2 | -------------------------------------------------------------------------------- /pytest-pyramid-server/README.md: -------------------------------------------------------------------------------- 1 | # Py.test Pyramid Server Fixture 2 | 3 | Pyramid server fixture for py.test. The server is session-scoped by default 4 | and run in a subprocess and temp dir, and as such is a 'real' server that you 5 | can point a Selenium webdriver at. 6 | 7 | ## Installation 8 | 9 | Install using your favourite package manager: 10 | 11 | ```bash 12 | pip install pytest-pyramid-server 13 | # or.. 14 | easy_install pytest-pyramid-server 15 | ``` 16 | 17 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 18 | 19 | ```python 20 | pytest_plugins = ['pytest_pyramid_server'] 21 | ``` 22 | 23 | ## Configuration 24 | 25 | This fixture searches for its configuration in the current working directory 26 | called 'testing.ini'. All .ini files in the cwd will be copied to the tempdir 27 | so that paster-style config chaining still works. For example: 28 | 29 | my-pyramid-app/ 30 | src/ # Project code is in here 31 | setup.py # Project setup.py 32 | development.ini # Development settings 33 | production.ini # Production settings 34 | testing.ini # Testing settings, will be used if tests 35 | # are invoked using 'py.test' from this 36 | # directory 37 | 38 | ## Example 39 | 40 | Here's a noddy test case showing the main functionality: 41 | 42 | ```python 43 | def test_pyramid_server(pyramid_server): 44 | # This is the http://{host}:{port} of the running server. It will attempt to resolve 45 | # to externally accessable IPs so a web browser can access it. 46 | assert pyramid_server.uri.startswith('http') 47 | 48 | # GET a document from the server. 49 | assert pyramid_server.get('/orders/macbooks', as_json=True) == {'id-1234': 'MPB-15inch'} 50 | 51 | # POST a document to the server. 52 | assert pyramid_server.post('/login', 'guest:password123').response_code == 200 53 | 54 | # path ``path`` object to the running config file (see https://path.readthedocs.io/) 55 | assert pyramid_server.working_config.endswith('testing.ini') 56 | ``` 57 | 58 | ## `PyramidServer` class 59 | 60 | Using this with the default `pyramid_server` py.test fixture is good enough for a lot of 61 | use-cases however you may wish to have more fine-grained control about the server configuration. 62 | To do this you can use the underlying server class directly - this is an implenentation of the 63 | `pytest-server-fixture` framework and as such acts as a context manager: 64 | 65 | ```python 66 | from pytest_pyramid import PyramidTestServer 67 | 68 | def test_custom_server(): 69 | with PyramidTestServer( 70 | # You can specify you own config directory and name 71 | config_dir='/my/config', 72 | config_fileme='my_testing.ini', 73 | 74 | # You can set arbitrary config variables in the constructor 75 | extra_config_vars={'my_config_section': {'my_dbname: 'foo', 76 | 'my_dbpass: 'bar'}} 77 | ) as server: 78 | assert not server.dead 79 | assert 'my_dbname = foo' in server.working_config.read_text() 80 | 81 | # Server should now be dead 82 | assert server.dead 83 | ``` 84 | 85 | ## `pytest-webdriver` and [PageObjects](https://page-objects.readthedocs.org/en/latest/) integration 86 | 87 | The `pytest-webdriver` plugin will detect when this plugin is active and set its default base 88 | URL to the url of the running server. This is a nice way of avoiding lots of string manipulation 89 | in your browser tests when using Page Objects: 90 | 91 | ```python 92 | from page_objects import PageObject, PageElement 93 | 94 | class LoginPage(PageObject): 95 | username = PageElement(id_='username') 96 | password = PageElement(name='password') 97 | login = PageElement(css='input[type="submit"]') 98 | 99 | def test_login_page(webdriver, pyramid_server): 100 | page = LoginPage(webdriver) 101 | page.login.click() 102 | page.get('/foo/bar') 103 | assert webdriver.getCurrentUrl() == pyramid_server.uri + '/foo/bar' 104 | ``` 105 | -------------------------------------------------------------------------------- /pytest-pyramid-server/pyramid_server_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Entrance point for the integration tests 3 | # 4 | from pyramid.response import Response 5 | from pyramid.config import Configurator 6 | 7 | 8 | def main(global_config, **settings): 9 | config = Configurator(settings=settings,) 10 | config.add_route('home', 'test') 11 | config.add_view(lambda request: Response('OK'), route_name='home') 12 | return config.make_wsgi_app() 13 | -------------------------------------------------------------------------------- /pytest-pyramid-server/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-pyramid-server/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Framework :: Pyramid', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | ] 20 | 21 | 22 | install_requires = ['pytest-server-fixtures', 23 | 'pytest', 24 | 'pyramid', 25 | 'waitress', 26 | ] 27 | 28 | tests_require = [ 29 | 'pyramid-debugtoolbar', 30 | ] 31 | 32 | entry_points = { 33 | 'pytest11': [ 34 | 'pyramid_server = pytest_pyramid_server', 35 | ], 36 | 'paste.app_factory': [ 37 | 'pyramid_server_test = pyramid_server_test:main', 38 | ], 39 | } 40 | 41 | if __name__ == '__main__': 42 | kwargs = common_setup('pytest_pyramid_server') 43 | kwargs.update(dict( 44 | name='pytest-pyramid-server', 45 | description='Pyramid server fixture for py.test', 46 | author='Edward Easton', 47 | author_email='eeaston@gmail.com', 48 | classifiers=classifiers, 49 | install_requires=install_requires, 50 | tests_require=tests_require, 51 | py_modules=['pytest_pyramid_server', 'pyramid_server_test'], 52 | entry_points=entry_points, 53 | )) 54 | setup(**kwargs) 55 | -------------------------------------------------------------------------------- /pytest-pyramid-server/tests/integration/config/testing.ini: -------------------------------------------------------------------------------- 1 | [app:main] 2 | use = egg:pytest-pyramid-server#pyramid_server_test 3 | url_prefix = test 4 | 5 | [server:main] 6 | use = egg:waitress#main 7 | -------------------------------------------------------------------------------- /pytest-pyramid-server/tests/integration/test_pyramid_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pytest_pyramid_server import InlinePyramidTestServer, PyramidTestServer 4 | 5 | CONFIG_DIR = os.path.dirname(__file__) + '/config' 6 | 7 | 8 | def test_InlinePyramidTestServer(): 9 | ipts = InlinePyramidTestServer(config_dir=CONFIG_DIR) 10 | ipts.start() 11 | assert ipts.check_server_up() 12 | ipts.kill() 13 | assert not ipts.check_server_up() 14 | 15 | 16 | def test_PyramidTestServer(): 17 | pts = PyramidTestServer(config_dir=CONFIG_DIR) 18 | pts.start() 19 | assert pts.check_server_up() 20 | pts.kill() 21 | assert not pts.check_server_up() 22 | -------------------------------------------------------------------------------- /pytest-qt-app/README.md: -------------------------------------------------------------------------------- 1 | # Pytest QT Fixture 2 | 3 | Set up a Q Application for QT with an X-Window Virtual Framebuffer (Xvfb). 4 | 5 | ## Installation 6 | 7 | Install using your favourite package installer: 8 | ```bash 9 | pip install pytest-qt-app 10 | # or 11 | easy_install pytest-qt-app 12 | ``` 13 | 14 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 15 | 16 | ```python 17 | pytest_plugins = ['pytest_qt_app'] 18 | ``` 19 | ## Configuration 20 | 21 | The fixtures are configured using the following evironment variables: 22 | 23 | | Setting | Description | Default 24 | | ------- | ----------- | ------- 25 | | SERVER_FIXTURES_XVFB | Xvfb server executable | `/usr/bin/Xvfb` 26 | 27 | ## Usage 28 | 29 | Here's a little test that shows it working: 30 | 31 | ```python 32 | from PyQt4 import Qtgui 33 | 34 | def test_q_application(q_application): 35 | # This shows the display is connected properly to the Xvfb 36 | assert QtGui.QX11Info.display() 37 | ``` -------------------------------------------------------------------------------- /pytest-qt-app/pytest_qt_app.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import os 3 | import pytest 4 | 5 | 6 | from pytest_shutil.env import set_env 7 | from pytest_server_fixtures.xvfb import XvfbServer 8 | 9 | 10 | class _TestQtApp(object): 11 | app = None 12 | 13 | 14 | TestQtApp = _TestQtApp() 15 | 16 | 17 | @pytest.yield_fixture(scope="session") 18 | def q_application(): 19 | """ Initialise a QT application with a Xvfb server 20 | """ 21 | try: 22 | from PyQt4 import QtGui 23 | except ImportError: 24 | pytest.skip('PyQT4 not installed, skipping test') 25 | 26 | global TestQtApp 27 | assert hasattr(TestQtApp, 'app'), "Can only initialize QApplication once per process" 28 | 29 | if TestQtApp.app is None: 30 | # TODO: investigate if this is still the case, if not just use the regular xvfb_server fixture 31 | if 'PYDEV_CONSOLE_ENCODING' in os.environ: 32 | # PyDev destroys session scoped fixtures after each test, so we can't clean up the XvfbServer 33 | global server 34 | server = XvfbServer() 35 | with set_env(XAUTHORITY=server.authfile, DISPLAY=server.display): 36 | TestQtApp.app = QtGui.QApplication([__name__, '-display', server.display]) 37 | yield TestQtApp 38 | else: 39 | with XvfbServer() as server: 40 | with set_env(XAUTHORITY=server.authfile, DISPLAY=server.display): 41 | TestQtApp.app = QtGui.QApplication([__name__, '-display', server.display]) 42 | yield TestQtApp 43 | TestQtApp.app.exit() 44 | del TestQtApp.app 45 | gc.collect() 46 | 47 | else: 48 | yield TestQtApp 49 | -------------------------------------------------------------------------------- /pytest-qt-app/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-qt-app/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Software Development :: User Interfaces', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Programming Language :: Python :: 3.6', 17 | 'Programming Language :: Python :: 3.7', 18 | ] 19 | 20 | install_requires = ['pytest', 21 | 'pytest-server-fixtures', 22 | 'pytest-shutil', 23 | ] 24 | 25 | tests_require = ['pytest-cov' 26 | ] 27 | 28 | entry_points = { 29 | 'pytest11': [ 30 | 'qt = pytest_qt_app', 31 | ] 32 | } 33 | 34 | if __name__ == '__main__': 35 | kwargs = common_setup('pytest_qt_app') 36 | kwargs.update(dict( 37 | name='pytest-qt-app', 38 | description='QT app fixture for py.test', 39 | author='Edward Easton', 40 | author_email='eeaston@gmail.com', 41 | classifiers=classifiers, 42 | install_requires=install_requires, 43 | tests_require=tests_require, 44 | py_modules=['pytest_qt_app'], 45 | entry_points=entry_points, 46 | )) 47 | setup(**kwargs) 48 | -------------------------------------------------------------------------------- /pytest-qt-app/tests/integration/test_q_application.py: -------------------------------------------------------------------------------- 1 | 2 | def test_q_application(q_application): 3 | from PyQt4 import QtGui 4 | assert QtGui.QX11Info.display() 5 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import os 3 | 4 | from pytest_fixture_config import Config 5 | from .util import get_random_id 6 | 7 | 8 | SESSION_ID_LEN = 8 9 | 10 | class FixtureConfig(Config): 11 | __slots__ = ( 12 | 'java_executable', 13 | 'jenkins_url', 14 | 'jenkins_war', 15 | 'jenkins_image', 16 | 'minio_executable', 17 | 'minio_image', 18 | 'mongo_bin', 19 | 'mongo_image', 20 | 'pg_config_executable', 21 | 'redis_executable', 22 | 'redis_image', 23 | 'httpd_executable', 24 | 'httpd_image', 25 | 'httpd_modules', 26 | 'fixture_hostname', 27 | 'xvfb_executable', 28 | 'disable_proxy', 29 | 'server_class', 30 | 'session_id', 31 | 'k8s_namespace', 32 | 'k8s_local_test' 33 | ) 34 | 35 | # Default values for system resource locations - patch this to change defaults 36 | try: 37 | DEFAULT_SERVER_FIXTURES_HOSTNAME = socket.gethostbyname(socket.gethostname()) 38 | except socket.gaierror: 39 | DEFAULT_SERVER_FIXTURES_HOSTNAME = '127.0.0.1' 40 | DEFAULT_SERVER_FIXTURES_SESSION_ID = get_random_id(SESSION_ID_LEN) 41 | DEFAULT_SERVER_FIXTURES_DISABLE_HTTP_PROXY = True 42 | DEFAULT_SERVER_FIXTURES_SERVER_CLASS = 'thread' 43 | DEFAULT_SERVER_FIXTURES_K8S_NAMESPACE = None 44 | DEFAULT_SERVER_FIXTURES_K8S_LOCAL_TEST = False 45 | DEFAULT_SERVER_FIXTURES_JAVA = 'java' 46 | DEFAULT_SERVER_FIXTURES_JENKINS_URL = 'http://acmejenkins.example.com' 47 | DEFAULT_SERVER_FIXTURES_JENKINS_WAR = '/usr/share/jenkins/jenkins.war' 48 | DEFAULT_SERVER_FIXTURES_JENKINS_IMAGE = 'jenkins/jenkins:2.138.3-alpine' 49 | DEFAULT_SERVER_FIXTURES_MINIO = 'minio' 50 | DEFAULT_SERVER_FIXTURES_MINIO_IMAGE = 'minio/minio:latest' 51 | DEFAULT_SERVER_FIXTURES_MONGO_BIN = 'mongod' 52 | DEFAULT_SERVER_FIXTURES_MONGO_IMAGE = 'mongo:3.6' 53 | DEFAULT_SERVER_FIXTURES_PG_CONFIG = 'pg_config' 54 | DEFAULT_SERVER_FIXTURES_REDIS = 'redis-server' 55 | DEFAULT_SERVER_FIXTURES_REDIS_IMAGE = 'redis:5.0.2-alpine' 56 | DEFAULT_SERVER_FIXTURES_HTTPD = 'apache2' 57 | DEFAULT_SERVER_FIXTURES_HTTPD_IMAGE = 'httpd:2.4.37' 58 | DEFAULT_SERVER_FIXTURES_HTTPD_MODULES = '/usr/lib/apache2/modules' 59 | DEFAULT_SERVER_FIXTURES_XVFB = 'Xvfb' 60 | 61 | 62 | # Global config for finding system resources. 63 | CONFIG = FixtureConfig( 64 | # Not using localhost here in case we are being used in a cluster-type job 65 | fixture_hostname=os.getenv('SERVER_FIXTURES_HOSTNAME', DEFAULT_SERVER_FIXTURES_HOSTNAME), 66 | disable_proxy=os.getenv('SERVER_FIXTURES_DISABLE_HTTP_PROXY', DEFAULT_SERVER_FIXTURES_DISABLE_HTTP_PROXY), 67 | server_class=os.getenv('SERVER_FIXTURES_SERVER_CLASS', DEFAULT_SERVER_FIXTURES_SERVER_CLASS), 68 | k8s_namespace=os.getenv('SERVER_FIXTURES_K8S_NAMESPACE', DEFAULT_SERVER_FIXTURES_K8S_NAMESPACE), 69 | k8s_local_test=os.getenv('SERVER_FIXTURES_K8S_LOCAL_TEST', DEFAULT_SERVER_FIXTURES_K8S_LOCAL_TEST), 70 | session_id=os.getenv('SERVER_FIXTURES_SESSION_ID', DEFAULT_SERVER_FIXTURES_SESSION_ID), 71 | java_executable=os.getenv('SERVER_FIXTURES_JAVA', DEFAULT_SERVER_FIXTURES_JAVA), 72 | jenkins_war=os.getenv('SERVER_FIXTURES_JENKINS_WAR', DEFAULT_SERVER_FIXTURES_JENKINS_WAR), 73 | jenkins_image=os.getenv('SERVER_FIXTURES_JENKINS_IMAGE', DEFAULT_SERVER_FIXTURES_JENKINS_IMAGE), 74 | minio_executable=os.getenv('SERVER_FIXTURES_MINIO', DEFAULT_SERVER_FIXTURES_MINIO), 75 | minio_image=os.getenv('SERVER_FIXTURES_MINIO_IMAGE', DEFAULT_SERVER_FIXTURES_MINIO_IMAGE), 76 | mongo_bin=os.getenv('SERVER_FIXTURES_MONGO_BIN', DEFAULT_SERVER_FIXTURES_MONGO_BIN), 77 | mongo_image=os.getenv('SERVER_FIXTURES_MONGO_IMAGE', DEFAULT_SERVER_FIXTURES_MONGO_IMAGE), 78 | pg_config_executable=os.getenv('SERVER_FIXTURES_PG_CONFIG', DEFAULT_SERVER_FIXTURES_PG_CONFIG), 79 | redis_executable=os.getenv('SERVER_FIXTURES_REDIS', DEFAULT_SERVER_FIXTURES_REDIS), 80 | redis_image=os.getenv('SERVER_FIXTURES_REDIS_IMAGE', DEFAULT_SERVER_FIXTURES_REDIS_IMAGE), 81 | httpd_executable=os.getenv('SERVER_FIXTURES_HTTPD', DEFAULT_SERVER_FIXTURES_HTTPD), 82 | httpd_modules=os.getenv('SERVER_FIXTURES_HTTPD_MODULES', DEFAULT_SERVER_FIXTURES_HTTPD_MODULES), 83 | httpd_image=os.getenv('SERVER_FIXTURES_HTTPD_IMAGE', DEFAULT_SERVER_FIXTURES_HTTPD_IMAGE), 84 | xvfb_executable=os.getenv('SERVER_FIXTURES_XVFB', DEFAULT_SERVER_FIXTURES_XVFB), 85 | ) 86 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/http.py: -------------------------------------------------------------------------------- 1 | import http.client 2 | import os 3 | import socket 4 | import logging 5 | import time 6 | import sys 7 | 8 | import pytest 9 | import requests 10 | from contextlib import contextmanager 11 | 12 | from pytest_shutil.env import unset_env 13 | from pytest_server_fixtures import CONFIG 14 | from .base import TestServer 15 | 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | @pytest.yield_fixture 21 | def simple_http_test_server(): 22 | """ Function-scoped py.test fixture to serve up a directory via HTTP. 23 | """ 24 | with SimpleHTTPTestServer() as s: 25 | s.start() 26 | yield s 27 | 28 | 29 | class HTTPTestServer(TestServer): 30 | # Bind to all sockets when creating the web-server, for selenium tests 31 | hostname = '0.0.0.0' 32 | 33 | def __init__(self, uri=None, **kwargs): 34 | self._uri = uri 35 | super(HTTPTestServer, self).__init__(**kwargs) 36 | 37 | @property 38 | def uri(self): 39 | if self._uri: 40 | return self._uri 41 | return "http://%s:%s" % (self.hostname, self.port) 42 | 43 | @contextmanager 44 | def handle_proxy(self): 45 | if CONFIG.disable_proxy: 46 | with unset_env(['http_proxy', 'https_proxy', 'HTTP_PROXY', ' HTTPS_PROXY']): 47 | yield 48 | else: 49 | yield 50 | 51 | def check_server_up(self): 52 | """ Check the server is up by polling self.uri 53 | """ 54 | try: 55 | log.debug('accessing URL: {0}'.format(self.uri)) 56 | with self.handle_proxy(): 57 | resp = requests.get(self.uri) 58 | acceptable_codes = (200, 403) # 403 server probably running in secure mode... 59 | log.debug('Querying %s received response code %s' % (self.uri, resp.status_code)) 60 | return resp.status_code in acceptable_codes 61 | except requests.ConnectionError as e: 62 | log.debug("Server not up yet (%s).." % e) 63 | return False 64 | 65 | def get(self, path, as_json=False, attempts=25): 66 | """ Queries the server using requests.GET and returns the response object. 67 | 68 | Parameters 69 | ---------- 70 | path : `str` 71 | Path to the resource, relative to 'http://hostname:port/' 72 | as_json : `bool` 73 | Returns the json object if True. Defaults to False. 74 | attempts: `int` 75 | This function will retry up to `attempts` times on connection errors, to handle 76 | the server still waking up. Defaults to 25. 77 | """ 78 | e = None 79 | for i in range(attempts): 80 | try: 81 | with self.handle_proxy(): 82 | returned = requests.get('http://%s:%d/%s' % (self.hostname, self.port, path)) 83 | return returned.json() if as_json else returned 84 | except (http.client.BadStatusLine, requests.ConnectionError) as e: 85 | time.sleep(int(i) / 10) 86 | pass 87 | raise e 88 | 89 | def post(self, path, data=None, attempts=25, as_json=False, headers=None): 90 | """ Posts data to the server using requests.POST and returns the response object. 91 | 92 | Parameters 93 | ---------- 94 | path : `str` 95 | Path to the resource, relative to 'http://hostname:port/' 96 | as_json : `bool` 97 | Returns the json response if True. Defaults to False. 98 | attempts: `int` 99 | This function will retry up to `attempts` times on connection errors, to handle 100 | the server still waking up. Defaults to 25. 101 | headers: `dict` 102 | Optional HTTP headers. 103 | """ 104 | e = None 105 | for i in range(attempts): 106 | try: 107 | with self.handle_proxy(): 108 | returned = requests.post('http://%s:%d/%s' % (self.hostname, self.port, path), data=data, headers=headers) 109 | return returned.json() if as_json else returned 110 | except (http.client.BadStatusLine, requests.ConnectionError) as e: 111 | time.sleep(int(i) / 10) 112 | pass 113 | raise e 114 | 115 | 116 | class SimpleHTTPTestServer(HTTPTestServer): 117 | """A Simple HTTP test server that serves up a folder of files over the web.""" 118 | 119 | def __init__(self, workspace=None, delete=None, **kwargs): 120 | kwargs.pop("hostname", None) # User can't set the hostname it is always 0.0.0.0 121 | # If we don't pass hostname="0.0.0.0" to our superclass's initialiser then the cleanup 122 | # code in kill won't work correctly. We don't set self.hostname however as we want our 123 | # uri property to still be correct. 124 | super(SimpleHTTPTestServer, self).__init__(workspace=workspace, delete=delete, hostname="0.0.0.0", **kwargs) 125 | self.cwd = self.document_root 126 | 127 | @property 128 | def uri(self): 129 | if self._uri: 130 | return self._uri 131 | return "http://%s:%s" % (socket.gethostname(), self.port) 132 | 133 | @property 134 | def run_cmd(self): 135 | http_server = 'http.server' if sys.version_info >= (3,0) else 'SimpleHTTPServer' 136 | return ["python", "-m", http_server, str(self.port)] 137 | 138 | @property 139 | def document_root(self): 140 | """This is the folder of files served up by this SimpleHTTPServer""" 141 | file_dir = os.path.join(str(self.workspace), "files") 142 | if not os.path.exists(file_dir): 143 | os.mkdir(file_dir) 144 | return file_dir 145 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/httpd.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import string 4 | import logging 5 | 6 | import pytest 7 | from pathlib import Path 8 | 9 | from pytest_fixture_config import yield_requires_config 10 | from pytest_server_fixtures import CONFIG 11 | 12 | from .http import HTTPTestServer 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | def is_rhel(): 18 | """"Check if OS is RHEL/Centos""" 19 | return 'el' in platform.uname()[2] 20 | 21 | @pytest.yield_fixture(scope='function') 22 | @yield_requires_config(CONFIG, ['httpd_executable', 'httpd_modules']) 23 | def httpd_server(): 24 | """ Function-scoped httpd server in a local thread. 25 | 26 | Methods 27 | ------- 28 | get() : Query url relative to the server root. 29 | .. Parse as json and retry failures by default. 30 | post() : Post payload to url relative to the server root. 31 | .. Parse as json and retry failures by default. 32 | """ 33 | test_server = HTTPDServer() 34 | test_server.start() 35 | yield test_server 36 | test_server.teardown() 37 | 38 | 39 | class HTTPDServer(HTTPTestServer): 40 | port_seed = 65531 41 | 42 | cfg_modules_template = """ 43 | LoadModule headers_module $modules/mod_headers.so 44 | LoadModule proxy_module $modules/mod_proxy.so 45 | LoadModule proxy_http_module $modules/mod_proxy_http.so 46 | LoadModule proxy_connect_module $modules/mod_proxy_connect.so 47 | LoadModule alias_module $modules/mod_alias.so 48 | LoadModule dir_module $modules/mod_dir.so 49 | LoadModule autoindex_module $modules/mod_autoindex.so 50 | 51 | LoadModule log_config_module $modules/mod_log_config.so 52 | 53 | LoadModule mime_module $modules/mod_mime.so 54 | LoadModule authz_core_module $modules/mod_authz_core.so 55 | """ 56 | 57 | cfg_rhel_template = """ 58 | LoadModule unixd_module modules/mod_unixd.so 59 | """ 60 | 61 | cfg_mpm_template = """ 62 | LoadModule mpm_prefork_module $modules/mod_mpm_prefork.so 63 | StartServers 1 64 | MinSpareServers 1 65 | MaxSpareServers 4 66 | ServerLimit 4 67 | MaxClients 4 68 | MaxRequestsPerChild 10000 69 | """ 70 | 71 | cfg_template = """ 72 | TypesConfig /etc/mime.types 73 | 74 | ServerRoot $server_root 75 | Listen $listen_addr 76 | PidFile $server_root/run/httpd.pid 77 | 78 | ErrorLog $log_dir/error.log 79 | LogFormat "%h %l %u %t \\"%r\\" %>s %b" common 80 | CustomLog $log_dir/access.log common 81 | LogLevel info 82 | 83 | $proxy_rules 84 | 85 | Alias / $document_root/ 86 | 87 | 88 | Options +Indexes 89 | 90 | """ 91 | 92 | def __init__(self, proxy_rules=None, extra_cfg='', document_root=None, log_dir=None, **kwargs): 93 | """ httpd Proxy Server 94 | 95 | Parameters 96 | ---------- 97 | proxy_rules: `dict` 98 | { proxy_src: proxy_dest }. Eg {'/downstream_url/' : server.uri} 99 | extra_cfg: `str` 100 | Any extra Apache config 101 | document_root : `str` 102 | Server document root, defaults to temporary workspace 103 | log_dir : `str` 104 | Server log directory, defaults to $(workspace)/logs 105 | """ 106 | self.proxy_rules = proxy_rules if proxy_rules is not None else {} 107 | 108 | if not is_rhel(): 109 | self.cfg_template = string.Template(self.cfg_modules_template + 110 | self.cfg_mpm_template + 111 | self.cfg_template + 112 | extra_cfg) 113 | else: 114 | self.cfg_template = string.Template(self.cfg_modules_template + 115 | self.cfg_rhel_template + 116 | self.cfg_mpm_template + 117 | self.cfg_template + 118 | extra_cfg) 119 | 120 | # Always print debug output for this process 121 | os.environ['DEBUG'] = '1' 122 | 123 | kwargs['hostname'] = kwargs.get('hostname', CONFIG.fixture_hostname) 124 | 125 | super(HTTPDServer, self).__init__(**kwargs) 126 | 127 | self.document_root = document_root or self.workspace 128 | self.document_root = Path(self.document_root) 129 | self.log_dir = log_dir or self.workspace / 'logs' 130 | self.log_dir = Path(self.log_dir) 131 | 132 | def pre_setup(self): 133 | """ Write out the config file 134 | """ 135 | self.config = self.workspace / 'httpd.conf' 136 | rules = [] 137 | for source in self.proxy_rules: 138 | rules.append("ProxyPass {0} {1}".format(source, self.proxy_rules[source])) 139 | rules.append("ProxyPassReverse {0} {1} \n".format(source, self.proxy_rules[source])) 140 | cfg = self.cfg_template.substitute( 141 | server_root=self.workspace, 142 | document_root=self.document_root, 143 | log_dir=self.log_dir, 144 | listen_addr="{host}:{port}".format(host=self.hostname, port=self.port), 145 | proxy_rules='\n'.join(rules), 146 | modules=CONFIG.httpd_modules, 147 | ) 148 | self.config.write_text(cfg) 149 | log.debug("=========== HTTPD Server Config =============\n{}".format(cfg)) 150 | 151 | # This is where it stores PID files 152 | (self.workspace / 'run').mkdir() 153 | if not os.path.exists(self.log_dir): 154 | self.log_dir.mkdir() 155 | 156 | @property 157 | def pid(self): 158 | try: 159 | return int((self.workspace / 'run' / 'httpd.pid').read_text()) 160 | except FileNotFoundError: 161 | return None 162 | 163 | @property 164 | def run_cmd(self): 165 | return [CONFIG.httpd_executable, '-f', str(self.config)] 166 | 167 | def kill(self, retries=5): 168 | pid = self.pid 169 | if pid is not None: 170 | self.kill_by_pid(self.pid, retries) 171 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/jenkins.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 25 Apr 2012 3 | 4 | @author: eeaston 5 | ''' 6 | import os.path 7 | import shutil 8 | 9 | import pytest 10 | 11 | from pytest_server_fixtures import CONFIG 12 | from pytest_fixture_config import yield_requires_config 13 | 14 | from .http import HTTPTestServer 15 | 16 | 17 | @pytest.yield_fixture(scope='session') 18 | @yield_requires_config(CONFIG, ['jenkins_war', 'java_executable']) 19 | def jenkins_server(): 20 | """ Session-scoped Jenkins server instance 21 | 22 | Attributes 23 | ---------- 24 | api (`jenkins.Jenkins`) : python-jenkins client API connected to this server 25 | .. also inherits all attributes from the `workspace` fixture 26 | """ 27 | with JenkinsTestServer() as p: 28 | p.start() 29 | yield p 30 | 31 | 32 | @pytest.yield_fixture(scope='module') 33 | @yield_requires_config(CONFIG, ['jenkins_war', 'java_executable']) 34 | def jenkins_server_module(): 35 | """ Module-scoped Jenkins server instance 36 | 37 | Attributes 38 | ---------- 39 | api (`jenkins.Jenkins`) : python-jenkins client API connected to this server 40 | .. also inherits all attributes from the `workspace` fixture 41 | """ 42 | with JenkinsTestServer() as p: 43 | p.start() 44 | yield p 45 | 46 | 47 | class JenkinsTestServer(HTTPTestServer): 48 | port_seed = 65533 49 | kill_retry_delay = 2 50 | 51 | def __init__(self, **kwargs): 52 | global jenkins 53 | try: 54 | import jenkins 55 | except ImportError: 56 | pytest.skip('python-jenkins not installed, skipping test') 57 | super(JenkinsTestServer, self).__init__(**kwargs) 58 | self.env = dict(JENKINS_HOME=self.workspace, 59 | JENKINS_RUN=self.workspace / 'run', 60 | # Use at most 1GB of RAM for the server 61 | JAVA_ARGS='-Xms1G -Xmx1G', 62 | RUN_STANDALONE='true', 63 | JENKINS_LOG=self.workspace / 'jenkins.log', 64 | ) 65 | self.api = jenkins.Jenkins(self.uri) 66 | 67 | @property 68 | def run_cmd(self): 69 | if not CONFIG.jenkins_war: 70 | raise ValueError("jenkins_war missing from org config") 71 | 72 | return [CONFIG.java_executable, 73 | '-jar', CONFIG.jenkins_war, 74 | '--httpPort=%s' % self.port, 75 | '--httpListenAddress=%s' % self.hostname, 76 | '--ajp13Port=-1', 77 | '--webroot={0}'.format(self.workspace / 'run' / 'war'), 78 | ] 79 | 80 | def load_plugins(self, plugins_repo, plugins=None): 81 | """plugins_repo is the place from which the plugins can be copied to this jenskins instance 82 | is plugins is None, all plugins will be copied, else is should be a list of the plugin names 83 | """ 84 | 85 | if not os.path.isdir(plugins_repo): 86 | raise ValueError('Plugin repository "%s" does not exist' % plugins_repo) 87 | 88 | # copy the plugins to the jenkins plugin directory 89 | available_plugins = dict(((os.path.splitext(os.path.basename(x))[0], os.path.join(plugins_repo, x)) 90 | for x in os.listdir(plugins_repo) if x.endswith('.hpi'))) 91 | 92 | if plugins is None: 93 | plugins = available_plugins.keys() 94 | else: 95 | if isinstance(plugins, str): 96 | plugins = [plugins] 97 | 98 | errors = [] 99 | for p in plugins: 100 | if p not in available_plugins: 101 | if p not in errors: 102 | errors.append(p) 103 | if errors: 104 | if len(errors) == 1: 105 | e = 'Plugin "%s" is not present in the repository' % errors[0] 106 | else: 107 | e = 'Plugins %s are not present in the repository' % sorted(errors) 108 | raise ValueError(e) 109 | 110 | for p in plugins: 111 | tgt = os.path.join(self.plugins_dir, '%s.hpi' % p) 112 | shutil.copy(available_plugins[p], tgt) 113 | 114 | @property 115 | def plugins_dir(self): 116 | return os.path.normpath(os.path.join(self.workspace, 'plugins')) 117 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/mongo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | from pytest_server_fixtures import CONFIG 5 | from pytest_fixture_config import yield_requires_config 6 | 7 | from .base2 import TestServerV2 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | def _mongo_server(): 13 | """ This does the actual work - there are several versions of this used 14 | with different scopes. 15 | """ 16 | test_server = MongoTestServer() 17 | try: 18 | test_server.start() 19 | yield test_server 20 | finally: 21 | test_server.teardown() 22 | 23 | 24 | @pytest.yield_fixture(scope='function') 25 | @yield_requires_config(CONFIG, ['mongo_bin']) 26 | def mongo_server(): 27 | """ Function-scoped MongoDB server started in a local thread. 28 | This also provides a temp workspace. 29 | We tear down, and cleanup mongos at the end of the test. 30 | 31 | For completeness, we tidy up any outstanding mongo temp directories 32 | at the start and end of each test session 33 | 34 | Attributes 35 | ---------- 36 | api (`pymongo.MongoClient`) : PyMongo Client API connected to this server 37 | .. also inherits all attributes from the `workspace` fixture 38 | """ 39 | for server in _mongo_server(): 40 | yield server 41 | 42 | 43 | @pytest.yield_fixture(scope='session') 44 | @yield_requires_config(CONFIG, ['mongo_bin']) 45 | def mongo_server_sess(): 46 | """ Same as mongo_server fixture, scoped as session instead. 47 | """ 48 | for server in _mongo_server(): 49 | yield server 50 | 51 | 52 | @pytest.yield_fixture(scope='class') 53 | @yield_requires_config(CONFIG, ['mongo_bin']) 54 | def mongo_server_cls(request): 55 | """ Same as mongo_server fixture, scoped for test classes. 56 | """ 57 | for server in _mongo_server(): 58 | request.cls.mongo_server = server 59 | yield server 60 | 61 | 62 | @pytest.yield_fixture(scope='module') 63 | @yield_requires_config(CONFIG, ['mongo_bin']) 64 | def mongo_server_module(): 65 | """ Same as mongo_server fixture, scoped for test modules. 66 | """ 67 | for server in _mongo_server(): 68 | yield server 69 | 70 | 71 | class MongoTestServer(TestServerV2): 72 | 73 | def __init__(self, delete=True, **kwargs): 74 | super(MongoTestServer, self).__init__(delete=delete, **kwargs) 75 | self._port = self._get_port(27017) 76 | self.api = None 77 | 78 | @property 79 | def cmd(self): 80 | return 'mongod' 81 | 82 | @property 83 | def cmd_local(self): 84 | return CONFIG.mongo_bin 85 | 86 | def get_args(self, **kwargs): 87 | cmd = [ 88 | '--bind_ip=%s' % self._listen_hostname, 89 | '--port=%s' % self.port, 90 | '--nounixsocket', 91 | '--syncdelay=0', 92 | '--nojournal', 93 | '--quiet', 94 | ] 95 | 96 | if 'workspace' in kwargs: 97 | cmd.append('--dbpath=%s' % str(kwargs['workspace'])) 98 | 99 | return cmd 100 | 101 | @property 102 | def image(self): 103 | return CONFIG.mongo_image 104 | 105 | @property 106 | def port(self): 107 | return self._port 108 | 109 | def check_server_up(self): 110 | """Test connection to the server.""" 111 | import pymongo 112 | from pymongo.errors import AutoReconnect, ConnectionFailure 113 | 114 | # Hostname must exist before continuing 115 | # Some server class (e.g. Docker) will only allocate an IP after the 116 | # container has started. 117 | if not self.hostname: 118 | return False 119 | 120 | log.info("Connecting to Mongo at %s:%s" % (self.hostname, self.port)) 121 | try: 122 | with pymongo.MongoClient(self.hostname, self.port, serverselectiontimeoutms=200) as initial_api: 123 | initial_api.list_database_names() 124 | 125 | # Configure the client with default timeouts in case the server goes slow 126 | self.api = pymongo.MongoClient(self.hostname, self.port) 127 | return True 128 | except (AutoReconnect, ConnectionFailure) as e: 129 | pass 130 | return False 131 | 132 | def teardown(self): 133 | if self.api: 134 | self.api.close() 135 | self.api = None 136 | super(MongoTestServer, self).teardown() 137 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/postgres.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import logging 5 | import subprocess 6 | 7 | import errno 8 | import pytest 9 | 10 | from pytest_server_fixtures import CONFIG 11 | from pytest_fixture_config import requires_config 12 | 13 | from .base import TestServer 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | @pytest.fixture(scope='session') 19 | @requires_config(CONFIG, ['pg_config_executable']) 20 | def postgres_server_sess(request): 21 | """A session-scoped Postgres Database fixture""" 22 | return _postgres_server(request) 23 | 24 | 25 | def _postgres_server(request): 26 | server = PostgresServer() 27 | server.start() 28 | request.addfinalizer(server.teardown) 29 | return server 30 | 31 | 32 | class PostgresServer(TestServer): 33 | """ 34 | Exposes a server.connect() method returning a raw psycopg2 connection. 35 | Also exposes a server.connection_config property returning a dict with connection parameters 36 | """ 37 | random_port = True 38 | 39 | def __init__(self, database_name="integration", skip_on_missing_postgres=False, **kwargs): 40 | self.database_name = database_name 41 | # TODO make skip configurable with a pytest flag 42 | self._fail = pytest.skip if skip_on_missing_postgres else pytest.exit 43 | super(PostgresServer, self).__init__(workspace=None, delete=True, preserve_sys_path=False, **kwargs) 44 | 45 | def kill(self, retries=5): 46 | if hasattr(self, 'pid'): 47 | try: 48 | os.kill(self.pid, self.kill_signal) 49 | except OSError as e: 50 | if e.errno == errno.ESRCH: # "No such process" 51 | pass 52 | else: 53 | raise 54 | 55 | def pre_setup(self): 56 | """ 57 | Find postgres server binary 58 | Set up connection parameters 59 | """ 60 | (self.workspace / 'db').mkdir() # pylint: disable=no-value-for-parameter 61 | 62 | try: 63 | self.pg_bin = subprocess.check_output([CONFIG.pg_config_executable, "--bindir"]).decode('utf-8').rstrip() 64 | except OSError as e: 65 | msg = "Failed to get pg_config --bindir: " + str(e) 66 | print(msg) 67 | self._fail(msg) 68 | initdb_path = self.pg_bin + '/initdb' 69 | if not os.path.exists(initdb_path): 70 | msg = "Unable to find pg binary specified by pg_config: {} is not a file".format(initdb_path) 71 | print(msg) 72 | self._fail(msg) 73 | try: 74 | subprocess.check_call([initdb_path, str(self.workspace / 'db')]) 75 | except OSError as e: 76 | msg = "Failed to launch postgres: " + str(e) 77 | print(msg) 78 | self._fail(msg) 79 | 80 | @property 81 | def connection_config(self): 82 | return { 83 | u'host': u'localhost', 84 | u'user': os.environ[u'USER'], 85 | u'port': self.port, 86 | u'database': self.database_name 87 | } 88 | 89 | @property 90 | def run_cmd(self): 91 | cmd = [ 92 | self.pg_bin + '/postgres', 93 | '-F', 94 | '-k', str(self.workspace / 'db'), 95 | '-D', str(self.workspace / 'db'), 96 | '-p', str(self.port), 97 | '-c', "log_min_messages=FATAL" 98 | ] # yapf: disable 99 | return cmd 100 | 101 | def check_server_up(self): 102 | from psycopg2 import OperationalError 103 | conn = None 104 | try: 105 | print("Connecting to Postgres at localhost:{}".format(self.port)) 106 | conn = self.connect('postgres') 107 | conn.set_session(autocommit=True) 108 | with conn.cursor() as cursor: 109 | cursor.execute("CREATE DATABASE " + self.database_name) 110 | self.connection = self.connect(self.database_name) 111 | with open(self.workspace / 'db' / 'postmaster.pid', 'r') as f: 112 | self.pid = int(f.readline().rstrip()) 113 | return True 114 | except OperationalError as e: 115 | print("Could not connect to test postgres: {}".format(e)) 116 | finally: 117 | if conn: 118 | conn.close() 119 | return False 120 | 121 | def connect(self, database=None): 122 | import psycopg2 123 | cfg = self.connection_config 124 | if database is not None: 125 | cfg[u'database'] = database 126 | return psycopg2.connect(**cfg) 127 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/redis.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 25 Apr 2012 3 | 4 | @author: eeaston 5 | 6 | ''' 7 | import socket 8 | 9 | import pytest 10 | 11 | from pytest_server_fixtures import CONFIG 12 | from pytest_fixture_config import requires_config 13 | 14 | from .base2 import TestServerV2 15 | 16 | 17 | def _redis_server(request): 18 | """ Does the redis server work, this is used within different scoped 19 | fixtures. 20 | """ 21 | test_server = RedisTestServer() 22 | request.addfinalizer(lambda p=test_server: p.teardown()) 23 | test_server.start() 24 | return test_server 25 | 26 | 27 | @pytest.fixture(scope='function') 28 | @requires_config(CONFIG, ['redis_executable']) 29 | def redis_server(request): 30 | """ Function-scoped Redis server in a local thread. 31 | 32 | Attributes 33 | ---------- 34 | api: (``redis.Redis``) Redis client API connected to this server 35 | .. also inherits all attributes from the `workspace` fixture 36 | """ 37 | return _redis_server(request) 38 | 39 | 40 | @pytest.fixture(scope='session') 41 | @requires_config(CONFIG, ['redis_executable']) 42 | def redis_server_sess(request): 43 | """ Same as redis_server fixture, scoped for test session 44 | """ 45 | return _redis_server(request) 46 | 47 | 48 | class RedisTestServer(TestServerV2): 49 | """This will look for 'redis_executable' in configuration and use as the 50 | redis-server to run. 51 | """ 52 | 53 | def __init__(self, db=0, delete=True, **kwargs): 54 | global redis 55 | import redis 56 | 57 | super(RedisTestServer, self).__init__(delete=delete, **kwargs) 58 | self.db = db 59 | self._api = None 60 | self._port = self._get_port(6379) 61 | 62 | @property 63 | def api(self): 64 | if not self.hostname: 65 | raise "Redis not ready" 66 | if not self._api: 67 | self._api = redis.Redis(host=self.hostname, port=self.port, db=self.db) 68 | return self._api 69 | 70 | @property 71 | def cmd(self): 72 | return "redis-server" 73 | 74 | @property 75 | def cmd_local(self): 76 | return CONFIG.redis_executable 77 | 78 | def get_args(self, **kwargs): 79 | cmd = [ 80 | "--bind", self._listen_hostname, 81 | "--port", str(self.port), 82 | "--timeout", "0", 83 | "--loglevel", "notice", 84 | "--databases", "1", 85 | "--maxmemory", "2gb", 86 | "--maxmemory-policy", "noeviction", 87 | "--appendonly", "no", 88 | "--slowlog-log-slower-than", "-1", 89 | "--slowlog-max-len", "1024", 90 | ] 91 | 92 | return cmd 93 | 94 | @property 95 | def image(self): 96 | return CONFIG.redis_image 97 | 98 | @property 99 | def port(self): 100 | return self._port 101 | 102 | def check_server_up(self): 103 | """ Ping the server 104 | """ 105 | print("pinging Redis at %s:%s db %s" % ( 106 | self.hostname, self.port, self.db 107 | )) 108 | 109 | if not self.hostname: 110 | return False 111 | 112 | try: 113 | return self.api.ping() 114 | except redis.ConnectionError as e: 115 | print("server not up yet (%s)" % e) 116 | return False 117 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/s3.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Pytest fixtures to launch a minio S3 server and get a bucket for it. 4 | """ 5 | 6 | import uuid 7 | from collections import namedtuple 8 | import logging 9 | import os 10 | 11 | import pytest 12 | from pytest_fixture_config import requires_config 13 | 14 | from . import CONFIG 15 | from .http import HTTPTestServer 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | def _s3_server(request): 20 | server = MinioServer() 21 | server.start() 22 | request.addfinalizer(server.teardown) 23 | return server 24 | 25 | 26 | @pytest.fixture(scope="session") 27 | @requires_config(CONFIG, ['minio_executable']) 28 | def s3_server(request): 29 | """ 30 | Creates a session-scoped temporary S3 server using the 'minio' tool. 31 | 32 | The primary method on the server object is `s3_server.get_s3_client()`, which returns a boto3 `Resource` 33 | (`boto3.resource('s3', ...)`) 34 | """ 35 | return _s3_server(request) 36 | 37 | BucketInfo = namedtuple('BucketInfo', ['client', 'name']) 38 | # Minio is a little too slow to start for each function call 39 | # Start it once per session and get a new bucket for each function instead. 40 | @pytest.fixture(scope="function") 41 | def s3_bucket(s3_server): # pylint: disable=redefined-outer-name 42 | """ 43 | Creates a function-scoped s3 bucket, 44 | returning a BucketInfo namedtuple with `s3_bucket.client` and `s3_bucket.name` fields 45 | """ 46 | client = s3_server.get_s3_client() 47 | bucket_name = str(uuid.uuid4()) 48 | client.create_bucket(Bucket=bucket_name) 49 | return BucketInfo(client, bucket_name) 50 | 51 | 52 | class MinioServer(HTTPTestServer): 53 | random_port = True 54 | aws_access_key_id = "MINIO_TEST_ACCESS" 55 | aws_secret_access_key = "MINIO_TEST_SECRET" 56 | 57 | def __init__(self, workspace=None, delete=None, preserve_sys_path=False, **kwargs): 58 | env = kwargs.get('env', os.environ.copy()) 59 | env.update({"MINIO_ACCESS_KEY": self.aws_access_key_id, "MINIO_SECRET_KEY": self.aws_secret_access_key}) 60 | kwargs['env'] = env 61 | kwargs['hostname'] = "0.0.0.0" # minio doesn't seem to allow binding to 127.0.0.0/8 62 | super(MinioServer, self).__init__(workspace=workspace, delete=delete, preserve_sys_path=preserve_sys_path, **kwargs) 63 | 64 | def get_s3_client(self): 65 | # Region name and signature are to satisfy minio 66 | import boto3 67 | import botocore.client 68 | s3 = boto3.resource( 69 | 's3', 70 | endpoint_url=self.boto_endpoint_url, 71 | aws_access_key_id=self.aws_access_key_id, 72 | aws_secret_access_key=self.aws_secret_access_key, 73 | region_name='us-east-1', 74 | config=botocore.client.Config(signature_version='s3v4'), 75 | ) 76 | return s3 77 | 78 | @property 79 | def datadir(self): 80 | return self.workspace / 'minio-db' 81 | 82 | @property 83 | def boto_endpoint_url(self): 84 | return self.uri 85 | 86 | def pre_setup(self): 87 | self.datadir.mkdir() # pylint: disable=no-value-for-parameter 88 | 89 | @property 90 | def run_cmd(self): 91 | cmdargs = [ 92 | CONFIG.minio_executable, 93 | "server", 94 | "--address", 95 | "{}:{}".format(self.hostname, self.port), 96 | str(self.datadir), 97 | ] 98 | return cmdargs 99 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/serverclass/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of how a server fixture will run. 3 | """ 4 | # flake8: noqa 5 | 6 | def create_server(server_class, **kwargs): 7 | if server_class == 'thread': 8 | from .thread import ThreadServer 9 | return ThreadServer( 10 | cmd=kwargs["cmd_local"], 11 | get_args=kwargs["get_args"], 12 | env=kwargs["env"], 13 | workspace=kwargs["workspace"], 14 | cwd=kwargs["cwd"], 15 | listen_hostname=kwargs["listen_hostname"], 16 | ) 17 | 18 | if server_class == 'docker': 19 | from .docker import DockerServer 20 | return DockerServer( 21 | server_type=kwargs["server_type"], 22 | cmd=kwargs["cmd"], 23 | get_args=kwargs["get_args"], 24 | env=kwargs["env"], 25 | image=kwargs["image"], 26 | labels=kwargs["labels"], 27 | ) 28 | 29 | if server_class == 'kubernetes': 30 | from .kubernetes import KubernetesServer 31 | return KubernetesServer( 32 | server_type=kwargs["server_type"], 33 | cmd=kwargs["cmd"], 34 | get_args=kwargs["get_args"], 35 | env=kwargs["env"], 36 | image=kwargs["image"], 37 | labels=kwargs["labels"], 38 | ) 39 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/serverclass/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common utils for all serverclasses 3 | """ 4 | import os 5 | import threading 6 | 7 | from pytest_server_fixtures import CONFIG 8 | from pytest_server_fixtures.util import get_random_id 9 | 10 | SERVER_ID_LEN = 8 11 | 12 | def merge_dicts(x, y): 13 | """Given two dicts, merge them into a new dict as a shallow copy.""" 14 | z = x.copy() 15 | z.update(y) 16 | return z 17 | 18 | 19 | def is_debug(): 20 | return 'DEBUG' in os.environ and os.environ['DEBUG'] == '1' 21 | 22 | 23 | class ServerFixtureNotRunningException(Exception): 24 | """Thrown when a kubernetes pod is not in running state.""" 25 | pass 26 | 27 | 28 | class ServerFixtureNotTerminatedException(Exception): 29 | """Thrown when a kubernetes pod is still running.""" 30 | pass 31 | 32 | 33 | class ServerClass(threading.Thread): 34 | """Example interface for ServerClass.""" 35 | 36 | def __init__(self, 37 | cmd, 38 | get_args, 39 | env): 40 | """ 41 | Initialise the server class. 42 | Server fixture will be started here. 43 | """ 44 | super(ServerClass, self).__init__() 45 | 46 | # set serverclass thread to a daemon thread 47 | self.daemon = True 48 | 49 | self._id = get_random_id(SERVER_ID_LEN) 50 | self._cmd = cmd 51 | self._get_args = get_args 52 | self._env = env or {} 53 | 54 | def run(self): 55 | """In a new thread, wait for the server to return.""" 56 | raise NotImplementedError("Concrete class should implement this") 57 | 58 | def launch(self): 59 | """Start the server.""" 60 | raise NotImplementedError("Concrete class should implement this") 61 | 62 | def teardown(self): 63 | """Kill the server.""" 64 | raise NotImplementedError("Concrete class should implement this") 65 | 66 | @property 67 | def is_running(self): 68 | """Tell if the server is running.""" 69 | raise NotImplementedError("Concrete class should implement this") 70 | 71 | @property 72 | def hostname(self): 73 | """Get server's hostname.""" 74 | raise NotImplementedError("Concrete class should implement this") 75 | 76 | @property 77 | def name(self): 78 | return "server-fixtures-%s-%s" % (CONFIG.session_id, self._id) 79 | 80 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/serverclass/docker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Docker server class implementation. 3 | """ 4 | import logging 5 | import docker 6 | 7 | from retry import retry 8 | from pytest_server_fixtures import CONFIG 9 | from .common import (ServerClass, 10 | merge_dicts, 11 | ServerFixtureNotRunningException, 12 | ServerFixtureNotTerminatedException) 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | class DockerServer(ServerClass): 18 | """Docker server class.""" 19 | 20 | def __init__(self, 21 | server_type, 22 | cmd, 23 | get_args, 24 | env, 25 | image, 26 | labels={}): 27 | super(DockerServer, self).__init__(cmd, get_args, env) 28 | 29 | self._image = image 30 | self._labels = merge_dicts(labels, { 31 | 'server-fixtures': 'docker-server-fixtures', 32 | 'server-fixtures/server-type': server_type, 33 | 'server-fixtures/session-id': CONFIG.session_id, 34 | }) 35 | 36 | self._client = docker.from_env() 37 | self._container = None 38 | 39 | def launch(self): 40 | try: 41 | log.debug('Launching container') 42 | self._container = self._client.containers.run( 43 | image=self._image, 44 | name=self.name, 45 | command=[self._cmd] + self._get_args(), 46 | environment=self._env, 47 | labels=self._labels, 48 | detach=True, 49 | auto_remove=True, 50 | ) 51 | self._wait_until_running() 52 | log.debug('Container is running at %s', self.hostname) 53 | except docker.errors.ImageNotFound as err: 54 | log.warning("Failed to start container, image %s not found", self._image) 55 | log.debug(err) 56 | raise 57 | except docker.errors.APIError as e: 58 | log.warning("Failed to start container: %s", e) 59 | raise 60 | 61 | self.start() 62 | 63 | def run(self): 64 | try: 65 | self._container.wait() 66 | except docker.errors.APIError as e: 67 | log.warning("Error while waiting for container: %s", e) 68 | log.debug(self._container.logs()) 69 | 70 | def teardown(self): 71 | if not self._container: 72 | return 73 | 74 | try: 75 | # stopping container will also remove it as 'auto_remove' is set 76 | self._container.stop() 77 | self._wait_until_terminated() 78 | except docker.errors.APIError as e: 79 | log.warning("Error when stopping the container: %s", e) 80 | 81 | @property 82 | def is_running(self): 83 | if not self._container: 84 | return False 85 | 86 | return self._get_status() == 'running' 87 | 88 | @property 89 | def hostname(self): 90 | if not self.is_running: 91 | raise ServerFixtureNotRunningException() 92 | return self._container.attrs['NetworkSettings']['IPAddress'] 93 | 94 | def _get_status(self): 95 | try: 96 | self._container.reload() 97 | return self._container.status 98 | except docker.errors.APIError as e: 99 | log.warning("Failed to get container status: %s", e) 100 | raise 101 | 102 | @retry(ServerFixtureNotRunningException, 103 | tries=28, 104 | delay=1, 105 | backoff=2, 106 | max_delay=10) 107 | def _wait_until_running(self): 108 | if not self.is_running: 109 | raise ServerFixtureNotRunningException() 110 | 111 | @retry(ServerFixtureNotTerminatedException, 112 | tries=28, 113 | delay=1, 114 | backoff=2, 115 | max_delay=10) 116 | def _wait_until_terminated(self): 117 | try: 118 | self._get_status() 119 | except docker.errors.APIError as e: 120 | if e.response.status_code == 404: 121 | return 122 | raise 123 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/serverclass/kubernetes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kubernetes server class implementation. 3 | """ 4 | 5 | import os 6 | import logging 7 | import uuid 8 | 9 | from kubernetes import config 10 | from kubernetes import client as k8sclient 11 | from kubernetes.client.rest import ApiException 12 | from retry import retry 13 | from pytest_server_fixtures import CONFIG 14 | from .common import (ServerClass, 15 | merge_dicts, 16 | ServerFixtureNotRunningException, 17 | ServerFixtureNotTerminatedException) 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | IN_CLUSTER = os.path.exists('/var/run/secrets/kubernetes.io/namespace') 22 | fixture_namespace = CONFIG.k8s_namespace 23 | 24 | if IN_CLUSTER: 25 | config.load_incluster_config() 26 | if not fixture_namespace: 27 | with open('/var/run/secrets/kubernetes.io/namespace', 'r') as f: 28 | fixture_namespace = f.read().strp() 29 | log.info("SERVER_FIXTURES_K8S_NAMESPACE is not set, using current namespace '%s'", fixture_namespace) 30 | 31 | if CONFIG.k8s_local_test: 32 | log.info("====== Running K8S Server Class in Test Mode =====") 33 | config.load_kube_config() 34 | fixture_namespace = 'default' 35 | 36 | 37 | class NotRunningInKubernetesException(Exception): 38 | """Thrown when code is not running as a Pod inside a Kubernetes cluster.""" 39 | pass 40 | 41 | 42 | class KubernetesServer(ServerClass): 43 | """Kubernetes server class.""" 44 | 45 | def __init__(self, 46 | server_type, 47 | cmd, 48 | get_args, 49 | env, 50 | image, 51 | labels={}): 52 | super(KubernetesServer, self).__init__(cmd, get_args, env) 53 | 54 | if not fixture_namespace: 55 | raise NotRunningInKubernetesException() 56 | 57 | self._image = image 58 | self._labels = merge_dicts(labels, { 59 | 'server-fixtures': 'kubernetes-server-fixtures', 60 | 'server-fixtures/server-type': server_type, 61 | 'server-fixtures/session-id': CONFIG.session_id, 62 | }) 63 | 64 | self._v1api = k8sclient.CoreV1Api() 65 | 66 | def launch(self): 67 | try: 68 | log.debug('%s Launching pod' % self._log_prefix) 69 | self._create_pod() 70 | self._wait_until_running() 71 | log.debug('%s Pod is running' % self._log_prefix) 72 | except ApiException as e: 73 | log.warning('%s Error while launching pod: %s', self._log_prefix, e) 74 | raise 75 | 76 | def run(self): 77 | pass 78 | 79 | def teardown(self): 80 | self._delete_pod() 81 | # TODO: provide an flag to skip the wait to speed up the tests? 82 | self._wait_until_teardown() 83 | 84 | @property 85 | def is_running(self): 86 | try: 87 | return self._get_pod_status().phase == 'Running' 88 | except ApiException as e: 89 | if e.status == 404: 90 | # return false if pod does not exists 91 | return False 92 | raise 93 | 94 | @property 95 | def hostname(self): 96 | if not self.is_running: 97 | raise ServerFixtureNotRunningException() 98 | return self._get_pod_status().pod_ip 99 | 100 | @property 101 | def namespace(self): 102 | return fixture_namespace 103 | 104 | @property 105 | def labels(self): 106 | return self._labels 107 | 108 | def _get_pod_spec(self): 109 | container = k8sclient.V1Container( 110 | name='fixture', 111 | image=self._image, 112 | command=self._get_cmd(), 113 | env=[k8sclient.V1EnvVar(name=k, value=v) for k, v in self._env.iteritems()], 114 | ) 115 | 116 | return k8sclient.V1PodSpec( 117 | containers=[container] 118 | ) 119 | 120 | def _create_pod(self): 121 | try: 122 | pod = k8sclient.V1Pod() 123 | pod.metadata = k8sclient.V1ObjectMeta(name=self.name, labels=self._labels) 124 | pod.spec = self._get_pod_spec() 125 | self._v1api.create_namespaced_pod(namespace=self.namespace, body=pod) 126 | except ApiException as e: 127 | log.error("%s Failed to create pod: %s", self._log_prefix, e.reason) 128 | raise 129 | 130 | def _delete_pod(self): 131 | try: 132 | body = k8sclient.V1DeleteOptions() 133 | # delete the pod without waiting 134 | body.grace_period_seconds = 1 135 | self._v1api.delete_namespaced_pod(namespace=self.namespace, name=self.name, body=body) 136 | except ApiException as e: 137 | log.error("%s Failed to delete pod: %s", self._log_prefix, e.reason) 138 | 139 | def _get_pod_status(self): 140 | try: 141 | resp = self._v1api.read_namespaced_pod_status(namespace=self.namespace, name=self.name) 142 | return resp.status 143 | except ApiException as e: 144 | log.error("%s Failed to read pod status: %s", self._log_prefix, e.reason) 145 | raise 146 | 147 | @retry(ServerFixtureNotRunningException, tries=28, delay=1, backoff=2, max_delay=10) 148 | def _wait_until_running(self): 149 | log.debug("%s Waiting for pod status to become running", self._log_prefix) 150 | if not self.is_running: 151 | raise ServerFixtureNotRunningException() 152 | 153 | @retry(ServerFixtureNotTerminatedException, tries=28, delay=1, backoff=2, max_delay=10) 154 | def _wait_until_teardown(self): 155 | try: 156 | self._get_pod_status() 157 | # waiting for pod to be deleted (expect ApiException with status 404) 158 | raise ServerFixtureNotTerminatedException() 159 | except ApiException as e: 160 | if e.status == 404: 161 | return 162 | raise 163 | 164 | @property 165 | def _log_prefix(self): 166 | return "[K8S %s:%s]" % (self.namespace, self.name) 167 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/serverclass/thread.py: -------------------------------------------------------------------------------- 1 | """ 2 | Thread server class implementation 3 | """ 4 | import logging 5 | import os 6 | import signal 7 | import subprocess 8 | import traceback 9 | import time 10 | import psutil 11 | 12 | from retry import retry 13 | 14 | from pytest_server_fixtures import CONFIG 15 | from pytest_server_fixtures.base import ProcessReader 16 | from .common import ServerClass, is_debug 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | 21 | # ThreadServer will attempt to kill all child processes recursively. 22 | KILL_RETRY_COUNT=15 # Total retry count to kill if not all child processes are terminated. 23 | KILL_RETRY_WAIT_SECS=1 # Wait time between two retries 24 | KILL_WAIT_SECS=5 # Time to wait for processes to terminate in a single retry. 25 | 26 | 27 | class ProcessStillRunningException(Exception): 28 | pass 29 | 30 | 31 | @retry(ProcessStillRunningException, 32 | tries=KILL_RETRY_COUNT, 33 | delay=KILL_RETRY_WAIT_SECS) 34 | def _kill_all(procs, sig): 35 | log.debug("Killing %d processes with signal %s" % (len(procs), sig)) 36 | for p in procs: 37 | p.send_signal(sig) 38 | 39 | log.debug("Waiting for %d processes to die" % len(procs)) 40 | gone, alive = psutil.wait_procs(procs, timeout=KILL_WAIT_SECS) 41 | 42 | if len(alive) == 0: 43 | log.debug("All processes are terminated") 44 | return 45 | 46 | log.warning("%d processes remainings: %s" % (len(alive), ",".join([p.name() for p in alive]))) 47 | raise ProcessStillRunningException() 48 | 49 | 50 | def _kill_proc_tree(pid, sig=signal.SIGKILL, timeout=None): 51 | parent = psutil.Process(pid) 52 | children = parent.children(recursive=True) 53 | children.append(parent) 54 | log.debug("Killing process tree for %d (total_procs_to_kill=%d)" % (parent.pid, len(children))) 55 | _kill_all(children, sig) 56 | 57 | 58 | class ThreadServer(ServerClass): 59 | """Thread server class.""" 60 | 61 | def __init__(self, 62 | cmd, 63 | get_args, 64 | env, 65 | workspace, 66 | cwd=None, 67 | listen_hostname=None): 68 | super(ThreadServer, self).__init__(cmd, get_args, env) 69 | 70 | self.exit = False 71 | self._workspace = workspace 72 | self._cwd = cwd 73 | self._hostname = listen_hostname 74 | self._proc = None 75 | 76 | def launch(self): 77 | log.debug("Launching thread server.") 78 | 79 | run_cmd = [self._cmd] + self._get_args(workspace=self._workspace) 80 | 81 | debug = is_debug() 82 | 83 | extra_args = dict() 84 | if debug: 85 | extra_args['stdout'] = subprocess.PIPE 86 | extra_args['stderr'] = subprocess.PIPE 87 | 88 | self._proc = subprocess.Popen(run_cmd, env=self._env, cwd=self._cwd, **extra_args) 89 | log.debug("Running server: %s" % ' '.join(run_cmd)) 90 | log.debug("CWD: %s" % self._cwd) 91 | 92 | if debug: 93 | ProcessReader(self._proc, self._proc.stdout, False).start() 94 | ProcessReader(self._proc, self._proc.stderr, True).start() 95 | 96 | self.start() 97 | 98 | def run(self): 99 | """Run in thread""" 100 | try: 101 | self._proc.wait() 102 | except OSError: 103 | if not self.exit: 104 | traceback.print_exc() 105 | 106 | @property 107 | def is_running(self): 108 | """Check if the main process is still running.""" 109 | # return False if the process is not started yet 110 | if not self._proc: 111 | return False 112 | # return False if there is a return code from the main process 113 | return self._proc.poll() is None 114 | 115 | @property 116 | def hostname(self): 117 | return self._hostname 118 | 119 | def teardown(self): 120 | if not self._proc: 121 | log.warning("No process is running, skip teardown.") 122 | return 123 | 124 | _kill_proc_tree(self._proc.pid) 125 | self._proc = None 126 | 127 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/util.py: -------------------------------------------------------------------------------- 1 | import string 2 | import random 3 | 4 | def get_random_id(id_len): 5 | return ''.join(random.sample(string.ascii_lowercase + string.digits, id_len)) 6 | 7 | -------------------------------------------------------------------------------- /pytest-server-fixtures/pytest_server_fixtures/xvfb.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import signal 4 | import subprocess 5 | import time 6 | from tempfile import mkdtemp 7 | 8 | import pytest 9 | 10 | from pytest_shutil.workspace import Workspace 11 | from pytest_fixture_config import yield_requires_config 12 | 13 | from pytest_server_fixtures import CONFIG 14 | 15 | 16 | @pytest.yield_fixture(scope='function') 17 | @yield_requires_config(CONFIG, ['xvfb_executable']) 18 | def xvfb_server(): 19 | """ Function-scoped Xvfb (X-Windows Virtual Frame Buffer) in a local thread. 20 | """ 21 | test_server = XvfbServer() 22 | yield test_server 23 | test_server.close() 24 | 25 | 26 | @pytest.yield_fixture(scope='session') 27 | @yield_requires_config(CONFIG, ['xvfb_executable']) 28 | def xvfb_server_sess(): 29 | """ Session-scoped Xvfb (X-Windows Virtual Frame Buffer) in a local thread. 30 | """ 31 | test_server = XvfbServer() 32 | yield test_server 33 | test_server.close() 34 | 35 | 36 | 37 | # TODO: make this a TestServer, clean up print statements for proper logging 38 | class XvfbServer(object): 39 | # see https://github.com/revnode/xvfb-run/blob/master/xvfb-run 40 | xvfb_command = 'Xvfb' 41 | xvfb_args = '-screen 0 1024x768x24 -nolisten tcp -reset -terminate'.split() 42 | display, authfile, process, fbmem = None, None, None, None 43 | 44 | def __init__(self): 45 | tmpdir = mkdtemp(prefix='XvfbServer.', dir=Workspace.get_base_tempdir()) 46 | for servernum in range(os.getpid(), 65536): 47 | if os.path.exists('/tmp/.X{0}-lock'.format(servernum)): 48 | continue 49 | self.display = ':' + str(servernum) 50 | self.authfile = os.path.join(tmpdir, 'Xauthority.' + self.display) 51 | mcookie = codecs.encode(os.urandom(16), "hex_codec") 52 | subprocess.check_call(['xauth', '-f', self.authfile, 'add', self.display, '.', mcookie]) 53 | errfile = os.path.join(tmpdir, 'Xvfb.' + self.display + '.err') 54 | with open(errfile, 'w') as f: # use a file instead of a pipe to simplify polling 55 | p = subprocess.Popen([self.xvfb_command, self.display, '-fbdir', tmpdir] + self.xvfb_args, 56 | stderr=f, env=dict(os.environ, XAUTHORITY=self.authfile)) 57 | self.fbmem = os.path.join(tmpdir, 'Xvfb_screen0') 58 | while not os.path.exists(self.fbmem): 59 | if p.poll() is not None: 60 | break 61 | time.sleep(0.1) 62 | else: 63 | p.poll() 64 | if p.returncode is not None: 65 | with open(errfile) as f: 66 | err = f.read() 67 | if 'Server is already active for display' in err: 68 | continue 69 | else: 70 | raise RuntimeError('Failed to start Xvfb', p.returncode, err) 71 | print('Xvfb started in ' + tmpdir) # for debugging 72 | self.process = p 73 | # If we terminate abnormally, ensure the Xvfb server is cleaned up after us. 74 | self._cleanup_script = subprocess.Popen(""" 75 | while kill -0 {0} 2>/dev/null; do sleep 1; done 76 | kill -INT {1} 77 | while kill -0 {1} 2>/dev/null; do sleep 1; done 78 | """.format(os.getpid(), p.pid), shell=True) 79 | break 80 | else: 81 | raise RuntimeError('Unable to find a free server number to start Xvfb') 82 | 83 | def close(self): 84 | if self.process is not None: 85 | if self.process.poll() is None: 86 | self.process.send_signal(signal.SIGINT) 87 | self.process.wait() 88 | self._cleanup_script.kill() 89 | self.process, self._cleanup_script = None, None 90 | 91 | def __enter__(self): 92 | return self 93 | 94 | def __exit__(self, *_args): 95 | self.close() 96 | -------------------------------------------------------------------------------- /pytest-server-fixtures/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-server-fixtures/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup, find_packages 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Programming Language :: Python :: 3.6', 17 | 'Programming Language :: Python :: 3.7', 18 | ] 19 | 20 | install_requires = ['pytest', 21 | 'pytest-shutil', 22 | 'pytest-fixture-config', 23 | 'requests', 24 | 'retry', 25 | 'psutil', 26 | ] 27 | 28 | extras_require = { 29 | 'jenkins': ["python-jenkins"], 30 | 'mongodb': ["pymongo>=3.6.0"], 31 | 'postgres': ["psycopg2-binary"], 32 | 'redis': ["redis"], 33 | 's3': ["boto3"], 34 | 'docker': ["docker"], 35 | 'kubernetes': ["kubernetes"], 36 | } 37 | 38 | tests_require = [ 39 | 'psutil', 40 | ] 41 | 42 | entry_points = { 43 | 'pytest11': [ 44 | 'httpd_server = pytest_server_fixtures.httpd', 45 | 'jenkins_server = pytest_server_fixtures.jenkins', 46 | 'mongodb_server = pytest_server_fixtures.mongo', 47 | 'postgres_server = pytest_server_fixtures.postgres', 48 | 'redis_server = pytest_server_fixtures.redis', 49 | 'xvfb_server = pytest_server_fixtures.xvfb', 50 | 's3 = pytest_server_fixtures.s3', 51 | ] 52 | } 53 | 54 | if __name__ == '__main__': 55 | kwargs = common_setup('pytest_server_fixtures') 56 | kwargs.update(dict( 57 | name='pytest-server-fixtures', 58 | description='Extensible server fixtures for py.test', 59 | author='Edward Easton', 60 | author_email='eeaston@gmail.com', 61 | classifiers=classifiers, 62 | install_requires=install_requires, 63 | extras_require=extras_require, 64 | tests_require=tests_require, 65 | packages=find_packages(exclude='tests'), 66 | entry_points=entry_points, 67 | )) 68 | setup(**kwargs) 69 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/jenkins_plugins/jython.hpi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/man-group/pytest-plugins/0018cc543229732b9c04fad909d2b7ee6167fa40/pytest-server-fixtures/tests/integration/jenkins_plugins/jython.hpi -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/jenkins_plugins/notification.hpi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/man-group/pytest-plugins/0018cc543229732b9c04fad909d2b7ee6167fa40/pytest-server-fixtures/tests/integration/jenkins_plugins/notification.hpi -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/test_httpd_proxy_server.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | 3 | def test_start_and_stop(httpd_server): 4 | assert httpd_server.check_server_up() 5 | pid = httpd_server.pid 6 | httpd_server.kill() 7 | assert not httpd_server.check_server_up() 8 | still_running = [i for i in psutil.process_iter() 9 | if i.pid == pid or i.ppid == pid] 10 | assert not still_running 11 | 12 | 13 | def test_logs(httpd_server): 14 | files = [i.name for i in httpd_server.log_dir.iterdir()] 15 | for log in ('access.log', 'error.log'): 16 | assert log in files 17 | 18 | 19 | def test_get_from_document_root(httpd_server): 20 | hello = httpd_server.document_root / 'hello.txt' 21 | hello.write_text('Hello World!') 22 | response = httpd_server.get('hello.txt') 23 | assert response.status_code == 200 24 | assert response.text == 'Hello World!' 25 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/test_jenkins_server.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from pytest import raises 3 | 4 | try: 5 | from unittest.mock import patch 6 | except ImportError: 7 | # python 2 8 | from mock import patch 9 | 10 | 11 | # patch out any changes you want to the Jenkins server here: 12 | # These are once-for-all changes! 13 | 14 | this_dir = os.path.dirname(__file__) 15 | PLUGIN_REPO = os.path.join(this_dir, 'jenkins_plugins') 16 | 17 | 18 | def test_load_plugins_fails_with_invalid_repo(jenkins_server_module): 19 | with raises(ValueError) as e: 20 | jenkins_server_module.load_plugins('junk_repo_name') 21 | assert str(e.value) == 'Plugin repository "junk_repo_name" does not exist' 22 | 23 | def test_load_plugins_fails_with_invalid_plugin_name_as_string(jenkins_server_module): 24 | with raises(ValueError) as e: 25 | jenkins_server_module.load_plugins(PLUGIN_REPO, 'junk') 26 | assert str(e.value) == 'Plugin "junk" is not present in the repository' 27 | 28 | def test_load_plugins_fails_with_invalid_plugin_name_as_list(jenkins_server_module): 29 | with raises(ValueError)as e: 30 | jenkins_server_module.load_plugins(PLUGIN_REPO, ['junk']) 31 | assert str(e.value) == 'Plugin "junk" is not present in the repository' 32 | 33 | def test_load_plugins_fails_with_invalid_plugin_name_no_duplicates_in_error_msg(jenkins_server_module): 34 | with raises(ValueError) as e: 35 | jenkins_server_module.load_plugins(PLUGIN_REPO, ['junk', 'junk']) 36 | assert str(e.value) == 'Plugin "junk" is not present in the repository' 37 | 38 | def test_load_plugins_fails_with_invalid_plugin_names_as_list(jenkins_server_module): 39 | with raises(ValueError)as e: 40 | jenkins_server_module.load_plugins(PLUGIN_REPO, ['zjunk', 'junk']) 41 | assert str(e.value) == "Plugins ['junk', 'zjunk'] are not present in the repository" 42 | 43 | def test_load_plugins_loads_only_nominated_plugins(jenkins_server_module): 44 | with patch('pytest_server_fixtures.jenkins.shutil.copy') as mock_copy: 45 | jenkins_server_module.load_plugins(PLUGIN_REPO, 'notification') 46 | assert mock_copy.call_count == 1 47 | tup = mock_copy.call_args_list[0][0] 48 | assert tup[0].endswith('jenkins_plugins/notification.hpi') 49 | assert str(tup[1]) == os.path.join(jenkins_server_module.workspace, 'plugins/notification.hpi') 50 | 51 | def test_load_plugins_loads_all_plugins(jenkins_server_module): 52 | with patch('pytest_server_fixtures.jenkins.shutil.copy') as mock_copy: 53 | jenkins_server_module.load_plugins(PLUGIN_REPO) 54 | assert mock_copy.call_count == len([x for x in os.listdir(PLUGIN_REPO) 55 | if x.endswith('.hpi')]) 56 | 57 | def test_jenkins_pre_server(jenkins_server_module): 58 | """ Creates template, creates the jenkins job 59 | """ 60 | assert jenkins_server_module.check_server_up() 61 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/test_mongo_server.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_mongo_server(mongo_server): 5 | assert mongo_server.check_server_up() 6 | assert mongo_server.delete 7 | mongo_server.api.db.test.insert_one({'a': 'b', 'c': 'd'}) 8 | assert mongo_server.api.db.test.find_one({'a': 'b'}, {'_id': False}) == {'a': 'b', 'c': 'd'} 9 | 10 | 11 | @pytest.mark.parametrize('count', range(3)) 12 | def test_mongo_server_multi(count, mongo_server): 13 | coll = mongo_server.api.some_database.some_collection 14 | assert coll.count_documents({}) == 0 15 | coll.insert_one({'a': 'b'}) 16 | assert coll.count_documents({}) == 1 17 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/test_postgres.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | def test_postgres_server(postgres_server_sess): 4 | conn = postgres_server_sess.connect('integration') 5 | cursor = conn.cursor() 6 | cursor.execute("CREATE TABLE test (id serial PRIMARY KEY, num integer, data varchar);") 7 | cursor.execute("INSERT INTO test (num, data) VALUES (%s, %s)", (100, "abc'def")) 8 | cursor.execute("SELECT * FROM test;") 9 | assert cursor.fetchone() == (1, 100, "abc'def") 10 | 11 | 12 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/test_redis_server.py: -------------------------------------------------------------------------------- 1 | def test_server_runner(redis_server): 2 | """ Boot up a server, push some keys into it 3 | """ 4 | assert redis_server.check_server_up() 5 | redis_server.api.set('foo', 'bar') 6 | assert redis_server.api.get('foo').decode('utf8') == 'bar' 7 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/test_s3_server.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | def test_connection(s3_bucket): 5 | client, bucket_name = s3_bucket 6 | bucket = client.Bucket(bucket_name) 7 | assert bucket is not None 8 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/integration/test_xvfb_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import time 4 | 5 | from itertools import chain, repeat 6 | 7 | try: 8 | from unittest.mock import patch 9 | except ImportError: 10 | # python 2 11 | from mock import patch 12 | 13 | import pytest 14 | from pytest import raises 15 | 16 | from pytest_server_fixtures.xvfb import XvfbServer 17 | 18 | 19 | def test_construct(xvfb_server): 20 | assert xvfb_server.display 21 | 22 | 23 | def test_connect_client(): 24 | with XvfbServer() as server: 25 | p = subprocess.Popen(['xdpyinfo', '-display', server.display], 26 | env=dict(os.environ, XAUTHORITY=server.authfile), stdout=subprocess.PIPE) 27 | dpyinfo, _ = p.communicate() 28 | assert p.returncode == 0 29 | assert server.display in str(dpyinfo) 30 | 31 | 32 | def test_terminates_on_last_client_exit(): 33 | with XvfbServer() as server: 34 | subprocess.check_call(['xdpyinfo', '-display', server.display], 35 | env=dict(os.environ, XAUTHORITY=server.authfile), stdout=open('/dev/null')) 36 | for _ in range(5): 37 | if server.process.poll() is not None: 38 | break 39 | time.sleep(0.1) # wait up to 0.5 seconds for the server to terminate 40 | assert server.process.poll() == 0 41 | 42 | 43 | def test_tries_to_find_free_server_num(): 44 | with XvfbServer() as server1: 45 | with XvfbServer() as server2: 46 | assert server1.display != server2.display 47 | 48 | 49 | def test_raises_if_fails_to_find_free_server_num(): 50 | _exists = os.path.exists 51 | with patch('os.path.exists', new=lambda f: "-lock" in f or _exists(f)): 52 | with raises(RuntimeError) as ex: 53 | XvfbServer() 54 | assert 'Unable to find a free server number to start Xvfb' in str(ex) 55 | 56 | 57 | def test_handles_unexpected_server_num_collision(): 58 | with XvfbServer() as server1: 59 | from os.path import exists as real_exists 60 | with patch('os.path.exists') as mock_exists: 61 | side_effect_chain = chain([lambda _: False], repeat(real_exists)) 62 | mock_exists.side_effect = lambda path: next(side_effect_chain)(path) 63 | with XvfbServer() as server2: 64 | assert server1.display != server2.display 65 | 66 | 67 | def test_handles_unexpected_failure_to_start(): 68 | with patch('pytest_server_fixtures.xvfb.XvfbServer.xvfb_command', 'false'): 69 | with raises(RuntimeError) as ex: 70 | XvfbServer() 71 | assert 'Failed to start Xvfb' in str(ex) 72 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/unit/serverclass/test_docker_unit.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import sentinel, patch, Mock 3 | except ImportError: 4 | # python 2 5 | from mock import sentinel, patch, Mock 6 | 7 | from pytest_server_fixtures.serverclass.docker import DockerServer 8 | 9 | @patch('pytest_server_fixtures.serverclass.docker.ServerClass.__init__') 10 | def test_init(mock_init): 11 | s = DockerServer(sentinel.server_type, 12 | sentinel.cmd, 13 | sentinel.get_args, 14 | sentinel.env, 15 | sentinel.image) 16 | 17 | mock_init.assert_called_with(sentinel.cmd, 18 | sentinel.get_args, 19 | sentinel.env) 20 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/unit/serverclass/test_kubernetes_unit.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | try: 4 | from unittest.mock import sentinel, patch, Mock 5 | except ImportError: 6 | # python 2 7 | from mock import sentinel, patch, Mock 8 | 9 | from pytest_server_fixtures.serverclass.kubernetes import KubernetesServer 10 | 11 | @pytest.mark.skip(reason="Need a way to run this test in Kubernetes") 12 | @patch('pytest_server_fixtures.serverclass.docker.ServerClass.__init__') 13 | def test_init(mock_init): 14 | s = KubernetesServer(sentinel.server_type, 15 | sentinel.cmd, 16 | sentinel.get_args, 17 | sentinel.env, 18 | sentinel.image) 19 | 20 | mock_init.assert_called_with(sentinel.cmd, 21 | sentinel.get_args, 22 | sentinel.env) 23 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/unit/serverclass/test_thread_unit.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import sentinel, patch, Mock 3 | except ImportError: 4 | # python 2 5 | from mock import sentinel, patch, Mock 6 | 7 | from pytest_server_fixtures import CONFIG 8 | from pytest_server_fixtures.serverclass.thread import ThreadServer 9 | 10 | 11 | @patch('pytest_server_fixtures.serverclass.thread.ServerClass.__init__') 12 | def test_init(mock_init): 13 | ts = ThreadServer(sentinel.cmd, 14 | sentinel.get_args, 15 | sentinel.env, 16 | sentinel.workspace, 17 | cwd=sentinel.cwd, 18 | listen_hostname=sentinel.listen_hostname) 19 | 20 | mock_init.assert_called_with(sentinel.cmd, 21 | sentinel.get_args, 22 | sentinel.env) 23 | assert ts._hostname == sentinel.listen_hostname 24 | assert ts._workspace == sentinel.workspace 25 | assert ts._cwd == sentinel.cwd 26 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/unit/test_server_unit.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import create_autospec, sentinel, call, patch, Mock 3 | except ImportError: 4 | # python 2 5 | from mock import create_autospec, sentinel, call, patch, Mock 6 | 7 | from pytest_server_fixtures.base import TestServer as _TestServer # So that pytest doesnt think this is a test case 8 | 9 | 10 | def test_init(): 11 | ws = Mock() 12 | with patch('pytest_shutil.workspace.Workspace.__init__', autospec=True) as init: 13 | ts = _TestServer(workspace=ws, delete=sentinel.delete, 14 | port=sentinel.port, hostname=sentinel.hostname) 15 | assert init.call_args_list == [call(ts, workspace=ws, delete=sentinel.delete)] 16 | assert ts.hostname == sentinel.hostname 17 | assert ts.port == sentinel.port 18 | assert ts.dead is False 19 | 20 | # Silence teardown warnings 21 | ts.dead = True 22 | ts.workspace = ws 23 | 24 | 25 | def test_kill_by_port(): 26 | server = _TestServer(hostname=sentinel.hostname, port=sentinel.port) 27 | server.run = Mock(side_effect=['100\n', '', '']) 28 | server._signal = Mock() 29 | with patch('socket.gethostbyname', return_value=sentinel.ip): 30 | server._find_and_kill_by_port(2, sentinel.signal) 31 | server.dead = True 32 | assert server.run.call_args_list == [call("netstat -anp 2>/dev/null | grep sentinel.ip:sentinel.port " 33 | "| grep LISTEN | awk '{ print $7 }' | cut -d'/' -f1", capture=True, cd='/'), 34 | call("netstat -anp 2>/dev/null | grep sentinel.ip:sentinel.port " 35 | "| grep LISTEN | awk '{ print $7 }' | cut -d'/' -f1", capture=True, cd='/')] 36 | assert server._signal.call_args_list == [call(100, sentinel.signal)] 37 | -------------------------------------------------------------------------------- /pytest-server-fixtures/tests/unit/test_server_v2_unit.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import create_autospec, sentinel, call, patch, Mock 3 | except ImportError: 4 | # python 2 5 | from mock import create_autospec, sentinel, call, patch, Mock 6 | 7 | from pytest_server_fixtures.base2 import TestServerV2 as _TestServerV2 # TODO: why as _TestServerV2? 8 | 9 | def test_init(): 10 | with patch('pytest_shutil.workspace.Workspace.__init__', autospec=True) as init: 11 | ts = _TestServerV2(cwd=sentinel.cwd, 12 | workspace=sentinel.workspace, 13 | delete=sentinel.delete, 14 | server_class=sentinel.server_class) 15 | assert init.call_args_list == [call(ts, workspace=sentinel.workspace, delete=sentinel.delete)] 16 | assert ts._cwd == sentinel.cwd 17 | assert ts._server_class == sentinel.server_class 18 | 19 | def test_hostname_when_server_is_not_started(): 20 | ts = _TestServerV2() 21 | assert ts.hostname == None 22 | -------------------------------------------------------------------------------- /pytest-shutil/README.md: -------------------------------------------------------------------------------- 1 | # pytest-shutil 2 | 3 | 4 | This library is a goodie-bag of Unix shell and environment management tools for automated tests. 5 | A summary of the available functions is below, look at the source for the full listing. 6 | 7 | ## Installation 8 | 9 | Install using your favourite package manager:: 10 | 11 | ```bash 12 | pip install pytest-shutil 13 | # or.. 14 | easy_install pytest-shutil 15 | ``` 16 | 17 | ## Workspace Fixture 18 | 19 | The workspace fixture is simply a temporary directory at function-scope with a few bells and whistles:: 20 | 21 | ```python 22 | # Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points) 23 | pytest_plugins = ['pytest_shutil'] 24 | 25 | def test_something(workspace): 26 | # Workspaces contain a handle to the path `path` object (see https://path.readthedocs.io/) 27 | path = workspace.workspace 28 | script = path / 'hello.sh' 29 | script.write_text('#!/bin/sh\n echo hello world!') 30 | 31 | # There is a 'run' method to execute things relative to the workspace root 32 | workspace.run('hello.sh') 33 | ``` 34 | 35 | ## ``pytest_shutil.env``: Shell helpers 36 | 37 | | function | description 38 | | --------- | ----------- 39 | | set_env | contextmanager to set env vars 40 | | unset_env | contextmanager to unset env vars 41 | | no_env | contextmanager to unset a single env var 42 | | no_cov | contextmanager to disable coverage in subprocesses 43 | 44 | ## ``pytest_shutil.cmdline``: Command-line helpers 45 | 46 | | function | description 47 | | --------- | ----------- 48 | | umask | contextmanager to set the umask 49 | | chdir | contextmanager to change to a directory 50 | | TempDir | contextmanager for a temporary directory 51 | | PrettyFormatter | simple text formatter for drawing title, paragrahs, hrs. 52 | | copy_files | copy all files from one directory to another 53 | | getch | cross-platform read of a single character from the screen 54 | | which | analoge of unix ``which`` 55 | | get_real_python_executable | find our system Python, useful when running under virtualenv 56 | 57 | ## ``pytest_shutil.run``: Running things in subprocesses 58 | 59 | | function | description 60 | | --------- | ----------- 61 | | run | run a command, with options for capturing output, checking return codes. 62 | | run_as_main | run a function as if it was the system entry point 63 | | run_module_as_main | run a module as if it was the system entry point 64 | | run_in_subprocess | run a function in a subprocess 65 | | run_with_coverage | run a command with coverage enabled 66 | -------------------------------------------------------------------------------- /pytest-shutil/pytest_shutil/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/man-group/pytest-plugins/0018cc543229732b9c04fad909d2b7ee6167fa40/pytest-shutil/pytest_shutil/__init__.py -------------------------------------------------------------------------------- /pytest-shutil/pytest_shutil/env.py: -------------------------------------------------------------------------------- 1 | """ Environment management utilities 2 | """ 3 | import os 4 | import sys 5 | import functools 6 | from contextlib import contextmanager 7 | 8 | 9 | # TODO: merge with cmdline 10 | @contextmanager 11 | def set_env(*args, **kwargs): 12 | """Context Mgr to set an environment variable 13 | 14 | """ 15 | def update_environment(env): 16 | for k, v in env.items(): 17 | if v is None: 18 | if k in os.environ: 19 | del os.environ[k] 20 | else: 21 | os.environ[k] = str(v) 22 | 23 | # Backward compatibility with the old interface which only allowed to 24 | # update a single environment variable. 25 | new_values = dict([(args[0], args[1])]) if len(args) == 2 else {} 26 | new_values.update((k, v) for k, v in kwargs.items()) 27 | 28 | # Save variables that are going to be updated. 29 | saved_values = dict((k, os.environ.get(k)) for k in new_values.keys()) 30 | 31 | # Update variables to their temporary values 32 | try: 33 | update_environment(new_values) 34 | yield 35 | finally: 36 | # Restore original environment 37 | update_environment(saved_values) 38 | 39 | 40 | set_home = functools.partial(set_env, 'HOME') 41 | 42 | 43 | @contextmanager 44 | def unset_env(env_var_skiplist): 45 | """Context Mgr to unset an environment variable temporarily.""" 46 | def update_environment(env): 47 | os.environ.clear() 48 | os.environ.update(env) 49 | 50 | # Save variables that are going to be updated. 51 | saved_values = dict(os.environ) 52 | 53 | new_values = dict((k, v) for k, v in os.environ.items() if k not in env_var_skiplist) 54 | 55 | # Update variables to their temporary values 56 | update_environment(new_values) 57 | (yield) 58 | # Restore original environment 59 | update_environment(saved_values) 60 | 61 | 62 | @contextmanager 63 | def no_env(key): 64 | """ 65 | Context Mgr to asserting no environment variable of the given name exists 66 | (sto enable the testing of the case where no env var of this name exists) 67 | """ 68 | try: 69 | orig_value = os.environ[key] 70 | del os.environ[key] 71 | env_has_key = True 72 | except KeyError: 73 | env_has_key = False 74 | 75 | yield 76 | if env_has_key: 77 | os.environ[key] = orig_value 78 | else: 79 | # there shouldn't be a key in org state.. just check that there isn't 80 | try: 81 | del os.environ[key] 82 | except KeyError: 83 | pass 84 | 85 | 86 | @contextmanager 87 | def no_cov(): 88 | """ Context manager to disable coverage in subprocesses. 89 | """ 90 | cov_keys = [i for i in os.environ.keys() if i.startswith('COV')] 91 | with unset_env(cov_keys): 92 | yield 93 | 94 | 95 | def get_clean_python_env(): 96 | """ Returns the shell environ stripped of its PYTHONPATH 97 | """ 98 | env = dict(os.environ) 99 | if 'PYTHONPATH' in env: 100 | del(env['PYTHONPATH']) 101 | return env 102 | 103 | 104 | def get_env_with_pythonpath(): 105 | """ Returns the shell environ with PYTHONPATH set to the current sys.path. 106 | This is useful for scripts run under 'python setup.py test', which adds 107 | a bunch of test dependencies to sys.path at run-time. 108 | """ 109 | env = get_clean_python_env() 110 | env['PYTHONPATH'] = ';'.join(sys.path) 111 | return env 112 | -------------------------------------------------------------------------------- /pytest-shutil/pytest_shutil/workspace.py: -------------------------------------------------------------------------------- 1 | """ Temporary directory fixtures 2 | """ 3 | import os 4 | import tempfile 5 | import shutil 6 | import logging 7 | import subprocess 8 | 9 | from pathlib import Path 10 | import pytest 11 | 12 | from . import cmdline 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | @pytest.yield_fixture() 18 | def workspace(): 19 | """ Function-scoped temporary workspace that cleans up on exit. 20 | 21 | Attributes 22 | ---------- 23 | workspace (`path.path`): Path to the workspace directory. 24 | debug (bool): If set to True, will log more debug when running commands. 25 | delete (bool): If True, will always delete the workspace on teardown; 26 | .. If None, delete the workspace unless teardown occurs via an exception; 27 | .. If False, never delete the workspace on teardown. 28 | 29 | """ 30 | ws = Workspace() 31 | yield ws 32 | ws.teardown() 33 | 34 | 35 | class Workspace(object): 36 | """ 37 | Creates a temp workspace, cleans up on teardown. Can also be used as a context manager. 38 | Has a 'run' method to execute commands relative to this directory. 39 | """ 40 | debug = False 41 | delete = True 42 | 43 | def __init__(self, workspace=None, delete=None): 44 | self.delete = delete 45 | 46 | log.debug("") 47 | log.debug("=======================================================") 48 | if workspace is None: 49 | self.workspace = Path(tempfile.mkdtemp(dir=self.get_base_tempdir())) 50 | log.debug("pytest_shutil created workspace %s" % self.workspace) 51 | 52 | else: 53 | self.workspace = Path(workspace) 54 | log.debug("pytest_shutil using workspace %s" % self.workspace) 55 | if 'DEBUG' in os.environ: 56 | self.debug = True 57 | if self.delete is not False: 58 | log.debug("This workspace will delete itself on teardown") 59 | log.debug("=======================================================") 60 | log.debug("") 61 | 62 | def __enter__(self): 63 | return self 64 | 65 | def __exit__(self, errtype, value, traceback): # @UnusedVariable 66 | if self.delete is None: 67 | self.delete = (errtype is None) 68 | self.teardown() 69 | 70 | def __del__(self): 71 | self.teardown() 72 | 73 | @staticmethod 74 | def get_base_tempdir(): 75 | """ Returns an appropriate dir to pass into 76 | tempfile.mkdtemp(dir=xxx) or similar. 77 | """ 78 | # Prefer CI server workspaces. TODO: look for env vars for other CI servers 79 | return os.getenv('WORKSPACE') 80 | 81 | def run(self, cmd, capture=False, check_rc=True, cd=None, shell=False, **kwargs): 82 | """ 83 | Run a command relative to a given directory, defaulting to the workspace root 84 | 85 | Parameters 86 | ---------- 87 | cmd : `str` or `list` 88 | Command string or list. Commands given as a string will be run in a subshell. 89 | capture : `bool` 90 | Capture and return output 91 | check_rc : `bool` 92 | Assert return code is zero 93 | cd : `str` 94 | Path to chdir to, defaults to workspace root 95 | """ 96 | if isinstance(cmd, str): 97 | shell = True 98 | else: 99 | # Some of the command components might be path objects or numbers 100 | cmd = [str(i) for i in cmd] 101 | 102 | if not cd: 103 | cd = self.workspace 104 | 105 | with cmdline.chdir(cd): 106 | log.debug("run: {0}".format(cmd)) 107 | if capture: 108 | p = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs) 109 | else: 110 | p = subprocess.Popen(cmd, shell=shell, **kwargs) 111 | (out, _) = p.communicate() 112 | 113 | if out is not None and not isinstance(out, str): 114 | out = out.decode('utf-8') 115 | 116 | if self.debug and capture: 117 | log.debug("Stdout/stderr:") 118 | log.debug(out) 119 | 120 | if check_rc and p.returncode != 0: 121 | err = subprocess.CalledProcessError(p.returncode, cmd) 122 | err.output = out 123 | if capture and not self.debug: 124 | log.error("Stdout/stderr:") 125 | log.error(out) 126 | raise err 127 | 128 | return out 129 | 130 | def teardown(self): 131 | if self.delete is not None and not self.delete: 132 | return 133 | if hasattr(self, 'workspace') and self.workspace.is_dir(): 134 | log.debug("") 135 | log.debug("=======================================================") 136 | log.debug("pytest_shutil deleting workspace %s" % self.workspace) 137 | log.debug("=======================================================") 138 | log.debug("") 139 | shutil.rmtree(self.workspace, ignore_errors=True) 140 | -------------------------------------------------------------------------------- /pytest-shutil/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-shutil/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup, find_packages 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Programming Language :: Python :: 3.6', 17 | 'Programming Language :: Python :: 3.7', 18 | ] 19 | 20 | install_requires = [ 21 | 'execnet', 22 | 'pytest', 23 | 'termcolor', 24 | 'importlib_metadata;python_version<"3.8"', 25 | ] 26 | 27 | tests_require = ['pytest', 28 | ] 29 | 30 | entry_points = { 31 | 'pytest11': [ 32 | 'workspace = pytest_shutil.workspace', 33 | ] 34 | } 35 | 36 | if __name__ == '__main__': 37 | kwargs = common_setup('pytest_shutil') 38 | kwargs.update(dict( 39 | name='pytest-shutil', 40 | description='A goodie-bag of unix shell and environment tools for py.test', 41 | author='Edward Easton', 42 | author_email='eeaston@gmail.com', 43 | classifiers=classifiers, 44 | install_requires=install_requires, 45 | tests_require=tests_require, 46 | packages=find_packages(exclude='tests'), 47 | entry_points=entry_points, 48 | )) 49 | setup(**kwargs) 50 | -------------------------------------------------------------------------------- /pytest-shutil/tests/integration/test_cmdline_integration.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pytest_shutil import cmdline 4 | 5 | 6 | def test_chdir(): 7 | here = os.getcwd() 8 | bindir = os.path.realpath('/bin') 9 | with cmdline.chdir(bindir): 10 | assert os.getcwd() == bindir 11 | assert os.getcwd() == here 12 | 13 | 14 | def test_chdir_goes_away(workspace): 15 | os.chdir(workspace.workspace) 16 | workspace.teardown() 17 | bindir = os.path.realpath('/bin') 18 | with cmdline.chdir(bindir): 19 | assert os.getcwd() == bindir 20 | assert os.getcwd() == '/' -------------------------------------------------------------------------------- /pytest-shutil/tests/integration/test_env_integration.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pytest_shutil import env, run 4 | 5 | TEMP_NAME = 'JUNK123_456_789' 6 | 7 | def test_set_env_ok_if_exists(): 8 | ev = os.environ[TEMP_NAME] = 'junk_name' 9 | try: 10 | with env.set_env(TEMP_NAME, 'anything'): 11 | out = run.run('env', capture_stdout=True) 12 | for o in out.split('\n'): 13 | if o.startswith(TEMP_NAME): 14 | assert o == '%s=anything' % TEMP_NAME 15 | break 16 | else: 17 | assert False, '%s not found in os.environ' % TEMP_NAME 18 | assert os.environ[TEMP_NAME] == ev 19 | 20 | finally: 21 | if TEMP_NAME in os.environ: 22 | del os.environ[TEMP_NAME] 23 | 24 | 25 | def test_set_env_ok_if_not_exists(): 26 | if TEMP_NAME in os.environ: 27 | del os.environ[TEMP_NAME] 28 | with env.set_env(TEMP_NAME, 'anything'): 29 | out = run.run('env', capture_stdout=True) 30 | for o in out.split('\n'): 31 | if o.startswith(TEMP_NAME): 32 | assert o == '%s=anything' % TEMP_NAME 33 | break 34 | else: 35 | assert False, '%s not found in os.environ' % TEMP_NAME 36 | 37 | 38 | def test_subprocecmdline(): 39 | ev = os.environ[TEMP_NAME] = 'junk_name' 40 | try: 41 | with env.no_env(TEMP_NAME): 42 | out = run.run('env', capture_stdout=True) 43 | for o in out.split('\n'): 44 | if o.startswith(TEMP_NAME): 45 | assert False, '%s found in os.environ' % TEMP_NAME 46 | 47 | assert os.environ[TEMP_NAME] == ev 48 | finally: 49 | if TEMP_NAME in os.environ: 50 | del os.environ[TEMP_NAME] 51 | 52 | 53 | def test_no_env_ok_if_not_exists(): 54 | if TEMP_NAME in os.environ: 55 | del os.environ[TEMP_NAME] 56 | with env.no_env(TEMP_NAME): 57 | out = run.run('env', capture_stdout=True) 58 | for o in out.split('\n'): 59 | if o.startswith(TEMP_NAME): 60 | assert False, '%s found in os.environ' % TEMP_NAME 61 | 62 | assert TEMP_NAME not in os.environ 63 | -------------------------------------------------------------------------------- /pytest-shutil/tests/integration/test_workspace_integration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import subprocess 4 | import sys 5 | import textwrap 6 | 7 | 8 | def test_workspace_run_displays_output_on_failure(): 9 | p = subprocess.Popen([sys.executable, '-c', """import logging 10 | logging.basicConfig(level=logging.DEBUG) 11 | from subprocess import CalledProcessError 12 | from pytest_shutil.workspace import Workspace 13 | try: 14 | Workspace().run('echo stdout; echo stderr >&2; false', capture=True) 15 | except CalledProcessError: 16 | pass 17 | else: 18 | raise RuntimeError("did not raise") 19 | """], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 20 | out = p.communicate()[0] 21 | assert p.returncode == 0 22 | assert 'stdout\n'.encode('utf-8') in out 23 | assert 'stderr\n'.encode('utf-8') in out 24 | 25 | 26 | def test_workspace_fixture_autodelete(monkeypatch, tmpdir): 27 | workspace = (tmpdir / 'tmp').mkdir() 28 | monkeypatch.setenv('WORKSPACE', workspace) 29 | testsuite = tmpdir.join('test.py') 30 | with testsuite.open('w') as fp: 31 | fp.write(textwrap.dedent( 32 | """ 33 | def test(workspace): 34 | (workspace.workspace / 'foo').touch() 35 | """)) 36 | subprocess.check_call([sys.executable, '-m', 'pytest', '-sv', str(testsuite)]) 37 | assert not glob.glob('{}/*/foo'.format(str(workspace))) 38 | assert not glob.glob('{}/*tmp*'.format(str(workspace))) 39 | -------------------------------------------------------------------------------- /pytest-shutil/tests/unit/test_cmdline.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pytest_shutil import cmdline 4 | 5 | 6 | def test_umask(workspace): 7 | f = workspace.workspace / 'foo' 8 | with cmdline.umask(0o202): 9 | f.touch() 10 | assert (f.stat().st_mode & 0o777) == 0o464 11 | 12 | def test_pretty_formatter(monkeypatch): 13 | monkeypatch.setenv("FORCE_COLOR", "1") 14 | f = cmdline.PrettyFormatter() 15 | f.title('A Title') 16 | f.hr() 17 | f.p('A Paragraph', 'red') 18 | assert f.buffer == [ 19 | '\x1b[1m\x1b[34m A Title\x1b[0m', 20 | '\x1b[1m\x1b[34m--------------------------------------------------------------------------------\x1b[0m', 21 | '\x1b[31mA Paragraph\x1b[0m' 22 | ] 23 | f.flush() 24 | 25 | 26 | def test_tempdir(): 27 | with cmdline.TempDir() as d: 28 | assert os.path.exists(d) 29 | assert not os.path.exists(d) 30 | 31 | 32 | def test_copy_files(workspace): 33 | d1 = workspace.workspace / 'd1' 34 | d2 = workspace.workspace / 'd2' 35 | os.makedirs(d1) 36 | os.makedirs(d2) 37 | (d1 / 'foo').touch() 38 | (d1 / 'bar').touch() 39 | cmdline.copy_files(d1, d2) 40 | assert (d2 / 'foo').exists() 41 | assert (d2 / 'bar').exists() 42 | -------------------------------------------------------------------------------- /pytest-shutil/tests/unit/test_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from pytest_shutil import env 6 | 7 | TEMP_NAME = 'JUNK123_456_789' 8 | 9 | 10 | def test_set_env_ok_if_exists(): 11 | ev = os.environ[TEMP_NAME] = 'junk_name' 12 | try: 13 | with env.set_env(TEMP_NAME, 'not_junk'): 14 | assert os.environ[TEMP_NAME] == 'not_junk' 15 | assert os.environ[TEMP_NAME] == ev 16 | finally: 17 | del os.environ[TEMP_NAME] 18 | 19 | 20 | def test_set_env_ok_if_not_exists(): 21 | if TEMP_NAME in os.environ: 22 | del os.environ[TEMP_NAME] 23 | 24 | with env.set_env(TEMP_NAME, 'anything'): 25 | assert os.environ[TEMP_NAME] == 'anything' 26 | assert TEMP_NAME not in os.environ 27 | 28 | 29 | def test_unset_env(): 30 | try: 31 | os.environ[TEMP_NAME] = 'junk_name' 32 | assert os.environ[TEMP_NAME] == 'junk_name' 33 | 34 | with env.unset_env([TEMP_NAME]): 35 | with pytest.raises(KeyError): # @UndefinedVariable 36 | os.environ[TEMP_NAME] 37 | 38 | assert os.environ[TEMP_NAME] == 'junk_name' 39 | finally: 40 | if TEMP_NAME in os.environ: 41 | del os.environ[TEMP_NAME] 42 | 43 | 44 | def test_no_env_ok_if_exists(): 45 | ev = os.environ[TEMP_NAME] = 'junk_name' 46 | try: 47 | with env.no_env(TEMP_NAME): 48 | assert TEMP_NAME not in os.environ 49 | assert os.environ[TEMP_NAME] == ev 50 | finally: 51 | if TEMP_NAME in os.environ: 52 | del os.environ[TEMP_NAME] 53 | 54 | 55 | def test_no_env_ok_if_not_exists(): 56 | if TEMP_NAME in os.environ: 57 | del os.environ[TEMP_NAME] 58 | with env.no_env(TEMP_NAME): 59 | assert TEMP_NAME not in os.environ 60 | assert TEMP_NAME not in os.environ 61 | 62 | 63 | 64 | def test_set_env_with_kwargs_updates(): 65 | test_env = {"TEST_ACME_TESTING_A": "a", 66 | "TEST_ACME_TESTING_B": "b", 67 | "TEST_ACME_TESTING_C": "c"} 68 | os.environ.update(test_env) 69 | with env.set_env("TEST_ACME_TESTING_A", 1, TEST_ACME_TESTING_B="fred", 70 | TEST_ACME_TESTING_C=None): 71 | assert os.environ["TEST_ACME_TESTING_A"] == "1" 72 | assert os.environ["TEST_ACME_TESTING_B"] == "fred" 73 | assert "C" not in os.environ 74 | assert os.environ["TEST_ACME_TESTING_A"] == "a" 75 | assert os.environ["TEST_ACME_TESTING_B"] == "b" 76 | assert os.environ["TEST_ACME_TESTING_C"] == "c" 77 | 78 | 79 | def test_set_home(): 80 | home = os.environ['HOME'] 81 | with env.set_home('/tmp'): 82 | assert os.environ['HOME'] == '/tmp' 83 | assert os.environ['HOME'] == home -------------------------------------------------------------------------------- /pytest-svn/README.md: -------------------------------------------------------------------------------- 1 | # Pytest SVN Fixture 2 | 3 | Creates an empty SVN repository for testing that cleans up after itself on teardown. 4 | 5 | ## Installation 6 | 7 | Install using your favourite package installer: 8 | ```bash 9 | pip install pytest-svn 10 | # or 11 | easy_install pytest-svn 12 | ``` 13 | 14 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 15 | 16 | ```python 17 | pytest_plugins = ['pytest_svn'] 18 | ``` 19 | 20 | ## Usage 21 | 22 | Here's a noddy test case that shows it working: 23 | 24 | ```python 25 | def test_svn_repo(svn_repo): 26 | # The fixture derives from `workspace` in `pytest-shutil`, so they contain 27 | # a handle to the path `path` object (see https://pathpy.readthedocs.io/) 28 | path = svn_repo.workspace 29 | file = path / 'hello.txt' 30 | file.write_text('hello world!') 31 | 32 | # We can also run things relative to the repo 33 | svn_repo.run('svn add hello.txt') 34 | 35 | # The fixture has a URI property you can use in downstream systems 36 | assert svn_repo.uri.startswith('file://') 37 | ``` -------------------------------------------------------------------------------- /pytest-svn/pytest_svn.py: -------------------------------------------------------------------------------- 1 | """ Repository fixtures 2 | """ 3 | import pytest 4 | from pytest_shutil.workspace import Workspace 5 | 6 | 7 | @pytest.yield_fixture() 8 | def svn_repo(): 9 | """ Function-scoped fixture to create a new svn repo in a temporary workspace. 10 | 11 | Attributes 12 | ---------- 13 | uri (str) : SVN repo uri. 14 | .. also inherits all attributes from the `workspace` fixture 15 | 16 | """ 17 | repo = SVNRepo() 18 | yield repo 19 | repo.teardown() 20 | 21 | 22 | class SVNRepo(Workspace): 23 | """ 24 | Creates an empty SVN repository in a temporary workspace. 25 | Cleans up on exit. 26 | 27 | Attributes 28 | ---------- 29 | uri : `str` 30 | repository base uri 31 | """ 32 | def __init__(self): 33 | super(SVNRepo, self).__init__() 34 | self.run('svnadmin create .', capture=True) 35 | self.uri = "file://%s" % self.workspace 36 | -------------------------------------------------------------------------------- /pytest-svn/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-svn/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Programming Language :: Python :: 3.6', 17 | 'Programming Language :: Python :: 3.7', 18 | ] 19 | 20 | install_requires = ['pytest', 21 | 'pytest-shutil', 22 | ] 23 | 24 | tests_require = [ 25 | ] 26 | 27 | entry_points = { 28 | 'pytest11': [ 29 | 'svn_repo = pytest_svn', 30 | ] 31 | } 32 | 33 | if __name__ == '__main__': 34 | kwargs = common_setup('pytest_svn') 35 | kwargs.update(dict( 36 | name='pytest-svn', 37 | description='SVN repository fixture for py.test', 38 | platforms=['unix', 'linux'], 39 | author='Edward Easton', 40 | author_email='eeaston@gmail.com', 41 | classifiers=classifiers, 42 | install_requires=install_requires, 43 | tests_require=tests_require, 44 | py_modules=['pytest_svn'], 45 | entry_points=entry_points, 46 | )) 47 | setup(**kwargs) 48 | -------------------------------------------------------------------------------- /pytest-svn/tests/integration/test_svn.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_foo(svn_repo): 4 | assert hasattr(svn_repo, 'uri') 5 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/README.md: -------------------------------------------------------------------------------- 1 | # Pytest Verbose Parametrize 2 | 3 | Pytest parametrize hook to generate ids for parametrized tests that are a little 4 | more descriptive than the default (which just outputs id numbers). 5 | 6 | ## Installation 7 | 8 | Install with your favourite package manager, and this plugin will automatically be enabled: 9 | ```bash 10 | pip install pytest-verbose-parametrize 11 | # or .. 12 | easy_install pytest-verbose-parametrize 13 | ``` 14 | ## Usage 15 | 16 | ```python 17 | import pytest 18 | 19 | @pytest.mark.parametrize(('f', 't'), [(sum, list), (len, int)]) 20 | def test_foo(f, t): 21 | assert isinstance(f([[1], [2]]), t) 22 | ``` 23 | 24 | In this example, the test ids will be generated as `test_foo[sum-list]`, 25 | `test_foo[len-int]` instead of the default `test_foo[1-2]`, `test_foo[3-4]`. 26 | 27 | ```bash 28 | $ py.test -v 29 | ============================= test session starts ====================================== 30 | platform linux2 -- Python 2.7.3 -- py-1.4.25 -- pytest-2.6.4 31 | plugins: verbose-parametrize 32 | collected 2 items 33 | 34 | unit/test_example.py::test_foo[sum-list] FAILED 35 | unit/test_example.py::test_foo[len-int] PASSED 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/pytest_verbose_parametrize.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | 3 | 4 | def _strize_arg(arg): 5 | try: 6 | s = arg.__name__ 7 | except AttributeError: 8 | s = str(arg) 9 | if len(s) > 32: 10 | s = s[:29] + '...' 11 | return s 12 | 13 | 14 | def pytest_generate_tests(metafunc): 15 | 16 | try: 17 | markers = metafunc.definition.get_closest_marker('parametrize') 18 | if not markers: 19 | return 20 | except AttributeError: 21 | # Deprecated in pytest >= 3.6 22 | # See https://docs.pytest.org/en/latest/mark.html#marker-revamp-and-iteration 23 | try: 24 | markers = metafunc.function.parametrize 25 | except AttributeError: 26 | return 27 | 28 | if 'ids' not in markers.kwargs: 29 | list_names = [] 30 | for i, argvalue in enumerate(markers.args[1]): 31 | if (not isinstance(argvalue, Iterable)) or isinstance(argvalue, str): 32 | argvalue = (argvalue,) 33 | name = '-'.join(_strize_arg(arg) for arg in argvalue) 34 | if len(name) > 64: 35 | name = name[:61] + '...' 36 | while name in list_names: 37 | name = '%s#%d' % (name, i) 38 | list_names.append(name) 39 | markers.kwargs['ids'] = list_names 40 | # In pytest versions pre-3.1.0 MarkInfo copies the 41 | # kwargs into an internal variable as well :/ 42 | if hasattr(markers, '_arglist'): 43 | markers._arglist[0][-1]['ids'] = list_names 44 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | tests/integration/parametrize_ids 10 | 11 | [bdist_wheel] 12 | universal = 0 13 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Programming Language :: Python :: 3.6', 17 | 'Programming Language :: Python :: 3.7', 18 | ] 19 | 20 | install_requires = ['pytest', 21 | ] 22 | 23 | tests_require = ['pytest-virtualenv', 24 | 'coverage', 25 | ] 26 | 27 | entry_points = { 28 | 'pytest11': [ 29 | 'verbose-parametrize = pytest_verbose_parametrize', 30 | ] 31 | } 32 | 33 | if __name__ == '__main__': 34 | kwargs = common_setup('pytest_verbose_parametrize') 35 | kwargs.update(dict( 36 | name='pytest-verbose-parametrize', 37 | description='More descriptive output for parametrized py.test tests', 38 | author='Edward Easton', 39 | author_email='eeaston@gmail.com', 40 | classifiers=classifiers, 41 | install_requires=install_requires, 42 | tests_require=tests_require, 43 | py_modules=['pytest_verbose_parametrize'], 44 | entry_points=entry_points, 45 | )) 46 | setup(**kwargs) 47 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/man-group/pytest-plugins/0018cc543229732b9c04fad909d2b7ee6167fa40/pytest-verbose-parametrize/tests/__init__.py -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/man-group/pytest-plugins/0018cc543229732b9c04fad909d2b7ee6167fa40/pytest-verbose-parametrize/tests/integration/__init__.py -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/integration/parametrize_ids/tests/unit/test_duplicates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize(('x', 'y', ), [(0, [1]), (0, [1]), (str(0), str([1]))]) 5 | def test_foo(x, y): 6 | assert str([int(x) + 1]) == y 7 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/integration/parametrize_ids/tests/unit/test_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize(('f', 't'), [(sum, list), (len, int)]) 5 | def test_foo(f, t): 6 | assert isinstance(f([[1], [2]]), t) 7 | 8 | 9 | def test_bar(): # unparametrized 10 | pass 11 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/integration/parametrize_ids/tests/unit/test_long_ids.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize(('x', 'y', ), [(list(range(100)), None), ]) 5 | def test_foo(x, y): 6 | assert y not in x 7 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/integration/parametrize_ids/tests/unit/test_non_parametrized.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_bar(): # unparametrized 4 | pass 5 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/integration/parametrize_ids/tests/unit/test_parametrized.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize(('f', 't'), [(sum, list), (len, int)]) 5 | def test_foo(f, t): 6 | assert isinstance(f([[1], [2]]), t) 7 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/integration/test_verbose_parametrize.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | from pkg_resources import resource_filename, get_distribution # @UnresolvedImport 5 | from pytest_shutil.run import run_with_coverage 6 | 7 | TEST_DIR = resource_filename('pytest_verbose_parametrize', 'tests/integration/parametrize_ids') 8 | PYTEST = os.path.join(os.path.dirname(sys.executable), 'pytest') 9 | PYTEST_VERSION = get_distribution("pytest").parsed_version 10 | MODULE_PREFIX = "" if PYTEST_VERSION.major >= 8 else "tests/integration/parametrize_ids/tests/unit/" 11 | 12 | 13 | def test_parametrize_ids_generates_ids(pytestconfig): 14 | output = run_with_coverage([PYTEST, '--collectonly', 'tests/unit/test_parametrized.py'], 15 | pytestconfig, cd=TEST_DIR) 16 | expected_lines = [f"", ""] 17 | for line in expected_lines: 18 | assert line in output 19 | 20 | 21 | def test_parametrize_ids_leaves_nonparametrized(pytestconfig): 22 | output = run_with_coverage([PYTEST, '--collectonly', 'tests/unit/test_non_parametrized.py'], 23 | pytestconfig, cd=TEST_DIR) 24 | expected_lines = [f"", ""] 25 | for line in expected_lines: 26 | assert line in output 27 | 28 | 29 | def test_handles_apparent_duplicates(pytestconfig): 30 | output = run_with_coverage([PYTEST, '--collectonly', 'tests/unit/test_duplicates.py'], 31 | pytestconfig, cd=TEST_DIR) 32 | expected = f''' 33 | 34 | 35 | 36 | ''' 37 | expected_lines = expected.splitlines() 38 | for line in expected_lines: 39 | assert line in output 40 | 41 | 42 | def test_truncates_long_ids(pytestconfig): 43 | output = run_with_coverage([PYTEST, '--collectonly', 'tests/unit/test_long_ids.py'], 44 | pytestconfig, cd=TEST_DIR) 45 | expected = f''' 46 | 47 | ''' 48 | expected_lines = expected.splitlines() 49 | for line in expected_lines: 50 | assert line in output 51 | -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/man-group/pytest-plugins/0018cc543229732b9c04fad909d2b7ee6167fa40/pytest-verbose-parametrize/tests/unit/__init__.py -------------------------------------------------------------------------------- /pytest-verbose-parametrize/tests/unit/test_verbose_parametrize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | from unittest.mock import Mock 4 | except ImportError: 5 | from mock import Mock 6 | 7 | from pytest_verbose_parametrize import pytest_generate_tests 8 | 9 | 10 | def get_metafunc(args): 11 | p = Mock(kwargs={}, args=args) 12 | p._arglist = ([args, {}],) 13 | metafunc = Mock() 14 | metafunc.function.parametrize = p # Deprecated 15 | metafunc.definition.get_closest_marker.return_value = p 16 | return metafunc 17 | 18 | 19 | def test_generates_ids_from_tuple(): 20 | metafunc = get_metafunc((None, [(1, 2, 3)])) 21 | pytest_generate_tests(metafunc) 22 | assert metafunc.function.parametrize.kwargs['ids'] == ['1-2-3'] 23 | 24 | 25 | def test_generates_ids_from_tuple_of_strings(): 26 | metafunc = get_metafunc((None, [("11", "22", "33")])) 27 | pytest_generate_tests(metafunc) 28 | assert metafunc.function.parametrize.kwargs['ids'] == ['11-22-33'] 29 | 30 | 31 | def test_truncates_args_tuple(): 32 | metafunc = get_metafunc((None, [tuple(range(100))])) 33 | pytest_generate_tests(metafunc) 34 | kwargs = metafunc.function.parametrize.kwargs 35 | assert len(kwargs['ids'][0]) == 64 36 | assert kwargs['ids'][0].endswith('...') 37 | 38 | 39 | def test_generates_ids_single_param(): 40 | metafunc = get_metafunc(("test_param", [1, 2, 3])) 41 | pytest_generate_tests(metafunc) 42 | assert metafunc.function.parametrize.kwargs['ids'] == ['1', '2', '3'] 43 | 44 | 45 | def test_generates_ids_single__string_param(): 46 | metafunc = get_metafunc(("test_param", ["111", "222", "333"])) 47 | pytest_generate_tests(metafunc) 48 | assert metafunc.function.parametrize.kwargs['ids'] == ['111', '222', '333'] 49 | 50 | 51 | def test_truncates_single_arg(): 52 | metafunc = get_metafunc((None, ["1" * 100])) 53 | pytest_generate_tests(metafunc) 54 | kwargs = metafunc.function.parametrize.kwargs 55 | assert len(kwargs['ids'][0]) == 32 56 | assert kwargs['ids'][0].endswith('...') 57 | 58 | 59 | def test_generates_ids_from_duplicates(): 60 | metafunc = get_metafunc((None, [(1, 2, 3), (1, 2, 3)])) 61 | pytest_generate_tests(metafunc) 62 | assert metafunc.function.parametrize.kwargs['ids'] == ['1-2-3', '1-2-3#1'] 63 | 64 | 65 | def test_generates_ids_from_apparent_duplicates(): 66 | metafunc = get_metafunc((None, [(1, 2, 3), ('1', '2', '3')])) 67 | pytest_generate_tests(metafunc) 68 | assert metafunc.function.parametrize.kwargs['ids'] == ['1-2-3', '1-2-3#1'] 69 | 70 | 71 | def test_ok_on_non_parametrized_function(): 72 | pytest_generate_tests(object()) 73 | 74 | 75 | def test_unicode_parameters(): 76 | metafunc = get_metafunc(("test_param", [u"111", u"¬˚ß∆∂", u"😀 😁 😂 🤣 😃 😄 😅 😆"])) 77 | pytest_generate_tests(metafunc) 78 | assert metafunc.function.parametrize.kwargs['ids'] == [u"111", u"¬˚ß∆∂", u"😀 😁 😂 🤣 😃 😄 😅 😆"] 79 | -------------------------------------------------------------------------------- /pytest-virtualenv/README.md: -------------------------------------------------------------------------------- 1 | # Py.test Virtualenv Fixture 2 | 3 | Create a Python virtual environment in your test that cleans up on teardown. 4 | The fixture has utility methods to install packages and list what's installed. 5 | 6 | ## Installation 7 | 8 | Install using your favourite package installer: 9 | ```bash 10 | pip install pytest-virtualenv 11 | # or 12 | easy_install pytest-virtualenv 13 | ``` 14 | 15 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 16 | 17 | ```python 18 | pytest_plugins = ['pytest_virtualenv'] 19 | ``` 20 | 21 | ## Configuration 22 | 23 | This fixture is configured using the following evironment variables 24 | 25 | | Setting | Description | Default 26 | | ------- | ----------- | ------- 27 | | VIRTUALENV_FIXTURE_EXECUTABLE | Which virtualenv executable will be used to create new venvs | `virtualenv` 28 | 29 | 30 | ## Fixture Attributes 31 | 32 | Here's a noddy test case to demonstrate the basic fixture attributes. 33 | For more information on `path.py` see https://pathpy.readthedocs.io/ 34 | 35 | ```python 36 | def test_virtualenv(virtualenv): 37 | # the 'virtualenv' attribute is a `path.py` object for the root of the virtualenv 38 | dirnames = virtualenv.virtualenv.dirs() 39 | assert {'bin', 'include', 'lib'}.intersection(set(dirnames)) 40 | 41 | # the 'python' attribute is a `path.py` object for the python executable 42 | assert virtualenv.python.endswith('/bin/python') 43 | ``` 44 | 45 | ## Installing Packages 46 | 47 | You can install packages by name and query what's installed. 48 | 49 | ```python 50 | def test_installing(virtualenv): 51 | virtualenv.install_package('coverage', installer='pip') 52 | 53 | # installed_packages() will return a list of `PackageEntry` objects. 54 | assert 'coverage' in [i.name for i in virtualenv.installed_packages()] 55 | ``` 56 | 57 | ## Developing Source Checkouts 58 | 59 | Any packages set up in the *test runner's* python environment (ie, the same runtime that 60 | ``py.test`` is installed in) as source checkouts using `python setup.py develop` will be 61 | detected as such and can be installed by name using `install_package`. 62 | By default they are installed into the virtualenv using `python setup.py develop`, there 63 | is an option to build and install an egg as well: 64 | 65 | ```python 66 | def test_installing_source(virtualenv): 67 | # Install a source checkout of my_package as an egg file 68 | virtualenv.install_package('my_package', build_egg=True) 69 | ``` 70 | 71 | 72 | ## Running Commands 73 | 74 | The test fixture has a `run` method which allows you to run commands with the correct 75 | paths set up as if you had activated the virtualenv first. 76 | 77 | ```python 78 | def test_run(virtualenv): 79 | python_exe_path = virtualenv.python 80 | runtime_exe = virtualenv.run("python -c 'import sys; print sys.executable'", capture=True) 81 | assert runtime_exe == python_exe_path 82 | ``` 83 | 84 | ## Running Commands With Coverage 85 | 86 | The test fixture has a `run_with_coverage` method which is like `run` but runs the command 87 | under coverage *inside the virtualenv*. This is useful for capturing test coverage on 88 | tools that are being tested outside the normal test runner environment. 89 | 90 | ```python 91 | def test_coverage(virtualenv): 92 | # You will have to install coverage first 93 | virtualenv.install_package(coverage) 94 | virtualenv.run_with_coverage(["my_entry_point", "--arg1", "--arg2"]) 95 | ``` -------------------------------------------------------------------------------- /pytest-virtualenv/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-virtualenv/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Utilities', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Operating System :: Microsoft :: Windows', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | ] 20 | 21 | install_requires = ['pytest-fixture-config', 22 | 'pytest-shutil', 23 | 'pytest', 24 | 'virtualenv', 25 | 'importlib-metadata', 26 | ] 27 | 28 | entry_points = { 29 | 'pytest11': [ 30 | 'virtualenv = pytest_virtualenv', 31 | ] 32 | } 33 | 34 | if __name__ == '__main__': 35 | kwargs = common_setup('pytest_virtualenv') 36 | kwargs.update(dict( 37 | name='pytest-virtualenv', 38 | description='Virtualenv fixture for py.test', 39 | author='Edward Easton', 40 | author_email='eeaston@gmail.com', 41 | classifiers=classifiers, 42 | install_requires=install_requires, 43 | py_modules=['pytest_virtualenv'], 44 | entry_points=entry_points, 45 | )) 46 | setup(**kwargs) 47 | -------------------------------------------------------------------------------- /pytest-virtualenv/tests/integration/test_tmpvirtualenv.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import pytest_virtualenv as venv 4 | 5 | 6 | def check_member(name, ips): 7 | return name in ips 8 | 9 | 10 | def test_installed_packages(): 11 | with venv.VirtualEnv() as v: 12 | ips = v.installed_packages() 13 | assert len(ips) > 0 14 | assert check_member("pip", ips) 15 | 16 | 17 | def test_install_version_from_current(): 18 | with venv.VirtualEnv() as v: 19 | v.install_package("flask", "1.1.1") 20 | v.install_package("virtualenv", version=venv.PackageVersion.CURRENT) 21 | v.install_package("pytest-virtualenv", version=venv.PackageVersion.CURRENT) 22 | out = v.run([ 23 | v.python, 24 | "-c", 25 | """import pytest_virtualenv as venv 26 | with venv.VirtualEnv() as v: 27 | v.install_package("flask", version=venv.PackageVersion.CURRENT) 28 | print("The Flask version is", v.installed_packages()["Flask"].version) 29 | 30 | """ 31 | ], capture=True) 32 | assert "The Flask version is 1.1.1" in out.strip() 33 | 34 | 35 | def test_install_egg_link_from_current(tmp_path): 36 | with open(tmp_path / "setup.py", "w") as fp: 37 | fp.write("""from setuptools import setup 38 | setup(name="foo", version="1.2", description="none available", install_requires=["requests"], py_modules=["foo"]) 39 | """) 40 | with open(tmp_path / "foo.py", "w") as fp: 41 | fp.write('print("hello")') 42 | 43 | with venv.VirtualEnv() as v: 44 | v.install_package("pip") 45 | v.install_package("wheel") 46 | v.install_package("virtualenv", version=venv.PackageVersion.CURRENT) 47 | v.install_package("pytest-virtualenv", version=venv.PackageVersion.CURRENT) 48 | v.run([v.python, "-m", "pip", "install", "-e", str(tmp_path)]) 49 | out = v.run([ 50 | v.python, 51 | "-c", 52 | """import pytest_virtualenv as venv 53 | with venv.VirtualEnv() as v: 54 | v.install_package("foo", version=venv.PackageVersion.CURRENT) 55 | print("The foo version is", v.installed_packages()["foo"].version) 56 | print("Requests installed:", "requests" in v.installed_packages()) 57 | """ 58 | ], capture=True) 59 | assert "The foo version is 1.2" in out 60 | assert "Requests installed: True" 61 | 62 | 63 | def test_install_pinned_version(): 64 | with venv.VirtualEnv() as v: 65 | v.install_package("flask", "1.1.1") 66 | assert v.installed_packages()["Flask"].version == "1.1.1" 67 | 68 | 69 | def test_install_latest(): 70 | with venv.VirtualEnv() as v: 71 | v.install_package("flask") 72 | assert v.installed_packages()["Flask"].version != "1.1.1" 73 | 74 | 75 | def test_keep_named_workspace(tmp_path): 76 | workspace = tmp_path / "new-workspace" 77 | workspace.mkdir() 78 | with venv.VirtualEnv(workspace=str(workspace)) as v: 79 | pass 80 | assert workspace.exists() 81 | 82 | 83 | def test_really_keep_named_workspace(tmp_path): 84 | workspace = tmp_path / "new-workspace" 85 | workspace.mkdir() 86 | with venv.VirtualEnv(workspace=str(workspace), delete_workspace=False) as v: 87 | pass 88 | assert workspace.exists() 89 | 90 | 91 | def test_delete_named_workspace(tmp_path): 92 | workspace = tmp_path / "new-workspace" 93 | workspace.mkdir() 94 | with venv.VirtualEnv(workspace=str(workspace), delete_workspace=True) as v: 95 | pass 96 | assert not workspace.exists() 97 | 98 | 99 | def test_delete_unamed_workspace(): 100 | with venv.VirtualEnv() as v: 101 | workspace = pathlib.Path(v.workspace) 102 | assert not workspace.exists() 103 | 104 | 105 | def test_really_delete_unamed_workspace(): 106 | with venv.VirtualEnv(delete_workspace=True) as v: 107 | workspace = pathlib.Path(v.workspace) 108 | assert not workspace.exists() 109 | 110 | 111 | def test_keep_unamed_workspace(): 112 | with venv.VirtualEnv(delete_workspace=False) as v: 113 | workspace = pathlib.Path(v.workspace) 114 | assert workspace.exists() 115 | -------------------------------------------------------------------------------- /pytest-virtualenv/tests/unit/test_package_entry.py: -------------------------------------------------------------------------------- 1 | from pytest_virtualenv import PackageEntry 2 | 3 | 4 | def test_issrc_dev_in_version_plus_path_to_source_True(): 5 | p = PackageEntry('acme.x', '1.3.10dev1', 'path/to/source') 6 | assert p.issrc 7 | 8 | 9 | def test_issrc_no_dev_in_version_plus_path_to_source_False(): 10 | p = PackageEntry('acme.x', '1.3.10', 'path/to/source') 11 | assert not p.issrc 12 | 13 | 14 | def test_isdev_path_to_source_blank_string_True(): 15 | p = PackageEntry('acme.x', '1.3.10dev1', '') 16 | assert p.isdev 17 | 18 | 19 | def test_issrc_path_to_source_None_False(): 20 | p = PackageEntry('acme.x', '1.3.10dev1', None) 21 | assert not p.issrc 22 | 23 | 24 | def test_isdev_dev_in_version_plus_path_to_source_False(): # issrc case 25 | p = PackageEntry('acme.x', '1.3.10dev1', 'anything') 26 | assert not p.isdev 27 | 28 | 29 | def test_isdev_dev_in_version_path_to_source_None_True(): 30 | p = PackageEntry('acme.x', '1.3.10dev1', None) 31 | assert p.isdev 32 | 33 | 34 | def test_isdev_no_dev_in_version_path_to_source_None_False(): 35 | p = PackageEntry('acme.x', '1.3.10', None) 36 | assert not p.isdev 37 | 38 | 39 | def test_isrel_no_dev_in_version_path_to_source_None_True(): 40 | p = PackageEntry('acme.x', '1.3.10', None) 41 | assert p.isrel 42 | 43 | 44 | def test_isrel_no_dev_in_version_plus_path_to_source_True(): 45 | p = PackageEntry('acme.x', '1.3.10', 'anything') 46 | assert p.isrel 47 | 48 | 49 | def test_isrel_no_dev_in_version_plus_path_to_source_None_False(): 50 | p = PackageEntry('acme.x', '1.3.10dev1', None) 51 | assert not p.isrel 52 | 53 | 54 | def test_match_dev_ok(): 55 | pe = PackageEntry('acme.x', '1.3.10dev1', None) 56 | assert pe.match(PackageEntry.ANY) 57 | assert pe.match(PackageEntry.DEV) 58 | assert not pe.match(PackageEntry.SRC) 59 | assert not pe.match(PackageEntry.REL) 60 | 61 | 62 | def test_match_source_ok(): 63 | pe = PackageEntry('acme.x', '1.3.10dev1', 'path/to/source') 64 | assert pe.match(PackageEntry.ANY) 65 | assert not pe.match(PackageEntry.DEV) 66 | assert pe.match(PackageEntry.SRC) 67 | 68 | 69 | def test_match_rel_ok(): 70 | pe = PackageEntry('acme.x', '1.3.10', None) 71 | assert pe.match(PackageEntry.ANY) 72 | assert not pe.match(PackageEntry.DEV) 73 | assert not pe.match(PackageEntry.SRC) 74 | assert pe.match(PackageEntry.REL) 75 | -------------------------------------------------------------------------------- /pytest-virtualenv/tests/unit/test_venv.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest_virtualenv as venv 4 | from pytest_shutil import env 5 | 6 | 7 | def test_PYTHONPATH_not_present_in_testing_env_if_set(): 8 | with env.set_env('PYTHONPATH', 'fred'): 9 | with mock.patch.object(venv.Workspace, 'run') as run: 10 | venv.VirtualEnv() 11 | call = run.mock_calls[0] 12 | assert 'PYTHONPATH' not in call[2]['env'] 13 | 14 | venv.VirtualEnv({'PYTHONPATH': 'john'}) 15 | call = run.mock_calls[1] 16 | assert 'PYTHONPATH' not in call[2]['env'] 17 | 18 | 19 | def test_PYTHONPATH_not_present_in_testing_env_if_unset(): 20 | with env.no_env('PYTHONPATH'): 21 | with mock.patch.object(venv.Workspace, 'run') as run: 22 | venv.VirtualEnv() 23 | call = run.mock_calls[0] 24 | assert 'PYTHONPATH' not in call[2]['env'] 25 | 26 | venv.VirtualEnv({'PYTHONPATH': 'john'}) 27 | call = run.mock_calls[1] 28 | assert 'PYTHONPATH' not in call[2]['env'] 29 | -------------------------------------------------------------------------------- /pytest-webdriver/README.md: -------------------------------------------------------------------------------- 1 | # Pytest Webdriver Fixture 2 | 3 | This fixture provides a configured webdriver for Selenium browser tests, that takes screenshots for you 4 | on test failures. 5 | 6 | 7 | ## Installation 8 | 9 | Install using your favourite package installer: 10 | ```bash 11 | pip install pytest-webdriver 12 | # or 13 | easy_install pytest-webdriver 14 | ``` 15 | 16 | Enable the fixture explicitly in your tests or conftest.py (not required when using setuptools entry points): 17 | 18 | ```python 19 | pytest_plugins = ['pytest_webdriver'] 20 | ``` 21 | 22 | ## Quickstart 23 | 24 | This fixture connects to a remote selenium webdriver and returns the browser handle. 25 | It is scoped on a per-function level so you get one browser window per test. 26 | 27 | To use this fixture, follow the following steps. 28 | 29 | 1. Nominate a browser host, and start up the webdriver executable on that host. 30 | 2. Download the latest zip file from here: https://sites.google.com/a/chromium.org/chromedriver/downloads 31 | 3. Unpack onto the target host, and run the unpacked chromedriver binary executable. 32 | 4. Set the environment variable ``SELENIUM_HOST`` to the IP address or hostname of the browser host. This defaults to the local hostname. 33 | 5. Set the environment variable ``SELENIUM_PORT`` to the port number of the webdriver server. The default port number is 4444. 34 | 6. Set the environment variable ``SELENIUM_BROWSER`` to the browser type. Defaults to ``chrome``. 35 | 7. Use the fixture as a test argument: 36 | 37 | ```python 38 | def test_mywebpage(webdriver): 39 | webdriver.get('http://www.google.com') 40 | ``` 41 | 42 | ## `SELENIUM_URI` setting 43 | 44 | You can also specify the selenium server address using a URI format using the SELENIUM_URL environment variable:: 45 | 46 | ```bash 47 | $ export SELENIUM_URI=http://localhost:4444/wd/hub 48 | ``` 49 | 50 | This is needed when dealing with selenium server and not chrome driver (see https://groups.google.com/forum/?fromgroups#!topic/selenium-users/xodZDJxt81o). 51 | If SELENIUM_URI is not defined SELENIUM_HOST & SELENIUM_PORT will be used. 52 | 53 | 54 | ## Automatic screenshots 55 | 56 | When one of your browser tests fail, this plugin will take a screenshot for you and save it in the current 57 | working directory. The name will match the logical path to the test function that failed, like: 58 | 59 | test_login_page__LoginPageTest__test_unicode.png 60 | 61 | 62 | ## `pytest-webdriver` and [PageObjects](https://page-objects.readthedocs.org/en/latest/) 63 | 64 | 65 | If there is a pyramid_server fixture from the also running in the current test, it will detect this and set the ``root_uri`` attribute on the webdriver instance: 66 | 67 | ```python 68 | def test_my_pyramid_app(webdriver, pyramid_server): 69 | assert webdriver.root_uri == pyramid_server.uri 70 | ``` 71 | 72 | Why is this needed, you may ask? It can be used by the `PageObjects` library to automatically set the base URL to your web app. This saves on a lot of string concatenation. For example: 73 | 74 | ```python 75 | from page_objects import PageObject, PageElement 76 | 77 | class LoginPage(PageObject): 78 | username = PageElement(id_='username') 79 | password = PageElement(name='password') 80 | login = PageElement(css='input[type="submit"]') 81 | 82 | def test_login_page(webdriver, pyramid_server): 83 | page = LoginPage(webdriver) 84 | page.login.click() 85 | page.get('/foo/bar') 86 | assert webdriver.getCurrentUrl() == pyramid_server.uri + '/foo/bar' 87 | ``` -------------------------------------------------------------------------------- /pytest-webdriver/pytest_webdriver.py: -------------------------------------------------------------------------------- 1 | """pytest: avoid already-imported warning: PYTEST_DONT_REWRITE.""" 2 | 3 | import os 4 | import traceback 5 | import logging 6 | import socket 7 | 8 | import pytest 9 | import py.builtin 10 | from pytest_fixture_config import Config 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class FixtureConfig(Config): 16 | __slots__ = ('host', 'port', 'uri', 'browser', 'phantomjs') 17 | 18 | CONFIG = FixtureConfig( 19 | host=os.getenv('SELENIUM_HOST', socket.gethostname()), 20 | port=os.getenv('SELENIUM_PORT', '4444'), 21 | uri=os.getenv('SELENIUM_URI'), 22 | browser=os.getenv('SELENIUM_BROWSER', 'chrome'), 23 | phantomjs=os.getenv('PHANTOMJS_BINARY', 'phantomjs'), 24 | ) 25 | 26 | 27 | def browser_to_use(webdriver, browser): 28 | """Recover the browser to use with the given webdriver instance. 29 | 30 | The browser string is case insensitive and needs to be one of the values 31 | from BROWSERS_CFG. 32 | 33 | """ 34 | browser = browser.strip().upper() 35 | 36 | # Have a look the following to see list of supported browsers: 37 | # 38 | # http://selenium.googlecode.com/git/docs/api/ 39 | # py/_modules/selenium/webdriver/common/desired_capabilities.html 40 | # 41 | b = getattr(webdriver.DesiredCapabilities(), browser, None) 42 | if not b: 43 | raise ValueError( 44 | "Unknown browser requested '{0}'.".format(browser) 45 | ) 46 | return b 47 | 48 | 49 | @pytest.yield_fixture(scope='function') 50 | def webdriver(request): 51 | """ Connects to a remote selenium webdriver and returns the browser handle. 52 | Scoped on a per-function level so you get one browser window per test. 53 | Creates screenshots automatically on test failures. 54 | 55 | Attributes 56 | ---------- 57 | root_uri: URI to the pyramid_server fixture if it's detected in the test run 58 | """ 59 | from selenium import webdriver 60 | 61 | # Look for the pyramid server funcarg in the current session, and save away its root uri 62 | root_uri = [] 63 | try: 64 | root_uri.append(request.getfixturevalue('pyramid_server').uri) 65 | except LookupError: 66 | pass 67 | 68 | if CONFIG.browser.lower() == 'phantomjs': 69 | driver = webdriver.PhantomJS(executable_path=CONFIG.phantomjs) 70 | elif CONFIG.browser.lower() == 'chrome': 71 | chrome_options = webdriver.ChromeOptions() 72 | chrome_options.add_argument('--headless') 73 | chrome_options.add_argument('--no-sandbox') 74 | chrome_options.add_argument('--disable-dev-shm-usage') 75 | driver = webdriver.Chrome(options=chrome_options) 76 | else: 77 | selenium_uri = CONFIG.uri 78 | if not selenium_uri: 79 | selenium_uri = 'http://{0}:{1}'.format(CONFIG.host, CONFIG.port) 80 | driver = webdriver.Remote( 81 | selenium_uri, 82 | browser_to_use(webdriver, CONFIG.browser) 83 | ) 84 | 85 | if root_uri: 86 | driver.__dict__['root_uri'] = root_uri[0] 87 | 88 | yield driver 89 | 90 | driver.close() 91 | 92 | 93 | @pytest.hookimpl(tryfirst=True) 94 | def pytest_runtest_makereport(item, call): 95 | """ Screenshot failing tests 96 | """ 97 | if not hasattr(item, 'funcargs') or not 'webdriver' in item.funcargs: 98 | return 99 | if not call.excinfo or call.excinfo.errisinstance(pytest.skip.Exception): 100 | return 101 | fname = item.nodeid.replace('/', '__') + '.png' 102 | py.builtin.print_("Saving screenshot to %s" % fname) 103 | try: 104 | item.funcargs['webdriver'].get_screenshot_as_file(fname) 105 | except: 106 | print(traceback.format_exc()) 107 | -------------------------------------------------------------------------------- /pytest-webdriver/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | # This section sets configuration for all invocations of py.test, 3 | # both standalone cmdline and running via setup.py 4 | norecursedirs = 5 | .git 6 | *.egg 7 | build 8 | dist 9 | 10 | [bdist_wheel] 11 | universal = 0 12 | -------------------------------------------------------------------------------- /pytest-webdriver/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 4 | 5 | from setuptools import setup 6 | from common_setup import common_setup 7 | 8 | classifiers = [ 9 | 'License :: OSI Approved :: MIT License', 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Topic :: Software Development :: Libraries', 12 | 'Topic :: Software Development :: Testing', 13 | 'Topic :: Software Development :: User Interfaces', 14 | 'Intended Audience :: Developers', 15 | 'Operating System :: POSIX', 16 | 'Programming Language :: Python :: 3.6', 17 | 'Programming Language :: Python :: 3.7', 18 | ] 19 | 20 | install_requires = ['py', 21 | 'pytest', 22 | 'pytest-fixture-config', 23 | 'selenium', 24 | ] 25 | 26 | tests_require = [] 27 | 28 | entry_points = { 29 | 'pytest11': [ 30 | 'webdriver = pytest_webdriver', 31 | ] 32 | } 33 | 34 | if __name__ == '__main__': 35 | kwargs = common_setup('pytest_webdriver') 36 | kwargs.update(dict( 37 | name='pytest-webdriver', 38 | description='Selenium webdriver fixture for py.test', 39 | author='Edward Easton', 40 | author_email='eeaston@gmail.com', 41 | classifiers=classifiers, 42 | install_requires=install_requires, 43 | tests_require=tests_require, 44 | py_modules=['pytest_webdriver'], 45 | entry_points=entry_points, 46 | )) 47 | setup(**kwargs) 48 | -------------------------------------------------------------------------------- /pytest-webdriver/tests/integration/test_integration.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import Mock, sentinel, patch 3 | except ImportError: 4 | # python 2 5 | from mock import Mock, sentinel, patch 6 | 7 | import pytest 8 | import selenium 9 | 10 | import pytest_webdriver 11 | 12 | @pytest.fixture() 13 | def pyramid_server(): 14 | return Mock(uri='http://www.example.com') 15 | 16 | 17 | def test_webdriver(pyramid_server, webdriver): 18 | assert webdriver.root_uri == 'http://www.example.com' 19 | webdriver.get('http://www.example.com') 20 | assert webdriver.current_url == 'https://www.example.com/' 21 | -------------------------------------------------------------------------------- /pytest-webdriver/tests/unit/test_webdriver.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import Mock, sentinel, patch 3 | except ImportError: 4 | # python 2 5 | from mock import Mock, sentinel, patch 6 | 7 | import pytest 8 | import selenium 9 | 10 | import pytest_webdriver 11 | 12 | 13 | def test_browser_to_use(): 14 | caps = Mock(CHROME=sentinel.chrome, UNKNOWN=None) 15 | wd = Mock(DesiredCapabilities = Mock(return_value = caps)) 16 | assert pytest_webdriver.browser_to_use(wd, 'chrome') == sentinel.chrome 17 | with pytest.raises(ValueError): 18 | pytest_webdriver.browser_to_use(wd, 'unknown') 19 | --------------------------------------------------------------------------------