Add regression flag
PiperOrigin-RevId: 585548529
diff --git a/ui_automator/ui_automator.py b/ui_automator/ui_automator.py
index 5a551a7..6670fb1 100644
--- a/ui_automator/ui_automator.py
+++ b/ui_automator/ui_automator.py
@@ -48,17 +48,31 @@
'Use --commission {DeviceName},{PairingCode},{GHARoom} to commission a'
' device to google fabric on GHA.'
)
+_REGRESSION_TESTS_FLAG_USAGE_GUIDE = (
+ 'Use --regtest to run regression tests infinitely. Add --repeat'
+ ' <repeat-times> to stop after repeat-times. `--regtest` must be'
+ ' used with `--commission`.'
+)
_COMMISSION = flags.DEFINE_list(
name='commission',
default=None,
help=_COMMISSIONING_FLAG_USAGE_GUIDE,
)
-
_DECOMMISSION = flags.DEFINE_string(
name='decommission',
default='',
help='Use --decommission {DeviceName} to remove the device on GHA.',
)
+_RUN_REGRESSION_TESTS = flags.DEFINE_boolean(
+ name='regtest',
+ default=False,
+ help=_REGRESSION_TESTS_FLAG_USAGE_GUIDE,
+)
+_REPEAT = flags.DEFINE_integer(
+ name='repeat',
+ default=None,
+ help=_REGRESSION_TESTS_FLAG_USAGE_GUIDE,
+)
def _validate_commissioning_arg(
@@ -294,6 +308,50 @@
f' on device({self._connected_device.device_info["serial"]}).'
) from e
+ def run_regression_tests(
+ self, repeat: int | None, *args: Any,
+ ) -> None:
+ """Executes automated regression tests.
+
+ A single execution of both commissioning and decommissioning constitutes one
+ cycle.
+
+ Args:
+ repeat: The value of flag `repeat`. If the value is None, regression
+ tests will be run repeatedly until keyboard interrupts.
+ *args: Any required value to run regression tests.
+
+ Raises:
+ ValueError: When the value of flag `repeat` is not positive.
+ """
+ if repeat and repeat <= 0:
+ raise ValueError('Number placed after `--repeat` must be positive.')
+
+ failure_count = 0
+ run_count = 0
+ self._logger.info(
+ 'Start running regression tests'
+ f' {str(repeat) + " times" if repeat is not None else "continuously"}.'
+ )
+ while repeat is None or run_count < repeat:
+ try:
+ device_name, _, _ = args
+ self.commission_device(*args)
+ self.decommission_device(device_name)
+ except errors.MoblySnippetError:
+ failure_count += 1
+ except KeyboardInterrupt:
+ self._logger.info('Tests interrupted by keyboard.')
+ break
+ run_count += 1
+
+ self._logger.info(
+ 'Ran %d times. Passed %d times. Failed %d times.',
+ run_count,
+ run_count - failure_count,
+ failure_count,
+ )
+
def _get_mbs_apk_path(self) -> str:
return os.path.join(
os.path.dirname(os.path.abspath(__file__)),
@@ -308,11 +366,14 @@
if _COMMISSION.value:
if len(_COMMISSION.value) != 3:
raise flags.IllegalFlagValueError(_COMMISSIONING_FLAG_USAGE_GUIDE)
-
- ui_automator.commission_device(*_COMMISSION.value)
-
- if _DECOMMISSION.value:
+ if _RUN_REGRESSION_TESTS.value:
+ ui_automator.run_regression_tests(_REPEAT.value, *_COMMISSION.value)
+ else:
+ ui_automator.commission_device(*_COMMISSION.value)
+ elif _DECOMMISSION.value:
ui_automator.decommission_device(_DECOMMISSION.value)
+ elif _RUN_REGRESSION_TESTS.value:
+ raise flags.IllegalFlagValueError(_REGRESSION_TESTS_FLAG_USAGE_GUIDE)
# TODO(b/309745485): Type of argv should be Sequence[str].
diff --git a/ui_automator/ui_automator_test.py b/ui_automator/ui_automator_test.py
index 671d41a..1e44ee9 100644
--- a/ui_automator/ui_automator_test.py
+++ b/ui_automator/ui_automator_test.py
@@ -51,6 +51,20 @@
'--decommission',
'm5stack',
]
+_FAKE_VALID_SYS_ARGV_FOR_REGRESSION_TESTS = [
+ _PYTHON_BIN_PATH + 'ui-automator',
+ '--commission',
+ 'm5stack,34970112332,Office',
+ '--regtest',
+ '--repeat',
+ '5',
+]
+_FAKE_INVALID_SYS_ARGV_FOR_REGRESSION_TESTS = [
+ _PYTHON_BIN_PATH + 'ui-automator',
+ '--regtest',
+ '--repeat',
+ '5',
+]
_FAKE_SYS_ARGV_FOR_COMMISSIONING_WITH_INVALID_LENGTH = [
_PYTHON_BIN_PATH + 'ui-automator',
'--commission',
@@ -457,12 +471,8 @@
)
mock_exit.assert_called_once()
- @flagsaver.flagsaver(
- (ui_automator._COMMISSION, ['m5'])
- )
- def test_commission_with_cmd_invalid_arg_should_raise_an_error(
- self
- ):
+ @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_COMMISSIONING_WITH_INVALID_LENGTH
):
@@ -561,6 +571,176 @@
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, 5, *['m5stack', '34970112332', '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()
+
+ @flagsaver.flagsaver(
+ (ui_automator._COMMISSION, ['m5stack', '34970112332', 'Office'])
+ )
+ @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_get_all_instances.return_value = [self.mock_android_device]
+
+ with self.assertLogs() as cm:
+ self.ui_automator.run_regression_tests(
+ 5, *['m5stack', '34970112332', '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(
+ cm.output[1], 'INFO:root:Ran 5 times. Passed 5 times. Failed 0 times.'
+ )
+
+ @flagsaver.flagsaver(
+ (ui_automator._COMMISSION, ['m5stack', '34970112332', 'Office'])
+ )
+ @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_executes_for_given_number_of_times_with_failure(
+ self,
+ mock_decommission_device,
+ mock_commission_device,
+ mock_get_all_instances,
+ ):
+ mock_commission_device.side_effect = [
+ None,
+ None,
+ errors.MoblySnippetError('fake_error'),
+ None,
+ None,
+ ]
+ mock_get_all_instances.return_value = [self.mock_android_device]
+
+ with self.assertLogs() as cm:
+ self.ui_automator.run_regression_tests(
+ 5, *['m5stack', '34970112332', 'Office']
+ )
+
+ self.assertEqual(mock_commission_device.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(
+ cm.output[1], 'INFO:root:Ran 5 times. Passed 4 times. Failed 1 times.'
+ )
+
+ 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)
+
+ @flagsaver.flagsaver(
+ (ui_automator._COMMISSION, ['m5stack', '34970112332', 'Office'])
+ )
+ @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_runs_continuously_until_keyboard_interrupts(
+ self,
+ mock_decommission_device,
+ mock_commission_device,
+ mock_get_all_instances,
+ ):
+ mock_commission_device.side_effect = [
+ None,
+ errors.MoblySnippetError('fake_error'),
+ None,
+ errors.MoblySnippetError('fake_error'),
+ None,
+ KeyboardInterrupt(),
+ ]
+ mock_get_all_instances.return_value = [self.mock_android_device]
+ with self.assertLogs() as cm:
+ self.ui_automator.run_regression_tests(
+ None, *['m5stack', '34970112332', 'Office']
+ )
+
+ self.assertEqual(mock_commission_device.call_count, 6)
+ self.assertEqual(mock_decommission_device.call_count, 3)
+ self.assertEqual(
+ cm.output[0], 'INFO:root:Start running regression tests continuously.'
+ )
+ self.assertEqual(cm.output[1], 'INFO:root:Tests interrupted by keyboard.')
+ self.assertEqual(
+ cm.output[2], 'INFO:root:Ran 5 times. Passed 3 times. Failed 2 times.'
+ )
+
if __name__ == '__main__':
unittest.main()