|  | @@ -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):
 |