# 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 from eventlet.green import subprocess import eventlet.queue import mock import testtools from neutron.agent.common import async_process from neutron.agent.common import utils 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_and_wait' ) as mock_kill_process_and_wait,\ 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_and_wait.assert_called_once_with( pid, signal.SIGKILL, None) def _test__kill_process_and_wait(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_and_wait_returns_true_for_valid_pid(self): self._test__kill_process_and_wait('1', True) def test__kill_process_and_wait_returns_false_for_execute_exception(self): self._test__kill_process_and_wait('1', False, 'Invalid') def test_kill_process_and_wait_with_different_signal(self): self._test__kill_process_and_wait( '1', True, kill_signal=signal.SIGTERM) def test__kill_process_timeout_reached(self): self.proc.run_as_root = True kill_timeout = 5 pid = '1' with mock.patch.object(utils, 'kill_process') as mock_kill_process, \ mock.patch.object(self.proc, '_process') as process_mock: process_mock.wait.side_effect = subprocess.TimeoutExpired( self.proc.cmd, kill_timeout) self.assertTrue( self.proc._kill_process_and_wait( pid, signal.SIGTERM, kill_timeout)) process_mock.wait.assert_called_once_with(kill_timeout) mock_kill_process.assert_has_calls([ mock.call(pid, signal.SIGTERM, self.proc.run_as_root), mock.call(pid, signal.SIGKILL, self.proc.run_as_root)]) 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, None) 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)