| """Unittest Lab exercise to test implementation of "Synonym Dictionary".""" |
| import os |
| import subprocess |
| import sys |
| import unittest |
| from unittest import mock |
| |
| from mobly.controllers import android_device |
| from mobly.controllers.android_device_lib import adb |
| from mobly.snippet import errors as snippet_errors |
| |
| from ui_automator import errors |
| from ui_automator import ui_automator |
| |
| _FAKE_MATTER_DEVICE_NAME = 'fake-matter-device-name' |
| _FAKE_GHA_ROOM = 'Office' |
| _FAKE_PAIRING_CODE = '34970112332' |
| _GOOGLE_HOME_APP = { |
| 'id': 'gha', |
| 'name': 'Google Home Application (GHA)', |
| 'minVersion': '3.1.18.1', |
| 'minMatter': '231456000', |
| 'minThread': '231456000', |
| } |
| _PYTHON_PATH = subprocess.check_output(['which', 'python']).decode('utf-8') |
| _PYTHON_BIN_PATH = _PYTHON_PATH[:-7] |
| _FAKE_VALID_SYS_ARGV = [ |
| _PYTHON_BIN_PATH + 'ui-automator', |
| '--commission', |
| 'm5stack,34970112332,Office', |
| ] |
| _FAKE_SYS_ARGV_WITH_INVALID_PAIRING_CODE = [ |
| _PYTHON_BIN_PATH + 'ui-automator', |
| '--commission', |
| 'm5stack,3497,Office', |
| ] |
| _FAKE_SYS_ARGV_WITH_INVALID_LENGTH = [ |
| _PYTHON_BIN_PATH + 'ui-automator', |
| '--commission', |
| '', |
| ] |
| |
| |
| class UIAutomatorTest(unittest.TestCase): |
| |
| def setUp(self): |
| """This method will be run before each of the test methods in the class.""" |
| super().setUp() |
| self.ui_automator = ui_automator.UIAutomator() |
| self.mock_android_device = mock.patch.object( |
| android_device, 'AndroidDevice' |
| ).start() |
| |
| @mock.patch.object(android_device, 'get_all_instances', autospec=True) |
| def test_load_device_raises_an_error_when_adb_not_installed( |
| self, mock_get_all_instances |
| ): |
| mock_get_all_instances.side_effect = adb.AdbError( |
| cmd='adb devices', |
| stdout='fake_msg', |
| stderr='adb command not found', |
| ret_code=1, |
| ) |
| with self.assertRaisesRegex( |
| errors.AdbError, |
| r'Please install adb and add it to your PATH environment variable\.', |
| ): |
| self.ui_automator.load_device() |
| |
| @mock.patch.object( |
| android_device, 'get_all_instances', autospec=True, return_value=[] |
| ) |
| def test_load_device_no_android_device_error( |
| self, unused_mock_get_all_instances |
| ): |
| with self.assertRaises(errors.NoAndroidDeviceError): |
| self.ui_automator.load_device() |
| |
| @mock.patch.object( |
| android_device, 'get_all_instances', autospec=True, return_value=[1, 2] |
| ) |
| def test_load_device_success(self, unused_mock_get_all_instances): |
| with self.assertLogs() as cm: |
| self.ui_automator.load_device() |
| self.assertEqual(cm.output, ['INFO:root:connected device: [1]']) |
| |
| def test_load_snippet_without_load_device_raises_error(self): |
| with self.assertRaises(errors.NoAndroidDeviceError): |
| self.ui_automator.load_snippet() |
| |
| @mock.patch.object(android_device, 'get_all_instances', autospec=True) |
| def test_load_snippet_success(self, mock_get_all_instances): |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| |
| self.ui_automator.load_device() |
| self.ui_automator.load_snippet() |
| |
| @mock.patch.object(android_device, 'get_all_instances', autospec=True) |
| def test_load_snippet_fails_with_server_start_pre_check_error( |
| self, mock_get_all_instances |
| ): |
| self.mock_android_device.load_snippet.side_effect = ( |
| snippet_errors.ServerStartPreCheckError('fake_ad', 'fake_msg') |
| ) |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| |
| self.ui_automator.load_device() |
| |
| with self.assertRaises(errors.AndroidDeviceNotReadyError): |
| self.ui_automator.load_snippet() |
| |
| @mock.patch.object(android_device, 'get_all_instances', autospec=True) |
| def test_load_snippet_fails_with_server_start_error( |
| self, mock_get_all_instances |
| ): |
| self.mock_android_device.load_snippet.side_effect = ( |
| snippet_errors.ServerStartError('fake_ad', 'fake_msg') |
| ) |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| |
| self.ui_automator.load_device() |
| |
| with self.assertRaises(errors.AndroidDeviceNotReadyError): |
| self.ui_automator.load_snippet() |
| |
| @mock.patch.object(android_device, 'get_all_instances', autospec=True) |
| def test_load_snippet_fails_with_protocol_error(self, mock_get_all_instances): |
| self.mock_android_device.load_snippet.side_effect = ( |
| snippet_errors.ProtocolError('fake_ad', 'fake_msg') |
| ) |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| |
| self.ui_automator.load_device() |
| |
| with self.assertRaises(errors.AndroidDeviceNotReadyError): |
| self.ui_automator.load_snippet() |
| |
| @mock.patch.object(android_device, 'get_all_instances', autospec=True) |
| def test_load_snippet_fails_with_snippet_error(self, mock_get_all_instances): |
| self.mock_android_device.load_snippet.side_effect = ( |
| android_device.SnippetError('fake_device', 'fake_msg') |
| ) |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| |
| self.ui_automator.load_device() |
| |
| with self.assertLogs(level='DEBUG') as cm: |
| self.ui_automator.load_snippet() |
| self.assertEqual( |
| cm.output, |
| [ |
| "DEBUG:root:'fake_device'::Service<SnippetManagementService>" |
| ' fake_msg' |
| ], |
| ) |
| |
| @mock.patch.object(android_device, 'get_all_instances', autospec=True) |
| @mock.patch.object( |
| os.path, 'dirname', autospec=True, return_value='/path/to/' |
| ) |
| def test_load_snippet_installs_apk_when_apk_is_not_installed( |
| self, mock_dirname, mock_get_all_instances |
| ): |
| self.mock_android_device.adb.shell.return_value = b'package:installed.apk\n' |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| self.ui_automator.load_device() |
| |
| self.ui_automator.load_snippet() |
| |
| self.mock_android_device.adb.install.assert_called_once_with( |
| ['-r', '-g', '/path/to/android/app/snippet-0.0.0.apk'] |
| ) |
| mock_dirname.assert_called_once() |
| |
| @mock.patch.object(android_device, 'get_all_instances', autospec=True) |
| @mock.patch.object(os.path, 'dirname', autospec=True) |
| def test_load_snippet_should_not_install_apk_when_apk_is_installed( |
| self, mock_dirname, mock_get_all_instances |
| ): |
| self.mock_android_device.adb.shell.return_value = ( |
| b'package:com.chip.interop.moblysnippet' |
| ) |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| self.ui_automator.load_device() |
| |
| self.ui_automator.load_snippet() |
| |
| self.mock_android_device.adb.install.assert_not_called() |
| mock_dirname.assert_not_called() |
| |
| @mock.patch.object(ui_automator.UIAutomator, 'load_device') |
| @mock.patch.object(ui_automator.UIAutomator, 'load_snippet') |
| def test_get_android_device_ready_raises_an_error_when_load_device_throws_an_error( |
| self, mock_load_snippet, mock_load_device |
| ): |
| mock_load_device.side_effect = errors.NoAndroidDeviceError( |
| 'No Android device connected to the host computer.' |
| ) |
| |
| @ui_automator.get_android_device_ready |
| def decorated_function(self): |
| del self |
| pass |
| |
| with self.assertRaisesRegex( |
| errors.AndroidDeviceNotReadyError, r'decorated_function failed\.' |
| ): |
| decorated_function(self.ui_automator) |
| |
| mock_load_device.assert_called_once() |
| mock_load_snippet.assert_not_called() |
| |
| @mock.patch.object(ui_automator.UIAutomator, 'load_device') |
| @mock.patch.object(ui_automator.UIAutomator, 'load_snippet') |
| def test_get_android_device_ready_raises_an_error_when_load_snippet_throws_an_error( |
| self, mock_load_snippet, mock_load_device |
| ): |
| mock_load_snippet.side_effect = errors.AndroidDeviceNotReadyError( |
| 'Check device(fake-serial) has installed required apk.' |
| ) |
| |
| @ui_automator.get_android_device_ready |
| def decorated_function(self): |
| del self |
| pass |
| |
| with self.assertRaisesRegex( |
| errors.AndroidDeviceNotReadyError, r'decorated_function failed\.' |
| ): |
| decorated_function(self.ui_automator) |
| |
| mock_load_snippet.assert_called_once() |
| mock_load_device.assert_called_once() |
| |
| @mock.patch.object(ui_automator.UIAutomator, 'load_device') |
| @mock.patch.object(ui_automator.UIAutomator, 'load_snippet') |
| def test_get_android_device_ready_raises_no_error_on_success( |
| self, mock_load_snippet, mock_load_device |
| ): |
| @ui_automator.get_android_device_ready |
| def decorated_function(self): |
| del self |
| pass |
| |
| decorated_function(self.ui_automator) |
| |
| mock_load_snippet.assert_called_once() |
| mock_load_device.assert_called_once() |
| |
| @mock.patch.object(android_device, 'get_all_instances') |
| @mock.patch.object(ui_automator.UIAutomator, 'load_snippet') |
| def test_commission_device_raises_an_error_when_device_is_not_ready( |
| self, mock_load_snippet, mock_get_all_instances |
| ): |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| error_msg = 'Check device(fake-serial) is ready.' |
| mock_load_snippet.side_effect = errors.AndroidDeviceNotReadyError(error_msg) |
| |
| with self.assertRaisesRegex( |
| errors.AndroidDeviceNotReadyError, 'commission_device failed.' |
| ): |
| self.ui_automator.commission_device( |
| _FAKE_MATTER_DEVICE_NAME, _FAKE_PAIRING_CODE, _FAKE_GHA_ROOM |
| ) |
| |
| @mock.patch.object(android_device, 'get_all_instances') |
| def test_commission_device_calls_a_method_in_snippet_with_desired_args( |
| self, mock_get_all_instances |
| ): |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| expected_matter_device = { |
| 'id': '_fake_matter_device_name', |
| 'name': _FAKE_MATTER_DEVICE_NAME, |
| 'pairingCode': _FAKE_PAIRING_CODE, |
| 'roomName': _FAKE_GHA_ROOM, |
| } |
| |
| self.ui_automator.commission_device( |
| _FAKE_MATTER_DEVICE_NAME, |
| _FAKE_PAIRING_CODE, |
| _FAKE_GHA_ROOM, |
| ) |
| |
| self.mock_android_device.mbs.commissionDevice.assert_called_once_with( |
| _GOOGLE_HOME_APP, expected_matter_device |
| ) |
| |
| @mock.patch.object(android_device, 'get_all_instances') |
| def test_commission_device_fails_when_snippet_method_throws_an_error( |
| self, mock_get_all_instances |
| ): |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| expected_error_message = ( |
| 'Unable to continue automated commissioning process on' |
| f' device({self.mock_android_device.device_info["serial"]}).' |
| ) |
| |
| with mock.patch.object(self.mock_android_device, 'mbs') as snippet: |
| snippet.commissionDevice.side_effect = Exception( |
| 'Can not find next button in the page.' |
| ) |
| with self.assertRaises(errors.MoblySnippetError) as exc: |
| self.ui_automator.commission_device( |
| _FAKE_MATTER_DEVICE_NAME, |
| _FAKE_PAIRING_CODE, |
| _FAKE_GHA_ROOM, |
| ) |
| |
| self.assertIn(expected_error_message, str(exc.exception)) |
| |
| @mock.patch.object(android_device, 'get_all_instances') |
| def test_commission_device_raises_an_error_when_device_name_exceeds_limit( |
| self, mock_get_all_instances |
| ): |
| invalid_device_name = 'fakeDeviceNameWith25Chars' |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| |
| with self.assertRaisesRegex( |
| ValueError, |
| 'Value of DeviceName is invalid. Device name should be no more than 24' |
| ' characters.', |
| ): |
| self.ui_automator.commission_device( |
| invalid_device_name, |
| _FAKE_PAIRING_CODE, |
| _FAKE_GHA_ROOM, |
| ) |
| |
| @mock.patch.object(android_device, 'get_all_instances') |
| def test_commission_device_raises_an_error_when_pairing_code_is_invalid( |
| self, mock_get_all_instances |
| ): |
| invalid_pairing_code = '123456789' |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| |
| with self.assertRaisesRegex( |
| ValueError, |
| 'Value of PairingCode is invalid. Paring code should be 11-digit or' |
| ' 21-digit numeric code.', |
| ): |
| self.ui_automator.commission_device( |
| device_name=_FAKE_MATTER_DEVICE_NAME, |
| pairing_code=invalid_pairing_code, |
| gha_room=_FAKE_GHA_ROOM, |
| ) |
| |
| @mock.patch.object(android_device, 'get_all_instances') |
| def test_commission_device_raises_an_error_when_gha_room_is_invalid( |
| self, mock_get_all_instances |
| ): |
| invalid_gha_room = 'Attic 2' |
| mock_get_all_instances.return_value = [self.mock_android_device] |
| |
| with self.assertRaisesRegex( |
| ValueError, |
| ( |
| 'Value of GHARoom is invalid. Valid values for GHARoom: Attic|Back' |
| ' door|Backyard|Basement|Bathroom|Bedroom|Den|Dining' |
| ' Room|Entryway|Family Room|Front door|Front' |
| ' Yard|Garage|Hallway|Kitchen|Living Room|Master' |
| ' Bedroom|Office|Shed|Side door' |
| ), |
| ): |
| self.ui_automator.commission_device( |
| device_name=_FAKE_MATTER_DEVICE_NAME, |
| pairing_code=_FAKE_PAIRING_CODE, |
| gha_room=invalid_gha_room, |
| ) |
| |
| @mock.patch.object(sys, 'exit') |
| @mock.patch.object(ui_automator.UIAutomator, 'commission_device') |
| def test_run_calls_commission_device_with_valid_arguments( |
| self, mock_commission_device, mock_exit |
| ): |
| with mock.patch.object(sys, 'argv', _FAKE_VALID_SYS_ARGV): |
| ui_automator.run() |
| |
| mock_commission_device.assert_called_once_with( |
| 'm5stack', '34970112332', 'Office' |
| ) |
| mock_exit.assert_called() |
| |
| @mock.patch.object(sys.stderr, 'write') |
| @mock.patch.object(sys, 'exit') |
| def test_commission_with_cmd_invalid_arg_should_stderr( |
| self, mock_exit, mock_stderr_write |
| ): |
| with mock.patch.object(sys, 'argv', _FAKE_SYS_ARGV_WITH_INVALID_LENGTH): |
| ui_automator.run() |
| |
| self.assertEqual(mock_stderr_write.call_count, 2) |
| first_call_args = ''.join(mock_stderr_write.call_args_list[0][0]) |
| self.assertEqual( |
| first_call_args, |
| 'FATAL Flags parsing error: flag --commission=[]: Use' |
| ' --commission {DeviceName},{PairingCode},{GHARoom} to commission a' |
| ' device to google fabric on GHA.\n', |
| ) |
| second_call_args = ''.join(mock_stderr_write.call_args_list[1][0]) |
| self.assertEqual( |
| second_call_args, |
| 'Pass --helpshort or --helpfull to see help on flags.\n', |
| ) |
| self.mock_android_device.mbs.commissionDevice.assert_not_called() |
| mock_exit.assert_called() |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |