blob: 383f62935a37a4582cf6959ac07ec4dbce3befc3 [file] [log] [blame]
# Copyright 2024 Google LLC
#
# 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.
"""Unittest Lab exercise to test implementation of "Synonym Dictionary"."""
import io
import os
import re
import subprocess
import sys
import time
import traceback
import unittest
from unittest import mock
from absl import flags
from absl.testing import flagsaver
from absl.testing import xml_reporter
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 test_reporter
from ui_automator import ui_automator
from ui_automator import unit_test_utils
from ui_automator import version
_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.removesuffix('python')
_FAKE_VALID_SYS_ARGV_FOR_COMMISSION = [
_PYTHON_BIN_PATH + 'ui-automator',
'--commission',
'm5stack,34970112332,Office',
]
_FAKE_VALID_SYS_ARGV_FOR_DECOMMISSION = [
_PYTHON_BIN_PATH + 'ui-automator',
'--decommission',
'm5stack',
]
_FAKE_VALID_SYS_ARGV_FOR_REGRESSION_TESTS = [
_PYTHON_BIN_PATH + 'ui-automator',
'--commission',
'm5stack,34970112332,Office',
'--regtest',
'--repeat',
'3',
]
_FAKE_INVALID_SYS_ARGV_FOR_REGRESSION_TESTS = [
_PYTHON_BIN_PATH + 'ui-automator',
'--regtest',
'--repeat',
'5',
]
_FAKE_SYS_ARGV_FOR_COMMISSION_WITH_INVALID_LENGTH = [
_PYTHON_BIN_PATH + 'ui-automator',
'--commission',
'm5',
]
_FAKE_SYS_ARGV_FOR_COMMISSION_WITH_EMPTY_VALUE = [
_PYTHON_BIN_PATH + 'ui-automator',
'--commission',
]
_FAKE_PROJECT_FOLDER = '/path/to/'
_EXPECTED_APK_PATH = f'{_FAKE_PROJECT_FOLDER}android/app/snippet.apk'
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()
self.addCleanup(mock.patch.stopall)
@flagsaver.flagsaver((ui_automator._NOTICES, True))
@mock.patch.object(sys, 'exit', autospec=True)
@mock.patch('builtins.print')
def test_run_with_notices_flag_should_print_license_notice(
self, mock_print, mock_exit
):
with mock.patch.object(
sys, 'argv', [_PYTHON_BIN_PATH + 'ui-automator', '--notices']
):
ui_automator.run()
mock_exit.assert_called_once()
self.assertIn("""Copyright 2024 Google LLC
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.""", mock_print.call_args.args[0])
@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=_FAKE_PROJECT_FOLDER
)
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', _EXPECTED_APK_PATH]
)
self.mock_android_device.adb.uninstall.assert_not_called()
mock_dirname.assert_called_once()
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(
os.path, 'dirname', autospec=True, return_value=_FAKE_PROJECT_FOLDER
)
def test_load_snippet_should_not_install_apk_when_correct_apk_installed(
self, mock_dirname, mock_get_all_instances
):
self.mock_android_device.adb.shell.side_effect = [
b'package:com.chip.interop.moblysnippet\n',
f'versionName={version.VERSION}'.encode('utf-8'),
]
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()
self.mock_android_device.adb.uninstall.assert_not_called()
mock_dirname.assert_not_called()
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(
os.path, 'dirname', autospec=True, return_value=_FAKE_PROJECT_FOLDER
)
def test_load_snippet_uninstalls_apk_before_installing_it_with_incorrect_apk(
self, mock_dirname, mock_get_all_instances
):
self.mock_android_device.adb.shell.side_effect = [
b'package:com.chip.interop.moblysnippet\n',
b'versionName=fake.version',
]
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.uninstall.assert_called_with(
'com.chip.interop.moblysnippet'
)
self.mock_android_device.adb.install.assert_called_once_with(
['-r', '-g', _EXPECTED_APK_PATH]
)
mock_dirname.assert_called_once()
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(
os.path, 'dirname', autospec=True, return_value=_FAKE_PROJECT_FOLDER
)
def test_load_snippet_installs_apk_when_no_apk_installed(
self, mock_dirname, mock_get_all_instances
):
self.mock_android_device.adb.shell.side_effect = [
b'package:installed.apk\n',
b'Unable to find package\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.uninstall.assert_not_called()
self.mock_android_device.adb.install.assert_called_once_with(
['-r', '-g', _EXPECTED_APK_PATH]
)
mock_dirname.assert_called_once()
@mock.patch.object(ui_automator.UIAutomator, 'load_device', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
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', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
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', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
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', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
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(ui_automator.UIAutomator, 'unload_snippet', autospec=True)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
def test_get_android_device_ready_calls_unload_snippet_on_finish(
self, mock_get_all_instances, mock_unload_snippet
):
mock_get_all_instances.return_value = [self.mock_android_device]
@ui_automator.get_android_device_ready
def decorated_function(self):
del self
pass
decorated_function(self.ui_automator)
mock_unload_snippet.assert_called_once()
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
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', autospec=True)
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"]}).'
)
self.mock_android_device.mbs.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', autospec=True)
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', autospec=True)
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', autospec=True)
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(sys, 'exit', autospec=True)
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
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_FOR_COMMISSION):
ui_automator.run()
mock_commission_device.assert_called_once_with(
mock.ANY, 'm5stack', '34970112332', 'Office'
)
mock_exit.assert_called_once()
@flagsaver.flagsaver((ui_automator._COMMISSION, ['m5']))
def test_commission_with_cmd_invalid_arg_should_raise_an_error(self):
with mock.patch.object(
sys, 'argv', _FAKE_SYS_ARGV_FOR_COMMISSION_WITH_INVALID_LENGTH
):
with self.assertRaises(flags.IllegalFlagValueError):
ui_automator.run()
self.mock_android_device.mbs.commissionDevice.assert_not_called()
@mock.patch.object(sys.stderr, 'write', autospec=True)
@mock.patch.object(sys, 'exit', autospec=True)
def test_commission_with_cmd_invalid_args_should_stderr(
self, mock_exit, mock_stderr_write
):
with mock.patch.object(
sys, 'argv', _FAKE_SYS_ARGV_FOR_COMMISSION_WITH_EMPTY_VALUE
):
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: Missing value for flag --commission\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()
@flagsaver.flagsaver((ui_automator._DECOMMISSION, 'm5stack'))
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
@mock.patch.object(sys, 'exit', autospec=True)
@mock.patch.object(
ui_automator.UIAutomator, 'decommission_device', autospec=True
)
def test_run_calls_decommission_device_with_valid_arguments(
self, mock_decommission_device, mock_exit, mock_commission_device
):
with mock.patch.object(sys, 'argv', _FAKE_VALID_SYS_ARGV_FOR_DECOMMISSION):
ui_automator.run()
mock_commission_device.assert_not_called()
mock_decommission_device.assert_called_once_with(mock.ANY, 'm5stack')
mock_exit.assert_called_once()
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
def test_decommission_device_raises_an_error_with_invalid_device_name(
self, mock_get_all_instances
):
mock_get_all_instances.return_value = [self.mock_android_device]
expected_error_message = (
'Value of DeviceName is invalid. Device name should be no more than 24'
' characters.'
)
with self.assertRaisesRegex(ValueError, expected_error_message):
self.ui_automator.decommission_device('device_name_longer_than_24_chars')
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
def test_decommission_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 = (
f'Unable to remove {_FAKE_MATTER_DEVICE_NAME} from GHA'
f' on device({self.mock_android_device.device_info["serial"]}).'
)
self.mock_android_device.mbs.removeDevice.side_effect = Exception(
'Can not remove the device.'
)
with self.assertRaises(errors.MoblySnippetError) as exc:
self.ui_automator.decommission_device(_FAKE_MATTER_DEVICE_NAME)
self.assertIn(expected_error_message, str(exc.exception))
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
def test_decommission_device_successfully_removes_a_device(
self, mock_get_all_instances
):
mock_get_all_instances.return_value = [self.mock_android_device]
with self.assertLogs() as cm:
self.ui_automator.decommission_device(_FAKE_MATTER_DEVICE_NAME)
self.mock_android_device.mbs.removeDevice.assert_called_once_with(
_FAKE_MATTER_DEVICE_NAME
)
self.assertEqual(
cm.output[2], 'INFO:root:Successfully remove the device on GHA.'
)
@flagsaver.flagsaver((ui_automator._RUN_REGRESSION_TESTS, True))
@flagsaver.flagsaver((ui_automator._REPEAT, 5))
@flagsaver.flagsaver(
(ui_automator._COMMISSION, ['m5stack', '34970112332', 'Office'])
)
@mock.patch.object(
ui_automator.UIAutomator, 'run_regression_tests', autospec=True
)
@mock.patch.object(sys, 'exit', autospec=True)
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
def test_run_calls_run_regression_tests_with_valid_arguments(
self, mock_commission_device, mock_exit, mock_run_regression_tests
):
with mock.patch.object(
sys, 'argv', _FAKE_VALID_SYS_ARGV_FOR_REGRESSION_TESTS
):
ui_automator.run()
mock_run_regression_tests.assert_called_once_with(
mock.ANY,
3,
ui_automator.RegTestSuiteType.COMMISSION,
device_name='m5stack',
pairing_code='34970112332',
gha_room='Office',
)
mock_commission_device.assert_not_called()
mock_exit.assert_called_once()
@flagsaver.flagsaver((ui_automator._RUN_REGRESSION_TESTS, True))
@flagsaver.flagsaver((ui_automator._REPEAT, 5))
@mock.patch.object(
ui_automator.UIAutomator, 'run_regression_tests', autospec=True
)
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
def test_run_regression_tests_with_invalid_args_should_raise_an_error(
self, mock_commission_device, mock_run_regression_tests
):
with mock.patch.object(
sys, 'argv', _FAKE_INVALID_SYS_ARGV_FOR_REGRESSION_TESTS
):
with self.assertRaisesRegex(
flags.IllegalFlagValueError,
(
'Use --regtest to run regression tests infinitely. Add --repeat'
' <repeat-times> to stop after repeat-times. `--regtest` must be'
' used with `--commission`.'
),
):
ui_automator.run()
mock_commission_device.assert_not_called()
mock_run_regression_tests.assert_not_called()
@mock.patch.object(time, 'sleep', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
@mock.patch.object(
ui_automator.UIAutomator, 'decommission_device', autospec=True
)
def test_run_regression_tests_should_be_conducted_for_the_designated_number_of_cycles(
self,
mock_decommission_device,
mock_commission_device,
mock_get_all_instances,
mock_open,
mock_sleep,
):
mock_sleep.return_value = None
mock_get_all_instances.return_value = [self.mock_android_device]
with self.assertLogs() as cm:
self.ui_automator.run_regression_tests(
5,
ui_automator.RegTestSuiteType.COMMISSION,
device_name='m5stack',
pairing_code='34970112332',
gha_room='Office',
)
self.assertEqual(mock_commission_device.call_count, 5)
self.assertEqual(mock_decommission_device.call_count, 5)
self.assertEqual(
cm.output[0], 'INFO:root:Start running regression tests 5 times.'
)
self.assertEqual(mock_open.call_count, 2)
@mock.patch.object(time, 'sleep', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
@mock.patch.object(
ui_automator.UIAutomator, 'decommission_device', autospec=True
)
@mock.patch.object(ui_automator.UIAutomator, 'is_device_exist', autospec=True)
def test_run_regression_tests_executes_for_given_number_of_times_with_failure(
self,
mock_is_device_exist,
mock_decommission_device,
mock_commission_device,
mock_get_all_instances,
mock_open,
mock_sleep,
):
mock_sleep.return_value = None
mock_commission_device.side_effect = [
None,
None,
errors.MoblySnippetError('fake_error'),
None,
None,
]
mock_is_device_exist.side_effect = [True, True, False, True, True]
mock_get_all_instances.return_value = [self.mock_android_device]
with self.assertLogs() as cm:
self.ui_automator.run_regression_tests(
5,
ui_automator.RegTestSuiteType.COMMISSION,
device_name='m5stack',
pairing_code='34970112332',
gha_room='Office',
)
self.assertEqual(mock_commission_device.call_count, 5)
self.assertEqual(mock_is_device_exist.call_count, 5)
self.assertEqual(mock_decommission_device.call_count, 4)
self.assertEqual(
cm.output[0], 'INFO:root:Start running regression tests 5 times.'
)
self.assertEqual(mock_open.call_count, 2)
def test_run_regression_tests_raises_an_error_with_invalid_input(self):
with self.assertRaisesRegex(
ValueError, 'Number placed after `--repeat` must be positive.'
):
self.ui_automator.run_regression_tests(
-5,
ui_automator.RegTestSuiteType.COMMISSION,
device_name='m5stack',
pairing_code='34970112332',
gha_room='Office',
)
@mock.patch.object(time, 'sleep', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
@mock.patch.object(
ui_automator.UIAutomator, 'decommission_device', autospec=True
)
@mock.patch.object(ui_automator.UIAutomator, 'is_device_exist', autospec=True)
def test_run_regression_tests_runs_continuously_until_keyboard_interrupts(
self,
mock_is_device_exist,
mock_decommission_device,
mock_commission_device,
mock_get_all_instances,
mock_open,
mock_sleep,
):
mock_sleep.return_value = None
mock_commission_device.side_effect = [
None,
errors.MoblySnippetError('fake_error'),
None,
errors.MoblySnippetError('fake_error'),
None,
KeyboardInterrupt(),
]
mock_is_device_exist.side_effect = [True, False, True, False, True]
mock_get_all_instances.return_value = [self.mock_android_device]
with self.assertLogs() as cm:
self.ui_automator.run_regression_tests(
None,
ui_automator.RegTestSuiteType.COMMISSION,
device_name='m5stack',
pairing_code='34970112332',
gha_room='Office',
)
self.assertEqual(mock_commission_device.call_count, 6)
self.assertEqual(mock_decommission_device.call_count, 3)
self.assertEqual(mock_is_device_exist.call_count, 5)
self.assertEqual(
cm.output[0], 'INFO:root:Start running regression tests continuously.'
)
self.assertEqual(mock_open.call_count, 2)
@flagsaver.flagsaver(
(ui_automator._COMMISSION, ['m5stack', '34970112332', 'Office'])
)
@mock.patch.object(sys, 'exit', autospec=True)
@mock.patch.object(time, 'time', autospec=True)
@mock.patch.object(time, 'sleep', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
@mock.patch.object(
ui_automator.UIAutomator, 'decommission_device', autospec=True
)
@mock.patch.object(ui_automator.UIAutomator, 'is_device_exist', autospec=True)
def test_run_calls_run_regression_tests_and_produces_summary_in_txt(
self,
mock_is_device_exist,
mock_decommission_device,
mock_commission_device,
mock_get_all_instances,
mock_open,
mock_sleep,
mock_time,
mock_exit,
):
txt_stream = io.StringIO()
mock_sleep.return_value = None
fake_error = errors.MoblySnippetError('error')
mock_commission_device.side_effect = [
fake_error,
None,
None,
]
mock_decommission_device.side_effect = [
None,
fake_error,
]
mock_is_device_exist.side_effect = [False, True, True]
mock_get_all_instances.return_value = [self.mock_android_device]
mock_open.side_effect = [io.StringIO(), txt_stream]
# mock_time called by startTestRun, startTest, stopTest, and stopTestRun.
# startTest and stopTest will be called when running test cases.
# There are 3 test_commission and 3 test_decommission in this test suite.
# So 6 test cases will call mock_time for 12 times.
mock_time.side_effect = [0] + list(range(12)) + [11]
expected_summary = unit_test_utils.make_summary(
test_date=time.strftime('%Y/%m/%d', time.localtime(0)),
duration=test_reporter.duration_formatter(11),
total_runs=3,
total_successful_runs=1,
)
expected_test_case_result = unit_test_utils.make_test_case_result(
3,
res_of_test_commission=['FAIL', 'PASS', 'PASS'],
res_of_test_decommission=['N/A', 'PASS', 'FAIL'],
)
err = (errors.MoblySnippetError, fake_error, fake_error.__traceback__)
fake_err_msg = ''.join(traceback.format_exception(*err))
with mock.patch.object(txt_stream, 'close'):
with mock.patch.object(
test_reporter.TestResult,
'_exc_info_to_string',
return_value=fake_err_msg,
):
with mock.patch.object(
sys, 'argv', _FAKE_VALID_SYS_ARGV_FOR_REGRESSION_TESTS
):
ui_automator.run()
self.assertEqual(mock_commission_device.call_count, 3)
self.assertEqual(mock_is_device_exist.call_count, 3)
self.assertEqual(mock_decommission_device.call_count, 2)
self.assertEqual(
expected_summary + '\n\n' + expected_test_case_result,
txt_stream.getvalue(),
)
mock_exit.assert_called_once()
@flagsaver.flagsaver(
(ui_automator._COMMISSION, ['m5stack', '34970112332', 'Office'])
)
@mock.patch.object(sys, 'exit', autospec=True)
@mock.patch.object(time, 'time', autospec=True)
@mock.patch.object(time, 'sleep', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(
ui_automator.UIAutomator, 'commission_device', autospec=True
)
@mock.patch.object(
ui_automator.UIAutomator, 'decommission_device', autospec=True
)
@mock.patch.object(ui_automator.UIAutomator, 'is_device_exist', autospec=True)
def test_run_calls_run_regression_tests_and_produces_summary_in_xml(
self,
mock_is_device_exist,
mock_decommission_device,
mock_commission_device,
mock_get_all_instances,
mock_open,
mock_sleep,
mock_time,
mock_exit,
):
xml_stream = io.StringIO()
mock_sleep.return_value = None
fake_error = errors.MoblySnippetError('error')
mock_commission_device.side_effect = [
fake_error,
None,
None,
]
mock_is_device_exist.side_effect = [False, True, True]
mock_decommission_device.side_effect = [
None,
fake_error,
]
mock_get_all_instances.return_value = [self.mock_android_device]
mock_open.side_effect = [xml_stream, io.StringIO()]
# mock_time called by startTestRun, startTest, stopTest, and stopTestRun.
# startTest and stopTest will be called when running test cases.
# There are 3 test_commission and 3 test_decommission in this test suite.
# So 6 test cases will call mock_time for 12 times.
mock_time.side_effect = [0] + list(range(12)) + [11]
expected_test_suite_re = unit_test_utils.OUTPUT_STRING % {
'suite_name': 'CommissionRegTest',
'tests': 6,
'failures': 0,
'errors': 2,
'run_time': 11,
'start_time': re.escape(unit_test_utils.iso_timestamp(0)),
}
err = (errors.MoblySnippetError, fake_error, fake_error.__traceback__)
fake_err_msg = ''.join(traceback.format_exception(*err))
expected_testcase1_re = unit_test_utils.TESTCASE_STRING_WITH_ERRORS % {
'run_time': 1,
'start_time': re.escape(unit_test_utils.iso_timestamp(0)),
'test_name': 'test_commission',
'class_name': 'google3.java.com.google.assistant.verticals.homeautomation.partners.ui_automator.commission_reg_test.CommissionRegTest',
'status': 'run',
'result': 'FAIL',
'message': xml_reporter._escape_xml_attr(str(err[1])),
'error_type': xml_reporter._escape_xml_attr(str(err[0])),
'error_msg': xml_reporter._escape_cdata(fake_err_msg),
}
expected_testcase2_re = unit_test_utils.TESTCASE_STRING_WITH_PROPERTIES % {
'run_time': 1,
'start_time': re.escape(unit_test_utils.iso_timestamp(2)),
'test_name': 'test_decommission',
'class_name': 'google3.java.com.google.assistant.verticals.homeautomation.partners.ui_automator.commission_reg_test.CommissionRegTest',
'status': 'notrun',
'result': 'N/A',
'properties': (
' <property name="skip_reason" value="%s"></property>'
% (xml_reporter._escape_xml_attr('Device was not commissioned.'),)
),
'message': '',
}
expected_testcase3_re = unit_test_utils.TESTCASE_STRING % {
'run_time': 1,
'start_time': re.escape(unit_test_utils.iso_timestamp(4)),
'test_name': 'test_commission',
'class_name': 'google3.java.com.google.assistant.verticals.homeautomation.partners.ui_automator.commission_reg_test.CommissionRegTest',
'status': 'run',
'result': 'PASS',
}
expected_testcase4_re = unit_test_utils.TESTCASE_STRING % {
'run_time': 1,
'start_time': re.escape(unit_test_utils.iso_timestamp(6)),
'test_name': 'test_decommission',
'class_name': 'google3.java.com.google.assistant.verticals.homeautomation.partners.ui_automator.commission_reg_test.CommissionRegTest',
'status': 'run',
'result': 'PASS',
}
expected_testcase5_re = unit_test_utils.TESTCASE_STRING % {
'run_time': 1,
'start_time': re.escape(unit_test_utils.iso_timestamp(8)),
'test_name': 'test_commission',
'class_name': 'google3.java.com.google.assistant.verticals.homeautomation.partners.ui_automator.commission_reg_test.CommissionRegTest',
'status': 'run',
'result': 'PASS',
}
expected_testcase6_re = unit_test_utils.TESTCASE_STRING_WITH_ERRORS % {
'run_time': 1,
'start_time': re.escape(unit_test_utils.iso_timestamp(10)),
'test_name': 'test_decommission',
'class_name': 'google3.java.com.google.assistant.verticals.homeautomation.partners.ui_automator.commission_reg_test.CommissionRegTest',
'status': 'run',
'result': 'FAIL',
'message': xml_reporter._escape_xml_attr(str(err[1])),
'error_type': xml_reporter._escape_xml_attr(str(err[0])),
'error_msg': xml_reporter._escape_cdata(fake_err_msg),
}
with mock.patch.object(
test_reporter.TestResult,
'_exc_info_to_string',
return_value=fake_err_msg,
):
with mock.patch.object(
sys, 'argv', _FAKE_VALID_SYS_ARGV_FOR_REGRESSION_TESTS
):
ui_automator.run()
self.assertEqual(mock_commission_device.call_count, 3)
self.assertEqual(mock_is_device_exist.call_count, 3)
self.assertEqual(mock_decommission_device.call_count, 2)
(testcases,) = re.search(
expected_test_suite_re, xml_stream.getvalue()
).groups()
[testcase1, testcase2, testcase3, testcase4, testcase5, testcase6] = (
testcases.split('\n </testcase>\n')
)
self.assertRegex(testcase1, expected_testcase1_re)
self.assertRegex(testcase2, expected_testcase2_re)
self.assertRegex(testcase3, expected_testcase3_re)
self.assertRegex(testcase4, expected_testcase4_re)
self.assertRegex(testcase5, expected_testcase5_re)
self.assertRegex(testcase6, expected_testcase6_re)
mock_exit.assert_called_once()
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
def test_get_report_info_includes_apk_versions_with_device_connected(
self,
mock_load_snippet,
mock_get_all_instances,
):
mock_get_all_instances.return_value = [self.mock_android_device]
self.mock_android_device.adb.shell.side_effect = [
b'versionName=0.0.0\n',
b'versionName=0.0.1\n',
]
report_info = self.ui_automator.get_report_info()
mock_load_snippet.assert_called_once()
self.assertDictEqual(
report_info, {'gha_version': '0.0.0', 'gms_core_version': '0.0.1'}
)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
def test_get_report_info_raises_an_error_when_no_device_connected(
self, mock_get_all_instances
):
mock_get_all_instances.return_value = []
with self.assertRaisesRegex(
errors.AndroidDeviceNotReadyError, 'get_report_info failed.'
):
self.ui_automator.get_report_info()
@mock.patch.object(time, 'sleep', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'get_report_info', autospec=True)
def test_run_regression_tests_should_insert_report_info_into_summary(
self,
mock_get_report_info,
mock_get_all_instances,
mock_open,
mock_sleep,
):
mock_sleep.return_value = None
fake_report_info: test_reporter.ReportInfo = {
'gha_version': '0.0.0',
'gms_core_version': '0.0.1',
'hub_version': '10.1.3',
'dut': 'm5stack',
'device_firmware': '10.20.12',
}
mock_get_report_info.return_value = fake_report_info
mock_get_all_instances.return_value = [self.mock_android_device]
with mock.patch.object(
test_reporter.TestResult, 'write_summary_in_txt', autospec=True
) as mock_write_summary_in_txt:
self.ui_automator.run_regression_tests(
3,
ui_automator.RegTestSuiteType.COMMISSION,
device_name='m5stack',
pairing_code='34970112332',
gha_room='Office',
)
self.assertEqual(
mock_write_summary_in_txt.call_args.kwargs.get('report_info'),
fake_report_info,
)
mock_open.assert_called_once()
@flagsaver.flagsaver((ui_automator._DUT, ['model', 'type', 'protocol']))
@mock.patch.object(ui_automator.UIAutomator, 'load_device', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
def test_get_report_info_includes_dut_value_from_flag_input(
self, mock_load_snippet, mock_load_device
):
report_info = self.ui_automator.get_report_info()
mock_load_device.assert_called_once()
mock_load_snippet.assert_called_once()
self.assertDictEqual(report_info, {'dut': '<model, type, protocol>'})
@flagsaver.flagsaver((ui_automator._DUT, None))
@mock.patch.object(ui_automator.UIAutomator, 'load_device', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
def test_get_report_info_returns_empty_dict_without_dut_flag_input(
self, mock_load_snippet, mock_load_device
):
report_info = self.ui_automator.get_report_info()
mock_load_device.assert_called_once()
mock_load_snippet.assert_called_once()
self.assertDictEqual(report_info, {})
@flagsaver.flagsaver((ui_automator._HUB, '10.1.3'))
@mock.patch.object(ui_automator.UIAutomator, 'load_device', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
def test_get_report_info_includes_hub_value_from_flag_input(
self, mock_load_snippet, mock_load_device
):
report_info = self.ui_automator.get_report_info()
mock_load_device.assert_called_once()
mock_load_snippet.assert_called_once()
self.assertDictEqual(report_info, {'hub_version': '10.1.3'})
@flagsaver.flagsaver((ui_automator._HUB, None))
@mock.patch.object(ui_automator.UIAutomator, 'load_device', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
def test_get_report_info_returns_empty_dict_without_hub_flag_input(
self, mock_load_snippet, mock_load_device
):
report_info = self.ui_automator.get_report_info()
mock_load_device.assert_called_once()
mock_load_snippet.assert_called_once()
self.assertDictEqual(report_info, {})
@flagsaver.flagsaver((ui_automator._DEVICE_FIRMWARE, '10.20.12'))
@mock.patch.object(ui_automator.UIAutomator, 'load_device', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
def test_get_report_info_includes_device_firmware_from_flag_input(
self, mock_load_snippet, mock_load_device
):
report_info = self.ui_automator.get_report_info()
mock_load_device.assert_called_once()
mock_load_snippet.assert_called_once()
self.assertDictEqual(report_info, {'device_firmware': '10.20.12'})
@flagsaver.flagsaver((ui_automator._DEVICE_FIRMWARE, None))
@mock.patch.object(ui_automator.UIAutomator, 'load_device', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
def test_get_report_info_returns_empty_dict_without_device_firmware_flag(
self, mock_load_snippet, mock_load_device
):
report_info = self.ui_automator.get_report_info()
mock_load_device.assert_called_once()
mock_load_snippet.assert_called_once()
self.assertDictEqual(report_info, {})
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
@mock.patch.object(ui_automator.UIAutomator, 'load_snippet', autospec=True)
def test_get_report_info_should_not_write_none_in_report_info(
self,
mock_load_snippet,
mock_get_all_instances,
):
mock_get_all_instances.return_value = [self.mock_android_device]
self.mock_android_device.adb.shell.side_effect = [
# Response for getting gha version.
b'Unable to find package\n',
# Response for getting gms core version.
b'Unable to find package\n',
]
report_info = self.ui_automator.get_report_info()
mock_load_snippet.assert_called_once()
self.assertDictEqual(report_info, {})
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
def test_is_device_exist_returns_true_when_device_exists(
self, mock_get_all_instances
):
mock_get_all_instances.return_value = [self.mock_android_device]
self.mock_android_device.mbs.isDeviceExist.return_value = True
self.assertTrue(self.ui_automator.is_device_exist(_FAKE_MATTER_DEVICE_NAME))
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
def test_is_device_exist_returns_false_when_device_does_not_exist(
self, mock_get_all_instances
):
mock_get_all_instances.return_value = [self.mock_android_device]
self.mock_android_device.mbs.isDeviceExist.return_value = False
self.assertFalse(
self.ui_automator.is_device_exist(_FAKE_MATTER_DEVICE_NAME)
)
@mock.patch.object(android_device, 'get_all_instances', autospec=True)
def test_is_device_exist_throws_an_error_when_snippet_method_throws_an_error(
self, mock_get_all_instances
):
mock_get_all_instances.return_value = [self.mock_android_device]
self.mock_android_device.mbs.isDeviceExist.side_effect = Exception(
'fake-error'
)
with self.assertRaises(errors.MoblySnippetError) as exc:
self.ui_automator.is_device_exist(_FAKE_MATTER_DEVICE_NAME)
expected_error_message = (
f'Unable to check if {_FAKE_MATTER_DEVICE_NAME} exists on GHA'
f' on device({self.mock_android_device.device_info["serial"]}).'
)
self.assertEqual(expected_error_message, str(exc.exception))
if __name__ == '__main__':
unittest.main()