""" Unit tests related to install_lab """ import unittest import install_lab from consts.timeout import HostTimeout from helper.install_lab import exec_cmd from unittest.mock import MagicMock, patch, call from utils import kpi class ExecCmdTestCase(unittest.TestCase): """ Class to test help function exec_cmd """ SUCCESS = 0 FAILED1 = 1 def setUp(self): """ Method to set up the parameters used on the tests in this class """ self.cmd = 'print ("Hello!")' self.reattempt_delay = HostTimeout.REATTEMPT_DELAY self.max_attempts = len(self.reattempt_delay) self.counter = 0 self.trigger = 0 self.result = None def dynamicMock(self, *args, **kwargs): """ Method to simulate a function with temporary failures """ process = MagicMock() process.__enter__.return_value = process self.counter += 1 if self.counter == self.trigger: self.counter = 0 attrs = { 'wait.return_value': None, 'stdout.readline.side_effect': [bytes('Hello!\n', 'utf-8'), b''] } process.configure_mock(**attrs) process.returncode = self.SUCCESS else: attrs = { 'wait.return_value': None, 'stdout.readline.side_effect': [b''] } process.configure_mock(**attrs) process.returncode = self.FAILED1 return process @patch('time.sleep') @patch('utils.install_log.LOG.error') @patch('utils.install_log.LOG.warning') @patch('utils.install_log.LOG.info') @patch('subprocess.Popen') def test_exec_cmd_1st_attempt_ok( self, m_s_Popen, m_LOG_info, m_LOG_warning, m_LOG_error, m_time_sleep ): """ Test successful call in the first attempt (normal case) This test focus on info messages. No warnings or error messages are expected. The expect result code is None (refer to the description of fault_tolerant decorator) """ # Setup m_time_sleep.return_value = 0 process = MagicMock() attrs = { 'wait.return_value': None, 'returncode': self.SUCCESS, 'stdout.readline.side_effect': [bytes('Hello!\n', 'utf-8'), b''] } process.configure_mock(**attrs) m_s_Popen.return_value.__enter__.return_value = process # Run self.result = exec_cmd(self.cmd) # Assert calls = [ call("#### Executing command on the host machine:\n$ %s\n", self.cmd), call("%s", "Hello!"), ] m_LOG_info.assert_has_calls(calls, any_order=False) self.assertEqual(m_LOG_warning.call_count, 0) self.assertEqual(m_LOG_error.call_count, 0) self.assertIsNone(self.result) @patch('time.sleep') @patch('utils.install_log.LOG.error') @patch('utils.install_log.LOG.warning') @patch('utils.install_log.LOG.info') @patch('subprocess.Popen') def test_exec_cmd_3rd_attempt_ok( self, m_s_Popen, m_LOG_info, m_LOG_warning, m_LOG_error, m_time_sleep ): """ Test successful call after a few retries (this may occor in system instability scenarios) This test focus on log info messages, for re-attempt scenarios. Warning messages are covered in next test. No error messages are expected. The expect result code is None (refer to the description of fault_tolerant decorator) """ # Setup m_s_Popen.side_effect = self.dynamicMock m_time_sleep.return_value = 0 self.trigger = 3 # Run self.result = exec_cmd(self.cmd) # Assert calls = [ call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[1])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[2])), call("#### Executing command on the host machine:\n$ %s\n", self.cmd), call("%s", "Hello!"), ] m_LOG_info.assert_has_calls(calls, any_order=False) self.assertEqual(m_LOG_error.call_count, 0) self.assertEqual(m_LOG_warning.call_count, 2) self.assertIsNone(self.result) @patch('time.sleep') @patch('utils.install_log.LOG.error') @patch('utils.install_log.LOG.warning') @patch('subprocess.Popen') def test_exec_cmd_5th_attempt_ok( self, m_s_Popen, m_LOG_warning, m_LOG_error, m_time_sleep ): """ Test successful call after a few retries (thsi may occor in system instability scenarios) This test focus on warning messages, for re-attempt scenarios. No error messages are expected. The expect result code is None (refer to the description of fault_tolerant decorator) """ # Setup m_s_Popen.side_effect = self.dynamicMock m_time_sleep.return_value = 0 self.trigger = 5 # Run self.result = exec_cmd(self.cmd) # Assert calls = [ call('#### Failed command:\n$ %s [attempt: %s/%s]\n', self.cmd, 1, 10), call('#### Failed command:\n$ %s [attempt: %s/%s]\n', self.cmd, 2, 10), call('#### Failed command:\n$ %s [attempt: %s/%s]\n', self.cmd, 3, 10), call('#### Failed command:\n$ %s [attempt: %s/%s]\n', self.cmd, 4, 10), ] m_LOG_warning.assert_has_calls(calls, any_order=False) self.assertEqual(m_LOG_error.call_count, 0) self.assertIsNone(self.result) @patch("sys.exit") @patch('time.sleep') @patch('utils.install_log.LOG.error') @patch('utils.install_log.LOG.warning') @patch('utils.install_log.LOG.info') @patch('subprocess.Popen') def test_exec_cmd_failed( self, m_s_Popen, m_LOG_info, m_LOG_warning, m_LOG_error, m_time_sleep, mock_exit ): """ Test unsuccessful call This may occur after a maximum number of retries, in strong system instability scenarios) This test focus on info and error messages, for the failure scenarios. The expected warning messages are the same as for successful calls, covered previously. The expect result code is None (refer to the description of fault_tolerant decorator) """ # Setup m_s_Popen.side_effect = self.dynamicMock m_LOG_warning.return_value = 0 m_time_sleep.return_value = 0 mock_exit.side_effect = SystemExit(1) self.trigger = self.max_attempts+1 with self.assertRaises(TimeoutError): # Run self.result = exec_cmd(self.cmd) # Assert calls = [ call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[1])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[2])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[3])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[4])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[5])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[6])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[7])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[8])), call('#### Executing command on the host machine:\n$ %s\n', self.cmd), call("Trying again after %s ... ", kpi.get_formated_time(self.reattempt_delay[9])), ] m_LOG_info.assert_has_calls(calls, any_order=False) m_LOG_error.assert_called_once_with( "#### Failed command:\n$ %s [attempt: %s/%s]\n", self.cmd, self.max_attempts, self.max_attempts ) self.assertIsNone(self.result) class UpdatePlatformCpusTestCase(unittest.TestCase): """ Class to test update_platform_cpus method """ @patch("install_lab.serial") def test_update_platform_cpus(self, mock_serial): """ Test update_platform_cpus method """ # Setup mock_stream = MagicMock() mock_hostname = "hostname" mock_cpu_num = 5 # Run install_lab.update_platform_cpus(mock_stream, mock_hostname, cpu_num=mock_cpu_num) # Assert command_string = ( "\nsource /etc/platform/openrc; system host-cpu-modify " f"{mock_hostname} -f platform -p0 {mock_cpu_num}" ) mock_serial.send_bytes.assert_called_once_with( mock_stream, command_string, prompt="keystone", timeout=300 ) class SetDnsTestCase(unittest.TestCase): """ Class to test set_dns method """ @patch("install_lab.serial") def test_set_dns(self, mock_serial): """ Test set_dns method """ # Setup mock_stream = MagicMock() mock_dns_ip = "8.8.8.8" # Run install_lab.set_dns(mock_stream, mock_dns_ip) # Assert command_string = ( "source /etc/platform/openrc; system dns-modify " f"nameservers={mock_dns_ip}" ) mock_serial.send_bytes.assert_called_once_with( mock_stream, command_string, prompt="keystone" ) class ConfigControllerTestCase(unittest.TestCase): """ Class to test config_controller method """ command_string = ( "ansible-playbook /usr/share/ansible/stx-ansible/playbooks/bootstrap.yml" ) mock_stream = MagicMock() mock_password = "Li69nux*" @patch("install_lab.serial") @patch("install_lab.host_helper.check_password") def test_config_controller_successful(self, mock_check_password, mock_serial): """ Test config_controller method with success """ # Setup mock_serial.expect_bytes.return_value = 0 # Run install_lab.config_controller(self.mock_stream, password=self.mock_password) # Assert calls = [ call(self.mock_stream, self.command_string, expect_prompt=False), call(self.mock_stream, 'echo [$?]', expect_prompt=False, log=False), ] mock_serial.send_bytes.assert_has_calls(calls, any_order=False) calls = [ call(self.mock_stream, "~$", timeout=HostTimeout.LAB_CONFIG), call(self.mock_stream, '[0]', timeout=HostTimeout.NORMAL_OP, log=False), ] mock_serial.expect_bytes.assert_has_calls(calls, any_order=False) mock_check_password.assert_called_once_with(self.mock_stream, password=self.mock_password) @patch("install_lab.serial") @patch("install_lab.host_helper.check_password") def test_config_controller_unsuccessful(self, mock_check_password, mock_serial): """ Test config_controller method without success raising an exception """ # Setup mock_serial.expect_bytes.return_value = 1 # Run with self.assertRaises(SystemExit): install_lab.config_controller(self.mock_stream, password=self.mock_password) # Assert calls = [ call(self.mock_stream, self.command_string, expect_prompt=False), call(self.mock_stream, 'echo [$?]', expect_prompt=False, log=False), ] mock_serial.send_bytes.assert_has_calls(calls, any_order=False) calls = [ call(self.mock_stream, "~$", timeout=HostTimeout.LAB_CONFIG), call(self.mock_stream, '[0]', timeout=HostTimeout.NORMAL_OP, log=False), ] mock_serial.expect_bytes.assert_has_calls(calls, any_order=False) mock_check_password.assert_called_once_with(self.mock_stream, password=self.mock_password) if __name__ == '__main__': unittest.main()