log_printer_test.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import itertools
  2. from io import StringIO
  3. from queue import Queue
  4. import pytest
  5. import requests
  6. from docker.errors import APIError
  7. from compose.cli.log_printer import build_log_generator
  8. from compose.cli.log_printer import build_log_presenters
  9. from compose.cli.log_printer import consume_queue
  10. from compose.cli.log_printer import QueueItem
  11. from compose.cli.log_printer import wait_on_exit
  12. from compose.cli.log_printer import watch_events
  13. from compose.container import Container
  14. from tests import mock
  15. @pytest.fixture
  16. def output_stream():
  17. output = StringIO()
  18. output.flush = mock.Mock()
  19. return output
  20. @pytest.fixture
  21. def mock_container():
  22. return mock.Mock(spec=Container, name_without_project='web_1')
  23. class TestLogPresenter:
  24. def test_monochrome(self, mock_container):
  25. presenters = build_log_presenters(['foo', 'bar'], True)
  26. presenter = next(presenters)
  27. actual = presenter.present(mock_container, "this line")
  28. assert actual == "web_1 | this line"
  29. def test_polychrome(self, mock_container):
  30. presenters = build_log_presenters(['foo', 'bar'], False)
  31. presenter = next(presenters)
  32. actual = presenter.present(mock_container, "this line")
  33. assert '\033[' in actual
  34. def test_wait_on_exit():
  35. exit_status = 3
  36. mock_container = mock.Mock(
  37. spec=Container,
  38. name='cname',
  39. wait=mock.Mock(return_value=exit_status))
  40. expected = '{} exited with code {}\n'.format(mock_container.name, exit_status)
  41. assert expected == wait_on_exit(mock_container)
  42. def test_wait_on_exit_raises():
  43. status_code = 500
  44. def mock_wait():
  45. resp = requests.Response()
  46. resp.status_code = status_code
  47. raise APIError('Bad server', resp)
  48. mock_container = mock.Mock(
  49. spec=Container,
  50. name='cname',
  51. wait=mock_wait
  52. )
  53. expected = 'Unexpected API error for {} (HTTP code {})\n'.format(
  54. mock_container.name, status_code,
  55. )
  56. assert expected in wait_on_exit(mock_container)
  57. class TestBuildLogGenerator:
  58. def test_no_log_stream(self, mock_container):
  59. mock_container.log_stream = None
  60. mock_container.logs.return_value = iter([b"hello\nworld"])
  61. log_args = {'follow': True}
  62. generator = build_log_generator(mock_container, log_args)
  63. assert next(generator) == "hello\n"
  64. assert next(generator) == "world"
  65. mock_container.logs.assert_called_once_with(
  66. stdout=True,
  67. stderr=True,
  68. stream=True,
  69. **log_args)
  70. def test_with_log_stream(self, mock_container):
  71. mock_container.log_stream = iter([b"hello\nworld"])
  72. log_args = {'follow': True}
  73. generator = build_log_generator(mock_container, log_args)
  74. assert next(generator) == "hello\n"
  75. assert next(generator) == "world"
  76. def test_unicode(self, output_stream):
  77. glyph = '\u2022\n'
  78. mock_container.log_stream = iter([glyph.encode('utf-8')])
  79. generator = build_log_generator(mock_container, {})
  80. assert next(generator) == glyph
  81. @pytest.fixture
  82. def thread_map():
  83. return {'cid': mock.Mock()}
  84. @pytest.fixture
  85. def mock_presenters():
  86. return itertools.cycle([mock.Mock()])
  87. class TestWatchEvents:
  88. def test_stop_event(self, thread_map, mock_presenters):
  89. event_stream = [{'action': 'stop', 'id': 'cid'}]
  90. watch_events(thread_map, event_stream, mock_presenters, ())
  91. assert not thread_map
  92. def test_start_event(self, thread_map, mock_presenters):
  93. container_id = 'abcd'
  94. event = {'action': 'start', 'id': container_id, 'container': mock.Mock()}
  95. event_stream = [event]
  96. thread_args = 'foo', 'bar'
  97. with mock.patch(
  98. 'compose.cli.log_printer.build_thread',
  99. autospec=True
  100. ) as mock_build_thread:
  101. watch_events(thread_map, event_stream, mock_presenters, thread_args)
  102. mock_build_thread.assert_called_once_with(
  103. event['container'],
  104. next(mock_presenters),
  105. *thread_args)
  106. assert container_id in thread_map
  107. def test_container_attach_event(self, thread_map, mock_presenters):
  108. container_id = 'abcd'
  109. mock_container = mock.Mock(is_restarting=False)
  110. mock_container.attach_log_stream.side_effect = APIError("race condition")
  111. event_die = {'action': 'die', 'id': container_id}
  112. event_start = {'action': 'start', 'id': container_id, 'container': mock_container}
  113. event_stream = [event_die, event_start]
  114. thread_args = 'foo', 'bar'
  115. watch_events(thread_map, event_stream, mock_presenters, thread_args)
  116. assert mock_container.attach_log_stream.called
  117. def test_other_event(self, thread_map, mock_presenters):
  118. container_id = 'abcd'
  119. event_stream = [{'action': 'create', 'id': container_id}]
  120. watch_events(thread_map, event_stream, mock_presenters, ())
  121. assert container_id not in thread_map
  122. class TestConsumeQueue:
  123. def test_item_is_an_exception(self):
  124. class Problem(Exception):
  125. pass
  126. queue = Queue()
  127. error = Problem('oops')
  128. for item in QueueItem.new('a'), QueueItem.new('b'), QueueItem.exception(error):
  129. queue.put(item)
  130. generator = consume_queue(queue, False)
  131. assert next(generator) == 'a'
  132. assert next(generator) == 'b'
  133. with pytest.raises(Problem):
  134. next(generator)
  135. def test_item_is_stop_without_cascade_stop(self):
  136. queue = Queue()
  137. for item in QueueItem.stop(), QueueItem.new('a'), QueueItem.new('b'):
  138. queue.put(item)
  139. generator = consume_queue(queue, False)
  140. assert next(generator) == 'a'
  141. assert next(generator) == 'b'
  142. def test_item_is_stop_with_cascade_stop(self):
  143. """Return the name of the container that caused the cascade_stop"""
  144. queue = Queue()
  145. for item in QueueItem.stop('foobar-1'), QueueItem.new('a'), QueueItem.new('b'):
  146. queue.put(item)
  147. generator = consume_queue(queue, True)
  148. assert next(generator) == 'foobar-1'
  149. def test_item_is_none_when_timeout_is_hit(self):
  150. queue = Queue()
  151. generator = consume_queue(queue, False)
  152. assert next(generator) is None