Merge pull request #1489 from dnephin/faster_integration_tests

Faster integration tests
This commit is contained in:
Aanand Prasad 2015-06-15 10:04:45 -07:00
commit 5231288b4e
15 changed files with 153 additions and 112 deletions

View File

@ -10,7 +10,9 @@ import sys
from docker.errors import APIError
import dockerpty
from .. import __version__, legacy
from .. import __version__
from .. import legacy
from ..const import DEFAULT_TIMEOUT
from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, NeedsBuildError
from ..config import parse_environment
@ -394,9 +396,8 @@ class TopLevelCommand(Command):
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
(default: 10)
"""
timeout = options.get('--timeout')
params = {} if timeout is None else {'timeout': int(timeout)}
project.stop(service_names=options['SERVICE'], **params)
timeout = float(options.get('--timeout') or DEFAULT_TIMEOUT)
project.stop(service_names=options['SERVICE'], timeout=timeout)
def restart(self, project, options):
"""
@ -408,9 +409,8 @@ class TopLevelCommand(Command):
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
(default: 10)
"""
timeout = options.get('--timeout')
params = {} if timeout is None else {'timeout': int(timeout)}
project.restart(service_names=options['SERVICE'], **params)
timeout = float(options.get('--timeout') or DEFAULT_TIMEOUT)
project.restart(service_names=options['SERVICE'], timeout=timeout)
def up(self, project, options):
"""
@ -439,9 +439,9 @@ class TopLevelCommand(Command):
image needs to be updated. (EXPERIMENTAL)
--no-recreate If containers already exist, don't recreate them.
--no-build Don't build an image, even if it's missing
-t, --timeout TIMEOUT When attached, use this timeout in seconds
for the shutdown. (default: 10)
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
when attached or when containers are already
running. (default: 10)
"""
insecure_registry = options['--allow-insecure-ssl']
detached = options['-d']
@ -452,6 +452,7 @@ class TopLevelCommand(Command):
allow_recreate = not options['--no-recreate']
smart_recreate = options['--x-smart-recreate']
service_names = options['SERVICE']
timeout = float(options.get('--timeout') or DEFAULT_TIMEOUT)
project.up(
service_names=service_names,
@ -460,6 +461,7 @@ class TopLevelCommand(Command):
smart_recreate=smart_recreate,
insecure_registry=insecure_registry,
do_build=not options['--no-build'],
timeout=timeout
)
to_attach = [c for s in project.get_services(service_names) for c in s.containers()]
@ -477,9 +479,7 @@ class TopLevelCommand(Command):
signal.signal(signal.SIGINT, handler)
print("Gracefully stopping... (press Ctrl+C again to force)")
timeout = options.get('--timeout')
params = {} if timeout is None else {'timeout': int(timeout)}
project.stop(service_names=service_names, **params)
project.stop(service_names=service_names, timeout=timeout)
def migrate_to_labels(self, project, _options):
"""

View File

@ -1,4 +1,5 @@
DEFAULT_TIMEOUT = 10
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
LABEL_ONE_OFF = 'com.docker.compose.oneoff'
LABEL_PROJECT = 'com.docker.compose.project'

View File

@ -6,7 +6,7 @@ from functools import reduce
from docker.errors import APIError
from .config import get_service_name_from_net, ConfigurationError
from .const import LABEL_PROJECT, LABEL_SERVICE, LABEL_ONE_OFF
from .const import LABEL_PROJECT, LABEL_SERVICE, LABEL_ONE_OFF, DEFAULT_TIMEOUT
from .service import Service
from .container import Container
from .legacy import check_for_legacy_containers
@ -211,7 +211,8 @@ class Project(object):
allow_recreate=True,
smart_recreate=False,
insecure_registry=False,
do_build=True):
do_build=True,
timeout=DEFAULT_TIMEOUT):
services = self.get_services(service_names, include_deps=start_deps)
@ -228,6 +229,7 @@ class Project(object):
plans[service.name],
insecure_registry=insecure_registry,
do_build=do_build,
timeout=timeout
)
]

View File

