servo: add ControlUnavailableError, has_control()

This change adds its own error class for servo erros where the control is
unavailable on the servod side. This simplifies some of the code as the autotest
code occasionally requires bridges to ensure compatibility with older and newer
servod versions.

As the new error class inherits from TestFail, existing code should still work.

Additionally, it adds a has_control() function to servo.py

This now allows for two flows:
- if the control is likely available, and just some lab compatibility
issues i.e. slow updating of labstations are to be guarded against,
try/except is probably the way to go
- if the test should handle things differently whether a control is
available or not, has_control() is likely the way to go.

BUG=chromium:924434
TEST=test_that --autotest_dir . $DUP_IP power_ServoChargeStress.3loop

// This test uses servo (thus goes through initiation flow) and uses a few
// controls. To verify set/get still work as expected.

Change-Id: I75738fad629faa5c93318d30246c894dcf900227
Signed-off-by: Ruben Rodriguez Buchillon <coconutruben@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/1749608
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Reviewed-by: Wai-Hong Tam <waihong@google.com>
Reviewed-by: Mary Ruthven <mruthven@chromium.org>
diff --git a/server/cros/servo/servo.py b/server/cros/servo/servo.py
index e076801..23dd258 100644
--- a/server/cros/servo/servo.py
+++ b/server/cros/servo/servo.py
@@ -23,6 +23,14 @@
 _USB_PROBE_TIMEOUT = 40
 
 
+# Regex to match XMLRPC errors due to a servod control not existing.
+NO_CONTROL_RE = re.compile(r'No control named (\w*\.?\w*)')
+
+class ControlUnavailableError(error.TestFail):
+    """Custom error class to indicate a control is unavailable on servod."""
+    pass
+
+
 def _extract_image_from_tarball(tarball, dest_dir, image_candidates):
     """Try extracting the image_candidates from the tarball.
 
@@ -634,19 +642,47 @@
         """
         return re.sub('^.*>:', '', xmlexc.faultString)
 
+    def has_control(self, control):
+        """Query servod server to determine if |control| is a valid control.
+
+        @param control: str, control name to query
+
+        @returns: true if |control| is a known control, false otherwise.
+        """
+        assert control
+        try:
+            # If the control exists, doc() will work.
+            self._server.doc(control)
+            return True
+        except xmlrpclib.Fault as e:
+            if re.search('No control %s' % control,
+                         self._get_xmlrpclib_exception(e)):
+                return False
+            raise e
 
     def get(self, gpio_name):
         """Get the value of a gpio from Servod.
 
         @param gpio_name Name of the gpio.
+
+        @returns: server response to |gpio_name| request.
+
+        @raise ControlUnavailableError: if |gpio_name| not a known control.
+        @raise error.TestFail: for all other failures doing get().
         """
         assert gpio_name
         try:
             return self._server.get(gpio_name)
         except  xmlrpclib.Fault as e:
-            err_msg = "Getting '%s' :: %s" % \
-                (gpio_name, self._get_xmlrpclib_exception(e))
-            raise error.TestFail(err_msg)
+            err_str = self._get_xmlrpclib_exception(e)
+            err_msg = "Getting '%s' :: %s" % (gpio_name, err_str)
+            unknown_ctrl = re.findall(NO_CONTROL_RE, err_str)
+            if unknown_ctrl:
+                raise ControlUnavailableError('No control named %r' %
+                                              unknown_ctrl[0])
+            else:
+                logging.error(err_msg)
+                raise error.TestFail(err_msg)
 
 
     def set(self, gpio_name, gpio_value):
@@ -672,6 +708,9 @@
 
         @param gpio_name Name of the gpio.
         @param gpio_value New setting for the gpio.
+
+        @raise ControlUnavailableError: if |gpio_name| not a known control.
+        @raise error.TestFail: for all other failures doing set().
         """
         # The real danger here is to pass a None value through the xmlrpc.
         assert gpio_name and gpio_value is not None
@@ -679,9 +718,15 @@
         try:
             self._server.set(gpio_name, gpio_value)
         except  xmlrpclib.Fault as e:
-            err_msg = "Setting '%s' to %r :: %s" % \
-                (gpio_name, gpio_value, self._get_xmlrpclib_exception(e))
-            raise error.TestFail(err_msg)
+            err_str = self._get_xmlrpclib_exception(e)
+            err_msg = "Setting '%s' :: %s" % (gpio_name, err_str)
+            unknown_ctrl = re.findall(NO_CONTROL_RE, err_str)
+            if unknown_ctrl:
+                raise ControlUnavailableError('No control named %r' %
+                                              unknown_ctrl[0])
+            else:
+                logging.error(err_msg)
+                raise error.TestFail(err_msg)
 
 
     def set_get_all(self, controls):