Chameleon: Implement display_HotPlugAtSuspend test

This change implements the display_HotPlugAtSuspend test which remotely
emulates external display hot-plug during suspend using Chameleon board.

The configs it tests are:
  (plugged_before_suspend, plugged_after_suspend, plugged_before_resume):
    (True, True, True)
    (True, False, False)
    (True, False, True)
    (False, True, True)
    (False, True, False)

It verifies DUT behavior response to these configs.

BUG=chromium:343790
TEST=manaul
$ test_that --fast --args "chameleon_host=$CHAMELEON_IP" $DUT_IP \
      e:display_HotPlugAtSuspend.*

Change-Id: I1b8515c7cebbb56e8a2290d6014947aabc046794
Reviewed-on: https://chromium-review.googlesource.com/186347
Reviewed-by: Wai-Hong Tam <waihong@chromium.org>
Commit-Queue: Wai-Hong Tam <waihong@chromium.org>
Tested-by: Wai-Hong Tam <waihong@chromium.org>
diff --git a/server/cros/chameleon/display_client.py b/server/cros/chameleon/display_client.py
index 27ee84b..e36f95d 100644
--- a/server/cros/chameleon/display_client.py
+++ b/server/cros/chameleon/display_client.py
@@ -122,10 +122,22 @@
         return self._display_xmlrpc_client.set_mirrored(is_mirrored)
 
 
-    def suspend_resume(self):
-        """Suspends the DUT for 10 seconds."""
+    def suspend_resume(self, suspend_time=10):
+        """Suspends the DUT for a given time in second.
+
+        @param suspend_time: Suspend time in second, default: 10s.
+        """
         # TODO(waihong): Use other general API instead of this RPC.
-        return self._display_xmlrpc_client.suspend_resume()
+        return self._display_xmlrpc_client.suspend_resume(suspend_time)
+
+
+    def suspend_resume_bg(self, suspend_time=10):
+        """Suspends the DUT for a given time in second in the background.
+
+        @param suspend_time: Suspend time in second, default: 10s.
+        """
+        # TODO(waihong): Use other general API instead of this RPC.
+        return self._display_xmlrpc_client.suspend_resume_bg(suspend_time)
 
 
     def reconnect_output_and_wait(self):
