Simplify local_host.run

+ Also preserves the original stack trace from utils.run for better
  error reporting.

BUG=chromium:684311
TEST=unittests

Change-Id: If8b386fe261e03a55e7419921a9368506c25173c
Reviewed-on: https://chromium-review.googlesource.com/567758
Commit-Ready: Prathmesh Prabhu <[email protected]>
Tested-by: Prathmesh Prabhu <[email protected]>
Reviewed-by: Prathmesh Prabhu <[email protected]>
Reviewed-by: Xixuan Wu <[email protected]>
diff --git a/client/bin/local_host.py b/client/bin/local_host.py
index 7d8db26..2f0a91b 100644
--- a/client/bin/local_host.py
+++ b/client/bin/local_host.py
@@ -3,11 +3,18 @@
 """
 This file contains the implementation of a host object for the local machine.
 """
+import distutils.core
+import glob
+import os
+import platform
+import shutil
+import sys
 
-import distutils.core, glob, os, platform, shutil
+import common
 from autotest_lib.client.common_lib import hosts, error
 from autotest_lib.client.bin import utils
 
+
 class LocalHost(hosts.Host):
     """This class represents a host running locally on the host."""
 
@@ -44,23 +51,20 @@
         @see common_lib.hosts.Host.run()
         """
         try:
-            result = utils.run(
-                command, timeout=timeout, ignore_status=True,
-                ignore_timeout=ignore_timeout,
-                stdout_tee=stdout_tee, stderr_tee=stderr_tee, stdin=stdin,
-                args=args)
-        except error.CmdError, e:
-            # this indicates a timeout exception
-            raise error.AutotestHostRunError('command timed out', e.result_obj)
-
-        if ignore_timeout and result is None:
-            # We have timed out, there is no result to report.
-            return None
-
-        if not ignore_status and result.exit_status > 0:
-            raise error.AutotestHostRunError('command execution error', result)
-
-        return result
+            return utils.run(
+                command, timeout=timeout, ignore_status=ignore_status,
+                ignore_timeout=ignore_timeout, stdout_tee=stdout_tee,
+                stderr_tee=stderr_tee, stdin=stdin, args=args)
+        except error.CmdTimeoutError as e:
+            # CmdTimeoutError is a subclass of CmdError, so must be caught first
+            new_error = error.AutotestHostRunTimeoutError(
+                    e.command, e.result_obj, additional_text=e.additional_text)
+            raise error.AutotestHostRunTimeoutError, new_error, \
+                    sys.exc_info()[2]
+        except error.CmdError as e:
+            new_error = error.AutotestHostRunCmdError(
+                    e.command, e.result_obj, additional_text=e.additional_text)
+            raise error.AutotestHostRunCmdError, new_error, sys.exc_info()[2]
 
 
     def list_files_glob(self, path_glob):
diff --git a/client/bin/local_host_unittest.py b/client/bin/local_host_unittest.py
index 51ca71d..f5bc5f5 100755
--- a/client/bin/local_host_unittest.py
+++ b/client/bin/local_host_unittest.py
@@ -71,24 +71,41 @@
 
 
     @mock.patch('autotest_lib.client.bin.local_host.utils.run')
-    def test_run_failure_raised(self, mock_run):
-        result = local_host.utils.CmdResult(
-                command='yes',
-                stdout='',
-                stderr='err',
-                exit_status=1,
-                duration=1,
-        )
-        mock_run.return_value = result
+    def test_run_cmd_failure_raised(self, mock_run):
+        mock_result = mock.MagicMock()
+        mock_run.side_effect = error.CmdError('yes', mock_result)
 
         host = local_host.LocalHost()
-        with self.assertRaises(error.AutotestHostRunError):
+        with self.assertRaises(error.AutotestHostRunCmdError) as exc_cm:
             host.run('yes', timeout=123)
 
+        self.assertEqual(exc_cm.exception.result_obj, mock_result)
         mock_run.assert_called_once_with(
-                result.command,
+                'yes',
                 timeout=123,
-                ignore_status=True,
+                ignore_status=False,
+                stdout_tee=local_host.utils.TEE_TO_LOGS,
+                stderr_tee=local_host.utils.TEE_TO_LOGS,
+                stdin=None,
+                ignore_timeout=False,
+                args=(),
+        )
+
+
+    @mock.patch('autotest_lib.client.bin.local_host.utils.run')
+    def test_run_cmd_timeout_raised(self, mock_run):
+        mock_result = mock.MagicMock()
+        mock_run.side_effect = error.CmdTimeoutError('yes', mock_result)
+
+        host = local_host.LocalHost()
+        with self.assertRaises(error.AutotestHostRunTimeoutError) as exc_cm:
+            host.run('yes', timeout=123)
+
+        self.assertEqual(exc_cm.exception.result_obj, mock_result)
+        mock_run.assert_called_once_with(
+                'yes',
+                timeout=123,
+                ignore_status=False,
                 stdout_tee=local_host.utils.TEE_TO_LOGS,
                 stderr_tee=local_host.utils.TEE_TO_LOGS,
                 stdin=None,
diff --git a/client/common_lib/error.py b/client/common_lib/error.py
index 4268870..f9babce 100644
--- a/client/common_lib/error.py
+++ b/client/common_lib/error.py
@@ -210,6 +210,30 @@
     pass
 
 
+class AutotestHostRunCmdError(AutotestHostRunError):
+    """Indicates that the command run via Host.run failed.
+
+    This is equivalent to CmdError when raised from a Host object instead of
+    directly on the DUT using utils.run
+    """
+
+    def __init__(self, command, result_obj, additional_text=''):
+        description = command
+        if additional_text:
+            description += ' (%s)' % additional_text
+        super(AutotestHostRunCmdError, self).__init__(description, result_obj)
+        self.command = command
+        self.additional_text = additional_text
+
+
+class AutotestHostRunTimeoutError(AutotestHostRunCmdError):
+    """Indicates that a command run via Host.run timed out.
+
+    This is equivalent to CmdTimeoutError when raised from a Host object instead
+    of directly on the DUT using utils.run
+    """
+
+
 # server-specific errors
 
 class AutoservError(Exception):