コード例 #1
0
class TestGetPlatformConfigPathFromArgs(unittest.TestCase):
    @given(filepath(), filepath())
    def test_get_config_path_from_args(self, path_a, path_b):
        with patch('cdflow.abspath') as abspath:
            prefix = '/a/path'
            abspath.side_effect = lambda path: '{}/{}'.format(prefix, path)
            args = [
                'release', '--platform-config', path_a,
                '--platform-config={}'.format(path_b), '42'
            ]

            assert get_platform_config_paths(args) == [
                '{}/{}'.format(prefix, path_a), '{}/{}'.format(prefix, path_b)
            ]

    def test_raises_exception_when_missing_flag(self):
        self.assertRaises(MissingPlatformConfigError,
                          get_platform_config_paths, [])

    def test_raises_exception_when_missing_value(self):
        self.assertRaises(
            MissingPlatformConfigError,
            get_platform_config_paths,
            ['--platform-config'],
        )
コード例 #2
0
class TestIntegration(unittest.TestCase):
    @given(filepath())
    def test_release(self, project_root):
        argv = [
            'release', '--platform-config', '../path/to/config',
            '--release-data ami_id=ami-z9876', '42'
        ]
        with patch('cdflow.docker') as docker, \
                patch('cdflow.os') as os, \
                patch('cdflow.abspath') as abspath, \
                patch('cdflow.open') as open_:
            abs_path_to_config = '/root/path/to/config'
            abspath.return_value = abs_path_to_config

            image = MagicMock(spec=Image)
            docker.from_env.return_value.images.pull.return_value = image
            image.attrs = {'RepoDigests': ['hash']}

            docker.from_env.return_value.containers.run.return_value.attrs = {
                'State': {
                    'ExitCode': 0,
                }
            }

            os.getcwd.return_value = project_root
            os.getenv.return_value = False

            config_file = MagicMock(spec=TextIOWrapper)
            config_file.read.return_value = yaml.dump({'team': 'a-team'})
            open_.return_value.__enter__.return_value = config_file

            exit_status = main(argv)

        assert exit_status == 0

        docker.from_env.assert_called_once()
        docker.from_env.return_value.images.pull.assert_called_once_with(
            'mergermarket/cdflow-commands:latest')
        docker.from_env.return_value.containers.run.assert_called_once_with(
            'mergermarket/cdflow-commands:latest',
            command=argv,
            environment={
                'AWS_ACCESS_KEY_ID': ANY,
                'AWS_SECRET_ACCESS_KEY': ANY,
                'AWS_SESSION_TOKEN': ANY,
                'FASTLY_API_KEY': ANY,
                'GITHUB_TOKEN': ANY,
                'CDFLOW_IMAGE_DIGEST': 'hash',
                'LOGENTRIES_ACCOUNT_KEY': ANY,
                'DATADOG_APP_KEY': ANY,
                'DATADOG_API_KEY': ANY,
            },
            detach=True,
            volumes={
                project_root: {
                    'bind': project_root,
                    'mode': 'rw',
                },
                abs_path_to_config: {
                    'bind': abs_path_to_config,
                    'mode': 'ro',
                },
                '/var/run/docker.sock': {
                    'bind': '/var/run/docker.sock',
                    'mode': 'ro',
                },
            },
            working_dir=project_root)

        docker.from_env.return_value.containers.run.return_value.logs.\
            assert_called_once_with(
                stream=True,
                follow=True,
                stdout=True,
                stderr=True,
            )

    @given(
        fixed_dictionaries({
            'project_root':
            filepath(),
            'environment':
            dictionaries(
                keys=text(alphabet=printable, min_size=1, max_size=3),
                values=text(alphabet=printable, min_size=1, max_size=3),
                max_size=3,
            ),
            'image_id':
            image_id(),
        }))
    def test_release_with_pinned_command_image(self, fixtures):
        argv = ['release', '42', '--platform-config', 'path/to/config']
        project_root = fixtures['project_root']
        environment = fixtures['environment']
        pinned_image_id = fixtures['image_id']
        environment['CDFLOW_IMAGE_ID'] = pinned_image_id

        with patch('cdflow.docker') as docker, \
                patch('cdflow.os') as os, \
                patch('cdflow.abspath') as abspath, \
                patch('cdflow.open') as open_:
            abs_path_to_config = '/root/path/to/config'
            abspath.return_value = abs_path_to_config

            image = MagicMock(spec=Image)
            docker.from_env.return_value.images.pull.return_value = image
            image.attrs = {'RepoDigests': ['hash']}

            docker.from_env.return_value.containers.run.return_value.attrs = {
                'State': {
                    'ExitCode': 0,
                }
            }

            os.getcwd.return_value = project_root
            os.getenv.return_value = False

            os.environ = environment

            config_file = MagicMock(spec=TextIOWrapper)
            config_file.read.return_value = yaml.dump({'team': 'a-team'})
            open_.return_value.__enter__.return_value = config_file

            exit_status = main(argv)

        assert exit_status == 0

        docker.from_env.return_value.images.pull.assert_called_once_with(
            pinned_image_id)
        docker.from_env.return_value.containers.run.assert_called_once_with(
            pinned_image_id,
            command=argv,
            environment={
                'AWS_ACCESS_KEY_ID': ANY,
                'AWS_SECRET_ACCESS_KEY': ANY,
                'AWS_SESSION_TOKEN': ANY,
                'FASTLY_API_KEY': ANY,
                'GITHUB_TOKEN': ANY,
                'CDFLOW_IMAGE_DIGEST': 'hash',
                'LOGENTRIES_ACCOUNT_KEY': ANY,
                'DATADOG_APP_KEY': ANY,
                'DATADOG_API_KEY': ANY,
            },
            detach=True,
            volumes={
                project_root: {
                    'bind': project_root,
                    'mode': 'rw',
                },
                abs_path_to_config: {
                    'bind': abs_path_to_config,
                    'mode': 'ro',
                },
                '/var/run/docker.sock': {
                    'bind': '/var/run/docker.sock',
                    'mode': 'ro',
                },
            },
            working_dir=project_root)

    @given(
        fixed_dictionaries({
            'project_root':
            filepath(),
            's3_bucket_and_key':
            s3_bucket_and_key(),
            'release_bucket':
            text(
                alphabet=VALID_ALPHABET,
                min_size=3,
                max_size=5,
            ),
        }))
    def test_classic_deploy(self, fixtures):
        argv = ['deploy', 'aslive', '42']

        with patch('cdflow.Session') as Session, \
                patch('cdflow.BytesIO') as BytesIO, \
                patch('cdflow.docker') as docker, \
                patch('cdflow.os') as os, \
                patch('cdflow.open') as open_:

            s3_resource = Mock()

            image_digest = 'sha:12345asdfg'
            s3_resource.Object.return_value.metadata = {
                'cdflow_image_digest': image_digest
            }

            Session.return_value.resource.return_value = s3_resource

            BytesIO.return_value.__enter__.return_value.read.return_value = '''
                {{
                    "release-bucket": "{}",
                    "classic-metadata-handling": true
                }}
            '''.format(fixtures['release_bucket'])

            config_file = MagicMock(spec=TextIOWrapper)
            config_file.read.return_value = yaml.dump({
                'account-scheme-url':
                's3://{}/{}'.format(*fixtures['s3_bucket_and_key']),
                'team':
                'a-team',
            })
            open_.return_value.__enter__.return_value = config_file

            docker_client = MagicMock(spec=DockerClient)
            docker.from_env.return_value = docker_client
            docker.from_env.return_value.containers.run.return_value.attrs = {
                'State': {
                    'ExitCode': 0,
                }
            }

            project_root = fixtures['project_root']
            os.getcwd.return_value = project_root

            exit_status = main(argv)

            assert exit_status == 0

            s3_resource.Object.assert_any_call(
                fixtures['s3_bucket_and_key'][0],
                fixtures['s3_bucket_and_key'][1],
            )

            docker_client.containers.run.assert_called_once_with(
                image_digest,
                command=argv,
                environment=ANY,
                detach=True,
                volumes={
                    project_root: ANY,
                    '/var/run/docker.sock': ANY
                },
                working_dir=project_root,
            )

            docker.from_env.return_value.containers.run.return_value.logs.\
                assert_called_once_with(
                    stream=True,
                    follow=True,
                    stdout=True,
                    stderr=True,
                )

    @given(
        fixed_dictionaries({
            'project_root':
            filepath(),
            's3_bucket_and_key':
            s3_bucket_and_key(),
            'release_bucket':
            text(
                alphabet=VALID_ALPHABET,
                min_size=3,
                max_size=5,
            ),
            'team_name':
            text(
                alphabet=VALID_ALPHABET,
                min_size=3,
                max_size=5,
            ),
            'component_name':
            text(
                alphabet=VALID_ALPHABET,
                min_size=3,
                max_size=5,
            ),
        }))
    def test_deploy(self, fixtures):
        version = '42'
        component_name = fixtures['component_name']
        argv = ['deploy', 'aslive', version, '--component', component_name]

        with patch('cdflow.Session') as Session, \
                patch('cdflow.BytesIO') as BytesIO, \
                patch('cdflow.docker') as docker, \
                patch('cdflow.os') as os, \
                patch('cdflow.open') as open_:

            s3_resource = Mock()

            image_digest = 'sha:12345asdfg'
            s3_resource.Object.return_value.metadata = {
                'cdflow_image_digest': image_digest
            }

            Session.return_value.resource.return_value = s3_resource

            BytesIO.return_value.__enter__.return_value.read.return_value = '''
                {{
                    "release-bucket": "{}"
                }}
            '''.format(fixtures['release_bucket'])

            config_file = MagicMock(spec=TextIOWrapper)
            config_file.read.return_value = yaml.dump({
                'account-scheme-url':
                's3://{}/{}'.format(*fixtures['s3_bucket_and_key']),
                'team':
                fixtures['team_name'],
            })
            open_.return_value.__enter__.return_value = config_file

            docker_client = MagicMock(spec=DockerClient)
            docker.from_env.return_value = docker_client
            docker.from_env.return_value.containers.run.return_value.attrs = {
                'State': {
                    'ExitCode': 0,
                }
            }

            project_root = fixtures['project_root']
            os.getcwd.return_value = project_root

            exit_status = main(argv)

            assert exit_status == 0

            s3_resource.Object.assert_any_call(
                fixtures['s3_bucket_and_key'][0],
                fixtures['s3_bucket_and_key'][1],
            )

            s3_resource.Object.assert_any_call(
                fixtures['release_bucket'],
                '{}/{}/{}-{}.zip'.format(
                    fixtures['team_name'],
                    component_name,
                    component_name,
                    version,
                ),
            )

            docker_client.containers.run.assert_called_once_with(
                image_digest,
                command=argv,
                environment=ANY,
                detach=True,
                volumes={
                    project_root: ANY,
                    '/var/run/docker.sock': ANY
                },
                working_dir=project_root,
            )

            docker.from_env.return_value.containers.run.return_value.logs.\
                assert_called_once_with(
                    stream=True,
                    follow=True,
                    stdout=True,
                    stderr=True,
                )

    @given(lists(elements=text(alphabet=printable, max_size=3), max_size=3))
    def test_invalid_arguments_passed_to_container_to_handle(self, argv):
        with patch('cdflow.docker') as docker, \
                patch('cdflow.os') as os, \
                patch('cdflow.open') as open_:

            account_id = '1234567890'
            config_file = MagicMock(spec=TextIOWrapper)
            config_file.read.return_value = json.dumps(
                {'platform_config': {
                    'account_id': account_id
                }})
            open_.return_value.__enter__.return_value = config_file

            error = ContainerError(container=CDFLOW_IMAGE_ID,
                                   exit_status=1,
                                   command=argv,
                                   image=CDFLOW_IMAGE_ID,
                                   stderr='help text')
            docker.from_env.return_value.containers.run.side_effect = error
            os.path.abspath.return_value = '/'
            exit_status = main(argv)

        assert exit_status == 1

        docker.from_env.return_value.containers.run.assert_called_once_with(
            CDFLOW_IMAGE_ID,
            command=argv,
            environment=ANY,
            detach=True,
            volumes=ANY,
            working_dir=ANY)
