diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/acceptance/cli_test.py | 99 | ||||
-rw-r--r-- | tests/integration/network_test.py | 20 | ||||
-rw-r--r-- | tests/integration/project_test.py | 47 | ||||
-rw-r--r-- | tests/integration/service_test.py | 43 | ||||
-rw-r--r-- | tests/unit/cli/main_test.py | 8 | ||||
-rw-r--r-- | tests/unit/cli_test.py | 10 | ||||
-rw-r--r-- | tests/unit/config/config_test.py | 28 | ||||
-rw-r--r-- | tests/unit/config/interpolation_test.py | 12 | ||||
-rw-r--r-- | tests/unit/project_test.py | 56 | ||||
-rw-r--r-- | tests/unit/service_test.py | 141 |
10 files changed, 379 insertions, 85 deletions
diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 7a0f22fc..07570580 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -177,6 +177,13 @@ class CLITestCase(DockerClientTestCase): returncode=0 ) + def test_shorthand_host_opt_interactive(self): + self.dispatch( + ['-H={0}'.format(os.environ.get('DOCKER_HOST', 'unix://')), + 'run', 'another', 'ls'], + returncode=0 + ) + def test_host_not_reachable(self): result = self.dispatch(['-H=tcp://doesnotexist:8000', 'ps'], returncode=1) assert "Couldn't connect to Docker daemon" in result.stderr @@ -491,16 +498,16 @@ class CLITestCase(DockerClientTestCase): def test_ps(self): self.project.get_service('simple').create_container() result = self.dispatch(['ps']) - assert 'simplecomposefile_simple_1' in result.stdout + assert 'simple-composefile_simple_1' in result.stdout def test_ps_default_composefile(self): self.base_dir = 'tests/fixtures/multiple-composefiles' self.dispatch(['up', '-d']) result = self.dispatch(['ps']) - assert 'multiplecomposefiles_simple_1' in result.stdout - assert 'multiplecomposefiles_another_1' in result.stdout - assert 'multiplecomposefiles_yetanother_1' not in result.stdout + assert 'multiple-composefiles_simple_1' in result.stdout + assert 'multiple-composefiles_another_1' in result.stdout + assert 'multiple-composefiles_yetanother_1' not in result.stdout def test_ps_alternate_composefile(self): config_path = os.path.abspath( @@ -511,9 +518,9 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['-f', 'compose2.yml', 'up', '-d']) result = self.dispatch(['-f', 'compose2.yml', 'ps']) - assert 'multiplecomposefiles_simple_1' not in result.stdout - assert 'multiplecomposefiles_another_1' not in result.stdout - assert 'multiplecomposefiles_yetanother_1' in result.stdout + assert 'multiple-composefiles_simple_1' not in result.stdout + assert 'multiple-composefiles_another_1' not in result.stdout + assert 'multiple-composefiles_yetanother_1' in result.stdout def test_ps_services_filter_option(self): self.base_dir = 'tests/fixtures/ps-services-filter' @@ -545,13 +552,11 @@ class CLITestCase(DockerClientTestCase): def test_pull(self): result = self.dispatch(['pull']) - assert sorted(result.stderr.split('\n'))[1:] == [ - 'Pulling another (busybox:latest)...', - 'Pulling simple (busybox:latest)...', - ] + assert 'Pulling simple' in result.stderr + assert 'Pulling another' in result.stderr def test_pull_with_digest(self): - result = self.dispatch(['-f', 'digest.yml', 'pull']) + result = self.dispatch(['-f', 'digest.yml', 'pull', '--no-parallel']) assert 'Pulling simple (busybox:latest)...' in result.stderr assert ('Pulling digest (busybox@' @@ -561,7 +566,7 @@ class CLITestCase(DockerClientTestCase): def test_pull_with_ignore_pull_failures(self): result = self.dispatch([ '-f', 'ignore-pull-failures.yml', - 'pull', '--ignore-pull-failures'] + 'pull', '--ignore-pull-failures', '--no-parallel'] ) assert 'Pulling simple (busybox:latest)...' in result.stderr @@ -576,7 +581,7 @@ class CLITestCase(DockerClientTestCase): def test_pull_with_parallel_failure(self): result = self.dispatch([ - '-f', 'ignore-pull-failures.yml', 'pull', '--parallel'], + '-f', 'ignore-pull-failures.yml', 'pull'], returncode=1 ) @@ -593,14 +598,14 @@ class CLITestCase(DockerClientTestCase): def test_pull_with_no_deps(self): self.base_dir = 'tests/fixtures/links-composefile' - result = self.dispatch(['pull', 'web']) + result = self.dispatch(['pull', '--no-parallel', 'web']) assert sorted(result.stderr.split('\n'))[1:] == [ 'Pulling web (busybox:latest)...', ] def test_pull_with_include_deps(self): self.base_dir = 'tests/fixtures/links-composefile' - result = self.dispatch(['pull', '--include-deps', 'web']) + result = self.dispatch(['pull', '--no-parallel', '--include-deps', 'web']) assert sorted(result.stderr.split('\n'))[1:] == [ 'Pulling db (busybox:latest)...', 'Pulling web (busybox:latest)...', @@ -902,18 +907,18 @@ class CLITestCase(DockerClientTestCase): assert len(self.project.containers(one_off=OneOffFilter.only, stopped=True)) == 2 result = self.dispatch(['down', '--rmi=local', '--volumes']) - assert 'Stopping v2full_web_1' in result.stderr - assert 'Stopping v2full_other_1' in result.stderr - assert 'Stopping v2full_web_run_2' in result.stderr - assert 'Removing v2full_web_1' in result.stderr - assert 'Removing v2full_other_1' in result.stderr - assert 'Removing v2full_web_run_1' in result.stderr - assert 'Removing v2full_web_run_2' in result.stderr - assert 'Removing volume v2full_data' in result.stderr - assert 'Removing image v2full_web' in result.stderr + assert 'Stopping v2-full_web_1' in result.stderr + assert 'Stopping v2-full_other_1' in result.stderr + assert 'Stopping v2-full_web_run_2' in result.stderr + assert 'Removing v2-full_web_1' in result.stderr + assert 'Removing v2-full_other_1' in result.stderr + assert 'Removing v2-full_web_run_1' in result.stderr + assert 'Removing v2-full_web_run_2' in result.stderr + assert 'Removing volume v2-full_data' in result.stderr + assert 'Removing image v2-full_web' in result.stderr assert 'Removing image busybox' not in result.stderr - assert 'Removing network v2full_default' in result.stderr - assert 'Removing network v2full_front' in result.stderr + assert 'Removing network v2-full_default' in result.stderr + assert 'Removing network v2-full_front' in result.stderr def test_down_timeout(self): self.dispatch(['up', '-d'], None) @@ -1559,6 +1564,16 @@ class CLITestCase(DockerClientTestCase): assert stdout == "operator\n" assert stderr == "" + @v3_only() + def test_exec_workdir(self): + self.base_dir = 'tests/fixtures/links-composefile' + os.environ['COMPOSE_API_VERSION'] = '1.35' + self.dispatch(['up', '-d', 'console']) + assert len(self.project.containers()) == 1 + + stdout, stderr = self.dispatch(['exec', '-T', '--workdir', '/etc', 'console', 'ls']) + assert 'passwd' in stdout + @v2_2_only() def test_exec_service_with_environment_overridden(self): name = 'service' @@ -1990,39 +2005,39 @@ class CLITestCase(DockerClientTestCase): proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) wait_on_condition(ContainerStateCondition( self.project.client, - 'simplecomposefile_simple_run_1', + 'simple-composefile_simple_run_1', 'running')) os.kill(proc.pid, signal.SIGINT) wait_on_condition(ContainerStateCondition( self.project.client, - 'simplecomposefile_simple_run_1', + 'simple-composefile_simple_run_1', 'exited')) def test_run_handles_sigterm(self): proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) wait_on_condition(ContainerStateCondition( self.project.client, - 'simplecomposefile_simple_run_1', + 'simple-composefile_simple_run_1', 'running')) os.kill(proc.pid, signal.SIGTERM) wait_on_condition(ContainerStateCondition( self.project.client, - 'simplecomposefile_simple_run_1', + 'simple-composefile_simple_run_1', 'exited')) def test_run_handles_sighup(self): proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) wait_on_condition(ContainerStateCondition( self.project.client, - 'simplecomposefile_simple_run_1', + 'simple-composefile_simple_run_1', 'running')) os.kill(proc.pid, signal.SIGHUP) wait_on_condition(ContainerStateCondition( self.project.client, - 'simplecomposefile_simple_run_1', + 'simple-composefile_simple_run_1', 'exited')) @mock.patch.dict(os.environ) @@ -2224,7 +2239,7 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['up', '-d', 'another']) wait_on_condition(ContainerStateCondition( self.project.client, - 'logscomposefile_another_1', + 'logs-composefile_another_1', 'exited')) self.dispatch(['kill', 'simple']) @@ -2233,8 +2248,8 @@ class CLITestCase(DockerClientTestCase): assert 'hello' in result.stdout assert 'test' in result.stdout - assert 'logscomposefile_another_1 exited with code 0' in result.stdout - assert 'logscomposefile_simple_1 exited with code 137' in result.stdout + assert 'logs-composefile_another_1 exited with code 0' in result.stdout + assert 'logs-composefile_simple_1 exited with code 137' in result.stdout def test_logs_default(self): self.base_dir = 'tests/fixtures/logs-composefile' @@ -2481,7 +2496,7 @@ class CLITestCase(DockerClientTestCase): container, = self.project.containers() expected_template = ' container {} {}' - expected_meta_info = ['image=busybox:latest', 'name=simplecomposefile_simple_1'] + expected_meta_info = ['image=busybox:latest', 'name=simple-composefile_simple_1'] assert expected_template.format('create', container.id) in lines[0] assert expected_template.format('start', container.id) in lines[1] @@ -2601,13 +2616,13 @@ class CLITestCase(DockerClientTestCase): result = wait_on_process(proc, returncode=1) - assert 'exitcodefrom_another_1 exited with code 1' in result.stdout + assert 'exit-code-from_another_1 exited with code 1' in result.stdout def test_images(self): self.project.get_service('simple').create_container() result = self.dispatch(['images']) assert 'busybox' in result.stdout - assert 'simplecomposefile_simple_1' in result.stdout + assert 'simple-composefile_simple_1' in result.stdout def test_images_default_composefile(self): self.base_dir = 'tests/fixtures/multiple-composefiles' @@ -2615,8 +2630,8 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['images']) assert 'busybox' in result.stdout - assert 'multiplecomposefiles_another_1' in result.stdout - assert 'multiplecomposefiles_simple_1' in result.stdout + assert 'multiple-composefiles_another_1' in result.stdout + assert 'multiple-composefiles_simple_1' in result.stdout @mock.patch.dict(os.environ) def test_images_tagless_image(self): @@ -2636,7 +2651,7 @@ class CLITestCase(DockerClientTestCase): self.project.get_service('foo').create_container() result = self.dispatch(['images']) assert '<none>' in result.stdout - assert 'taglessimage_foo_1' in result.stdout + assert 'tagless-image_foo_1' in result.stdout def test_up_with_override_yaml(self): self.base_dir = 'tests/fixtures/override-yaml-files' diff --git a/tests/integration/network_test.py b/tests/integration/network_test.py index 2ff610fb..a2493fda 100644 --- a/tests/integration/network_test.py +++ b/tests/integration/network_test.py @@ -1,7 +1,10 @@ from __future__ import absolute_import from __future__ import unicode_literals +import pytest + from .testcases import DockerClientTestCase +from compose.config.errors import ConfigurationError from compose.const import LABEL_NETWORK from compose.const import LABEL_PROJECT from compose.network import Network @@ -15,3 +18,20 @@ class NetworkTest(DockerClientTestCase): labels = net_data['Labels'] assert labels[LABEL_NETWORK] == net.name assert labels[LABEL_PROJECT] == net.project + + def test_network_external_default_ensure(self): + net = Network( + self.client, 'composetest', 'foonet', + external=True + ) + + with pytest.raises(ConfigurationError): + net.ensure() + + def test_network_external_overlay_ensure(self): + net = Network( + self.client, 'composetest', 'foonet', + driver='overlay', external=True + ) + + assert net.ensure() is None diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 0acb8028..3960d12e 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import json import os import random +import shutil import tempfile import py @@ -1538,6 +1539,52 @@ class ProjectTest(DockerClientTestCase): ) in str(e.value) @v2_only() + @no_cluster('inspect volume by name defect on Swarm Classic') + def test_initialize_volumes_updated_driver_opts(self): + vol_name = '{0:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{0}'.format(vol_name) + tmpdir = tempfile.mkdtemp(prefix='compose_test_') + self.addCleanup(shutil.rmtree, tmpdir) + driver_opts = {'o': 'bind', 'device': tmpdir, 'type': 'none'} + + config_data = build_config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top' + }], + volumes={ + vol_name: { + 'driver': 'local', + 'driver_opts': driver_opts + } + }, + ) + project = Project.from_config( + name='composetest', + config_data=config_data, client=self.client + ) + project.volumes.initialize() + + volume_data = self.get_volume_data(full_vol_name) + assert volume_data['Name'].split('/')[-1] == full_vol_name + assert volume_data['Driver'] == 'local' + assert volume_data['Options'] == driver_opts + + driver_opts['device'] = '/opt/data/localdata' + project = Project.from_config( + name='composetest', + config_data=config_data, + client=self.client + ) + with pytest.raises(config.ConfigurationError) as e: + project.volumes.initialize() + assert 'Configuration for volume {0} specifies "device" driver_opt {1}'.format( + vol_name, driver_opts['device'] + ) in str(e.value) + + @v2_only() def test_initialize_volumes_updated_blank_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 6e86a02d..d8f4d094 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -122,10 +122,19 @@ class ServiceTest(DockerClientTestCase): assert container.get('HostConfig.CpuShares') == 73 def test_create_container_with_cpu_quota(self): - service = self.create_service('db', cpu_quota=40000) + service = self.create_service('db', cpu_quota=40000, cpu_period=150000) container = service.create_container() container.start() assert container.get('HostConfig.CpuQuota') == 40000 + assert container.get('HostConfig.CpuPeriod') == 150000 + + @pytest.mark.xfail(raises=OperationFailedError, reason='not supported by kernel') + def test_create_container_with_cpu_rt(self): + service = self.create_service('db', cpu_rt_runtime=40000, cpu_rt_period=150000) + container = service.create_container() + container.start() + assert container.get('HostConfig.CpuRealtimeRuntime') == 40000 + assert container.get('HostConfig.CpuRealtimePeriod') == 150000 @v2_2_only() def test_create_container_with_cpu_count(self): @@ -1096,6 +1105,38 @@ class ServiceTest(DockerClientTestCase): service.build() assert service.image() + def test_build_with_gzip(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write('\n'.join([ + 'FROM busybox', + 'COPY . /src', + 'RUN cat /src/hello.txt' + ])) + with open(os.path.join(base_dir, 'hello.txt'), 'w') as f: + f.write('hello world\n') + + service = self.create_service('build_gzip', build={ + 'context': text_type(base_dir), + }) + service.build(gzip=True) + assert service.image() + + @v2_1_only() + def test_build_with_isolation(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write('FROM busybox\n') + + service = self.create_service('build_isolation', build={ + 'context': text_type(base_dir), + 'isolation': 'default', + }) + service.build() + assert service.image() + def test_start_container_stays_unprivileged(self): service = self.create_service('web') container = create_and_start_container(service).inspect() diff --git a/tests/unit/cli/main_test.py b/tests/unit/cli/main_test.py index b46a3ee2..1a2dfbcf 100644 --- a/tests/unit/cli/main_test.py +++ b/tests/unit/cli/main_test.py @@ -154,3 +154,11 @@ class TestCallDocker(object): assert fake_call.call_args[0][0] == [ 'docker', '--host', 'tcp://mydocker.net:2333', 'ps' ] + + def test_with_host_option_shorthand_equal(self): + with mock.patch('subprocess.call') as fake_call: + call_docker(['ps'], {'--host': '=tcp://mydocker.net:2333'}) + + assert fake_call.call_args[0][0] == [ + 'docker', '--host', 'tcp://mydocker.net:2333', 'ps' + ] diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 47eaabf9..7c8a1423 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -30,12 +30,12 @@ class CLITestCase(unittest.TestCase): test_dir = py._path.local.LocalPath('tests/fixtures/simple-composefile') with test_dir.as_cwd(): project_name = get_project_name('.') - assert 'simplecomposefile' == project_name + assert 'simple-composefile' == project_name def test_project_name_with_explicit_base_dir(self): base_dir = 'tests/fixtures/simple-composefile' project_name = get_project_name(base_dir) - assert 'simplecomposefile' == project_name + assert 'simple-composefile' == project_name def test_project_name_with_explicit_uppercase_base_dir(self): base_dir = 'tests/fixtures/UpperCaseDir' @@ -45,7 +45,7 @@ class CLITestCase(unittest.TestCase): def test_project_name_with_explicit_project_name(self): name = 'explicit-project-name' project_name = get_project_name(None, project_name=name) - assert 'explicitprojectname' == project_name + assert 'explicit-project-name' == project_name @mock.patch.dict(os.environ) def test_project_name_from_environment_new_var(self): @@ -59,7 +59,7 @@ class CLITestCase(unittest.TestCase): with mock.patch.dict(os.environ): os.environ['COMPOSE_PROJECT_NAME'] = '' project_name = get_project_name(base_dir) - assert 'simplecomposefile' == project_name + assert 'simple-composefile' == project_name @mock.patch.dict(os.environ) def test_project_name_with_environment_file(self): @@ -80,7 +80,7 @@ class CLITestCase(unittest.TestCase): def test_get_project(self): base_dir = 'tests/fixtures/longer-filename-composefile' project = get_project(base_dir) - assert project.name == 'longerfilenamecomposefile' + assert project.name == 'longer-filename-composefile' assert project.client assert project.services diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 7a9bb944..8a75648a 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals +import codecs import os import shutil import tempfile @@ -1623,6 +1624,21 @@ class ConfigTest(unittest.TestCase): assert 'line 3, column 32' in exc.exconly() + def test_load_yaml_with_bom(self): + tmpdir = py.test.ensuretemp('bom_yaml') + self.addCleanup(tmpdir.remove) + bom_yaml = tmpdir.join('docker-compose.yml') + with codecs.open(str(bom_yaml), 'w', encoding='utf-8') as f: + f.write('''\ufeff + version: '2.3' + volumes: + park_bom: + ''') + assert config.load_yaml(str(bom_yaml)) == { + 'version': '2.3', + 'volumes': {'park_bom': None} + } + def test_validate_extra_hosts_invalid(self): with pytest.raises(ConfigurationError) as exc: config.load(build_config_details({ @@ -4927,6 +4943,18 @@ class SerializeTest(unittest.TestCase): serialized_config = yaml.load(serialize_config(config_dict)) assert '8080:80/tcp' in serialized_config['services']['web']['ports'] + def test_serialize_ports_with_ext_ip(self): + config_dict = config.Config(version=V3_5, services=[ + { + 'ports': [types.ServicePort('80', '8080', None, None, '127.0.0.1')], + 'image': 'alpine', + 'name': 'web' + } + ], volumes={}, networks={}, secrets={}, configs={}) + + serialized_config = yaml.load(serialize_config(config_dict)) + assert '127.0.0.1:8080:80/tcp' in serialized_config['services']['web']['ports'] + def test_serialize_configs(self): service_dict = { 'image': 'example/web', diff --git a/tests/unit/config/interpolation_test.py b/tests/unit/config/interpolation_test.py index 2ba698fb..0d0e7d28 100644 --- a/tests/unit/config/interpolation_test.py +++ b/tests/unit/config/interpolation_test.py @@ -420,3 +420,15 @@ def test_interpolate_unicode_values(): interpol("$FOO") == '十六夜 咲夜' interpol("${BAR}") == '十六夜 咲夜' + + +def test_interpolate_no_fallthrough(): + # Test regression on docker/compose#5829 + variable_mapping = { + 'TEST:-': 'hello', + 'TEST-': 'hello', + } + interpol = Interpolator(TemplateWithDefaults, variable_mapping).interpolate + + assert interpol('${TEST:-}') == '' + assert interpol('${TEST-}') == '' diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index b4994a99..83a01475 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import datetime import docker +import pytest from docker.errors import NotFound from .. import mock @@ -13,10 +14,13 @@ from compose.config.config import Config from compose.config.types import VolumeFromSpec from compose.const import COMPOSEFILE_V1 as V1 from compose.const import COMPOSEFILE_V2_0 as V2_0 +from compose.const import COMPOSEFILE_V2_4 as V2_4 from compose.const import LABEL_SERVICE from compose.container import Container +from compose.errors import OperationFailedError from compose.project import NoSuchService from compose.project import Project +from compose.project import ProjectError from compose.service import ImageType from compose.service import Service @@ -561,3 +565,55 @@ class ProjectTest(unittest.TestCase): def test_no_such_service_unicode(self): assert NoSuchService('十六夜 咲夜'.encode('utf-8')).msg == 'No such service: 十六夜 咲夜' assert NoSuchService('十六夜 咲夜').msg == 'No such service: 十六夜 咲夜' + + def test_project_platform_value(self): + service_config = { + 'name': 'web', + 'image': 'busybox:latest', + } + config_data = Config( + version=V2_4, services=[service_config], networks={}, volumes={}, secrets=None, configs=None + ) + + project = Project.from_config(name='test', client=self.mock_client, config_data=config_data) + assert project.get_service('web').options.get('platform') is None + + project = Project.from_config( + name='test', client=self.mock_client, config_data=config_data, default_platform='windows' + ) + assert project.get_service('web').options.get('platform') == 'windows' + + service_config['platform'] = 'linux/s390x' + project = Project.from_config(name='test', client=self.mock_client, config_data=config_data) + assert project.get_service('web').options.get('platform') == 'linux/s390x' + + project = Project.from_config( + name='test', client=self.mock_client, config_data=config_data, default_platform='windows' + ) + assert project.get_service('web').options.get('platform') == 'linux/s390x' + + @mock.patch('compose.parallel.ParallelStreamWriter._write_noansi') + def test_error_parallel_pull(self, mock_write): + project = Project.from_config( + name='test', + client=self.mock_client, + config_data=Config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + }], + networks=None, + volumes=None, + secrets=None, + configs=None, + ), + ) + + self.mock_client.pull.side_effect = OperationFailedError('pull error') + with pytest.raises(ProjectError): + project.pull(parallel_pull=True) + + self.mock_client.pull.side_effect = OperationFailedError(b'pull error') + with pytest.raises(ProjectError): + project.pull(parallel_pull=True) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 9128b955..4ccc4865 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -5,6 +5,7 @@ import docker import pytest from docker.constants import DEFAULT_DOCKER_API_VERSION from docker.errors import APIError +from docker.errors import NotFound from .. import mock from .. import unittest @@ -20,6 +21,7 @@ from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE from compose.const import SECRETS_PATH from compose.container import Container +from compose.errors import OperationFailedError from compose.parallel import ParallelStreamWriter from compose.project import OneOffFilter from compose.service import build_ulimits @@ -399,7 +401,8 @@ class ServiceTest(unittest.TestCase): self.mock_client.pull.assert_called_once_with( 'someimage', tag='sometag', - stream=True) + stream=True, + platform=None) mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...') def test_pull_image_no_tag(self): @@ -408,7 +411,8 @@ class ServiceTest(unittest.TestCase): self.mock_client.pull.assert_called_once_with( 'ababab', tag='latest', - stream=True) + stream=True, + platform=None) @mock.patch('compose.service.log', autospec=True) def test_pull_image_digest(self, mock_log): @@ -417,9 +421,30 @@ class ServiceTest(unittest.TestCase): self.mock_client.pull.assert_called_once_with( 'someimage', tag='sha256:1234', - stream=True) + stream=True, + platform=None) mock_log.info.assert_called_once_with('Pulling foo (someimage@sha256:1234)...') + @mock.patch('compose.service.log', autospec=True) + def test_pull_image_with_platform(self, mock_log): + self.mock_client.api_version = '1.35' + service = Service( + 'foo', client=self.mock_client, image='someimage:sometag', platform='windows/x86_64' + ) + service.pull() + assert self.mock_client.pull.call_count == 1 + call_args = self.mock_client.pull.call_args + assert call_args[1]['platform'] == 'windows/x86_64' + + @mock.patch('compose.service.log', autospec=True) + def test_pull_image_with_platform_unsupported_api(self, mock_log): + self.mock_client.api_version = '1.33' + service = Service( + 'foo', client=self.mock_client, image='someimage:sometag', platform='linux/arm' + ) + with pytest.raises(OperationFailedError): + service.pull() + @mock.patch('compose.service.Container', autospec=True) def test_recreate_container(self, _): mock_container = mock.create_autospec(Container) @@ -471,23 +496,8 @@ class ServiceTest(unittest.TestCase): _, args, _ = mock_log.warn.mock_calls[0] assert 'was built because it did not already exist' in args[0] - self.mock_client.build.assert_called_once_with( - tag='default_foo', - dockerfile=None, - path='.', - pull=False, - forcerm=False, - nocache=False, - rm=True, - buildargs={}, - labels=None, - cache_from=None, - network_mode=None, - target=None, - shmsize=None, - extra_hosts=None, - container_limits={'memory': None}, - ) + assert self.mock_client.build.call_count == 1 + self.mock_client.build.call_args[1]['tag'] == 'default_foo' def test_ensure_image_exists_no_build(self): service = Service('foo', client=self.mock_client, build={'context': '.'}) @@ -513,23 +523,8 @@ class ServiceTest(unittest.TestCase): service.ensure_image_exists(do_build=BuildAction.force) assert not mock_log.warn.called - self.mock_client.build.assert_called_once_with( - tag='default_foo', - dockerfile=None, - path='.', - pull=False, - forcerm=False, - nocache=False, - rm=True, - buildargs={}, - labels=None, - cache_from=None, - network_mode=None, - target=None, - shmsize=None, - extra_hosts=None, - container_limits={'memory': None}, - ) + assert self.mock_client.build.call_count == 1 + self.mock_client.build.call_args[1]['tag'] == 'default_foo' def test_build_does_not_pull(self): self.mock_client.build.return_value = [ @@ -542,6 +537,19 @@ class ServiceTest(unittest.TestCase): assert self.mock_client.build.call_count == 1 assert not self.mock_client.build.call_args[1]['pull'] + def test_build_does_with_platform(self): + self.mock_client.api_version = '1.35' + self.mock_client.build.return_value = [ + b'{"stream": "Successfully built 12345"}', + ] + + service = Service('foo', client=self.mock_client, build={'context': '.'}, platform='linux') + service.build() + + assert self.mock_client.build.call_count == 1 + call_args = self.mock_client.build.call_args + assert call_args[1]['platform'] == 'linux' + def test_build_with_override_build_args(self): self.mock_client.build.return_value = [ b'{"stream": "Successfully built 12345"}', @@ -559,6 +567,33 @@ class ServiceTest(unittest.TestCase): assert called_build_args['arg1'] == build_args['arg1'] assert called_build_args['arg2'] == 'arg2' + def test_build_with_isolation_from_service_config(self): + self.mock_client.build.return_value = [ + b'{"stream": "Successfully built 12345"}', + ] + + service = Service('foo', client=self.mock_client, build={'context': '.'}, isolation='hyperv') + service.build() + + assert self.mock_client.build.call_count == 1 + called_build_args = self.mock_client.build.call_args[1] + assert called_build_args['isolation'] == 'hyperv' + + def test_build_isolation_from_build_override_service_config(self): + self.mock_client.build.return_value = [ + b'{"stream": "Successfully built 12345"}', + ] + + service = Service( + 'foo', client=self.mock_client, build={'context': '.', 'isolation': 'default'}, + isolation='hyperv' + ) + service.build() + + assert self.mock_client.build.call_count == 1 + called_build_args = self.mock_client.build.call_args[1] + assert called_build_args['isolation'] == 'default' + def test_config_dict(self): self.mock_client.inspect_image.return_value = {'Id': 'abcd'} service = Service( @@ -888,6 +923,38 @@ class ServiceTest(unittest.TestCase): 'ftp_proxy': override_options['environment']['FTP_PROXY'], })) + def test_create_when_removed_containers_are_listed(self): + # This is aimed at simulating a race between the API call to list the + # containers, and the ones to inspect each of the listed containers. + # It can happen that a container has been removed after we listed it. + + # containers() returns a container that is about to be removed + self.mock_client.containers.return_value = [ + {'Id': 'rm_cont_id', 'Name': 'rm_cont', 'Image': 'img_id'}, + ] + + # inspect_container() will raise a NotFound when trying to inspect + # rm_cont_id, which at this point has been removed + def inspect(name): + if name == 'rm_cont_id': + raise NotFound(message='Not Found') + + if name == 'new_cont_id': + return {'Id': 'new_cont_id'} + + raise NotImplementedError("incomplete mock") + + self.mock_client.inspect_container.side_effect = inspect + + self.mock_client.inspect_image.return_value = {'Id': 'imageid'} + + self.mock_client.create_container.return_value = {'Id': 'new_cont_id'} + + # We should nonetheless be able to create a new container + service = Service('foo', client=self.mock_client) + + assert service.create_container().id == 'new_cont_id' + class TestServiceNetwork(unittest.TestCase): def setUp(self): |