diff options
Diffstat (limited to 'ironic_python_agent/tests/agent.py')
| -rw-r--r-- | ironic_python_agent/tests/agent.py | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/ironic_python_agent/tests/agent.py b/ironic_python_agent/tests/agent.py new file mode 100644 index 00000000..02487914 --- /dev/null +++ b/ironic_python_agent/tests/agent.py @@ -0,0 +1,224 @@ +""" +Copyright 2013 Rackspace, 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 json +import time +import unittest + +import mock +import pkg_resources +from wsgiref import simple_server + +from ironic_python_agent import agent +from ironic_python_agent import base +from ironic_python_agent import encoding +from ironic_python_agent import errors +from ironic_python_agent import hardware + +EXPECTED_ERROR = RuntimeError('command execution failed') + + +def foo_execute(*args, **kwargs): + if kwargs['fail']: + raise EXPECTED_ERROR + else: + return 'command execution succeeded' + + +class FakeMode(base.BaseAgentMode): + def __init__(self): + super(FakeMode, self).__init__('FAKE') + + +class TestHeartbeater(unittest.TestCase): + def setUp(self): + self.mock_agent = mock.Mock() + self.heartbeater = agent.IronicPythonAgentHeartbeater(self.mock_agent) + self.heartbeater.api = mock.Mock() + self.heartbeater.hardware = mock.create_autospec( + hardware.HardwareManager) + self.heartbeater.stop_event = mock.Mock() + + @mock.patch('ironic_python_agent.agent._time') + @mock.patch('random.uniform') + def test_heartbeat(self, mocked_uniform, mocked_time): + time_responses = [] + uniform_responses = [] + heartbeat_responses = [] + wait_responses = [] + expected_stop_event_calls = [] + + # FIRST RUN: + # initial delay is 0 + expected_stop_event_calls.append(mock.call(0)) + wait_responses.append(False) + # next heartbeat due at t=100 + heartbeat_responses.append(100) + # random interval multiplier is 0.5 + uniform_responses.append(0.5) + # time is now 50 + time_responses.append(50) + + # SECOND RUN: + # (100 - 50) * .5 = 25 (t becomes ~75) + expected_stop_event_calls.append(mock.call(25.0)) + wait_responses.append(False) + # next heartbeat due at t=180 + heartbeat_responses.append(180) + # random interval multiplier is 0.4 + uniform_responses.append(0.4) + # time is now 80 + time_responses.append(80) + + # THIRD RUN: + # (180 - 80) * .4 = 40 (t becomes ~120) + expected_stop_event_calls.append(mock.call(40.0)) + wait_responses.append(False) + # this heartbeat attempt fails + heartbeat_responses.append(Exception('uh oh!')) + # we check the time to generate a fake deadline, now t=125 + time_responses.append(125) + # random interval multiplier is 0.5 + uniform_responses.append(0.5) + # time is now 125.5 + time_responses.append(125.5) + + # FOURTH RUN: + # (125.5 - 125.0) * .5 = 0.25 + expected_stop_event_calls.append(mock.call(0.25)) + # Stop now + wait_responses.append(True) + + # Hook it up and run it + mocked_time.side_effect = time_responses + mocked_uniform.side_effect = uniform_responses + self.heartbeater.api.heartbeat.side_effect = heartbeat_responses + self.heartbeater.stop_event.wait.side_effect = wait_responses + self.heartbeater.run() + + # Validate expectations + self.assertEqual(self.heartbeater.stop_event.wait.call_args_list, + expected_stop_event_calls) + self.assertEqual(self.heartbeater.error_delay, 2.7) + + +class TestBaseAgent(unittest.TestCase): + def setUp(self): + self.encoder = encoding.RESTJSONEncoder(indent=4) + self.agent = agent.IronicPythonAgent('https://fake_api.example.' + 'org:8081/', + ('203.0.113.1', 9990), + ('192.0.2.1', 9999)) + + def assertEqualEncoded(self, a, b): + # Evidently JSONEncoder.default() can't handle None (??) so we have to + # use encode() to generate JSON, then json.loads() to get back a python + # object. + a_encoded = self.encoder.encode(a) + b_encoded = self.encoder.encode(b) + self.assertEqual(json.loads(a_encoded), json.loads(b_encoded)) + + def test_get_status(self): + started_at = time.time() + self.agent.started_at = started_at + + status = self.agent.get_status() + self.assertTrue(isinstance(status, agent.IronicPythonAgentStatus)) + self.assertEqual(status.started_at, started_at) + self.assertEqual(status.version, + pkg_resources.get_distribution('ironic-python-agent') + .version) + + def test_execute_command(self): + do_something_impl = mock.Mock() + self.agent.mode_implementation = FakeMode() + command_map = self.agent.mode_implementation.command_map + command_map['do_something'] = do_something_impl + + self.agent.execute_command('fake.do_something', foo='bar') + do_something_impl.assert_called_once_with('do_something', foo='bar') + + def test_execute_invalid_command(self): + self.assertRaises(errors.InvalidCommandError, + self.agent.execute_command, + 'do_something', + foo='bar') + + @mock.patch('wsgiref.simple_server.make_server', autospec=True) + def test_run(self, wsgi_server_cls): + wsgi_server = wsgi_server_cls.return_value + wsgi_server.start.side_effect = KeyboardInterrupt() + + self.agent.heartbeater = mock.Mock() + self.agent.api_client.lookup_node = mock.Mock() + self.agent.run() + + listen_addr = ('192.0.2.1', 9999) + wsgi_server_cls.assert_called_once_with( + listen_addr[0], + listen_addr[1], + self.agent.api, + server_class=simple_server.WSGIServer) + wsgi_server.serve_forever.assert_called_once() + + self.agent.heartbeater.start.assert_called_once_with() + + def test_async_command_success(self): + result = base.AsyncCommandResult('foo_command', {'fail': False}, + foo_execute) + expected_result = { + 'id': result.id, + 'command_name': 'foo_command', + 'command_params': { + 'fail': False, + }, + 'command_status': 'RUNNING', + 'command_result': None, + 'command_error': None, + } + self.assertEqualEncoded(result, expected_result) + + result.start() + result.join() + + expected_result['command_status'] = 'SUCCEEDED' + expected_result['command_result'] = 'command execution succeeded' + + self.assertEqualEncoded(result, expected_result) + + def test_async_command_failure(self): + result = base.AsyncCommandResult('foo_command', {'fail': True}, + foo_execute) + expected_result = { + 'id': result.id, + 'command_name': 'foo_command', + 'command_params': { + 'fail': True, + }, + 'command_status': 'RUNNING', + 'command_result': None, + 'command_error': None, + } + self.assertEqualEncoded(result, expected_result) + + result.start() + result.join() + + expected_result['command_status'] = 'FAILED' + expected_result['command_error'] = errors.CommandExecutionError( + str(EXPECTED_ERROR)) + + self.assertEqualEncoded(result, expected_result) |
