Merge "Added serials to the raised AndroidDeviceErrors"
diff --git a/acts/framework/acts/controllers/android_device.py b/acts/framework/acts/controllers/android_device.py
index c809f5d..2f72d65 100755
--- a/acts/framework/acts/controllers/android_device.py
+++ b/acts/framework/acts/controllers/android_device.py
@@ -64,15 +64,17 @@
 RELEASE_ID_REGEXES = [re.compile(r'\w+\.\d+\.\d+'), re.compile(r'N\w+')]
 
 
+class AndroidDeviceConfigError(Exception):
+    """Raised when AndroidDevice configs are malformatted."""
+
+
 class AndroidDeviceError(error.ActsError):
-    """Raised when there is an error in AndroidDevice
-    """
-    pass
+    """Raised when there is an error in AndroidDevice."""
 
 
 class DoesNotExistError(AndroidDeviceError):
-    """Raised when something that does not exist is referenced.
-    """
+    """Raised when something that does not exist is referenced."""
+
 
 
 def create(configs):
@@ -86,11 +88,11 @@
         A list of AndroidDevice objects.
     """
     if not configs:
-        raise AndroidDeviceError(ANDROID_DEVICE_EMPTY_CONFIG_MSG)
+        raise AndroidDeviceConfigError(ANDROID_DEVICE_EMPTY_CONFIG_MSG)
     elif configs == ANDROID_DEVICE_PICK_ALL_TOKEN:
         ads = get_all_instances()
     elif not isinstance(configs, list):
-        raise AndroidDeviceError(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG)
+        raise AndroidDeviceConfigError(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG)
     elif isinstance(configs[0], str):
         # Configs is a list of serials.
         ads = get_instances(configs)
@@ -103,7 +105,8 @@
     for ad in ads:
         if not ad.is_connected():
             raise DoesNotExistError(("Android device %s is specified in config"
-                                     " but is not attached.") % ad.serial)
+                                     " but is not attached.") % ad.serial,
+                                    serial=ad.serial)
     _start_services_on_ads(ads)
     return ads
 
@@ -163,17 +166,19 @@
     for ad in ads:
         running_ads.append(ad)
         if not ad.ensure_screen_on():
-            ad.log.error("User window cannot come up")
+            ad.log.error('User window cannot come up')
             destroy(running_ads)
-            raise AndroidDeviceError("User window cannot come up")
+            raise AndroidDeviceError('User window cannot come up',
+                                     serial=ad.serial)
         if not ad.skip_sl4a and not ad.is_sl4a_installed():
-            ad.log.error("sl4a.apk is not installed")
+            ad.log.error('sl4a.apk is not installed')
             destroy(running_ads)
-            raise AndroidDeviceError("The required sl4a.apk is not installed")
+            raise AndroidDeviceError('The required sl4a.apk is not installed',
+                                     serial=ad.serial)
         try:
             ad.start_services(skip_sl4a=ad.skip_sl4a)
         except:
-            ad.log.exception("Failed to start some services, abort!")
+            ad.log.exception('Failed to start some services, abort!')
             destroy(running_ads)
             raise
 
@@ -244,12 +249,12 @@
     results = []
     for c in configs:
         try:
-            serial = c.pop("serial")
+            serial = c.pop('serial')
         except KeyError:
-            raise AndroidDeviceError(
+            raise AndroidDeviceConfigError(
                 "Required value 'serial' is missing in AndroidDevice config %s."
                 % c)
-        ssh_config = c.pop("ssh_config", None)
+        ssh_config = c.pop('ssh_config', None)
         ssh_connection = None
         if ssh_config is not None:
             ssh_settings = settings.from_config(ssh_config)
@@ -325,14 +330,14 @@
 
     filtered = filter_devices(ads, _get_device_filter)
     if not filtered:
-        raise AndroidDeviceError(
+        raise ValueError(
             "Could not find a target device that matches condition: %s." %
             kwargs)
     elif len(filtered) == 1:
         return filtered[0]
     else:
         serials = [ad.serial for ad in filtered]
-        raise AndroidDeviceError("More than one device matched: %s" % serials)
+        raise ValueError("More than one device matched: %s" % serials)
 
 
 def take_bug_reports(ads, test_name, begin_time):
@@ -619,7 +624,7 @@
             if hasattr(self, k) and k != "skip_sl4a":
                 raise AndroidDeviceError(
                     "Attempting to set existing attribute %s on %s" %
-                    (k, self.serial))
+                    (k, self.serial), serial=self.serial)
             setattr(self, k, v)
 
     def root_adb(self):
@@ -773,9 +778,10 @@
                               logcat is no longer running.
         """
         if self.is_adb_logcat_on:
