Skip to content

Commit

Permalink
fix(package): fix binary package error `NameError: name 'help' is not…
Browse files Browse the repository at this point in the history
… defined`
  • Loading branch information
Toilal committed Feb 15, 2021
1 parent 47bf72c commit b066513
Show file tree
Hide file tree
Showing 2 changed files with 347 additions and 3 deletions.
9 changes: 6 additions & 3 deletions setup.py
Expand Up @@ -68,11 +68,11 @@
"pre-commit",
"tox",
"pylint",
"astroid<2.5", # https://github.com/PyCQA/pylint/issues/4093
"docker-compose",
"astroid<2.5", # https://github.com/PyCQA/pylint/issues/4093
"pytest",
"coverage",
"docker-compose",
"pytest-docker-compose @ git+https://github.com/Toilal/pytest-docker-compose@configuration-inside-tests",
"waiting",
"pytest-mock",
"pytest-cov",
Expand All @@ -84,7 +84,10 @@
entry_points = {
'console_scripts': [
'ddb = ddb.__main__:console_script'
]
],
"pytest11": [
"docker_compose=tests.pytest_docker_compose:plugin",
],
}

with io.open('ddb/__version__.py', 'r') as f:
Expand Down
341 changes: 341 additions & 0 deletions tests/pytest_docker_compose/__init__.py
@@ -0,0 +1,341 @@
# https://github.com/pytest-docker-compose/pytest-docker-compose/pull/64
# pylint: skip-file

import os
import time
from typing import List
from pathlib import Path
import warnings
from datetime import datetime

import pytest
from compose.cli.command import project_from_options
from compose.config.errors import ComposeFileNotFound
from compose.container import Container
from compose.project import Project
from compose.service import ImageType

class ContainersAlreadyExist(Exception):
"""Raised when running containers are unexpectedly found"""
pass


class ContainerDoesntExist(Exception):
"""Raised when container doesn't exists"""
pass


class ContainerNotRunning(Exception):
"""Raised when container doesn't exists"""
pass


__all__ = [
"DockerComposePlugin",
"NetworkInfo",
"plugin",
]


class NetworkInfo:
def __init__(self, container_port: str, hostname: str, host_port: int, ):
"""
Container for info about how to connect to a service exposed by a
Docker container.
:param container_port: Port (and usually also protocol name) exposed
internally on the container.
:param hostname: Hostname to use when accessing this service.
:param host_port: Port number to use when accessing this service.
"""
self.container_port = container_port
self.hostname = hostname
self.host_port = host_port


def create_network_info_for_container(container: Container):
"""
Generates :py:class:`NetworkInfo` instances corresponding to all available
port bindings in a container
"""
# If ports are exposed by the docker container but not by docker expose
# container.ports looks like this:
# container.ports == {'4369/tcp': None,
# '5984/tcp': [{'HostIp': '0.0.0.0', 'HostPort': '32872'}],
# '9100/tcp': None}
return [NetworkInfo(container_port=container_port,
hostname=port_config["HostIp"] or "localhost",
host_port=port_config["HostPort"], )
for container_port, port_configs in container.ports.items()
if port_configs is not None for port_config in port_configs]


class DockerComposePlugin:
"""
Integrates docker-compose into pytest integration tests.
"""

def __init__(self):
self.function_scoped_container_getter = self.generate_scoped_containers_fixture('function')
self.class_scoped_container_getter = self.generate_scoped_containers_fixture('class')
self.module_scoped_container_getter = self.generate_scoped_containers_fixture('module')
self.session_scoped_container_getter = self.generate_scoped_containers_fixture('session')

# noinspection SpellCheckingInspection
@staticmethod
def pytest_addoption(parser):
"""
Adds custom options to the ``pytest`` command.
https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_addoption
"""
group = parser.getgroup("docker_compose", "integration tests")

group.addoption(
"--docker-compose",
action='append',
dest="docker_compose",
default=[],
help="Path to docker-compose.yml file, or directory containing same.",
)

group.addoption(
"--docker-compose-directory",
dest="project_dir",
default=[],
help="Path to docker-compose directory",
)

group.addoption("--docker-compose-remove-volumes", action="store_true",
default=False, help="Remove docker container volumes after tests")

group.addoption("--docker-compose-no-build", action="store_true",
default=False, help="Boolean to not build docker containers")

group.addoption("--use-running-containers", action="store_true",
default=False, help="Boolean to use a running set of containers "
"instead of calling 'docker-compose up'")

@classmethod
def project_from_options_for_each_dir(cls, dirs, *args, **kwargs):
"""
Create the Docker project from options, trying each project_dirs.
"""
exc = None
for project_dir in dirs:
try:
project = project_from_options(project_dir=project_dir, *args, **kwargs)
return project, project_dir
except ComposeFileNotFound as local_exc:
exc = local_exc
raise exc

@classmethod
def projectdir_from_basedirs_and_docker_compose_options(cls, basedirs, docker_compose_options):
"""
Get the project directory from possible basedirs and given --docker-compose options.
"""
for basedir in basedirs:
found = True
for docker_compose_option in docker_compose_options:
docker_compose_path = Path(basedir).joinpath(docker_compose_option)
if not docker_compose_path.is_file() and not docker_compose_path.is_dir():
found = False
break
if found:
return basedir
return basedirs[-1] # Fallback to last one

@pytest.fixture(scope="session")
def all_docker_projects(self):
"""
Contains all docker projects for the current session, as a dict.
"""
return {}

@classmethod
def get_docker_project(cls, request, all_docker_projects):
"""
Get the Docker project, creating a new one if it doesn't exists.
Returns the project instance, which can be used to start and stop the Docker containers.
"""
testdir = request.fspath.dirname
project_dir = request.config.getoption("project_dir")