diff --git a/server/site_tests/display_HotPlugAtSuspend/control.extended b/server/site_tests/display_HotPlugAtSuspend/control.extended
new file mode 100644
index 0000000..7090bfd
--- /dev/null
+++ b/server/site_tests/display_HotPlugAtSuspend/control.extended
@@ -0,0 +1,28 @@
+# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from autotest_lib.server import utils
+
+AUTHOR = "chromeos-chameleon"
+NAME = "display_HotPlugAtSuspend.extended"
+PURPOSE = "Remotely controlled display hot-plug and suspend test."
+CRITERIA = "This test will fail if DUT doesn't see the display after resume."
+SUITE = "chameleon"
+TIME = "SHORT"
+TEST_CATEGORY = "Functional"
+TEST_CLASS = "display"
+TEST_TYPE = "server"
+
+DOC = """
+This test remotely emulates external display hot-plug and suspend/resume.
+"""
+
+args_dict = utils.args_to_dict(args)
+chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
+
+def run(machine):
+    host = hosts.create_host(machine, chameleon_args=chameleon_args)
+    job.run_test("display_HotPlugAtSuspend", host=host, tag="extended")
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/display_HotPlugAtSuspend/control.mirrored b/server/site_tests/display_HotPlugAtSuspend/control.mirrored
new file mode 100644
index 0000000..d42ded3
--- /dev/null
+++ b/server/site_tests/display_HotPlugAtSuspend/control.mirrored
@@ -0,0 +1,29 @@
+# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from autotest_lib.server import utils
+
+AUTHOR = "chromeos-chameleon"
+NAME = "display_HotPlugAtSuspend.mirrored"
+PURPOSE = "Remotely controlled display hot-plug and suspend test."
+CRITERIA = "This test will fail if DUT doesn't see the display after resume."
+SUITE = "chameleon"
+TIME = "SHORT"
+TEST_CATEGORY = "Functional"
+TEST_CLASS = "display"
+TEST_TYPE = "server"
+
+DOC = """
+This test remotely emulates external display hot-plug and suspend/resume.
+"""
+
+args_dict = utils.args_to_dict(args)
+chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
+
+def run(machine):
+    host = hosts.create_host(machine, chameleon_args=chameleon_args)
+    job.run_test("display_HotPlugAtSuspend", host=host, test_mirrored=True,
+                 tag="mirrored")
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/display_HotPlugAtSuspend/display_HotPlugAtSuspend.py b/server/site_tests/display_HotPlugAtSuspend/display_HotPlugAtSuspend.py
new file mode 100644
index 0000000..0ee6083
--- /dev/null
+++ b/server/site_tests/display_HotPlugAtSuspend/display_HotPlugAtSuspend.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This is a display hot-plug and suspend test using the Chameleon board."""
+
+import logging
+import time
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.server.cros.chameleon import chameleon_test
+
+
+class display_HotPlugAtSuspend(chameleon_test.ChameleonTest):
+    """Display hot-plug and suspend test.
+
+    This test talks to a Chameleon board and a DUT to set up, run, and verify
+    DUT behavior response to different configuration of hot-plug during
+    suspend/resume.
+    """
+    version = 1
+    PLUG_CONFIGS = [
+        # (plugged_before_suspend, plugged_after_suspend, plugged_before_resume)
+        (True, True, True),
+        (True, False, False),
+        (True, False, True),
+        (False, True, True),
+        (False, True, False),
+    ]
+    # Duration of suspend, in second.
+    SUSPEND_DURATION = 15
+    # Time for the transition of suspend.
+    SUSPEND_TRANSITION_TIME = 2
+    # Time margin to do plug/unplug before resume.
+    TIME_MARGIN_BEFORE_RESUME = 5
+    # Allow a range of pixel value difference.
+    PIXEL_DIFF_VALUE_MARGIN = 5
+    # A range of pixel number which a cursor covers. We accept this number
+    # of pixels not matched in the case of a cursor showed.
+    CURSOR_PIXEL_NUMBER = 100
+    # Time to wait the calibration image stable, like waiting the info
+    # window "DisplayTestExtension triggered full screen" disappeared.
+    CALIBRATION_IMAGE_SETUP_TIME = 10
+
+
+    def cleanup(self):
+        # Make the connector plugged at the end.
+        self.chameleon_port.plug()
+        super(display_HotPlugAtSuspend, self).cleanup()
+
+
+    def run_once(self, host, test_mirrored=False):
+        width, height = self.chameleon_port.get_resolution()
+        logging.info('See the display on Chameleon: port %d (%s) %dx%d',
+                     self.chameleon_port.get_connector_id(),
+                     self.chameleon_port.get_connector_type(),
+                     width, height)
+        # Keep the original connector name, for later comparison.
+        expected_connector = self.display_client.get_connector_name()
+        logging.info('See the display on DUT: %s', expected_connector)
+
+        logging.info('Set mirrored: %s', test_mirrored)
+        self.display_client.set_mirrored(test_mirrored)
+
+        errors = []
+        for (plugged_before_suspend, plugged_after_suspend,
+             plugged_before_resume) in self.PLUG_CONFIGS:
+            logging.info('TESTING THE CASE: %s > suspend > %s > %s > resume',
+                         'plug' if plugged_before_suspend else 'unplug',
+                         'plug' if plugged_after_suspend else 'unplug',
+                         'plug' if plugged_before_resume else 'unplug')
+            boot_id = host.get_boot_id()
+            if plugged_before_suspend:
+                self.chameleon_port.plug()
+            else:
+                self.chameleon_port.unplug()
+
+            logging.info('Going to suspend, for %d seconds...',
+                         self.SUSPEND_DURATION)
+            time_before_suspend = time.time()
+            self.display_client.suspend_resume_bg(self.SUSPEND_DURATION)
+
+            # Confirm DUT suspended.
+            logging.info('- Wait for sleep...')
+            time.sleep(self.SUSPEND_TRANSITION_TIME)
+            host.test_wait_for_sleep()
+            if plugged_after_suspend:
+                self.chameleon_port.plug()
+            else:
+                self.chameleon_port.unplug()
+
+            current_time = time.time()
+            sleep_time = (self.SUSPEND_DURATION -
+                          (current_time - time_before_suspend) -
+                          self.TIME_MARGIN_BEFORE_RESUME)
+            logging.info('- Sleep for %.2f seconds...', sleep_time)
+            time.sleep(sleep_time)
+            if plugged_before_resume:
+                self.chameleon_port.plug()
+            else:
+                self.chameleon_port.unplug()
+            time.sleep(self.TIME_MARGIN_BEFORE_RESUME)
+
+            logging.info('- Wait for resume...')
+            host.test_wait_for_resume(boot_id)
+
+            logging.info('Resumed back')
+            current_connector = self.display_client.get_connector_name()
+            # Check the DUT behavior: see the external display?
+            if plugged_before_resume:
+                if not current_connector:
+                    raise error.TestFail('Failed to see the external display')
+                elif current_connector != expected_connector:
+                    raise error.TestFail(
+                            'See a different display: %s != %s' %
+                            (current_connector, expected_connector))
+
+                logging.info('Waiting the calibration image stable.')
+                self.display_client.load_calibration_image((width, height))
+                self.display_client.move_cursor_to_bottom_right()
+                time.sleep(self.CALIBRATION_IMAGE_SETUP_TIME)
+
+                error_message = self.check_screen_with_chameleon(
+                        'SCREEN-%dx%d-%c-S-%c-P-R' % (
+                             width, height,
+                             'P' if plugged_before_suspend else 'U',
+                             'P' if plugged_after_suspend else 'U'),
+                        self.PIXEL_DIFF_VALUE_MARGIN,
+                        self.CURSOR_PIXEL_NUMBER)
+                if error_message:
+                    errors.append(error_message)
+            else:
+                if current_connector:
+                    raise error.TestFail(
+                            'See a not-expected external display: %s' %
+                             current_connector)
+
+        if errors:
+            raise error.TestFail('; '.join(errors))