コード例 #3
0
class TestGetVersion(unittest.TestCase):
    @given(
        text(alphabet=VALID_ALPHABET,
             min_size=1).filter(lambda v: not v == '-v'))
    def test_get_version_during_deploy(self, version):
        argv = ['deploy', 'test', version]
        found_version = get_version(argv)

        assert found_version == version

    @given(
        text(alphabet=VALID_ALPHABET,
             min_size=1).filter(lambda v: not v == '-v'))
    def test_get_version_during_release(self, version):
        argv = ['release', version]
        found_version = get_version(argv)

        assert found_version == version

    @given(
        fixed_dictionaries({
            'version': (text(alphabet=VALID_ALPHABET,
                             min_size=1).filter(lambda v: not v == '-v')),
            'options':
            lists(
                elements=sampled_from(
                    ('-c foo', '--component bar',
                     '--platform-config path/to/config', '-v', '--verbose',
                     '-p', '--plan-only')),
                unique=True,
            ),
        }))
    def test_get_version_when_options_present(self, fixtures):
        version = fixtures['version']
        extra = fixtures['options'] + [version]

        shuffle(extra)

        extra = chain.from_iterable(e.split(' ') for e in extra)

        argv = ['release'] + list(extra)

        assert version == get_version(argv)

    def test_missing_version_returns_nothing(self):
        argv = ['release']
        found_version = get_version(argv)

        assert found_version is None

    @given(
        fixed_dictionaries({
            'version': (text(alphabet=VALID_ALPHABET,
                             min_size=1).filter(lambda v: not v == '-v')),
            'path':
            filepath(),
        }))
    def test_get_version_when_platform_config_present(self, fixtures):
        version = fixtures['version']
        path = fixtures['path']
        argv = ['release', '--platform-config', path, version]

        found_version = get_version(argv)

        assert found_version == version
