|
|
@@ -11,6 +11,7 @@ import subprocess
|
|
|
import time
|
|
|
from collections import Counter
|
|
|
from collections import namedtuple
|
|
|
+from functools import reduce
|
|
|
from operator import attrgetter
|
|
|
|
|
|
import pytest
|
|
|
@@ -19,6 +20,7 @@ import yaml
|
|
|
from docker import errors
|
|
|
|
|
|
from .. import mock
|
|
|
+from ..helpers import BUSYBOX_IMAGE_WITH_TAG
|
|
|
from ..helpers import create_host_file
|
|
|
from compose.cli.command import get_project
|
|
|
from compose.config.errors import DuplicateOverrideFileFound
|
|
|
@@ -62,6 +64,12 @@ def wait_on_process(proc, returncode=0):
|
|
|
return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
|
|
|
|
|
|
|
|
|
+def dispatch(base_dir, options, project_options=None, returncode=0):
|
|
|
+ project_options = project_options or []
|
|
|
+ proc = start_process(base_dir, project_options + options)
|
|
|
+ return wait_on_process(proc, returncode=returncode)
|
|
|
+
|
|
|
+
|
|
|
def wait_on_condition(condition, delay=0.1, timeout=40):
|
|
|
start_time = time.time()
|
|
|
while not condition():
|
|
|
@@ -149,9 +157,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
return self._project
|
|
|
|
|
|
def dispatch(self, options, project_options=None, returncode=0):
|
|
|
- project_options = project_options or []
|
|
|
- proc = start_process(self.base_dir, project_options + options)
|
|
|
- return wait_on_process(proc, returncode=returncode)
|
|
|
+ return dispatch(self.base_dir, options, project_options, returncode)
|
|
|
|
|
|
def execute(self, container, cmd):
|
|
|
# Remove once Hijack and CloseNotifier sign a peace treaty
|
|
|
@@ -170,6 +176,13 @@ class CLITestCase(DockerClientTestCase):
|
|
|
# Prevent tearDown from trying to create a project
|
|
|
self.base_dir = None
|
|
|
|
|
|
+ def test_quiet_build(self):
|
|
|
+ self.base_dir = 'tests/fixtures/build-args'
|
|
|
+ result = self.dispatch(['build'], None)
|
|
|
+ quietResult = self.dispatch(['build', '-q'], None)
|
|
|
+ assert result.stdout != ""
|
|
|
+ assert quietResult.stdout == ""
|
|
|
+
|
|
|
def test_help_nonexistent(self):
|
|
|
self.base_dir = 'tests/fixtures/no-composefile'
|
|
|
result = self.dispatch(['help', 'foobar'], returncode=1)
|
|
|
@@ -258,7 +271,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
'volumes_from': ['service:other:rw'],
|
|
|
},
|
|
|
'other': {
|
|
|
- 'image': 'busybox:latest',
|
|
|
+ 'image': BUSYBOX_IMAGE_WITH_TAG,
|
|
|
'command': 'top',
|
|
|
'volumes': ['/data'],
|
|
|
},
|
|
|
@@ -324,6 +337,21 @@ class CLITestCase(DockerClientTestCase):
|
|
|
'version': '2.4'
|
|
|
}
|
|
|
|
|
|
+ def test_config_with_env_file(self):
|
|
|
+ self.base_dir = 'tests/fixtures/default-env-file'
|
|
|
+ result = self.dispatch(['--env-file', '.env2', 'config'])
|
|
|
+ json_result = yaml.load(result.stdout)
|
|
|
+ assert json_result == {
|
|
|
+ 'services': {
|
|
|
+ 'web': {
|
|
|
+ 'command': 'false',
|
|
|
+ 'image': 'alpine:latest',
|
|
|
+ 'ports': ['5644/tcp', '9998/tcp']
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 'version': '2.4'
|
|
|
+ }
|
|
|
+
|
|
|
def test_config_with_dot_env_and_override_dir(self):
|
|
|
self.base_dir = 'tests/fixtures/default-env-file'
|
|
|
result = self.dispatch(['--project-directory', 'alt/', 'config'])
|
|
|
@@ -616,7 +644,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
def test_pull_with_digest(self):
|
|
|
result = self.dispatch(['-f', 'digest.yml', 'pull', '--no-parallel'])
|
|
|
|
|
|
- assert 'Pulling simple (busybox:latest)...' in result.stderr
|
|
|
+ assert 'Pulling simple ({})...'.format(BUSYBOX_IMAGE_WITH_TAG) in result.stderr
|
|
|
assert ('Pulling digest (busybox@'
|
|
|
'sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b520'
|
|
|
'04ee8502d)...') in result.stderr
|
|
|
@@ -627,12 +655,19 @@ class CLITestCase(DockerClientTestCase):
|
|
|
'pull', '--ignore-pull-failures', '--no-parallel']
|
|
|
)
|
|
|
|
|
|
- assert 'Pulling simple (busybox:latest)...' in result.stderr
|
|
|
+ assert 'Pulling simple ({})...'.format(BUSYBOX_IMAGE_WITH_TAG) in result.stderr
|
|
|
assert 'Pulling another (nonexisting-image:latest)...' in result.stderr
|
|
|
assert ('repository nonexisting-image not found' in result.stderr or
|
|
|
'image library/nonexisting-image:latest not found' in result.stderr or
|
|
|
'pull access denied for nonexisting-image' in result.stderr)
|
|
|
|
|
|
+ def test_pull_with_build(self):
|
|
|
+ result = self.dispatch(['-f', 'pull-with-build.yml', 'pull'])
|
|
|
+
|
|
|
+ assert 'Pulling simple' not in result.stderr
|
|
|
+ assert 'Pulling from_simple' not in result.stderr
|
|
|
+ assert 'Pulling another ...' in result.stderr
|
|
|
+
|
|
|
def test_pull_with_quiet(self):
|
|
|
assert self.dispatch(['pull', '--quiet']).stderr == ''
|
|
|
assert self.dispatch(['pull', '--quiet']).stdout == ''
|
|
|
@@ -747,6 +782,27 @@ class CLITestCase(DockerClientTestCase):
|
|
|
]
|
|
|
assert not containers
|
|
|
|
|
|
+ @pytest.mark.xfail(True, reason='Flaky on local')
|
|
|
+ def test_build_rm(self):
|
|
|
+ containers = [
|
|
|
+ Container.from_ps(self.project.client, c)
|
|
|
+ for c in self.project.client.containers(all=True)
|
|
|
+ ]
|
|
|
+
|
|
|
+ assert not containers
|
|
|
+
|
|
|
+ self.base_dir = 'tests/fixtures/simple-dockerfile'
|
|
|
+ self.dispatch(['build', '--no-rm', 'simple'], returncode=0)
|
|
|
+
|
|
|
+ containers = [
|
|
|
+ Container.from_ps(self.project.client, c)
|
|
|
+ for c in self.project.client.containers(all=True)
|
|
|
+ ]
|
|
|
+ assert containers
|
|
|
+
|
|
|
+ for c in self.project.client.containers(all=True):
|
|
|
+ self.addCleanup(self.project.client.remove_container, c, force=True)
|
|
|
+
|
|
|
def test_build_shm_size_build_option(self):
|
|
|
pull_busybox(self.client)
|
|
|
self.base_dir = 'tests/fixtures/build-shm-size'
|
|
|
@@ -1108,6 +1164,22 @@ class CLITestCase(DockerClientTestCase):
|
|
|
]
|
|
|
assert len(remote_volumes) > 0
|
|
|
|
|
|
+ @v2_only()
|
|
|
+ def test_up_no_start_remove_orphans(self):
|
|
|
+ self.base_dir = 'tests/fixtures/v2-simple'
|
|
|
+ self.dispatch(['up', '--no-start'], None)
|
|
|
+
|
|
|
+ services = self.project.get_services()
|
|
|
+
|
|
|
+ stopped = reduce((lambda prev, next: prev.containers(
|
|
|
+ stopped=True) + next.containers(stopped=True)), services)
|
|
|
+ assert len(stopped) == 2
|
|
|
+
|
|
|
+ self.dispatch(['-f', 'one-container.yml', 'up', '--no-start', '--remove-orphans'], None)
|
|
|
+ stopped2 = reduce((lambda prev, next: prev.containers(
|
|
|
+ stopped=True) + next.containers(stopped=True)), services)
|
|
|
+ assert len(stopped2) == 1
|
|
|
+
|
|
|
@v2_only()
|
|
|
def test_up_no_ansi(self):
|
|
|
self.base_dir = 'tests/fixtures/v2-simple'
|
|
|
@@ -1380,7 +1452,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
if v['Name'].split('/')[-1].startswith('{}_'.format(self.project.name))
|
|
|
]
|
|
|
|
|
|
- assert set([v['Name'].split('/')[-1] for v in volumes]) == set([volume_with_label])
|
|
|
+ assert set([v['Name'].split('/')[-1] for v in volumes]) == {volume_with_label}
|
|
|
assert 'label_key' in volumes[0]['Labels']
|
|
|
assert volumes[0]['Labels']['label_key'] == 'label_val'
|
|
|
|
|
|
@@ -2045,7 +2117,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
for _, config in networks.items():
|
|
|
# TODO: once we drop support for API <1.24, this can be changed to:
|
|
|
# assert config['Aliases'] == [container.short_id]
|
|
|
- aliases = set(config['Aliases'] or []) - set([container.short_id])
|
|
|
+ aliases = set(config['Aliases'] or []) - {container.short_id}
|
|
|
assert not aliases
|
|
|
|
|
|
@v2_only()
|
|
|
@@ -2065,7 +2137,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
for _, config in networks.items():
|
|
|
# TODO: once we drop support for API <1.24, this can be changed to:
|
|
|
# assert config['Aliases'] == [container.short_id]
|
|
|
- aliases = set(config['Aliases'] or []) - set([container.short_id])
|
|
|
+ aliases = set(config['Aliases'] or []) - {container.short_id}
|
|
|
assert not aliases
|
|
|
|
|
|
assert self.lookup(container, 'app')
|
|
|
@@ -2301,6 +2373,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
assert 'another' in result.stdout
|
|
|
assert 'exited with code 0' in result.stdout
|
|
|
|
|
|
+ @pytest.mark.skip(reason="race condition between up and logs")
|
|
|
def test_logs_follow_logs_from_new_containers(self):
|
|
|
self.base_dir = 'tests/fixtures/logs-composefile'
|
|
|
self.dispatch(['up', '-d', 'simple'])
|
|
|
@@ -2327,6 +2400,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
assert '{} exited with code 0'.format(another_name) in result.stdout
|
|
|
assert '{} exited with code 137'.format(simple_name) in result.stdout
|
|
|
|
|
|
+ @pytest.mark.skip(reason="race condition between up and logs")
|
|
|
def test_logs_follow_logs_from_restarted_containers(self):
|
|
|
self.base_dir = 'tests/fixtures/logs-restart-composefile'
|
|
|
proc = start_process(self.base_dir, ['up'])
|
|
|
@@ -2347,6 +2421,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
) == 3
|
|
|
assert result.stdout.count('world') == 3
|
|
|
|
|
|
+ @pytest.mark.skip(reason="race condition between up and logs")
|
|
|
def test_logs_default(self):
|
|
|
self.base_dir = 'tests/fixtures/logs-composefile'
|
|
|
self.dispatch(['up', '-d'])
|
|
|
@@ -2473,10 +2548,12 @@ class CLITestCase(DockerClientTestCase):
|
|
|
self.dispatch(['up', '-d'])
|
|
|
assert len(project.get_service('web').containers()) == 2
|
|
|
assert len(project.get_service('db').containers()) == 1
|
|
|
+ assert len(project.get_service('worker').containers()) == 0
|
|
|
|
|
|
- self.dispatch(['up', '-d', '--scale', 'web=3'])
|
|
|
+ self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'worker=1'])
|
|
|
assert len(project.get_service('web').containers()) == 3
|
|
|
assert len(project.get_service('db').containers()) == 1
|
|
|
+ assert len(project.get_service('worker').containers()) == 1
|
|
|
|
|
|
def test_up_scale_scale_down(self):
|
|
|
self.base_dir = 'tests/fixtures/scale'
|
|
|
@@ -2485,22 +2562,26 @@ class CLITestCase(DockerClientTestCase):
|
|
|
self.dispatch(['up', '-d'])
|
|
|
assert len(project.get_service('web').containers()) == 2
|
|
|
assert len(project.get_service('db').containers()) == 1
|
|
|
+ assert len(project.get_service('worker').containers()) == 0
|
|
|
|
|
|
self.dispatch(['up', '-d', '--scale', 'web=1'])
|
|
|
assert len(project.get_service('web').containers()) == 1
|
|
|
assert len(project.get_service('db').containers()) == 1
|
|
|
+ assert len(project.get_service('worker').containers()) == 0
|
|
|
|
|
|
def test_up_scale_reset(self):
|
|
|
self.base_dir = 'tests/fixtures/scale'
|
|
|
project = self.project
|
|
|
|
|
|
- self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'db=3'])
|
|
|
+ self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'db=3', '--scale', 'worker=3'])
|
|
|
assert len(project.get_service('web').containers()) == 3
|
|
|
assert len(project.get_service('db').containers()) == 3
|
|
|
+ assert len(project.get_service('worker').containers()) == 3
|
|
|
|
|
|
self.dispatch(['up', '-d'])
|
|
|
assert len(project.get_service('web').containers()) == 2
|
|
|
assert len(project.get_service('db').containers()) == 1
|
|
|
+ assert len(project.get_service('worker').containers()) == 0
|
|
|
|
|
|
def test_up_scale_to_zero(self):
|
|
|
self.base_dir = 'tests/fixtures/scale'
|
|
|
@@ -2509,10 +2590,12 @@ class CLITestCase(DockerClientTestCase):
|
|
|
self.dispatch(['up', '-d'])
|
|
|
assert len(project.get_service('web').containers()) == 2
|
|
|
assert len(project.get_service('db').containers()) == 1
|
|
|
+ assert len(project.get_service('worker').containers()) == 0
|
|
|
|
|
|
- self.dispatch(['up', '-d', '--scale', 'web=0', '--scale', 'db=0'])
|
|
|
+ self.dispatch(['up', '-d', '--scale', 'web=0', '--scale', 'db=0', '--scale', 'worker=0'])
|
|
|
assert len(project.get_service('web').containers()) == 0
|
|
|
assert len(project.get_service('db').containers()) == 0
|
|
|
+ assert len(project.get_service('worker').containers()) == 0
|
|
|
|
|
|
def test_port(self):
|
|
|
self.base_dir = 'tests/fixtures/ports-composefile'
|
|
|
@@ -2664,7 +2747,7 @@ class CLITestCase(DockerClientTestCase):
|
|
|
self.base_dir = 'tests/fixtures/extends'
|
|
|
self.dispatch(['up', '-d'], None)
|
|
|
|
|
|
- assert set([s.name for s in self.project.services]) == set(['mydb', 'myweb'])
|
|
|
+ assert set([s.name for s in self.project.services]) == {'mydb', 'myweb'}
|
|
|
|
|
|
# Sort by name so we get [db, web]
|
|
|
containers = sorted(
|
|
|
@@ -2676,15 +2759,9 @@ class CLITestCase(DockerClientTestCase):
|
|
|
web = containers[1]
|
|
|
db_name = containers[0].name_without_project
|
|
|
|
|
|
- assert set(get_links(web)) == set(
|
|
|
- ['db', db_name, 'extends_{}'.format(db_name)]
|
|
|
- )
|
|
|
+ assert set(get_links(web)) == {'db', db_name, 'extends_{}'.format(db_name)}
|
|
|
|
|
|
- expected_env = set([
|
|
|
- "FOO=1",
|
|
|
- "BAR=2",
|
|
|
- "BAZ=2",
|
|
|
- ])
|
|
|
+ expected_env = {"FOO=1", "BAR=2", "BAZ=2"}
|
|
|
assert expected_env <= set(web.get('Config.Env'))
|
|
|
|
|
|
def test_top_services_not_running(self):
|