power_WakeSources: Add test to verify wake sources.

As part of dark resume enablement, we need to verify wake sources respond
properly. This CL focuses on verifying that input events (power button,
lid and base attach/detach) result in a full wake. This CL also verifies
that RTC causes a dark resume. Future changes will enable verification of
other wake sources.

BUG=chromium:820668
TEST=Tested locally with poppy.

Change-Id: I399a0510826a9b4ceae24c8c104f308ddfd61895
Reviewed-on: https://chromium-review.googlesource.com/1129372
Commit-Ready: Ravi Chandra Sadineni <ravisadineni@chromium.org>
Tested-by: Ravi Chandra Sadineni <ravisadineni@chromium.org>
Reviewed-by: Puthikorn Voravootivat <puthik@chromium.org>
Reviewed-by: Todd Broch <tbroch@chromium.org>
diff --git a/client/bin/input/input_device.py b/client/bin/input/input_device.py
index e32fd86..3fd06c6 100755
--- a/client/bin/input/input_device.py
+++ b/client/bin/input/input_device.py
@@ -528,6 +528,10 @@
                 (not BTN_TOOL_FINGER in self.events[EV_KEY]) and
                 (EV_ABS in self.events))
 
+    def is_lid(self):
+        return ((EV_SW in self.events) and
+                (SW_LID in self.events[EV_SW]))
+
     def is_mt_b(self):
         return self.is_mt() and ABS_MT_SLOT in self.events[EV_ABS]
 
diff --git a/client/cros/dark_resume_listener.py b/client/cros/dark_resume_listener.py
index fc585e8..48b8f8f 100644
--- a/client/cros/dark_resume_listener.py
+++ b/client/cros/dark_resume_listener.py
@@ -9,6 +9,7 @@
 import dbus.mainloop.glib
 import gobject
 
+from autotest_lib.client.cros import upstart
 
 class DarkResumeListener(object):
     """Server which listens for dark resume-related DBus signals to count how
@@ -24,6 +25,7 @@
         dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
         self._bus = dbus.SystemBus()
         self._count = 0
+        self._stop_resuspend = False
 
         self._bus.add_signal_receiver(handler_function=self._saw_dark_resume,
                                       signal_name=self.SIGNAL_NAME)
@@ -53,3 +55,16 @@
 
     def _saw_dark_resume(self, unused):
         self._count += 1
+        if self._stop_resuspend:
+            # Restart powerd to stop re-suspend.
+            upstart.restart_job('powerd')
+
+
+    def stop_resuspend(self, should_stop):
+        """
+        Whether to stop suspend after seeing a dark resume.
+
+        @param should_stop: Whether to stop system from re-suspending.
+        """
+        self._stop_resuspend = should_stop
+
diff --git a/client/cros/dark_resume_xmlrpc_server.py b/client/cros/dark_resume_xmlrpc_server.py
index e6fe398..ada8acf 100755
--- a/client/cros/dark_resume_xmlrpc_server.py
+++ b/client/cros/dark_resume_xmlrpc_server.py
@@ -12,7 +12,7 @@
 from autotest_lib.client.cros import dark_resume_listener
 from autotest_lib.client.cros import xmlrpc_server
 from autotest_lib.client.cros.power import sys_power
-
+from autotest_lib.client.cros.power import power_utils
 
 class DarkResumeXmlRpcDelegate(xmlrpc_server.XmlRpcDelegate):
     """Exposes methods called remotely during dark resume autotests.
@@ -30,9 +30,26 @@
 
 
     @xmlrpc_server.dbus_safe(None)
-    def suspend_bg_for_dark_resume(self):
-        """Suspends this system indefinitely for dark resume."""
-        sys_power.suspend_bg_for_dark_resume()
+    def suspend_bg_for_dark_resume(self, suspend_for_secs):
+        """
+        Suspends the system for dark resume.
+        @param suspend_for_secs : If not 0, sets a rtc alarm to
+            wake the system after|suspend_for_secs| secs.
+        """
+        if suspend_for_secs == 0 :
+            sys_power.suspend_bg_for_dark_resume()
+        else:
+            sys_power.do_suspend(suspend_for_secs)
+
+
+    @xmlrpc_server.dbus_safe(None)
+    def set_stop_resuspend(self, stop_resuspend):
+        """
+        Stops resuspend on seeing a dark resume.
+        @param stop_resuspend: Stops resuspend of the device on seeing a
+            a dark resume if |stop_resuspend| is true.
+        """
+        self._listener.stop_resuspend(stop_resuspend)
 
 
     @xmlrpc_server.dbus_safe(0)
@@ -42,6 +59,16 @@
         return self._listener.count
 
 
+    @xmlrpc_server.dbus_safe(False)
+    def has_lid(self):
+        """
+        Checks whether the DUT has lid.
+
+        @return: Returns True if the device has a lid, False otherwise.
+        """
+
+        return power_utils.has_lid()
+
 if __name__ == '__main__':
     logging.basicConfig(level=logging.DEBUG)
     handler = logging.handlers.SysLogHandler(address = '/dev/log')
diff --git a/client/cros/power/power_utils.py b/client/cros/power/power_utils.py
index fe73297..41ef7b0 100644
--- a/client/cros/power/power_utils.py
+++ b/client/cros/power/power_utils.py
@@ -8,6 +8,7 @@
 import shutil
 import time
 from autotest_lib.client.bin import utils
+from autotest_lib.client.bin.input.input_device import InputDevice
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.cros import upstart
 
@@ -95,6 +96,18 @@
     return os.path.isdir('/sys/devices/virtual/powercap/intel-rapl/')
 
 
+def has_lid():
+    """
+    Checks whether the device has lid.
+
+    @return: Returns True if the device has a lid, False otherwise.
+    """
+    INPUT_DEVICE_LIST = "/dev/input/event*"
+
+    return any(InputDevice(node).is_lid() for node in
+               glob.glob(INPUT_DEVICE_LIST))
+
+
 def _call_dbus_method(destination, path, interface, method_name, args):
     """Performs a generic dbus method call."""
     command = ('dbus-send --type=method_call --system '
diff --git a/server/cros/dark_resume_utils.py b/server/cros/dark_resume_utils.py
index 11442c7..027a609 100644
--- a/server/cros/dark_resume_utils.py
+++ b/server/cros/dark_resume_utils.py
@@ -3,7 +3,6 @@
 # found in the LICENSE file.
 
 import logging
-import time
 
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.cros import constants
@@ -30,23 +29,25 @@
     """
 
 
-    def __init__(self, proxy, host):
+    def __init__(self, proxy, host, suspend_for):
         """Set up for a dark-resume-ready suspend to be carried out using
         |proxy| and for the subsequent wakeup to be carried out using
         |host|.
 
         @param proxy: a dark resume xmlrpc server proxy object for the DUT
         @param host: a servo host connected to the DUT
-
+        @param suspend_for : If not 0, sets a rtc alarm to wake the system after
+            |suspend_for| secs.
         """
         self._client_proxy = proxy
         self._host = host
+        self._suspend_for = suspend_for
 
 
     def __enter__(self):
         """Suspend the DUT."""
         logging.info('Suspending DUT (in background)...')
-        self._client_proxy.suspend_bg_for_dark_resume()
+        self._client_proxy.suspend_bg_for_dark_resume(self._suspend_for)
 
 
     def __exit__(self, exception, value, traceback):
@@ -83,22 +84,6 @@
         logging.debug('Enabling dark resume')
         host.run('echo 0 > %s/disable_dark_resume' % TMP_POWER_DIR)
 
-        logging.debug('Enabling USB ports in dark resume')
-
-        dev_contents = host.run('cat %s/dark_resume_devices' % POWER_DEFAULTS,
-                                ignore_status=True).stdout
-        dev_list = dev_contents.split('\n')
-        new_dev_list = filter(lambda dev: dev.find('usb') == -1, dev_list)
-        new_dev_contents = '\n'.join(new_dev_list)
-        host.run('echo -e \'%s\' > %s/dark_resume_devices' %
-                 (new_dev_contents, TMP_POWER_DIR))
-
-        if duration > 0:
-            # override suspend durations preference for dark resume
-            logging.info('setting dark_resume_suspend_durations=%d', duration)
-            host.run('echo 0.0 %d > %s/dark_resume_suspend_durations' %
-                     (duration, TMP_POWER_DIR))
-
         # bind the tmp directory to the power preference directory
         host.run('mount --bind %s %s' % (TMP_POWER_DIR, POWER_DIR))
 
@@ -121,10 +106,21 @@
         self._host.run('stop powerd; start powerd')
 
 
-    def suspend(self):
-        """Returns a DarkResumeSuspend context manager that allows safe suspending
-        of the DUT."""
-        return DarkResumeSuspend(self._client_proxy, self._host)
+    def suspend(self, suspend_for=0):
+        """
+        Returns a DarkResumeSuspend context manager that allows safe
+        suspending of the DUT.
+        @param suspend_for : If not 0, sets a rtc alarm to wake the system after
+            |suspend_for| secs.
+        """
+        return DarkResumeSuspend(self._client_proxy, self._host, suspend_for)
+
+
+    def stop_resuspend_on_dark_resume(self, stop_resuspend=True):
+        """
+        If |stop_resuspend| is True, stops re-suspend on seeing a dark resume.
+        """
+        self._client_proxy.set_stop_resuspend(stop_resuspend)
 
 
     def count_dark_resumes(self):
@@ -142,6 +138,12 @@
         return self._client_proxy.get_dark_resume_count()
 
 
+
+    def host_has_lid(self):
+        """Returns True if the DUT has a lid."""
+        return self._client_proxy.has_lid()
+
+
     def _get_xmlrpc_proxy(self):
         """Get a dark resume XMLRPC proxy for the host this DarkResumeUtils is
         attached to.
@@ -171,25 +173,22 @@
 
 
 def _wake_dut(host):
-    """Make sure |host| is up. If we can't wake it with normal keys, hit the
-    power button."""
+    """
+    Make sure |host| is up by pressing power button.
 
+    @raises error.TestFail: If we cannot wake the |host| up. This means the
+            DUT has to be woken up manually. Should not happen mostly.
+    """
     woken = False
     for i in range(RESUME_CTRL_RETRIES):
-        # Double tap servo key to make sure we signal user activity to Chrome.
-        # The first key might occur during the kernel suspend pathway, which
-        # causes the suspend to abort, but might put us in dark resume since
-        # the keypress is not forwarded to Chrome.
-        host.servo.ctrl_key()
-        time.sleep(0.5)
-        host.servo.ctrl_key()
-
+        # Check before pressing the power button. Or you might suspend/shutdown
+        # the system if already in S0.
         if host.wait_up(timeout=RESUME_GRACE_PERIOD):
             woken = True
             break
-        logging.debug('Wake attempt #%d failed', i+1)
+        logging.debug('Wake attempt #%d ', i+1)
+        host.servo.power_short_press()
 
     if not woken:
         logging.warning('DUT did not wake -- trouble ahead')
-        host.servo.power_key()
         raise error.TestFail('DUT did not wake')
diff --git a/server/hosts/cros_label.py b/server/hosts/cros_label.py
index bdd1b0a..be5d76f 100644
--- a/server/hosts/cros_label.py
+++ b/server/hosts/cros_label.py
@@ -558,7 +558,7 @@
     # TODO(kevcheng): See if we can determine if this label is applicable a
     # better way (crbug.com/592146).
     _NAME = 'lucidsleep'
-    LUCID_SLEEP_BOARDS = ['samus', 'lulu']
+    LUCID_SLEEP_BOARDS = ['poppy']
 
     def exists(self, host):
         board = host.get_board().replace(ds_constants.BOARD_PREFIX, '')
diff --git a/server/site_tests/power_WakeSources/control b/server/site_tests/power_WakeSources/control
new file mode 100644
index 0000000..416eaf1
--- /dev/null
+++ b/server/site_tests/power_WakeSources/control
@@ -0,0 +1,30 @@
+# Copyright 2018 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.
+#
+# Test expects to be run on a jailbroken device in developer mode.
+
+from autotest_lib.server import utils
+
+AUTHOR = "ravisadineni"
+NAME = "power_WakeSources"
+TIME = "SHORT"
+TEST_CATEGORY = "Functional"
+TEST_CLASS = "power"
+TEST_TYPE = "server"
+DEPENDENCIES = "servo, lucidsleep"
+
+DOC = """
+Tests the following
+    1. Wakes by input devices trigger a full wake.
+    2. Wake by RTC triggers a dark resume.
+"""
+
+args_dict = utils.args_to_dict(args)
+servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
+
+def run(machine):
+    host = hosts.create_host(machine, servo_args=servo_args)
+    job.run_test("power_WakeSources", host=host, disable_sysinfo=True)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/power_WakeSources/power_WakeSources.py b/server/site_tests/power_WakeSources/power_WakeSources.py
new file mode 100644
index 0000000..6445f50
--- /dev/null
+++ b/server/site_tests/power_WakeSources/power_WakeSources.py
@@ -0,0 +1,248 @@
+# Copyright 2018 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.
+
+import logging
+import time
+
+from autotest_lib.client.common_lib import enum, error
+from autotest_lib.server import test
+from autotest_lib.server.cros.dark_resume_utils import DarkResumeUtils
+from autotest_lib.server.cros.faft.config.config import Config as FAFTConfig
+from autotest_lib.server.cros.servo import chrome_ec
+
+
+# Possible states base can be forced into.
+BASE_STATE = enum.Enum('ATTACH', 'DETACH', 'RESET')
+
+
+ # List of wake sources expected to cause a full resume.
+FULL_WAKE_SOURCES = ['PWR_BTN', 'LID_OPEN', 'BASE_ATTACH',
+                     'BASE_DETACH', 'INTERNAL_KB']
+
+# Max time taken by the system to resume.
+RESUME_DURATION_SECS = 5
+
+# Time in future after which RTC goes off.
+RTC_WAKE_SECS = 30
+
+# Max time taken by the system to suspend.
+SUSPEND_DURATION_SECS = 5
+
+# Time to allow lid transition to take effect.
+WAIT_TIME_LID_TRANSITION_SECS = 5
+
+
+class power_WakeSources(test.test):
+    """
+    Verify that wakes from input devices can trigger a full
+    resume. Currently tests :
+        1. power button
+        2. lid open
+        3. base attach
+        4. base detach
+
+    Also tests RTC triggers a dark resume.
+
+    """
+    version = 1
+
+    def _after_resume(self, wake_source):
+        """Cleanup to perform after resuming the device.
+
+        @param wake_source: Wake source that has been tested.
+        """
+        if wake_source in ['BASE_ATTACH', 'BASE_DETACH']:
+            self._force_base_state(BASE_STATE.RESET)
+
+    def _before_suspend(self, wake_source):
+        """Prep before suspend.
+
+        @param wake_source: Wake source that is going to be tested.
+
+        @return: Boolean, whether _before_suspend action is successful.
+        """
+        if wake_source == 'BASE_ATTACH':
+            # Force detach before suspend so that attach won't be ignored.
+            return self._force_base_state(BASE_STATE.DETACH)
+        if wake_source == 'BASE_DETACH':
+            # Force attach before suspend so that detach won't be ignored.
+            return self._force_base_state(BASE_STATE.ATTACH)
+        if wake_source == 'LID_OPEN':
+            # Set the power policy for lid closed action to suspend.
+            return self._host.run(
+                'set_power_policy --lid_closed_action suspend',
+                ignore_status=True).exit_status == 0
+        return True
+
+    def _force_base_state(self, base_state):
+        """Send EC command to force the |base_state|.
+
+        @param base_state: State to force base to. One of |BASE_STATE| enum.
+
+        @return: False if the command does not exist in the current EC build.
+
+        @raise error.TestFail : If base state change fails.
+        """
+        ec_cmd = 'basestate '
+        ec_arg = {
+            BASE_STATE.ATTACH: 'a',
+            BASE_STATE.DETACH: 'd',
+            BASE_STATE.RESET: 'r'
+        }
+
+        ec_cmd += ec_arg[base_state]
+
+        try:
+            self._ec.send_command(ec_cmd)
+        except error.TestFail as e:
+            if 'No control named' in str(e):
+                # Since the command is added recently, this might not exist on
+                # every board.
+                logging.warning('basestate command does not exist on the EC. '
+                                'Please verify the base state manually.')
+                return False
+            else:
+                raise e
+        return True
+
+    def _is_valid_wake_source(self, wake_source):
+        """Check if |wake_source| is valid for DUT.
+
+        @param wake_source: wake source to verify.
+        @return: True if |wake_source| is a valid wake source for this specific
+            DUT
+        """
+        if wake_source.startswith('BASE'):
+            if self._host.run('which hammerd', ignore_status=True).\
+                exit_status == 0:
+                # Smoke test to see if EC has support to reset base.
+                return self._force_base_state(BASE_STATE.RESET)
+        if wake_source == 'LID_OPEN':
+            return self._dr_utils.host_has_lid()
+        if wake_source == 'INTERNAL_KB':
+            return self._faft_config.has_keyboard
+        return True
+
+    def _test_full_wake(self, wake_source):
+        """Test if |wake_source| triggers a full resume.
+
+        @param wake_source: wake source to test. One of |FULL_WAKE_SOURCES|.
+        @return: True, if we are able to successfully test the |wake source|
+            triggers a full wake.
+        """
+        is_success = True
+        logging.info('Testing wake by %s triggers a '
+                     'full wake when dark resume is enabled.', wake_source)
+        if not self._before_suspend(wake_source):
+            logging.error('Before suspend action failed for %s', wake_source)
+            is_success = False
+        else:
+            count_before = self._dr_utils.count_dark_resumes()
+            with self._dr_utils.suspend() as _:
+                logging.info('DUT suspended! Waiting to resume...')
+                # Wait at least |SUSPEND_DURATION_SECS| secs for the kernel to
+                # fully suspend.
+                time.sleep(SUSPEND_DURATION_SECS)
+                self._trigger_wake(wake_source)
+                # Wait at least |RESUME_DURATION_SECS| secs for the device to
+                # resume.
+                time.sleep(RESUME_DURATION_SECS)
+
+                if not self._host.is_up():
+                    logging.error('Device did not resume from suspend for %s',
+                                  wake_source)
+                    is_success = False
+
+            count_after = self._dr_utils.count_dark_resumes()
+            if count_before != count_after:
+                logging.error('%s caused a dark resume.', wake_source)
+                is_success = False
+        self._after_resume(wake_source)
+        return is_success
+
+    def _test_rtc(self):
+        """Suspend the device and test if RTC triggers a dark_resume.
+
+        @return boolean, true if RTC alarm caused a dark resume.
+        """
+
+        logging.info('Testing RTC triggers dark resume when enabled.')
+
+        count_before = self._dr_utils.count_dark_resumes()
+        with self._dr_utils.suspend(RTC_WAKE_SECS) as _:
+            logging.info('DUT suspended! Waiting to resume...')
+            time.sleep(SUSPEND_DURATION_SECS + RTC_WAKE_SECS +
+                       RESUME_DURATION_SECS)
+
+            if not self._host.is_up():
+                logging.error('Device did not resume from suspend for RTC')
+                return False
+
+        count_after = self._dr_utils.count_dark_resumes()
+        if count_before != count_after - 1:
+            logging.error(' RTC did not cause a dark resume.'
+                          'count before = %d, count after = %d',
+                          count_before, count_after)
+            return False
+        return True
+
+    def _trigger_wake(self, wake_source):
+        """Trigger wake using the given |wake_source|.
+
+        @param wake_source : wake_source that is being tested.
+            One of |FULL_WAKE_SOURCES|.
+        """
+        if wake_source == 'PWR_BTN':
+            self._host.servo.power_short_press()
+        elif wake_source == 'LID_OPEN':
+            self._host.servo.lid_close()
+            time.sleep(WAIT_TIME_LID_TRANSITION_SECS)
+            self._host.servo.lid_open()
+        elif wake_source == 'BASE_ATTACH':
+            self._force_base_state(BASE_STATE.ATTACH)
+        elif wake_source == 'BASE_DETACH':
+            self._force_base_state(BASE_STATE.DETACH)
+        elif wake_source == 'INTERNAL_KB':
+            self._host.servo.ctrl_key()
+
+    def cleanup(self):
+        self._dr_utils.stop_resuspend_on_dark_resume(False)
+        self._dr_utils.teardown()
+
+    def initialize(self, host):
+        """Initialize wake sources tests.
+
+        @param host: Host on which the test will be run.
+        """
+        self._host = host
+        self._dr_utils = DarkResumeUtils(host)
+        self._dr_utils.stop_resuspend_on_dark_resume()
+        self._ec = chrome_ec.ChromeEC(self._host.servo)
+        self._faft_config = FAFTConfig(self._host.get_platform())
+
+    def run_once(self):
+        """Body of the test."""
+
+        test_ws = set(ws for ws in FULL_WAKE_SOURCES if \
+            self._is_valid_wake_source(ws))
+        passed_ws = set(ws for ws in test_ws if self._test_full_wake(ws))
+        failed_ws = test_ws.difference(passed_ws)
+        skipped_ws = set(FULL_WAKE_SOURCES).difference(test_ws)
+
+        if self._test_rtc():
+            passed_ws.add('RTC')
+        else:
+            failed_ws.add('RTC')
+        if len(passed_ws):
+            logging.info('[%s] woke the device as expected.',
+                         ''.join(str(elem) + ', ' for elem in passed_ws))
+        if skipped_ws:
+            logging.info('[%s] are not wake sources on this platform. '
+                         'Please test manually if not the case.',
+                         ''.join(str(elem) + ', ' for elem in skipped_ws))
+
+        if len(failed_ws):
+            raise error.TestFail(
+                '[%s] wake sources did not behave as expected.'
+                % (''.join(str(elem) + ', ' for elem in failed_ws)))