コード例 #4
0
ファイル: test_docker.py プロジェクト: mergermarket/cdflow
class TestDockerRun(unittest.TestCase):
    @given(
        fixed_dictionaries({
            'environment_variables':
            fixed_dictionaries({
                'AWS_ACCESS_KEY_ID':
                text(alphabet=printable, min_size=10),
                'AWS_SECRET_ACCESS_KEY':
                text(alphabet=printable, min_size=10),
                'AWS_SESSION_TOKEN':
                text(alphabet=printable, min_size=10),
                'FASTLY_API_KEY':
                text(alphabet=printable, min_size=10),
                'GITHUB_TOKEN':
                text(alphabet=printable, min_size=10),
                'CDFLOW_IMAGE_DIGEST':
                text(min_size=12),
            }),
            'image_id':
            image_id(),
            'project_root':
            filepath(),
            'platform_config_paths':
            lists(elements=filepath(), max_size=1),
            'command':
            lists(text(alphabet=printable)),
        }))
    def test_run_args(self, fixtures):
        docker_client = MagicMock(spec=DockerClient)
        image_id = fixtures['image_id']
        command = fixtures['command']
        project_root = fixtures['project_root']
        platform_config_paths = fixtures['platform_config_paths']
        environment_variables = fixtures['environment_variables']

        container = MagicMock(spec=Container)
        container.attrs = {'State': {'ExitCode': 0}}
        docker_client.containers.create.return_value = container

        exit_status, output = docker_run(
            docker_client,
            image_id,
            command,
            project_root,
            environment_variables,
            platform_config_paths,
        )

        assert exit_status == 0
        assert output == ''

        expected_volumes = {
            project_root: {
                'bind': project_root,
                'mode': 'rw',
            },
            '/var/run/docker.sock': {
                'bind': '/var/run/docker.sock',
                'mode': 'ro',
            },
        }

        for platform_config_path in platform_config_paths:
            expected_volumes[platform_config_path] = {
                'bind': platform_config_path,
                'mode': 'ro',
            }

        docker_client.containers.create.assert_called_once_with(
            image_id,
            command=command,
            environment=environment_variables,
            detach=True,
            volumes=expected_volumes,
            working_dir=project_root,
        )

        container.start.assert_called_once()

    @given(
        fixed_dictionaries({
            'environment_variables':
            fixed_dictionaries({
                'AWS_ACCESS_KEY_ID':
                text(alphabet=printable, min_size=10),
                'AWS_SECRET_ACCESS_KEY':
                text(alphabet=printable, min_size=10),
                'AWS_SESSION_TOKEN':
                text(alphabet=printable, min_size=10),
                'FASTLY_API_KEY':
                text(alphabet=printable, min_size=10),
                'GITHUB_TOKEN':
                text(alphabet=printable, min_size=10),
                'CDFLOW_IMAGE_DIGEST':
                text(min_size=12),
            }),
            'image_id':
            image_id(),
            'project_root':
            filepath(),
            'command':
            lists(text(alphabet=printable)),
        }))
    def test_run_args_without_platform_config(self, fixtures):
        docker_client = MagicMock(spec=DockerClient)
        image_id = fixtures['image_id']
        command = fixtures['command']
        project_root = fixtures['project_root']
        environment_variables = fixtures['environment_variables']

        container = MagicMock(spec=Container)
        container.attrs = {'State': {'ExitCode': 0}}
        docker_client.containers.create.return_value = container

        exit_status, output = docker_run(docker_client, image_id, command,
                                         project_root, environment_variables)

        assert exit_status == 0
        assert output == ''

        docker_client.containers.create.assert_called_once_with(
            image_id,
            command=command,
            environment=environment_variables,
            detach=True,
            volumes={
                project_root: {
                    'bind': project_root,
                    'mode': 'rw',
                },
                '/var/run/docker.sock': {
                    'bind': '/var/run/docker.sock',
                    'mode': 'ro',
                },
            },
            working_dir=project_root,
        )
        container.start.assert_called_once()

    @given(
        fixed_dictionaries({
            'environment_variables':
            fixed_dictionaries({
                'AWS_ACCESS_KEY_ID':
                text(alphabet=printable, min_size=10),
                'AWS_SECRET_ACCESS_KEY':
                text(alphabet=printable, min_size=10),
                'AWS_SESSION_TOKEN':
                text(alphabet=printable, min_size=10),
                'FASTLY_API_KEY':
                text(alphabet=printable, min_size=10),
                'GITHUB_TOKEN':
                text(alphabet=printable, min_size=10),
                'CDFLOW_IMAGE_DIGEST':
                text(min_size=12),
            }),
            'image_id':
            image_id(),
            'project_root':
            filepath(),
            'command':
            lists(text(alphabet=printable)),
        }))
    def test_run_shell_command(self, fixtures):
        docker_client = MagicMock(spec=docker.from_env())
        image_id = fixtures['image_id']
        command = ['shell'] + fixtures['command']
        project_root = fixtures['project_root']
        environment_variables = fixtures['environment_variables']

        container = MagicMock(spec=Container)
        container.attrs = {'State': {'ExitCode': 0}}
        docker_client.containers.create.return_value = container

        with patch('cdflow.dockerpty') as dockerpty, patch(
                'cdflow.check_output', ) as check_output:
            check_output.return_value = '42\n'
            exit_status, output = docker_run(docker_client, image_id, command,
                                             project_root,
                                             environment_variables)

            dockerpty.start.assert_called_once_with(
                docker_client.api,
                container.id,
            )

        assert exit_status == 0
        assert output == 'Shell end'

        expected_environment_variable = deepcopy(environment_variables)
        expected_environment_variable['COLUMNS'] = 42
        expected_environment_variable['LINES'] = 42

        docker_client.containers.create.assert_called_once_with(
            image_id,
            command=command,
            environment=expected_environment_variable,
            volumes={
                project_root: {
                    'bind': project_root,
                    'mode': 'rw',
                },
                '/var/run/docker.sock': {
                    'bind': '/var/run/docker.sock',
                    'mode': 'ro',
                },
            },
            working_dir=project_root,
            tty=True,
            stdin_open=True,
        )

    @given(
        fixed_dictionaries({
            'environment_variables':
            fixed_dictionaries({
                'AWS_ACCESS_KEY_ID':
                text(alphabet=printable, min_size=10),
                'AWS_SECRET_ACCESS_KEY':
                text(alphabet=printable, min_size=10),
                'AWS_SESSION_TOKEN':
                text(alphabet=printable, min_size=10),
                'FASTLY_API_KEY':
                text(alphabet=printable, min_size=10),
                'GITHUB_TOKEN':
                text(alphabet=printable, min_size=10),
                'CDFLOW_IMAGE_DIGEST':
                text(min_size=12),
            }),
            'image_id':
            image_id(),
            'project_root':
            filepath(),
            'platform_config_paths':
            lists(elements=filepath(), max_size=1),
            'command':
            lists(text(alphabet=printable)),
        }))
    def test_error_from_docker(self, fixtures):
        image_id = fixtures['image_id']
        command = fixtures['command']
        project_root = fixtures['project_root']
        platform_config_paths = fixtures['platform_config_paths']
        environment_variables = fixtures['environment_variables']

        docker_client = MagicMock(spec=DockerClient)
        docker_client.containers.create.side_effect = DockerException

        exit_status, output = docker_run(
            docker_client,
            image_id,
            command,
            project_root,
            environment_variables,
            platform_config_paths,
        )

        assert exit_status == 1
        assert output == str(DockerException())

    @given(
        fixed_dictionaries({
            'image_id':
            image_id(),
            'command':
            lists(text(alphabet=printable)),
            'project_root':
            filepath(),
            'platform_config_paths':
            lists(elements=filepath(), max_size=1),
            'environment_variables':
            dictionaries(
                keys=text(alphabet=VALID_ALPHABET),
                values=text(alphabet=VALID_ALPHABET),
                min_size=1,
            ),
        }))
    def test_follow_container_logs(self, fixtures):
        docker_client = MagicMock(spec=DockerClient)

        container = MagicMock(spec=Container)
        logs = MagicMock()
        messages = [m.encode('utf-8') for m in ('Running', 'the', 'command')]
        logs.__iter__.return_value = iter(messages)
        container.logs.return_value = logs

        container.attrs = {'State': {'ExitCode': 0}}

        docker_client.containers.create.return_value = container

        with patch('cdflow.print') as print_:
            docker_run(
                docker_client,
                fixtures['image_id'],
                fixtures['command'],
                fixtures['project_root'],
                fixtures['environment_variables'],
                fixtures['platform_config_paths'],
            )

            container.logs.assert_called_once_with(stream=True,
                                                   follow=True,
                                                   stdout=True,
                                                   stderr=True)

            assert print_.call_args_list[0][1]['end'] == ''
            assert print_.call_args_list[0][0][0] == messages[0]\
                .decode('utf-8')
            assert print_.call_args_list[1][0][0] == messages[1]\
                .decode('utf-8')
            assert print_.call_args_list[2][0][0] == messages[2]\
                .decode('utf-8')

    @given(
        fixed_dictionaries({
            'image_id':
            image_id(),
            'command':
            lists(text(alphabet=printable)),
            'project_root':
            filepath(),
            'platform_config_paths':
            lists(elements=filepath(), max_size=1),
            'environment_variables':
            dictionaries(
                keys=text(alphabet=VALID_ALPHABET),
                values=text(alphabet=VALID_ALPHABET),
                min_size=1,
            ),
        }))
    def test_container_can_be_removed_at_script_exit(self, fixtures):
        docker_client = MagicMock(spec=DockerClient)
        container = MagicMock(spec=Container)
        container.attrs = {'State': {'ExitCode': 0}}

        docker_client.containers.create.return_value = container

        with patch('cdflow.atexit') as atexit:
            docker_run(
                docker_client,
                fixtures['image_id'],
                fixtures['command'],
                fixtures['project_root'],
                fixtures['environment_variables'],
                fixtures['platform_config_paths'],
            )

            atexit.register.assert_called_once_with(_remove_container,
                                                    container)

    def test_remove_container(self):
        container = MagicMock(spec=Container)

        _remove_container(container)

        container.stop.assert_called_once_with()
        container.remove.assert_called_once_with()

    def test_remove_still_running_container(self):
        container = MagicMock(spec=Container)

        container.stop.side_effect = ReadTimeout

        try:
            _remove_container(container)
        except Exception as e:
            self.fail('Exception was raised: {}'.format(e))

        container.stop.assert_called_once_with()
        container.remove.assert_called_once_with()

    @given(
        fixed_dictionaries({
            'image_id':
            image_id(),
            'command':
            lists(text(alphabet=printable)),
            'project_root':
            filepath(),
            'platform_config_paths':
            lists(elements=filepath(), max_size=1),
            'environment_variables':
            dictionaries(
                keys=text(alphabet=VALID_ALPHABET),
                values=text(alphabet=VALID_ALPHABET),
                min_size=1,
            ),
            'exit_code':
            integers(min_value=-255, max_value=256),
        }))
    def test_exit_code_from_container_is_returned(self, fixtures):
        assume(fixtures['exit_code'] != 0)
        docker_client = MagicMock(spec=DockerClient)
        container = MagicMock(spec=Container)

        container.attrs = {
            'State': {
                'ExitCode': fixtures['exit_code'],
            }
        }

        docker_client.containers.create.return_value = container

        exit_status, output = docker_run(
            docker_client,
            fixtures['image_id'],
            fixtures['command'],
            fixtures['project_root'],
            fixtures['environment_variables'],
            fixtures['platform_config_paths'],
        )

        assert exit_status == fixtures['exit_code']
        assert output == ''