summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/acceptance/cli_test.py99
-rw-r--r--tests/integration/network_test.py20
-rw-r--r--tests/integration/project_test.py47
-rw-r--r--tests/integration/service_test.py43
-rw-r--r--tests/unit/cli/main_test.py8
-rw-r--r--tests/unit/cli_test.py10
-rw-r--r--tests/unit/config/config_test.py28
-rw-r--r--tests/unit/config/interpolation_test.py12
-rw-r--r--tests/unit/project_test.py56
-rw-r--r--tests/unit/service_test.py141
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):