neutron/neutron/tests/unit/agent/linux/test_async_process.py

315 lines
13 KiB
Python

# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import signal
import eventlet.event
import eventlet.queue
import mock
import testtools
from neutron.agent.common import utils
from neutron.agent.linux import async_process
from neutron.tests import base
from neutron.tests.unit.agent.linux import failing_process
class TestAsyncProcess(base.BaseTestCase):
def setUp(self):
super(TestAsyncProcess, self).setUp()
self.proc = async_process.AsyncProcess(['fake'])
def test_construtor_raises_exception_for_negative_respawn_interval(self):
with testtools.ExpectedException(ValueError):
async_process.AsyncProcess(['fake'], respawn_interval=-1)
def test__spawn(self):
expected_process = 'Foo'
proc = self.proc
with mock.patch.object(utils, 'create_process') as mock_create_process:
mock_create_process.return_value = [expected_process, None]
with mock.patch('eventlet.spawn') as mock_spawn:
proc._spawn()
self.assertTrue(self.proc._is_running)
self.assertIsInstance(proc._kill_event, eventlet.event.Event)
self.assertEqual(proc._process, expected_process)
mock_spawn.assert_has_calls([
mock.call(proc._watch_process,
proc._read_stdout,
proc._kill_event),
mock.call(proc._watch_process,
proc._read_stderr,
proc._kill_event),
])
self.assertEqual(len(proc._watchers), 2)
def test__pid_none(self):
pid = 1
self.proc._pid = None
with mock.patch.object(self.proc, '_process') as _process:
with mock.patch.object(utils,
'get_root_helper_child_pid') as func:
func.return_value = pid
self.assertEqual(self.proc.pid, pid)
func.assert_called_once_with(_process.pid, ['fake'],
run_as_root=False)
self.assertEqual(self.proc._pid, pid)
def test__pid_not_none(self):
self.proc._pid = 1
with mock.patch.object(self.proc, '_process'),\
mock.patch.object(utils, 'get_root_helper_child_pid') as func:
self.assertEqual(self.proc.pid, 1)
func.assert_not_called()
def test__handle_process_error_kills_with_respawn(self):
with mock.patch.object(self.proc, '_kill') as kill:
self.proc._handle_process_error()
kill.assert_has_calls([mock.call(signal.SIGKILL)])
def test__handle_process_error_kills_without_respawn(self):
self.proc.respawn_interval = 1
with mock.patch.object(self.proc, '_kill') as kill:
with mock.patch.object(self.proc, '_spawn') as spawn:
with mock.patch('eventlet.sleep') as sleep:
self.proc._handle_process_error()
kill.assert_has_calls([mock.call(signal.SIGKILL)])
sleep.assert_has_calls([mock.call(self.proc.respawn_interval)])
spawn.assert_called_once_with()
def test__handle_process_error_no_crash_if_started(self):
self.proc._is_running = True
with mock.patch.object(self.proc, '_kill'):
with mock.patch.object(self.proc, '_spawn') as mock_spawn:
self.proc._handle_process_error()
mock_spawn.assert_not_called()
def _watch_process_exception(self):
raise Exception('Error!')
def _test__watch_process(self, callback, kill_event):
self.proc._is_running = True
self.proc._kill_event = kill_event
# Ensure the test times out eventually if the watcher loops endlessly
with self.assert_max_execution_time():
with mock.patch.object(self.proc,
'_handle_process_error') as func:
self.proc._watch_process(callback, kill_event)
if not kill_event.ready():
func.assert_called_once_with()
def test__watch_process_exits_on_callback_failure(self):
self._test__watch_process(lambda: None, eventlet.event.Event())
def test__watch_process_exits_on_exception(self):
self._test__watch_process(self._watch_process_exception,
eventlet.event.Event())
with mock.patch.object(self.proc,
'_handle_process_error') as func:
self.proc._watch_process(self._watch_process_exception,
self.proc._kill_event)
func.assert_not_called()
def test__watch_process_exits_on_sent_kill_event(self):
kill_event = eventlet.event.Event()
kill_event.send()
self._test__watch_process(None, kill_event)
def _test_read_output_queues_and_returns_result(self, output):
queue = eventlet.queue.LightQueue()
mock_stream = mock.Mock()
with mock.patch.object(mock_stream, 'readline') as mock_readline:
mock_readline.return_value = output
result = self.proc._read(mock_stream, queue)
if output:
self.assertEqual(output, result)
self.assertEqual(output, queue.get_nowait())
else:
self.assertFalse(result)
self.assertTrue(queue.empty())
def test__read_queues_and_returns_output(self):
self._test_read_output_queues_and_returns_result('foo')
def test__read_returns_none_for_missing_output(self):
self._test_read_output_queues_and_returns_result('')
def test_start_raises_exception_if_process_already_started(self):
self.proc._is_running = True
with testtools.ExpectedException(async_process.AsyncProcessException):
self.proc.start()
def test_start_invokes__spawn(self):
with mock.patch.object(self.proc, '_spawn') as mock_start:
self.proc.start()
mock_start.assert_called_once_with()
def test__iter_queue_returns_empty_list_for_empty_queue(self):
result = list(self.proc._iter_queue(eventlet.queue.LightQueue(),
False))
self.assertEqual([], result)
def test__iter_queue_returns_queued_data(self):
queue = eventlet.queue.LightQueue()
queue.put('foo')
result = list(self.proc._iter_queue(queue, False))
self.assertEqual(result, ['foo'])
def _test_iter_output_calls_iter_queue_on_output_queue(self, output_type):
expected_value = 'foo'
with mock.patch.object(self.proc, '_iter_queue') as mock_iter_queue:
mock_iter_queue.return_value = expected_value
target_func = getattr(self.proc, 'iter_%s' % output_type, None)
value = target_func()
self.assertEqual(value, expected_value)
queue = getattr(self.proc, '_%s_lines' % output_type, None)
mock_iter_queue.assert_called_with(queue, False)
def test_iter_stdout(self):
self._test_iter_output_calls_iter_queue_on_output_queue('stdout')
def test_iter_stderr(self):
self._test_iter_output_calls_iter_queue_on_output_queue('stderr')
def test__kill_targets_process_for_pid(self):
pid = 1
with mock.patch.object(self.proc, '_kill_event'
) as mock_kill_event,\
mock.patch.object(utils, 'get_root_helper_child_pid',
return_value=pid),\
mock.patch.object(self.proc, '_kill_process'
) as mock_kill_process,\
mock.patch.object(self.proc, '_process'):
self.proc._kill(signal.SIGKILL)
self.assertIsNone(self.proc._kill_event)
self.assertFalse(self.proc._is_running)
self.assertIsNone(self.proc._pid)
mock_kill_event.send.assert_called_once_with()
if pid:
mock_kill_process.assert_called_once_with(pid, signal.SIGKILL)
def _test__kill_process(self, pid, expected, exception_message=None,
kill_signal=signal.SIGKILL):
self.proc.run_as_root = True
if exception_message:
exc = RuntimeError(exception_message)
else:
exc = None
with mock.patch.object(utils, 'kill_process',
side_effect=exc) as mock_kill_process:
actual = self.proc._kill_process(pid, kill_signal)
self.assertEqual(expected, actual)
mock_kill_process.assert_called_with(pid,
kill_signal,
self.proc.run_as_root)
def test__kill_process_returns_true_for_valid_pid(self):
self._test__kill_process('1', True)
def test__kill_process_returns_false_for_execute_exception(self):
self._test__kill_process('1', False, 'Invalid')
def test_kill_process_with_different_signal(self):
self._test__kill_process('1', True, kill_signal=signal.SIGTERM)
def test_stop_calls_kill_with_provided_signal_number(self):
self.proc._is_running = True
with mock.patch.object(self.proc, '_kill') as mock_kill:
self.proc.stop(kill_signal=signal.SIGTERM)
mock_kill.assert_called_once_with(signal.SIGTERM)
def test_stop_raises_exception_if_already_started(self):
with testtools.ExpectedException(async_process.AsyncProcessException):
self.proc.stop()
def test_cmd(self):
for expected, cmd in (('ls -l file', ['ls', '-l', 'file']),
('fake', ['fake'])):
proc = async_process.AsyncProcess(cmd)
self.assertEqual(expected, proc.cmd)
class TestAsyncProcessLogging(base.BaseTestCase):
def setUp(self):
super(TestAsyncProcessLogging, self).setUp()
self.log_mock = mock.patch.object(async_process, 'LOG').start()
def _test__read_stdout_logging(self, enable):
proc = async_process.AsyncProcess(['fakecmd'], log_output=enable)
with mock.patch.object(proc, '_read', return_value='fakedata'),\
mock.patch.object(proc, '_process'):
proc._read_stdout()
self.assertEqual(enable, self.log_mock.debug.called)
def _test__read_stderr_logging(self, enable):
proc = async_process.AsyncProcess(['fake'], log_output=enable)
with mock.patch.object(proc, '_read', return_value='fakedata'),\
mock.patch.object(proc, '_process'):
proc._read_stderr()
self.assertEqual(enable, self.log_mock.error.called)
def test__read_stdout_logging_enabled(self):
self._test__read_stdout_logging(enable=True)
def test__read_stdout_logging_disabled(self):
self._test__read_stdout_logging(enable=False)
def test__read_stderr_logging_enabled(self):
self._test__read_stderr_logging(enable=True)
def test__read_stderr_logging_disabled(self):
self._test__read_stderr_logging(enable=False)
class TestAsyncProcessDieOnError(base.BaseTestCase):
def test__read_stderr_returns_none_on_error(self):
proc = async_process.AsyncProcess(['fakecmd'], die_on_error=True)
with mock.patch.object(proc, '_read', return_value='fakedata'),\
mock.patch.object(proc, '_process'):
self.assertIsNone(proc._read_stderr())
class TestFailingAsyncProcess(base.BaseTestCase):
def setUp(self):
super(TestFailingAsyncProcess, self).setUp()
path = self.get_temp_file_path('async.tmp', self.get_new_temp_dir())
self.process = async_process.AsyncProcess(['python',
failing_process.__file__,
path],
respawn_interval=0)
def test_failing_async_process_handle_error_once(self):
with mock.patch.object(self.process, '_handle_process_error')\
as handle_error_mock:
self.process.start()
self.process._process.wait()
# Wait for the monitor process to complete
for thread in self.process._watchers:
thread.wait()
self.assertEqual(1, handle_error_mock.call_count)