Repository navigation
Fix UnboundLocalError when an optional AndroidDevice fails to initialize - #1069
Conversation
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.
xpconanfan
left a comment
There was a problem hiding this comment.
LGTM, thanks for the fix.
Verified: the diagnosis is correct — ad is unbound when AndroidDevice(serial) itself raises, so an optional device's failure turned into UnboundLocalError (first config) or got logged under the previous device (later configs). Switching to logging.exception(..., serial) removes the dependency on ad while keeping the serial in the message. The new test fails on main with UnboundLocalError and passes with the change; the surrounding get_instances_with_configs tests still pass locally.
xpconanfan
left a comment
There was a problem hiding this comment.
The one-line fix is correct, and I confirmed the new test fails on main with UnboundLocalError and passes with the change. Before merging, please tighten the test so it actually pins the behavior this PR fixes:
-
Assert the log, not just the return value.
assertEqual(ads, [mock_ad])would also pass if theexceptbranch were a barecontinue. The point of the fix is that the skip is logged by serial without touchingad, so wrap the call inwith self.assertLogs(level='ERROR') as cm:and assert'Skipping optional device 1 due to error.'is incm.output. -
Cover the "later config" case described in the PR. With the failing device first, the misattribution path (error logged under the previous device's
ad.log) is never exercised. Add a case withconfigs = [{'serial': '1'}, {'serial': '2', 'required': False}]where the second constructor raises, and assertmock_ad.log.exception.assert_not_called()plus the module-level log above. -
Nit: key the failure on the serial rather than call position, so the test doesn't silently depend on construction order:
def _make_ad(serial): if serial == '1': raise Exception('Something went wrong.') return mock_ad mock_ad_class.side_effect = _make_ad
Thanks for the fix!
|
Thanks for the careful review @xpconanfan, all three are done in 22c5f0c:
Both tests fail on main and pass with the fix. |
xpconanfan
left a comment
There was a problem hiding this comment.
LGTM, thanks for addressing the review comments!
Root cause: in
get_instances_with_configs, thetryblock covers bothAndroidDevice(serial)andad.load_config(c). When the constructor itself raises for a device configured withrequired: False, theexceptbranch callsad.log.exception(...), butadwas never assigned for that device. For the first config this raisesUnboundLocalErrorinstead of skipping the optional device; for later configs it logs the error under the previous device.Fix: log the skip with
logging.exceptionand the serial, so it no longer depends onad. One line inmobly/controllers/android_device.py.Test: added
test_get_instances_with_configs_skips_optional_device_on_init_error, where the first (optional) device raises in the constructor and the second succeeds. Before the fix it fails withUnboundLocalError: cannot access local variable 'ad'; after the fix it passes and only the second device is returned.