if project_dir:
basedirs = [project_dir]
else:
basedirs = [d for d in (os.path.join(testdir, 'docker-compose'), testdir, '.') if Path(d).is_dir()]

project = None
for basedir in basedirs:
try:
project = all_docker_projects[basedir]
break
except KeyError:
pass

if not project:
docker_compose_options = request.config.getoption("docker_compose")
if docker_compose_options:
files = []

split_deprecated_displayed = False

splitted_docker_compose_options = []
for docker_compose_option in docker_compose_options:
if ',' in docker_compose_option:
if not split_deprecated_displayed:
warnings.warn(DeprecationWarning(
"Using ',' in --docker-compose option to specify multiple compose files is deprecated."
"You can now use --docker-compose many times in the same command."))
split_deprecated_displayed = True

for splitted_docker_compose_option in docker_compose_option.split(','):
splitted_docker_compose_options.append(splitted_docker_compose_option)
else:
splitted_docker_compose_options.append(docker_compose_option)

if not project_dir:
project_dir = cls.projectdir_from_basedirs_and_docker_compose_options(
basedirs, docker_compose_options)

for docker_compose_option in docker_compose_options:
docker_compose_path = Path(project_dir).joinpath(docker_compose_option)

if docker_compose_path.is_dir():
docker_compose_path = docker_compose_path.joinpath("docker-compose.yml")

if not docker_compose_path.is_file():
raise ValueError(
"Unable to find `{docker_compose}` "
"for integration tests in following directories: `{basedirs}`.".format(
docker_compose=docker_compose_option,
basedirs=basedirs
),
)

files.append(str(docker_compose_path.relative_to(project_dir)))

project_key = '|'.join(files)
project = project_from_options(
project_dir=str(project_dir),
options={"--file": files},
)
else:
project, project_key = cls.project_from_options_for_each_dir(basedirs, options={})

all_docker_projects[project_key] = project

if not request.config.getoption("--docker-compose-no-build"):
project.build()

if request.config.getoption("--use-running-containers"):
if not request.config.getoption("--docker-compose-no-build"):
warnings.warn(UserWarning(
"You used the '--use-running-containers' without the "
"'--docker-compose-no-build' flag, the newly build "
"containers won't be used if there are already "
"containers running!"))
current_containers = project.containers()
containers = project.up()
if not set(current_containers) == set(containers):
warnings.warn(UserWarning(
"You used the '--use-running-containers' but "
"pytest-docker-compose could not find all containers "
"running. The remaining containers have been started."))
else:
if any(project.containers()):
raise ContainersAlreadyExist(
"There are already existing containers, please remove all "
"containers by running 'docker-compose down' before using "
"the pytest-docker-compose plugin. Alternatively, you "
"can use the '--use-running-containers' flag to indicate "
"you will use the currently running containers.")
return project

@classmethod
def generate_scoped_containers_fixture(cls, scope: str):
"""
Create scoped fixtures that retrieve or spin up all containers, and add
network info objects to containers and then yield the containers for
duration of test.
After the tests wrap up the fixture prints the logs of each containers
and tears them down unless '--use-running-containers' was supplied.
"""

@pytest.fixture(scope=scope) # type: ignore
def scoped_containers_fixture(request, all_docker_projects):
docker_project = cls.get_docker_project(request, all_docker_projects)

now = datetime.utcnow()
if request.config.getoption("--use-running-containers"):
containers = docker_project.containers() # type: List[Container]
else:
if any(docker_project.containers()):
raise ContainersAlreadyExist(
'pytest-docker-compose tried to start containers but there are'
' already running containers: %s, you probably scoped your'
' tests wrong' % docker_project.containers())
containers = docker_project.up()
if not containers:
raise ValueError("`docker-compose` didn't launch any containers!")

container_getter = ContainerGetter(docker_project)
yield container_getter

if request.config.getoption("--verbose"):
for container in sorted(containers, key=lambda c: c.name):
header = "Logs from {name}:".format(name=container.name)
print(header, '\n', "=" * len(header))
print(container.logs(since=now).decode("utf-8", errors="replace")
or "(no logs)", '\n')

if not request.config.getoption("--use-running-containers"):
docker_project.down(ImageType.none, request.config.getoption("--docker-compose-remove-volumes"))

scoped_containers_fixture.__wrapped__.__doc__ = """
Spins up the containers for the Docker project and returns an
object that can retrieve the containers. The returned containers
all have one additional attribute called network_info to simplify
accessing the hostnames and exposed port numbers for each container.
This set of containers is scoped to '%s'
""" % scope
return scoped_containers_fixture


plugin = DockerComposePlugin()


class ContainerGetter:
"""
A class that retrieves containers from the docker project and adds a
convenience wrapper for the available ports
"""

def __init__(self, docker_project: Project) -> None:
self.docker_project = docker_project

def get(self, key: str, wait_running=True, timeout=15) -> Container:
containers = self.docker_project.containers(service_names=[key], stopped=True)
if not containers:
raise ContainerDoesntExist("The service '%s' doesn't exists" % key)

container = containers[0]
if wait_running:
start = time.time()
while not container.is_running or container.is_restarting:
if time.time() - start >= timeout:
raise ContainerNotRunning("The service '%s' is still %s after %s seconds" %
(key, container.human_readable_state, timeout))
time.sleep(0.1)
containers = self.docker_project.containers(service_names=[key], stopped=True)
if not containers:
raise ContainerDoesntExist("The service '%s' doesn't exists" % key)
container = containers[0]

# network_info is added only for wait running as it may lead to race condition issues.
container.network_info = create_network_info_for_container(container)
return container

0 comments on commit b066513

Please sign in to comment.