Skip to content

Commit

Permalink
First draft.
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreLouisCaron committed Mar 7, 2017
1 parent d83ab66 commit fa228dc
Show file tree
Hide file tree
Showing 15 changed files with 577 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-

[run]
branch = True
source = pytest_docker

[paths]
source =
src/pytest_docker
.tox/*/lib/python*/site-packages/pytest_docker
.tox/pypy*/site-packages/pytest_docker
.tox/*/site-packages/pytest_docker
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-

*~
*.pyc
/.coverage
/.cache/
/.tox/
/htmlcov/
/pytest_docker.egg-info/
/src/pytest_docker.egg-info/
36 changes: 36 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-

sudo: required
services:
- docker

language: python

python:
- "2.7"
- "3.5"

before_install:
- pip --version
- docker --version
- docker-compose --version

install:
- pip install tox
- pip install coveralls

before_script:
# Pre-cache images to prevent timeout in service availability checks.
- docker-compose -f tests/docker-compose.yml pull
- docker-compose -f tests/docker-compose.yml build

script:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then tox -e py27; fi
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then tox -e py35; fi

after_script:
# Always leave a clean slate.
- docker-compose -f tests/docker-compose.yml down

after_success:
- coveralls
42 changes: 42 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,48 @@ containers. Specify all containers you need and ``pytest-docker`` will use

.. _`py.test`: http://doc.pytest.org/
.. _`Docker Compose`: https://docs.docker.com/compose/

Usage
=====

Here is the basic recipe for writing a test that depends on a service that
responds over HTTP:

.. source-code::

import pytest
import requests

from requests.exceptions import (
ConnectionError,
)

def is_responsive(url):
"""Check if something responds to ``url``."""
try:
response = requests.get(url)
if response.status_code == 200:
return True
except ConnectionError:
return False

@pytest.fixture(scope='session')
def some_http_service(docker_ip, docker_services):
"""Ensure that "some service" is up and responsive."""
url = 'http://' % (
docker_ip,
docker_services.port_for('abc', 123),
)
docker_services.wait_until_responsive(
timeout=30.0, pause=0.1,
check=lambda: is_responsive(url)
)
return url

def test_something(some_service):
"""Sample test."""
response = requests.get(some_service)
response.raise_for_status()


Contributing
Expand Down
47 changes: 47 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-


from setuptools import (
find_packages,
setup,
)


setup(
name='pytest-docker',
url='https://github.com/AndreLouisCaron/pytest-docker',
version='0.0.0',
license='MIT',
maintainer='Andre Caron',
maintainer_email='andre.l.caron@gmail.com',
classifiers=[
'Framework :: Pytest',
'Development Status :: 3 - Alpha',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Topic :: Utilities',
'Intended Audience :: Developers',
'Operating System :: Unix',
'Operating System :: POSIX',
'Operating System :: Microsoft :: Windows',
],
keywords=[
'docker',
'docker-compose',
'pytest',
],
packages=find_packages(where='src'),
package_dir={
'': 'src',
},
entry_points={
'pytest11': [
'docker = pytest_docker',
],
},
install_requires=[
'attrs>=16,<17',
],
)
123 changes: 123 additions & 0 deletions src/pytest_docker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-


import attr
import os
import pytest
import re
import subprocess
import time
import timeit


def execute(command, success_codes=(0,)):
"""Run a shell command."""
try:
output = subprocess.check_output(
command, stderr=subprocess.STDOUT, shell=True,
)
status = 0
except subprocess.CalledProcessError as error:
output = error.output
status = error.returncode
command = error.cmd
output = output.decode('utf-8')
if status not in success_codes:
raise Exception(
'Command %r returned %d: """%s""".' % (command, status, output)
)
return output


@pytest.fixture(scope='session')
def docker_ip():
"""Determine IP address for TCP connections to Docker containers."""

# When talking to the Docker daemon via a UNIX socket, route all TCP
# traffic to docker containers via the TCP loopback interface.
docker_host = os.environ.get('DOCKER_HOST', '').strip()
if not docker_host:
return '127.0.0.1'

match = re.match('^tcp://(.+?):\d+$', docker_host)
if not match:
raise ValueError(
'Invalid value for DOCKER_HOST: "%s".' % (docker_host,)
)
return match.group(1)


@attr.s(frozen=True)
class Services(object):
"""."""

_compose_file = attr.ib()
_services = attr.ib(init=False, default=attr.Factory(dict))

def port_for(self, service, port):
"""Get the effective bind port for a service."""

# Lookup in the cache.
cache = self._services.get(service, {}).get(port, None)
if cache is not None:
return cache

output = execute('docker-compose -f "%s" port %s %d' % (
self._compose_file, service, port,
))
endpoint = output.strip()
if not endpoint:
raise ValueError(
'Could not detect port for "%s:%d".' % (service, port)
)

# Usually, the IP address here is 0.0.0.0, so we don't use it.
match = int(endpoint.split(':', 1)[1])

# Store it in cache in case we request it multiple times.
self._services.setdefault(service, {})[port] = match

return match

def wait_until_responsive(self, check, timeout, pause,
clock=timeit.default_timer):
"""Wait until a service is responsive."""

ref = clock()
now = ref
while (now - ref) < timeout:
if check():
return
time.sleep(pause)
now = clock()

raise Exception(
'Timeout reached while waiting on service!'
)


@pytest.fixture(scope='session')
def docker_compose_file():
"""."""
return 'docker-compose.yml'


@pytest.fixture(scope='session')
def docker_services(docker_compose_file):
"""Ensure all Docker-based services are up and running."""

# Spawn containers.
execute('docker-compose -f "%s" up -d' % (docker_compose_file,))

# Let test(s) run.
yield Services(compose_file=docker_compose_file)

# Clean up.
execute('docker-compose -f "%s" down' % (docker_compose_file,))


__all__ = (
'docker_compose_file',
'docker_ip',
'docker_services',
)
13 changes: 13 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-


import os.path
import pytest


HERE = os.path.dirname(os.path.abspath(__file__))


@pytest.fixture(scope='session')
def docker_compose_file():
return os.path.join(HERE, 'docker-compose.yml')
5 changes: 5 additions & 0 deletions tests/containers/hello/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-

FROM python:3.6-alpine

CMD [ "python", "-m", "http.server", "80" ]
6 changes: 6 additions & 0 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-

hello:
build: "./containers/hello"
ports:
- "80"
8 changes: 8 additions & 0 deletions tests/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-

coverage
docutils
flake8
mock
pytest
requests
8 changes: 8 additions & 0 deletions tests/test_docker_compose_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-


from pytest_docker import docker_compose_file


def test_docker_compose_file():
docker_compose_file() == 'docker-compose.yml'
36 changes: 36 additions & 0 deletions tests/test_docker_ip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-


import mock
import pytest

from pytest_docker import docker_ip


def test_docker_ip_native():
environ = {}
with mock.patch('os.environ', environ):
assert docker_ip() == '127.0.0.1'


def test_docker_ip_remote():
environ = {
'DOCKER_HOST': 'tcp://1.2.3.4:2376',
}
with mock.patch('os.environ', environ):
assert docker_ip() == '1.2.3.4'


@pytest.mark.parametrize('docker_host', [
'http://1.2.3.4:2376',
])
def test_docker_ip_remote_invalid(docker_host):
environ = {
'DOCKER_HOST': docker_host,
}
with mock.patch('os.environ', environ):
with pytest.raises(ValueError) as exc:
print(docker_ip())
assert str(exc.value) == (
'Invalid value for DOCKER_HOST: "%s".' % (docker_host,)
)
Loading

0 comments on commit fa228dc

Please sign in to comment.