├── .gitignore ├── .travis.yml ├── README.md ├── pytest_docker_pexpect ├── __init__.py ├── bare.py ├── docker.py └── plugin.py ├── setup.py ├── tests └── test_plugin.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | .env 60 | .idea 61 | 62 | # vim temporary files 63 | .*.swp 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - os: linux 5 | dist: xenial 6 | python: "nightly" 7 | - os: linux 8 | dist: xenial 9 | python: "3.8-dev" 10 | - os: linux 11 | dist: xenial 12 | python: "3.7-dev" 13 | - os: linux 14 | dist: xenial 15 | python: "3.7" 16 | - os: linux 17 | dist: trusty 18 | python: "3.6-dev" 19 | - os: linux 20 | dist: trusty 21 | python: "3.6" 22 | - os: linux 23 | dist: trusty 24 | python: "3.5" 25 | - os: linux 26 | dist: trusty 27 | python: "3.4" 28 | - os: linux 29 | dist: trusty 30 | python: "2.7" 31 | allow_failures: 32 | - python: nightly 33 | - python: 3.8-dev 34 | - python: 3.7-dev 35 | - python: 3.6-dev 36 | services: 37 | - docker 38 | install: 39 | - pip install -U . 40 | script: 41 | - py.test -vv --capture=sys 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-docker-pexpect [![Build Status](https://travis-ci.org/nvbn/pytest-docker-pexpect.svg?branch=master)](https://travis-ci.org/nvbn/pytest-docker-pexpect) 2 | 3 | py.test plugin for writing simple functional tests with pexpect and docker. 4 | 5 | ## Installation 6 | 7 | ```python 8 | pip install pytest-docker-pexpect 9 | ``` 10 | 11 | ## Usage 12 | 13 | The plugin provides `spawnu` fixture, that could be called like 14 | `spawnu(tag, dockerfile_content, command)`, it returns `pexpect.spwanu` attached to `command` 15 | runned inside a container that built with `tag` and `dockerfile`: 16 | 17 | ```python 18 | def test_echo(spawnu): 19 | proc = spawnu(u'ubuntu', u'FROM ubuntu:latest', u'bash') 20 | proc.sendline(u'ls') 21 | ``` 22 | 23 | Current working directory available inside the container in `/src`. 24 | 25 | It's also possible to pass arguments to `docker run` with `spawnu`: 26 | 27 | ```python 28 | spawnu(u'ubuntu', u'FROM ubuntu:latest', u'bash', 29 | docker_run_arguments=[u'--expose', u'80']) 30 | ``` 31 | 32 | `spawnu` provides [pexpect API](https://pexpect.readthedocs.io/en/stable/api/pexpect.html#spawn-class) 33 | and additional docker-specific API: 34 | 35 | * `proc.docker_container_id` – container id 36 | * `proc.docker_inspect()` – decoded json output of `docker inspect` 37 | * `proc.docker_stats()` – decoded json output of `docker stats` 38 | 39 | 40 | Also the plugin provides `TIMEOUT` fixture, that can be used for simple asserts, like: 41 | 42 | ```python 43 | assert proc.expect([TIMEOUT, u'1']) 44 | ``` 45 | 46 | `run_without_docker` fixtures, that indicates that docker isn't used. 47 | 48 | If you want to disable tests if docker isn't available, use `@pytest.mark.skip_without_docker`. 49 | 50 | If you want to run parametrized test only once without docker, use 51 | `@pytest.mark.once_without_docker`. 52 | 53 | ## Usage without docker 54 | 55 | With flag `--run-without-docker` tests can be run in environment without docker. 56 | In this mode tests runs only for first container and docker initialization steps are skipped. 57 | Be careful, in this mode all commands will be execute directly on local system! 58 | 59 | ## Licensed under MIT 60 | -------------------------------------------------------------------------------- /pytest_docker_pexpect/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvbn/pytest-docker-pexpect/0ad49a021420511d4fa4c6e3aadbdeb1a03e00dc/pytest_docker_pexpect/__init__.py -------------------------------------------------------------------------------- /pytest_docker_pexpect/bare.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pexpect 3 | 4 | 5 | def spawnu(source_root, tag, dockerfile, command, docker_run_arguments=None): 6 | """Creates pexpect's spwanu attached to command. 7 | 8 | :type source_root: basestring 9 | :type tag: basestring 10 | :type dockerfile: basestring 11 | :type command: basestring 12 | :rtype: pexpect.spawnu 13 | 14 | """ 15 | proc = pexpect.spawnu(command, logfile=sys.stderr) 16 | proc.docker_container_id = None 17 | proc.docker_inspect = lambda: {} 18 | proc.docker_stats = lambda: {} 19 | return proc 20 | -------------------------------------------------------------------------------- /pytest_docker_pexpect/docker.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import shutil 4 | from tempfile import mkdtemp 5 | import os 6 | import json 7 | import atexit 8 | import pexpect 9 | 10 | 11 | def build_container(tag, dockerfile): 12 | """Builds docker container. 13 | 14 | :type tag: basestring 15 | :type dockerfile: basestring 16 | :type source_root: basestring 17 | 18 | """ 19 | tmpdir = mkdtemp() 20 | try: 21 | dockerfile_path = os.path.join(tmpdir, 'Dockerfile') 22 | with open(dockerfile_path, 'w') as file: 23 | file.write(dockerfile) 24 | if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir]) != 0: 25 | raise Exception("Can't build a container") 26 | finally: 27 | shutil.rmtree(tmpdir) 28 | 29 | 30 | 31 | def inspect(container_id): 32 | """Returns output of `docker --inspect` 33 | 34 | :type container_id: basestring 35 | :rtype: dict 36 | 37 | """ 38 | proc = subprocess.Popen( 39 | ['docker', 'inspect', '--format', '{{json .}}', container_id], 40 | stdout=subprocess.PIPE) 41 | data = proc.stdout.read().decode() 42 | return json.loads(data) 43 | 44 | 45 | def stats(container_id): 46 | """Returns output of `docker --stats` 47 | 48 | :type container_id: basestring 49 | :rtype: dict 50 | 51 | """ 52 | proc = subprocess.Popen( 53 | ['docker', 'stats', '--no-stream', 54 | '--format', '{{json .}}', container_id], 55 | stdout=subprocess.PIPE) 56 | data = proc.stdout.read().decode() 57 | return json.loads(data) 58 | 59 | 60 | def run(source_root, tag, command, docker_run_arguments): 61 | """Runs docker container in detached mode. 62 | 63 | :type source_root: basestring 64 | :type tag: basestring 65 | :type command: basestring 66 | :rtype: basestring 67 | 68 | """ 69 | proc = subprocess.Popen( 70 | ['docker', 'run', '--rm=true', '--volume', 71 | '{}:/src'.format(source_root), 72 | '--tty=true', '--interactive=true', 73 | '--detach'] + docker_run_arguments + [tag, command], 74 | stdout=subprocess.PIPE) 75 | return proc.stdout.readline().decode()[:-1] 76 | 77 | 78 | def kill(container_id): 79 | """Kills docker container. 80 | 81 | :type container_id: basestring 82 | 83 | """ 84 | subprocess.call(['docker', 'kill', container_id], 85 | stdout=subprocess.PIPE) 86 | 87 | 88 | def spawnu(source_root, tag, dockerfile, command, docker_run_arguments=None): 89 | """Creates pexpect spawnu attached to docker. 90 | 91 | :type source_root: basestring 92 | :type tag: basestring 93 | :type dockerfile: basestring 94 | :type command: basestring 95 | :type docker_run_arguments: list 96 | :rtype: pexpect.spawnu 97 | 98 | """ 99 | if docker_run_arguments is None: 100 | docker_run_arguments = [] 101 | 102 | build_container(tag, dockerfile) 103 | container_id = run(source_root, tag, command, docker_run_arguments) 104 | atexit.register(kill, container_id) 105 | 106 | spawned = pexpect.spawnu('docker', ['attach', container_id], 107 | logfile=sys.stderr) 108 | 109 | spawned.docker_container_id = container_id 110 | spawned.docker_inspect = lambda: inspect(container_id) 111 | spawned.docker_stats = lambda: stats(container_id) 112 | 113 | return spawned 114 | -------------------------------------------------------------------------------- /pytest_docker_pexpect/plugin.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import os 3 | import six 4 | import pytest 5 | import pexpect 6 | from . import docker, bare 7 | 8 | 9 | def pytest_addoption(parser): 10 | """Adds `--run-without-docker` argument.""" 11 | group = parser.getgroup("docker_pexpect") 12 | group.addoption('--run-without-docker', action="store_true", default=False, 13 | help="Don't use docker") 14 | 15 | 16 | @pytest.fixture 17 | def run_without_docker(request): 18 | """Equals to `True` when containers not used.""" 19 | return request.config.getoption('run_without_docker') 20 | 21 | 22 | @pytest.fixture 23 | def spawnu(run_without_docker): 24 | """Returns `spawnu` function depending on current mode.""" 25 | spawnu_fn = bare.spawnu if run_without_docker else docker.spawnu 26 | cwd = os.getcwdu() if six.PY2 else os.getcwd() 27 | return partial(spawnu_fn, cwd) 28 | 29 | 30 | @pytest.fixture(scope="function") 31 | def TIMEOUT(): 32 | return pexpect.TIMEOUT 33 | 34 | 35 | @pytest.fixture(autouse=True) 36 | def skip_without_docker(request, run_without_docker): 37 | if request.node.get_closest_marker('skip_without_docker') and run_without_docker: 38 | pytest.skip('skipped without docker') 39 | 40 | 41 | @pytest.fixture(autouse=True) 42 | def once_without_docker(request, run_without_docker): 43 | if request.node.get_closest_marker('once_without_docker') and run_without_docker: 44 | if request.node.function not in once_without_docker._ran: 45 | once_without_docker._ran.add(request.node.function) 46 | else: 47 | pytest.skip('skipped without docker') 48 | once_without_docker._ran = set() 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = '0.9' 4 | 5 | setup(name='pytest-docker-pexpect', 6 | version=version, 7 | description="pytest plugin for writing functional tests with pexpect and docker", 8 | author='Vladimir Iakovlev', 9 | author_email='nvbn.rm@gmail.com', 10 | url='https://github.com/nvbn/pytest-docker-pexpect', 11 | license='MIT', 12 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 13 | include_package_data=True, 14 | zip_safe=False, 15 | install_requires=['pexpect', 'pytest', 'six'], 16 | entry_points={'pytest11': [ 17 | 'docker_pexpect = pytest_docker_pexpect.plugin']}) 18 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture( 5 | params=((u'pytest-docker-pexpect/ubuntu-bash', 6 | u'''FROM ubuntu:latest 7 | RUN apt-get update 8 | RUN apt-get install -yy bash''', 9 | u'bash'), 10 | (u'pytest-docker-pexpect/ubuntu-zsh', 11 | u'''FROM ubuntu:latest 12 | RUN apt-get update 13 | RUN apt-get install -yy zsh''', 14 | u'zsh', [u'--expose', u'8000']))) 15 | def proc(request, spawnu): 16 | return spawnu(*request.param) 17 | 18 | 19 | @pytest.mark.once_without_docker 20 | def test_echo(proc, TIMEOUT): 21 | """Ensures that all works.""" 22 | proc.sendline(u'echo 1') 23 | assert proc.expect([TIMEOUT, u'1']) 24 | 25 | 26 | @pytest.mark.skip_without_docker 27 | def test_docker_api(proc): 28 | """Ensures that docker specific API works.""" 29 | assert len(proc.docker_container_id) 30 | assert proc.docker_inspect()['Id'].startswith(proc.docker_container_id) 31 | assert proc.docker_stats()['Container'] == proc.docker_container_id 32 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,py35,py36,py37 3 | 4 | [testenv] 5 | commands = py.test -v --capture=sys 6 | --------------------------------------------------------------------------------