diff --git a/.dockerignore b/.dockerignore index 055ae7ed1..eccd86dda 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,5 @@ coverage-html docs/_site venv .tox +**/__pycache__ +*.pyc diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 343e4974f..fc05de351 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -20,8 +20,6 @@ from docker import errors from .. import mock from ..helpers import create_host_file -from ..helpers import is_cluster -from ..helpers import no_cluster from compose.cli.command import get_project from compose.config.errors import DuplicateOverrideFileFound from compose.container import Container @@ -29,6 +27,8 @@ from compose.project import OneOffFilter from compose.utils import nanoseconds_from_time_seconds from tests.integration.testcases import DockerClientTestCase from tests.integration.testcases import get_links +from tests.integration.testcases import is_cluster +from tests.integration.testcases import no_cluster from tests.integration.testcases import pull_busybox from tests.integration.testcases import SWARM_SKIP_RM_VOLUMES from tests.integration.testcases import v2_1_only @@ -116,7 +116,7 @@ class CLITestCase(DockerClientTestCase): def tearDown(self): if self.base_dir: self.project.kill() - self.project.remove_stopped() + self.project.down(None, True) for container in self.project.containers(stopped=True, one_off=OneOffFilter.only): container.remove(force=True) @@ -1214,6 +1214,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(proc.returncode, 1) @v2_only() + @no_cluster('Container PID mode does not work across clusters') def test_up_with_pid_mode(self): c = self.client.create_container( 'busybox', 'top', name='composetest_pid_mode_container', @@ -1244,8 +1245,8 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(self.project.containers()), 1) stdout, stderr = self.dispatch(['exec', '-T', 'console', 'ls', '-1d', '/']) - self.assertEqual(stdout, "/\n") self.assertEqual(stderr, "") + self.assertEqual(stdout, "/\n") def test_exec_custom_user(self): self.base_dir = 'tests/fixtures/links-composefile' @@ -1826,7 +1827,13 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['logs', '-f']) - assert result.stdout.count('\n') == 5 + if not is_cluster(self.client): + assert result.stdout.count('\n') == 5 + else: + # Sometimes logs are picked up from old containers that haven't yet + # been removed (removal in Swarm is async) + assert result.stdout.count('\n') >= 5 + assert 'simple' in result.stdout assert 'another' in result.stdout assert 'exited with code 0' in result.stdout @@ -1882,7 +1889,10 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['up']) result = self.dispatch(['logs', '--tail', '2']) - assert result.stdout.count('\n') == 3 + assert 'c\n' in result.stdout + assert 'd\n' in result.stdout + assert 'a\n' not in result.stdout + assert 'b\n' not in result.stdout def test_kill(self): self.dispatch(['up', '-d'], None) @@ -2045,8 +2055,8 @@ class CLITestCase(DockerClientTestCase): return result.stdout.rstrip() assert get_port(3000) == container.get_local_port(3000) - assert ':49152' in get_port(3001) - assert ':49153' in get_port(3002) + assert ':53222' in get_port(3001) + assert ':53223' in get_port(3002) def test_port_with_scale(self): self.base_dir = 'tests/fixtures/ports-composefile-scale' diff --git a/tests/fixtures/ports-composefile/expanded-notation.yml b/tests/fixtures/ports-composefile/expanded-notation.yml index 6fbe59176..09a7a2bf9 100644 --- a/tests/fixtures/ports-composefile/expanded-notation.yml +++ b/tests/fixtures/ports-composefile/expanded-notation.yml @@ -6,10 +6,10 @@ services: ports: - target: 3000 - target: 3001 - published: 49152 + published: 53222 - target: 3002 - published: 49153 + published: 53223 protocol: tcp - target: 3003 - published: 49154 + published: 53224 protocol: udp diff --git a/tests/helpers.py b/tests/helpers.py index 662353c93..59efd2557 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,12 +1,8 @@ from __future__ import absolute_import from __future__ import unicode_literals -import functools import os -from docker.errors import APIError -from pytest import skip - from compose.config.config import ConfigDetails from compose.config.config import ConfigFile from compose.config.config import load @@ -48,34 +44,3 @@ def create_host_file(client, filename): "Container exited with code {}:\n{}".format(exitcode, output)) finally: client.remove_container(container, force=True) - - -def is_cluster(client): - nodes = None - - def get_nodes_number(): - try: - return len(client.nodes()) - except APIError: - # If the Engine is not part of a Swarm, the SDK will raise - # an APIError - return 0 - - if nodes is None: - # Only make the API call if the value hasn't been cached yet - nodes = get_nodes_number() - - return nodes > 1 - - -def no_cluster(reason): - def decorator(f): - @functools.wraps(f) - def wrapper(self, *args, **kwargs): - if is_cluster(self.client): - skip("Test will not be run in cluster mode: %s" % reason) - return - return f(self, *args, **kwargs) - return wrapper - - return decorator diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index ce95c5f21..5ead7b8e7 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -12,8 +12,6 @@ from docker.errors import NotFound from .. import mock from ..helpers import build_config as load_config from ..helpers import create_host_file -from ..helpers import is_cluster -from ..helpers import no_cluster from .testcases import DockerClientTestCase from .testcases import SWARM_SKIP_CONTAINERS_ALL from compose.config import config @@ -33,6 +31,8 @@ from compose.errors import NoHealthCheckConfigured from compose.project import Project from compose.project import ProjectError from compose.service import ConvergenceStrategy +from tests.integration.testcases import is_cluster +from tests.integration.testcases import no_cluster from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_2_only from tests.integration.testcases import v2_only diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 350f7398b..ff75015df 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -13,8 +13,6 @@ from six import StringIO from six import text_type from .. import mock -from ..helpers import is_cluster -from ..helpers import no_cluster from .testcases import DockerClientTestCase from .testcases import get_links from .testcases import pull_busybox @@ -38,6 +36,8 @@ from compose.service import ConvergenceStrategy from compose.service import NetworkMode from compose.service import PidMode from compose.service import Service +from tests.integration.testcases import is_cluster +from tests.integration.testcases import no_cluster from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_2_only from tests.integration.testcases import v2_only @@ -635,7 +635,10 @@ class ServiceTest(DockerClientTestCase): with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: f.write("FROM busybox\n") - self.create_service('web', build={'context': base_dir}).build() + service = self.create_service('web', build={'context': base_dir}) + service.build() + self.addCleanup(self.client.remove_image, service.image_name) + assert self.client.inspect_image('composetest_web') def test_build_non_ascii_filename(self): @@ -648,7 +651,9 @@ class ServiceTest(DockerClientTestCase): with open(os.path.join(base_dir.encode('utf8'), b'foo\xE2bar'), 'w') as f: f.write("hello world\n") - self.create_service('web', build={'context': text_type(base_dir)}).build() + service = self.create_service('web', build={'context': text_type(base_dir)}) + service.build() + self.addCleanup(self.client.remove_image, service.image_name) assert self.client.inspect_image('composetest_web') def test_build_with_image_name(self): @@ -683,6 +688,7 @@ class ServiceTest(DockerClientTestCase): build={'context': text_type(base_dir), 'args': {"build_version": "1"}}) service.build() + self.addCleanup(self.client.remove_image, service.image_name) assert service.image() assert "build_version=1" in service.image()['ContainerConfig']['Cmd'] @@ -699,6 +705,8 @@ class ServiceTest(DockerClientTestCase): build={'context': text_type(base_dir), 'args': {"build_version": "1"}}) service.build(build_args_override={'build_version': '2'}) + self.addCleanup(self.client.remove_image, service.image_name) + assert service.image() assert "build_version=2" in service.image()['ContainerConfig']['Cmd'] @@ -714,9 +722,12 @@ class ServiceTest(DockerClientTestCase): 'labels': {'com.docker.compose.test': 'true'} }) service.build() + self.addCleanup(self.client.remove_image, service.image_name) + assert service.image() assert service.image()['Config']['Labels']['com.docker.compose.test'] == 'true' + @no_cluster('Container networks not on Swarm') def test_build_with_network(self): base_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base_dir) @@ -739,6 +750,8 @@ class ServiceTest(DockerClientTestCase): }) service.build() + self.addCleanup(self.client.remove_image, service.image_name) + assert service.image() def test_start_container_stays_unprivileged(self): @@ -1130,6 +1143,8 @@ class ServiceTest(DockerClientTestCase): build={'context': base_dir, 'cache_from': ['build1']}) service.build() + self.addCleanup(self.client.remove_image, service.image_name) + assert service.image() @mock.patch.dict(os.environ) diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index 7d600f323..53361a820 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -1,13 +1,14 @@ from __future__ import absolute_import from __future__ import unicode_literals +import functools import os import pytest +from docker.errors import APIError from docker.utils import version_lt from .. import unittest -from ..helpers import is_cluster from compose.cli.docker_client import docker_client from compose.config.config import resolve_environment from compose.config.environment import Environment @@ -25,6 +26,7 @@ from compose.service import Service SWARM_SKIP_CONTAINERS_ALL = os.environ.get('SWARM_SKIP_CONTAINERS_ALL', '0') != '0' SWARM_SKIP_CPU_SHARES = os.environ.get('SWARM_SKIP_CPU_SHARES', '0') != '0' SWARM_SKIP_RM_VOLUMES = os.environ.get('SWARM_SKIP_RM_VOLUMES', '0') != '0' +SWARM_ASSUME_MULTINODE = os.environ.get('SWARM_ASSUME_MULTINODE', '0') != '0' def pull_busybox(client): @@ -141,3 +143,35 @@ class DockerClientTestCase(unittest.TestCase): volumes = self.client.volumes(filters={'name': volume_name})['Volumes'] assert len(volumes) > 0 return self.client.inspect_volume(volumes[0]['Name']) + + +def is_cluster(client): + if SWARM_ASSUME_MULTINODE: + return True + + def get_nodes_number(): + try: + return len(client.nodes()) + except APIError: + # If the Engine is not part of a Swarm, the SDK will raise + # an APIError + return 0 + + if not hasattr(is_cluster, 'nodes') or is_cluster.nodes is None: + # Only make the API call if the value hasn't been cached yet + is_cluster.nodes = get_nodes_number() + + return is_cluster.nodes > 1 + + +def no_cluster(reason): + def decorator(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if is_cluster(self.client): + pytest.skip("Test will not be run in cluster mode: %s" % reason) + return + return f(self, *args, **kwargs) + return wrapper + + return decorator diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index 772631a5b..ecc71d0b1 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals from docker.errors import DockerException -from ..helpers import no_cluster from .testcases import DockerClientTestCase +from .testcases import no_cluster from compose.const import LABEL_PROJECT from compose.const import LABEL_VOLUME from compose.volume import Volume diff --git a/tox.ini b/tox.ini index 61bc05745..749be3faa 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,8 @@ passenv = DOCKER_CERT_PATH DOCKER_TLS_VERIFY DOCKER_VERSION + SWARM_SKIP_* + SWARM_ASSUME_MULTINODE setenv = HOME=/tmp deps =