Merge pull request #3075 from dnephin/reactive_logs

Display logs for new containers started after the command is run
This commit is contained in:
Aanand Prasad 2016-03-16 21:03:25 +00:00
commit 371ea479f5
9 changed files with 411 additions and 274 deletions

View file

@ -1,27 +1,23 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import itertools
import pytest
import six
from six.moves.queue import Queue
from compose.cli.log_printer import LogPrinter
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
def build_mock_container(reader):
return mock.Mock(
spec=Container,
name='myapp_web_1',
name_without_project='web_1',
has_api_logs=True,
log_stream=None,
logs=reader,
wait=mock.Mock(return_value=0),
)
@pytest.fixture
def output_stream():
output = six.StringIO()
@ -31,68 +27,151 @@ def output_stream():
@pytest.fixture
def mock_container():
def reader(*args, **kwargs):
yield b"hello\nworld"
return build_mock_container(reader)
return mock.Mock(spec=Container, name_without_project='web_1')
class TestLogPrinter(object):
class TestLogPresenter(object):
def test_single_container(self, output_stream, mock_container):
LogPrinter([mock_container], output=output_stream, log_args={'follow': True}).run()
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"
output = output_stream.getvalue()
assert 'hello' in output
assert 'world' in output
# Call count is 2 lines + "container exited line"
assert output_stream.flush.call_count == 3
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_single_container_without_stream(self, output_stream, mock_container):
LogPrinter([mock_container], output=output_stream).run()
output = output_stream.getvalue()
assert 'hello' in output
assert 'world' in output
# Call count is 2 lines
assert output_stream.flush.call_count == 2
def test_wait_on_exit():
exit_status = 3
mock_container = mock.Mock(
spec=Container,
name='cname',
wait=mock.Mock(return_value=exit_status))
def test_monochrome(self, output_stream, mock_container):
LogPrinter([mock_container], output=output_stream, monochrome=True).run()
assert '\033[' not in output_stream.getvalue()
expected = '{} exited with code {}\n'.format(mock_container.name, exit_status)
assert expected == wait_on_exit(mock_container)
def test_polychrome(self, output_stream, mock_container):
LogPrinter([mock_container], output=output_stream).run()
assert '\033[' in output_stream.getvalue()
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'
glyph = u'\u2022\n'
mock_container.log_stream = iter([glyph.encode('utf-8')])
def reader(*args, **kwargs):
yield glyph.encode('utf-8') + b'\n'
generator = build_log_generator(mock_container, {})
assert next(generator) == glyph
container = build_mock_container(reader)
LogPrinter([container], output=output_stream).run()
output = output_stream.getvalue()
if six.PY2:
output = output.decode('utf-8')
assert glyph in output
@pytest.fixture
def thread_map():
return {'cid': mock.Mock()}
def test_wait_on_exit(self):
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)
@pytest.fixture
def mock_presenters():
return itertools.cycle([mock.Mock()])
def test_generator_with_no_logs(self, mock_container, output_stream):
mock_container.has_api_logs = False
mock_container.log_driver = 'none'
LogPrinter([mock_container], output=output_stream).run()
output = output_stream.getvalue()
assert "WARNING: no logs are available with the 'none' log driver\n" in output
assert "exited with code" not in output
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_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):
queue = Queue()
for item in QueueItem.stop(), QueueItem.new('a'), QueueItem.new('b'):
queue.put(item)
assert list(consume_queue(queue, True)) == []
def test_item_is_none_when_timeout_is_hit(self):
queue = Queue()
generator = consume_queue(queue, False)
assert next(generator) is None

View file

@ -8,8 +8,8 @@ import pytest
from compose import container
from compose.cli.errors import UserError
from compose.cli.formatter import ConsoleWarningFormatter
from compose.cli.main import build_log_printer
from compose.cli.main import convergence_strategy_from_opts
from compose.cli.main import filter_containers_to_service_names
from compose.cli.main import setup_console_handler
from compose.service import ConvergenceStrategy
from tests import mock
@ -32,7 +32,7 @@ def logging_handler():
class TestCLIMainTestCase(object):
def test_build_log_printer(self):
def test_filter_containers_to_service_names(self):
containers = [
mock_container('web', 1),
mock_container('web', 2),
@ -41,18 +41,18 @@ class TestCLIMainTestCase(object):
mock_container('another', 1),
]
service_names = ['web', 'db']
log_printer = build_log_printer(containers, service_names, True, False, {'follow': True})
assert log_printer.containers == containers[:3]
actual = filter_containers_to_service_names(containers, service_names)
assert actual == containers[:3]
def test_build_log_printer_all_services(self):
def test_filter_containers_to_service_names_all(self):
containers = [
mock_container('web', 1),
mock_container('db', 1),
mock_container('other', 1),
]
service_names = []
log_printer = build_log_printer(containers, service_names, True, False, {'follow': True})
assert log_printer.containers == containers
actual = filter_containers_to_service_names(containers, service_names)
assert actual == containers
class TestSetupConsoleHandlerTestCase(object):

View file

@ -1,61 +0,0 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import unittest
from time import sleep
from compose.cli.multiplexer import Multiplexer
class MultiplexerTest(unittest.TestCase):
def test_no_iterators(self):
mux = Multiplexer([])
self.assertEqual([], list(mux.loop()))
def test_empty_iterators(self):
mux = Multiplexer([
(x for x in []),
(x for x in []),
])
self.assertEqual([], list(mux.loop()))
def test_aggregates_output(self):
mux = Multiplexer([
(x for x in [0, 2, 4]),
(x for x in [1, 3, 5]),
])
self.assertEqual(
[0, 1, 2, 3, 4, 5],
sorted(list(mux.loop())),
)
def test_exception(self):
class Problem(Exception):
pass
def problematic_iterator():
yield 0
yield 2
raise Problem(":(")
mux = Multiplexer([
problematic_iterator(),
(x for x in [1, 3, 5]),
])
with self.assertRaises(Problem):
list(mux.loop())
def test_cascade_stop(self):
def fast_stream():
for num in range(3):
yield "stream1 %s" % num
def slow_stream():
sleep(5)
yield "stream2 FAIL"
mux = Multiplexer([fast_stream(), slow_stream()], cascade_stop=True)
assert "stream2 FAIL" not in set(mux.loop())

View file

@ -309,6 +309,7 @@ class ProjectTest(unittest.TestCase):
'image': 'example/image',
},
'time': dt_with_microseconds(1420092061, 2),
'container': Container(None, {'Id': 'abcde'}),
},
{
'type': 'container',
@ -320,6 +321,7 @@ class ProjectTest(unittest.TestCase):
'image': 'example/image',
},
'time': dt_with_microseconds(1420092061, 3),
'container': Container(None, {'Id': 'abcde'}),
},
{
'type': 'container',
@ -331,6 +333,7 @@ class ProjectTest(unittest.TestCase):
'image': 'example/db',
},
'time': dt_with_microseconds(1420092061, 4),
'container': Container(None, {'Id': 'ababa'}),
},
]