@ -13,6 +13,7 @@ from docker.utils import create_host_config, LogConfig
from . import __version__
from .config import DOCKER_CONFIG_KEYS, merge_environment
from .const import (
DEFAULT_TIMEOUT,
LABEL_CONTAINER_NUMBER,
LABEL_ONE_OFF,
LABEL_PROJECT,
@ -251,26 +252,6 @@ class Service(object):
else:
return self.options['image']
def converge(self,
allow_recreate=True,
smart_recreate=False,
insecure_registry=False,
do_build=True):
"""
If a container for this service doesn't exist, create and start one. If there are
any, stop them, create+start new ones, and remove the old containers.
"""
plan = self.convergence_plan(
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
)
return self.execute_convergence_plan(
plan,
insecure_registry=insecure_registry,
do_build=do_build,
)
def convergence_plan(self,
allow_recreate=True,
smart_recreate=False):
@ -311,7 +292,8 @@ class Service(object):
def execute_convergence_plan(self,
plan,
insecure_registry=False,
do_build=True):
do_build=True,
timeout=DEFAULT_TIMEOUT):
(action, containers) = plan
if action == 'create':
@ -328,6 +310,7 @@ class Service(object):
self.recreate_container(
c,
insecure_registry=insecure_registry,
timeout=timeout
)
for c in containers
]
@ -349,7 +332,8 @@ class Service(object):
def recreate_container(self,
container,
insecure_registry=False):
insecure_registry=False,
timeout=DEFAULT_TIMEOUT):
"""Recreate a container.
The original container is renamed to a temporary name so that data
@ -358,7 +342,7 @@ class Service(object):
"""
log.info("Recreating %s..." % container.name)
try:
container.stop()
container.stop(timeout=timeout)
except APIError as e:
if (e.response.status_code == 500
and e.explanation

View File

@ -1,2 +1,3 @@
FROM busybox:latest
LABEL com.docker.compose.test_image=true
CMD echo "success"

View File

@ -1,3 +1,4 @@
FROM busybox
FROM busybox:latest
LABEL com.docker.compose.test_image=true
VOLUME /data
CMD top

View File

@ -1,2 +1,3 @@
FROM busybox:latest
LABEL com.docker.compose.test_image=true
ENTRYPOINT echo "From prebuilt entrypoint"

View File

@ -1,2 +1,3 @@
FROM busybox:latest
LABEL com.docker.compose.test_image=true
CMD echo "success"

View File

@ -24,6 +24,7 @@ class CLITestCase(DockerClientTestCase):
self.project.remove_stopped()
for container in self.project.containers(stopped=True, one_off=True):
container.remove(force=True)
super(CLITestCase, self).tearDown()
@property
def project(self):
@ -161,6 +162,19 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(old_ids, new_ids)
def test_up_with_timeout(self):
self.command.dispatch(['up', '-d', '-t', '1'], None)
service = self.project.get_service('simple')
another = self.project.get_service('another')
self.assertEqual(len(service.containers()), 1)
self.assertEqual(len(another.containers()), 1)
# Ensure containers don't have stdin and stdout connected in -d mode
config = service.containers()[0].inspect()['Config']
self.assertFalse(config['AttachStderr'])
self.assertFalse(config['AttachStdout'])
self.assertFalse(config['AttachStdin'])
@patch('dockerpty.start')
def test_run_service_without_links(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/links-composefile'
@ -207,13 +221,10 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(old_ids, new_ids)
@patch('dockerpty.start')
def test_run_without_command(self, __):
def test_run_without_command(self, _):
self.command.base_dir = 'tests/fixtures/commands-composefile'
self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test')
for c in self.project.containers(stopped=True, one_off=True):
c.remove()
self.command.dispatch(['run', 'implicit'], None)
service = self.project.get_service('implicit')
containers = service.containers(stopped=True, one_off=True)

View File

@ -1,12 +1,15 @@
from docker.errors import APIError
from compose import legacy
from compose.project import Project
from .testcases import DockerClientTestCase
class ProjectTest(DockerClientTestCase):
class LegacyTestCase(DockerClientTestCase):
def setUp(self):
super(ProjectTest, self).setUp()
super(LegacyTestCase, self).setUp()
self.containers = []
db = self.create_service('db')
web = self.create_service('web', links=[(db, 'db')])
@ -23,12 +26,25 @@ class ProjectTest(DockerClientTestCase):
**service.options
)
self.client.start(container)
self.containers.append(container)
# Create a single one-off legacy container
self.client.create_container(
self.containers.append(self.client.create_container(
name='{}_{}_run_1'.format(self.project.name, self.services[0].name),
**self.services[0].options
)
))
def tearDown(self):
super(LegacyTestCase, self).tearDown()
for container in self.containers:
try:
self.client.kill(container)
except APIError:
pass
try:
self.client.remove_container(container)
except APIError:
pass
def get_legacy_containers(self, **kwargs):
return list(legacy.get_legacy_containers(

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals
from compose import config
from compose.const import LABEL_PROJECT
from compose.project import Project
from compose.container import Container
from .testcases import DockerClientTestCase
@ -55,6 +57,7 @@ class ProjectTest(DockerClientTestCase):
image='busybox:latest',
volumes=['/var/data'],
name='composetest_data_container',
labels={LABEL_PROJECT: 'composetest'},
)
project = Project.from_dicts(
name='composetest',
@ -69,9 +72,6 @@ class ProjectTest(DockerClientTestCase):
db = project.get_service('db')
self.assertEqual(db.volumes_from, [data_container])
project.kill()
project.remove_stopped()
def test_net_from_service(self):
project = Project.from_dicts(
name='composetest',
@ -95,15 +95,13 @@ class ProjectTest(DockerClientTestCase):
net = project.get_service('net')
self.assertEqual(web._get_net(), 'container:' + net.containers()[0].id)
project.kill()
project.remove_stopped()
def test_net_from_container(self):
net_container = Container.create(
self.client,
image='busybox:latest',
name='composetest_net_container',
command='top'
command='top',
labels={LABEL_PROJECT: 'composetest'},
)
net_container.start()
@ -123,9 +121,6 @@ class ProjectTest(DockerClientTestCase):
web = project.get_service('web')
self.assertEqual(web._get_net(), 'container:' + net_container.id)
project.kill()
project.remove_stopped()
def test_start_stop_kill_remove(self):
web = self.create_service('web')
db = self.create_service('db')
@ -171,9 +166,6 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(web.containers()), 0)
project.kill()
project.remove_stopped()
def test_project_up_starts_uncreated_services(self):
db = self.create_service('db')
web = self.create_service('web', links=[(db, 'db')])
@ -205,9 +197,6 @@ class ProjectTest(DockerClientTestCase):
self.assertNotEqual(db_container.id, old_db_id)
self.assertEqual(db_container.get('Volumes./etc'), db_volume_path)
project.kill()
project.remove_stopped()
def test_project_up_with_no_recreate_running(self):
web = self.create_service('web')
db = self.create_service('db', volumes=['/var/db'])
@ -228,9 +217,6 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
db_volume_path)
project.kill()
project.remove_stopped()
def test_project_up_with_no_recreate_stopped(self):
web = self.create_service('web')
db = self.create_service('db', volumes=['/var/db'])
@ -258,9 +244,6 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
db_volume_path)
project.kill()
project.remove_stopped()
def test_project_up_without_all_services(self):
console = self.create_service('console')
db = self.create_service('db')
@ -273,9 +256,6 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(console.containers()), 1)
project.kill()
project.remove_stopped()
def test_project_up_starts_links(self):
console = self.create_service('console')
db = self.create_service('db', volumes=['/var/db'])
@ -291,9 +271,6 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(console.containers()), 0)
project.kill()
project.remove_stopped()
def test_project_up_starts_depends(self):
project = Project.from_dicts(
name='composetest',
@ -329,9 +306,6 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(project.get_service('data').containers()), 1)
self.assertEqual(len(project.get_service('console').containers()), 0)
project.kill()
project.remove_stopped()
def test_project_up_with_no_deps(self):
project = Project.from_dicts(
name='composetest',
@ -368,9 +342,6 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(project.get_service('data').containers(stopped=True)), 1)
self.assertEqual(len(project.get_service('console').containers()), 0)
project.kill()
project.remove_stopped()
def test_unscale_after_restart(self):
web = self.create_service('web')
project = Project('composetest', [web], self.client)
@ -395,5 +366,3 @@ class ProjectTest(DockerClientTestCase):
project.up()
service = project.get_service('web')
self.assertEqual(len(service.containers()), 1)
project.kill()
project.remove_stopped()

