From 841ac398809958342137b18329c6be697cdc6c20 Mon Sep 17 00:00:00 2001 From: kwy404 Date: Sat, 10 Oct 2026 21:15:13 -0300 Subject: [PATCH 1/2] Fix UnboundLocalError when an optional AndroidDevice fails to initialize When `AndroidDevice(serial)` raised for a device configured with `required: False`, the except block logged through `ad.log`, but `ad` was never assigned for that device. This raised UnboundLocalError for the first config, or logged under the previous device otherwise. Log the skip with the serial instead so the device is skipped as intended. --- mobly/controllers/android_device.py | 2 +- tests/mobly/controllers/android_device_test.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py index 06748d76..c0e4845a 100644 --- a/mobly/controllers/android_device.py +++ b/mobly/controllers/android_device.py @@ -332,7 +332,7 @@ def get_instances_with_configs(configs): except Exception: if is_required: raise - ad.log.exception('Skipping this optional device due to error.') + logging.exception('Skipping optional device %s due to error.', serial) continue results.append(ad) return results diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py index 940906c5..3430f902 100755 --- a/tests/mobly/controllers/android_device_test.py +++ b/tests/mobly/controllers/android_device_test.py @@ -251,6 +251,22 @@ def test_get_instances_with_configs_invalid_config(self): ): android_device.get_instances_with_configs([config]) + @mock.patch('mobly.controllers.android_device.list_fastboot_devices') + @mock.patch('mobly.controllers.android_device.list_adb_devices') + @mock.patch('mobly.controllers.android_device.list_adb_devices_by_usb_id') + @mock.patch('mobly.controllers.android_device.AndroidDevice') + def test_get_instances_with_configs_skips_optional_device_on_init_error( + self, mock_ad_class, mock_list_adb_usb, mock_list_adb, mock_list_fastboot + ): + mock_list_fastboot.return_value = [] + mock_list_adb.return_value = ['1', '2'] + mock_list_adb_usb.return_value = [] + mock_ad = mock.MagicMock() + mock_ad_class.side_effect = [Exception('Something went wrong.'), mock_ad] + configs = [{'serial': '1', 'required': False}, {'serial': '2'}] + ads = android_device.get_instances_with_configs(configs) + self.assertEqual(ads, [mock_ad]) + @mock.patch('mobly.controllers.android_device.list_fastboot_devices') @mock.patch('mobly.controllers.android_device.list_adb_devices') @mock.patch('mobly.controllers.android_device.list_adb_devices_by_usb_id') From 22c5f0c0e956dd43f5f1639689402ddbd542f7cc Mon Sep 17 00:00:00 2001 From: kwy404 Date: Mon, 12 Oct 2026 16:06:58 -0300 Subject: [PATCH 2/2] Assert the skip log and cover a later optional device in the tests --- .../mobly/controllers/android_device_test.py | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py index 3430f902..1486d590 100755 --- a/tests/mobly/controllers/android_device_test.py +++ b/tests/mobly/controllers/android_device_test.py @@ -262,10 +262,54 @@ def test_get_instances_with_configs_skips_optional_device_on_init_error( mock_list_adb.return_value = ['1', '2'] mock_list_adb_usb.return_value = [] mock_ad = mock.MagicMock() - mock_ad_class.side_effect = [Exception('Something went wrong.'), mock_ad] + + def _make_ad(serial): + if serial == '1': + raise Exception('Something went wrong.') + return mock_ad + + mock_ad_class.side_effect = _make_ad configs = [{'serial': '1', 'required': False}, {'serial': '2'}] - ads = android_device.get_instances_with_configs(configs) + with self.assertLogs(level='ERROR') as cm: + ads = android_device.get_instances_with_configs(configs) + self.assertEqual(ads, [mock_ad]) + self.assertTrue( + any( + 'Skipping optional device 1 due to error.' in line + for line in cm.output + ) + ) + mock_ad.log.exception.assert_not_called() + + @mock.patch('mobly.controllers.android_device.list_fastboot_devices') + @mock.patch('mobly.controllers.android_device.list_adb_devices') + @mock.patch('mobly.controllers.android_device.list_adb_devices_by_usb_id') + @mock.patch('mobly.controllers.android_device.AndroidDevice') + def test_get_instances_with_configs_skips_later_optional_device_on_init_error( + self, mock_ad_class, mock_list_adb_usb, mock_list_adb, mock_list_fastboot + ): + mock_list_fastboot.return_value = [] + mock_list_adb.return_value = ['1', '2'] + mock_list_adb_usb.return_value = [] + mock_ad = mock.MagicMock() + + def _make_ad(serial): + if serial == '2': + raise Exception('Something went wrong.') + return mock_ad + + mock_ad_class.side_effect = _make_ad + configs = [{'serial': '1'}, {'serial': '2', 'required': False}] + with self.assertLogs(level='ERROR') as cm: + ads = android_device.get_instances_with_configs(configs) self.assertEqual(ads, [mock_ad]) + self.assertTrue( + any( + 'Skipping optional device 2 due to error.' in line + for line in cm.output + ) + ) + mock_ad.log.exception.assert_not_called() @mock.patch('mobly.controllers.android_device.list_fastboot_devices') @mock.patch('mobly.controllers.android_device.list_adb_devices')