-            raise AndroidDeviceError(("Android device {} already has an adb "
-                                      "logcat thread going on. Cannot start "
-                                      "another one.").format(self.serial))
+            raise AndroidDeviceError(
+                'Android device %s already has a running adb logcat thread. '
+                'Cannot start another one.' % self.serial,
+                serial=self.serial)
         # Disable adb log spam filter. Have to stop and clear settings first
         # because 'start' doesn't support --clear option before Android N.
         self.adb.shell("logpersist.stop --clear")
@@ -807,8 +813,9 @@
         """
         if not self.is_adb_logcat_on:
             raise AndroidDeviceError(
-                "Android device %s does not have an ongoing adb logcat "
-                "collection." % self.serial)
+                'Android device %s does not have an ongoing adb logcat '
+                'collection.' % self.serial,
+                serial=self.serial)
         # Set the last timestamp to the current timestamp. This may cause
         # a race condition that allows the same line to be logged twice,
         # but it does not pose a problem for our logging purposes.
@@ -938,7 +945,8 @@
             out = self.adb.shell("bugreportz", timeout=BUG_REPORT_TIMEOUT)
             if not out.startswith("OK"):
                 raise AndroidDeviceError(
-                    "Failed to take bugreport on %s: %s" % (self.serial, out))
+                    'Failed to take bugreport on %s: %s' % (self.serial, out),
+                    serial=self.serial)
             br_out_path = out.split(':')[1].strip().split()[0]
             self.adb.pull("%s %s" % (br_out_path, full_out_path))
         else:
@@ -1147,7 +1155,8 @@
                 pass
             time.sleep(5)
         raise AndroidDeviceError(
-            "Device %s booting process timed out." % self.serial)
+            'Device %s booting process timed out.' % self.serial,
+            serial=self.serial)
 
     def reboot(self, stop_at_lock_screen=False):
         """Reboots the device.
@@ -1195,7 +1204,8 @@
             return
         if not self.ensure_screen_on():
             self.log.error("User window cannot come up")
-            raise AndroidDeviceError("User window cannot come up")
+            raise AndroidDeviceError("User window cannot come up",
+                                     serial=self.serial)
         self.start_services(self.skip_sl4a)
 
     def restart_runtime(self):
@@ -1212,7 +1222,8 @@
         self.root_adb()
         if not self.ensure_screen_on():
             self.log.error("User window cannot come up")
-            raise AndroidDeviceError("User window cannot come up")
+            raise AndroidDeviceError('User window cannot come up',
+                                     serial=self.serial)
         self.start_services(self.skip_sl4a)
 
     def search_logcat(self, matching_string, begin_time=None):
diff --git a/acts/framework/acts/error.py b/acts/framework/acts/error.py
index f11e337..8e89499 100644
--- a/acts/framework/acts/error.py
+++ b/acts/framework/acts/error.py
@@ -10,7 +10,9 @@
         class_name = self.__class__.__name__
         self.message = self.__class__.__doc__
         self.error_code = getattr(ActsErrorCode, class_name)