View File

@ -2,8 +2,9 @@ from __future__ import unicode_literals
from __future__ import absolute_import
import os
from os import path
import mock
from docker.errors import APIError
import mock
import tempfile
import shutil
import six
@ -18,11 +19,11 @@ from compose.const import (
)
from compose.service import (
ConfigError,
ConvergencePlan,
Service,
build_extra_hosts,
)
from compose.container import Container
from docker.errors import APIError
from .testcases import DockerClientTestCase
@ -235,7 +236,12 @@ class ServiceTest(DockerClientTestCase):
def test_create_container_with_volumes_from(self):
volume_service = self.create_service('data')
volume_container_1 = volume_service.create_container()
volume_container_2 = Container.create(self.client, image='busybox:latest', command=["top"])
volume_container_2 = Container.create(
self.client,
image='busybox:latest',
command=["top"],
labels={LABEL_PROJECT: 'composetest'},
)
host_service = self.create_service('host', volumes_from=[volume_service, volume_container_2])
host_container = host_service.create_container()
host_service.start_container(host_container)
@ -244,7 +250,7 @@ class ServiceTest(DockerClientTestCase):
self.assertIn(volume_container_2.id,
host_container.get('HostConfig.VolumesFrom'))
def test_converge(self):
def test_execute_convergence_plan_recreate(self):
service = self.create_service(
'db',
environment={'FOO': '1'},
@ -264,7 +270,8 @@ class ServiceTest(DockerClientTestCase):
num_containers_before = len(self.client.containers(all=True))
service.options['environment']['FOO'] = '2'
new_container = service.converge()[0]
new_container, = service.execute_convergence_plan(
ConvergencePlan('recreate', [old_container]))
self.assertEqual(new_container.get('Config.Entrypoint'), ['top'])
self.assertEqual(new_container.get('Config.Cmd'), ['-d', '1'])
@ -281,7 +288,7 @@ class ServiceTest(DockerClientTestCase):
self.client.inspect_container,
old_container.id)
def test_converge_when_containers_are_stopped(self):
def test_execute_convergence_plan_when_containers_are_stopped(self):
service = self.create_service(
'db',
environment={'FOO': '1'},
@ -290,11 +297,21 @@ class ServiceTest(DockerClientTestCase):
command=['-d', '1']
)
service.create_container()
self.assertEqual(len(service.containers(stopped=True)), 1)
service.converge()
self.assertEqual(len(service.containers(stopped=True)), 1)
def test_converge_with_image_declared_volume(self):
containers = service.containers(stopped=True)
self.assertEqual(len(containers), 1)
container, = containers
self.assertFalse(container.is_running)
service.execute_convergence_plan(ConvergencePlan('start', [container]))
containers = service.containers()
self.assertEqual(len(containers), 1)
container.inspect()
self.assertEqual(container, containers[0])
self.assertTrue(container.is_running)
def test_execute_convergence_plan_with_image_declared_volume(self):
service = Service(
project='composetest',
name='db',
@ -306,7 +323,8 @@ class ServiceTest(DockerClientTestCase):
self.assertEqual(old_container.get('Volumes').keys(), ['/data'])
volume_path = old_container.get('Volumes')['/data']
new_container = service.converge()[0]
new_container, = service.execute_convergence_plan(
ConvergencePlan('recreate', [old_container]))
self.assertEqual(new_container.get('Volumes').keys(), ['/data'])
self.assertEqual(new_container.get('Volumes')['/data'], volume_path)
@ -408,7 +426,7 @@ class ServiceTest(DockerClientTestCase):
self.assertEqual(len(self.client.images(name='composetest_test')), 1)
def test_start_container_uses_tagged_image_if_it_exists(self):
self.client.build('tests/fixtures/simple-dockerfile', tag='composetest_test')
self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test')
service = Service(
name='test',
client=self.client,

View File

@ -12,8 +12,8 @@ from .testcases import DockerClientTestCase
class ProjectTestCase(DockerClientTestCase):
def run_up(self, cfg, **kwargs):
if 'smart_recreate' not in kwargs:
kwargs['smart_recreate'] = True
kwargs.setdefault('smart_recreate', True)
kwargs.setdefault('timeout', 0.1)
project = self.make_project(cfg)
project.up(**kwargs)
@ -153,7 +153,31 @@ class ProjectWithDependenciesTest(ProjectTestCase):
self.assertEqual(new_containers - old_containers, set())
def converge(service,
allow_recreate=True,
smart_recreate=False,
insecure_registry=False,
do_build=True):
"""
If a container for this service doesn't exist, create and start one. If there are
any, stop them, create+start new ones, and remove the old containers.
"""
plan = service.convergence_plan(
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
)
return service.execute_convergence_plan(
plan,
insecure_registry=insecure_registry,
do_build=do_build,
timeout=0.1,
)
class ServiceStateTest(DockerClientTestCase):
"""Test cases for Service.convergence_plan."""
def test_trigger_create(self):
web = self.create_service('web')
self.assertEqual(('create', []), web.convergence_plan(smart_recreate=True))
@ -216,18 +240,19 @@ class ServiceStateTest(DockerClientTestCase):
def test_trigger_recreate_with_build(self):
context = tempfile.mkdtemp()
base_image = "FROM busybox\nLABEL com.docker.compose.test_image=true\n"
try:
dockerfile = os.path.join(context, 'Dockerfile')
with open(dockerfile, 'w') as f:
f.write('FROM busybox\n')
f.write(base_image)
web = self.create_service('web', build=context)
container = web.create_container()
with open(dockerfile, 'w') as f:
f.write('FROM busybox\nCMD echo hello world\n')
f.write(base_image + 'CMD echo hello world\n')
web.build()
web = self.create_service('web', build=context)
@ -249,15 +274,15 @@ class ConfigHashTest(DockerClientTestCase):
def test_config_hash_with_custom_labels(self):
web = self.create_service('web', labels={'foo': '1'})
container = web.converge()[0]
container = converge(web)[0]
self.assertIn(LABEL_CONFIG_HASH, container.labels)
self.assertIn('foo', container.labels)
def test_config_hash_sticks_around(self):
web = self.create_service('web', command=["top"])
container = web.converge()[0]
container = converge(web)[0]
self.assertIn(LABEL_CONFIG_HASH, container.labels)
web = self.create_service('web', command=["top", "-d", "1"])
container = web.converge()[0]
container = converge(web)[0]
self.assertIn(LABEL_CONFIG_HASH, container.labels)

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
from __future__ import absolute_import
from compose.service import Service
from compose.config import make_service_dict
from compose.const import LABEL_PROJECT
from compose.cli.docker_client import docker_client
from compose.progress_stream import stream_output
from .. import unittest
@ -12,15 +13,15 @@ class DockerClientTestCase(unittest.TestCase):
def setUpClass(cls):
cls.client = docker_client()
# TODO: update to use labels in #652
def setUp(self):
for c in self.client.containers(all=True):
if c['Names'] and 'composetest' in c['Names'][0]:
self.client.kill(c['Id'])
self.client.remove_container(c['Id'])
for i in self.client.images():
if isinstance(i.get('Tag'), basestring) and 'composetest' in i['Tag']:
self.client.remove_image(i)
def tearDown(self):
for c in self.client.containers(
all=True,
filters={'label': '%s=composetest' % LABEL_PROJECT}):
self.client.kill(c['Id'])
self.client.remove_container(c['Id'])
for i in self.client.images(
filters={'label': 'com.docker.compose.test_image'}):
self.client.remove_image(i)
def create_service(self, name, **kwargs):
if 'image' not in kwargs and 'build' not in kwargs:
@ -36,5 +37,6 @@ class DockerClientTestCase(unittest.TestCase):
)
def check_build(self, *args, **kwargs):
kwargs.setdefault('rm', True)
build_output = self.client.build(*args, **kwargs)
stream_output(build_output, open('/dev/null', 'w'))

View File

@ -246,7 +246,7 @@ class ServiceTest(unittest.TestCase):
service.image = lambda: {'Id': 'abc123'}
new_container = service.recreate_container(mock_container)
mock_container.stop.assert_called_once_with()
mock_container.stop.assert_called_once_with(timeout=10)
self.mock_client.rename.assert_called_once_with(
mock_container.id,
'%s_%s' % (mock_container.short_id, mock_container.name))
@ -254,6 +254,15 @@ class ServiceTest(unittest.TestCase):
new_container.start.assert_called_once_with()
mock_container.remove.assert_called_once_with()
@mock.patch('compose.service.Container', autospec=True)
def test_recreate_container_with_timeout(self, _):
mock_container = mock.create_autospec(Container)
self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
service = Service('foo', client=self.mock_client, image='someimage')
service.recreate_container(mock_container, timeout=1)
mock_container.stop.assert_called_once_with(timeout=1)
def test_parse_repository_tag(self):
self.assertEqual(parse_repository_tag("root"), ("root", ""))
self.assertEqual(parse_repository_tag("root:tag"), ("root", "tag"))