from __future__ import absolute_import from __future__ import unicode_literals import itertools import pytest import requests import six from docker.errors import APIError from six.moves.queue import Queue from compose.cli.log_printer import build_log_generator from compose.cli.log_printer import build_log_presenters from compose.cli.log_printer import build_no_log_generator from compose.cli.log_printer import consume_queue from compose.cli.log_printer import QueueItem from compose.cli.log_printer import wait_on_exit from compose.cli.log_printer import watch_events from compose.container import Container from tests import mock @pytest.fixture def output_stream(): output = six.StringIO() output.flush = mock.Mock() return output @pytest.fixture def mock_container(): return mock.Mock(spec=Container, name_without_project='web_1') class TestLogPresenter(object): def test_monochrome(self, mock_container): presenters = build_log_presenters(['foo', 'bar'], True) presenter = next(presenters) actual = presenter.present(mock_container, "this line") assert actual == "web_1 | this line" def test_polychrome(self, mock_container): presenters = build_log_presenters(['foo', 'bar'], False) presenter = next(presenters) actual = presenter.present(mock_container, "this line") assert '\033[' in actual def test_wait_on_exit(): exit_status = 3 mock_container = mock.Mock( spec=Container, name='cname', wait=mock.Mock(return_value=exit_status)) expected = '{} exited with code {}\n'.format(mock_container.name, exit_status) assert expected == wait_on_exit(mock_container) def test_wait_on_exit_raises(): status_code = 500 def mock_wait(): resp = requests.Response() resp.status_code = status_code raise APIError('Bad server', resp) mock_container = mock.Mock( spec=Container, name='cname', wait=mock_wait ) expected = 'Unexpected API error for {} (HTTP code {})\n'.format( mock_container.name, status_code, ) assert expected in wait_on_exit(mock_container) def test_build_no_log_generator(mock_container): mock_container.has_api_logs = False mock_container.log_driver = 'none' output, = build_no_log_generator(mock_container, None) assert "WARNING: no logs are available with the 'none' log driver\n" in output assert "exited with code" not in output class TestBuildLogGenerator(object): def test_no_log_stream(self, mock_container): mock_container.log_stream = None mock_container.logs.return_value = iter([b"hello\nworld"]) log_args = {'follow': True} generator = build_log_generator(mock_container, log_args) assert next(generator) == "hello\n" assert next(generator) == "world" mock_container.logs.assert_called_once_with( stdout=True, stderr=True, stream=True, **log_args) def test_with_log_stream(self, mock_container): mock_container.log_stream = iter([b"hello\nworld"]) log_args = {'follow': True} generator = build_log_generator(mock_container, log_args) assert next(generator) == "hello\n" assert next(generator) == "world" def test_unicode(self, output_stream): glyph = u'\u2022\n' mock_container.log_stream = iter([glyph.encode('utf-8')]) generator = build_log_generator(mock_container, {}) assert next(generator) == glyph @pytest.fixture def thread_map(): return {'cid': mock.Mock()} @pytest.fixture def mock_presenters(): return itertools.cycle([mock.Mock()]) class TestWatchEvents(object): def test_stop_event(self, thread_map, mock_presenters): event_stream = [{'action': 'stop', 'id': 'cid'}] watch_events(thread_map, event_stream, mock_presenters, ()) assert not thread_map def test_start_event(self, thread_map, mock_presenters): container_id = 'abcd' event = {'action': 'start', 'id': container_id, 'container': mock.Mock()} event_stream = [event] thread_args = 'foo', 'bar' with mock.patch( 'compose.cli.log_printer.build_thread', autospec=True ) as mock_build_thread: watch_events(thread_map, event_stream, mock_presenters, thread_args) mock_build_thread.assert_called_once_with( event['container'], next(mock_presenters), *thread_args) assert container_id in thread_map def test_container_attach_event(self, thread_map, mock_presenters): container_id = 'abcd' mock_container = mock.Mock(is_restarting=False) mock_container.attach_log_stream.side_effect = APIError("race condition") event_die = {'action': 'die', 'id': container_id} event_start = {'action': 'start', 'id': container_id, 'container': mock_container} event_stream = [event_die, event_start] thread_args = 'foo', 'bar' watch_events(thread_map, event_stream, mock_presenters, thread_args) assert mock_container.attach_log_stream.called def test_other_event(self, thread_map, mock_presenters): container_id = 'abcd' event_stream = [{'action': 'create', 'id': container_id}] watch_events(thread_map, event_stream, mock_presenters, ()) assert container_id not in thread_map class TestConsumeQueue(object): def test_item_is_an_exception(self): class Problem(Exception): pass queue = Queue() error = Problem('oops') for item in QueueItem.new('a'), QueueItem.new('b'), QueueItem.exception(error): queue.put(item) generator = consume_queue(queue, False) assert next(generator) == 'a' assert next(generator) == 'b' with pytest.raises(Problem): next(generator) def test_item_is_stop_without_cascade_stop(self): queue = Queue() for item in QueueItem.stop(), QueueItem.new('a'), QueueItem.new('b'): queue.put(item) generator = consume_queue(queue, False) assert next(generator) == 'a' assert next(generator) == 'b' def test_item_is_stop_with_cascade_stop(self): """Return the name of the container that caused the cascade_stop""" queue = Queue() for item in QueueItem.stop('foobar-1'), QueueItem.new('a'), QueueItem.new('b'): queue.put(item) generator = consume_queue(queue, True) assert next(generator) == 'foobar-1' def test_item_is_none_when_timeout_is_hit(self): queue = Queue() generator = consume_queue(queue, False) assert next(generator) is None