-        self.extra = args
+        self.extra = kwargs
+        if len(args) > 0:
+            self.extra['details'] = args
 
     def json_str(self):
         """Converts this error to a string in json format.
diff --git a/acts/framework/tests/acts_android_device_test.py b/acts/framework/tests/acts_android_device_test.py
index 17220c4..efbb115 100755
--- a/acts/framework/tests/acts_android_device_test.py
+++ b/acts/framework/tests/acts_android_device_test.py
@@ -182,13 +182,13 @@
 
     def test_create_with_empty_config(self):
         expected_msg = android_device.ANDROID_DEVICE_EMPTY_CONFIG_MSG
-        with self.assertRaisesRegex(android_device.AndroidDeviceError,
+        with self.assertRaisesRegex(android_device.AndroidDeviceConfigError,
                                     expected_msg):
             android_device.create([])
 
     def test_create_with_not_list_config(self):
         expected_msg = android_device.ANDROID_DEVICE_NOT_LIST_CONFIG_MSG
-        with self.assertRaisesRegex(android_device.AndroidDeviceError,
+        with self.assertRaisesRegex(android_device.AndroidDeviceConfigError,
                                     expected_msg):
             android_device.create("HAHA")
 
@@ -212,16 +212,14 @@
         ads = get_mock_ads(5)
         expected_msg = ("Could not find a target device that matches condition"
                         ": {'serial': 5}.")
-        with self.assertRaisesRegex(android_device.AndroidDeviceError,
-                                    expected_msg):
+        with self.assertRaisesRegex(ValueError, expected_msg):
             ad = android_device.get_device(ads, serial=len(ads))
 
     def test_get_device_too_many_matches(self):
         ads = get_mock_ads(5)
         target_serial = ads[1].serial = ads[0].serial
         expected_msg = "More than one device matched: \[0, 0\]"
-        with self.assertRaisesRegex(android_device.AndroidDeviceError,
-                                    expected_msg):
+        with self.assertRaisesRegex(ValueError, expected_msg):
             ad = android_device.get_device(ads, serial=target_serial)
 
     def test_start_services_on_ads(self):
@@ -402,8 +400,8 @@
         start_proc_mock.assert_called_with(
             adb_cmd % (ad.serial, expected_log_path))
         self.assertEqual(ad.adb_logcat_file_path, expected_log_path)
-        expected_msg = ("Android device .* already has an adb logcat thread "
-                        "going on. Cannot start another one.")
+        expected_msg = ('Android device .* already has a running adb logcat '
+                        'thread. Cannot start another one.')
         # Expect error if start is called back to back.
         with self.assertRaisesRegex(android_device.AndroidDeviceError,
                                     expected_msg):
diff --git a/acts/framework/tests/acts_error_test.py b/acts/framework/tests/acts_error_test.py
index 2527b54..e19ad07 100755
--- a/acts/framework/tests/acts_error_test.py
+++ b/acts/framework/tests/acts_error_test.py
@@ -19,18 +19,28 @@
 
 
 class ActsErrorTest(unittest.TestCase):
-    def test_error_without_args(self):
+
+    def test_assert_key_pulled_from_acts_error_code(self):
         e = error.ActsError()
         self.assertEqual(e.error_code, 100)
+
+    def test_assert_description_pulled_from_docstring(self):
+        e = error.ActsError()
         self.assertEqual(e.message, 'Base Acts Error')
-        self.assertEqual(e.extra, ())
+
+    def test_error_without_args(self):
+        e = error.ActsError()
+        self.assertEqual(e.extra['details'], ())
 
     def test_error_with_args(self):
-        args = 'hello'
-        e = error.ActsError(args)
-        self.assertEqual(e.error_code, 100)
-        self.assertEqual(e.message, 'Base Acts Error')
-        self.assertEqual(e.extra, ('hello',))
+        args = ['hello']
+        e = error.ActsError(*args)
+        self.assertEqual(e.extra['details'], args)
+
+    def test_error_with_kwargs(self):
+        e = error.ActsError(key='value')
+        self.assertTrue('key' in e.extra.keys())
+        self.assertTrue('value' in e.extra['key'])
 
 
 if __name__ == '__main__':