blob: 829b7d3f5973a495cb1832105923b096aa4d09b7 [file] [log] [blame]
"""Unittest Lab exercise to test implementation of "Synonym Dictionary"."""
import os
import subprocess
import sys
import traceback
import unittest
from unittest import mock
from absl import app
from absl.testing import flagsaver
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,
)
@flagsaver.flagsaver(
(ui_automator._COMMISSION, ['m5stack', '34970112332', 'Office'])
)
@mock.patch.object(ui_automator.UIAutomator, 'commission_device')
def test_main_calls_commission_device_with_valid_arguments(
self, mock_commission_device
):
ui_automator.main(_FAKE_VALID_SYS_ARGV)
mock_commission_device.assert_called_once_with(
'm5stack', '34970112332', 'Office'
)
@flagsaver.flagsaver(
(ui_automator._COMMISSION, ['m5stack', '3497', 'Office'])
)
@mock.patch.object(android_device, 'get_all_instances')
def test_main_should_print_a_hint_when_cmd_args_have_invalid_value(
self,
mock_get_all_instances,
):
mock_get_all_instances.return_value = [self.mock_android_device]
with self.assertRaises(ValueError) as e:
ui_automator.main(_FAKE_SYS_ARGV_WITH_INVALID_PAIRING_CODE)
self.assertIn(
'Value of PairingCode is invalid. Paring code should be 11-digit or'
' 21-digit numeric code.',
str(traceback.format_exception(e.exception)),
)
self.mock_android_device.mbs.commissionDevice.assert_not_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):
app.run(ui_automator.main)
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()