autotest: Encapsulate dark_resume_utils, add DR counter

Currently, tests have no way of telling the difference between
full resumes and dark resumes, and dark resumes may be short enough
that wait_up isn't going to catch them. This adds a listener on the
DUT which counts DarkSuspendImminent DBus signals, which are sent
as soon as powerd notices we are in dark resume, and adds a function
which allows us to wait for some period of time and then ask the
listener how many signals it noticed.

dark_resume_utils now takes the form of a class instead of a bunch
of functions you call during intialization and teardown because there
is now state which needs to persist across those parts of the test.
It also exposes a better way to suspend for dark resume tests because
we can't rely on the RTC timer waking us up (since that will cause a
dark resume and resuspend), and makes sure that this doesn't put us
to sleep forever.

BUG=chromium:466731
TEST=convert WakeOnDisconnect/WoWLAN to use this, make sure
  tests pass

Change-Id: Icb0d7b555685dad7673cbc81840e40eccd282f38
Reviewed-on: https://chromium-review.googlesource.com/260038
Trybot-Ready: Samuel Tan <samueltan@chromium.org>
Tested-by: Samuel Tan <samueltan@chromium.org>
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Reviewed-by: Samuel Tan <samueltan@chromium.org>
Commit-Queue: Samuel Tan <samueltan@chromium.org>
diff --git a/server/cros/dark_resume_utils.py b/server/cros/dark_resume_utils.py
index 8e030c2..634db1b 100644
--- a/server/cros/dark_resume_utils.py
+++ b/server/cros/dark_resume_utils.py
@@ -3,57 +3,185 @@
 # found in the LICENSE file.
 
 import logging
+import time
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros import dark_resume_xmlrpc_server
+from autotest_lib.server import autotest
 
 POWER_DIR = '/var/lib/power_manager'
 TMP_POWER_DIR = '/tmp/power_manager'
 POWER_DEFAULTS = '/usr/share/power_manager/board_specific'
 
+RESUME_CTRL_RETRIES = 3
+RESUME_GRACE_PERIOD = 10
+XMLRPC_BRINGUP_TIMEOUT_SECONDS = 60
 
-def dark_resume_setup(host):
-    """Set up powerd preferences so we will properly go into dark resume,
-    and still be able to communicate with the DUT.
 
-    @param host: the DUT to set up dark resume for
+class DarkResumeSuspend(object):
+    """Context manager which exposes the dark resume-specific suspend
+    functionality.
 
+    This is required because using the RTC for a dark resume test will
+    cause the system to wake up in dark resume and resuspend, which is
+    not what we want. Instead, we suspend indefinitely, but make sure we
+    don't leave the DUT asleep by always running code to wake it up via
+    servo.
     """
-    logging.info('Setting up dark resume preferences')
-
-    # Make temporary directory, which will be used to hold
-    # temporary preferences. We want to avoid writing into
-    # /var/lib so we don't have to save any state.
-    logging.debug('Creating temporary powerd prefs at %s', TMP_POWER_DIR)
-    host.run('mkdir -p %s' % TMP_POWER_DIR)
-
-    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))
-
-    # bind the tmp directory to the power preference directory
-    host.run('mount --bind %s %s' % (TMP_POWER_DIR, POWER_DIR))
-
-    logging.debug('Restarting powerd with new settings')
-    host.run('restart powerd')
 
 
-def dark_resume_teardown(host):
-    """Clean up changes made by dark_resume_setup.
+    def __init__(self, proxy, host):
+        """Set up for a dark-resume-ready suspend to be carried out using
+        |proxy| and for the subsequent wakeup to be carried out using
+        |servo|.
 
-    @param host: the DUT to remove dark resume prefs for
+        @param proxy: a dark resume xmlrpc server proxy object for the DUT
+        @param servo: a servo host connected to the DUT
 
+        """
+        self._client_proxy = proxy
+        self._host = host
+
+
+    def __enter__(self):
+        """Suspend the DUT."""
+        logging.info('Suspending DUT (in background)...')
+        self._client_proxy.suspend_bg_for_dark_resume()
+
+
+    def __exit__(self, exception, value, traceback):
+        """Wake up the DUT."""
+        logging.info('Waking DUT from server.')
+        _wake_dut(self._host)
+
+
+class DarkResumeUtils(object):
+    """Class containing common functionality for tests which exercise dark
+    resume pathways. We set up powerd to allow dark resume and also configure
+    the suspended devices so that the backchannel can stay up. We can also
+    check for the number of dark resumes that have happened in a particular
+    suspend request.
     """
-    logging.info('Tearing down dark resume preferences')
 
-    logging.debug('Cleaning up temporary powerd bind mounts')
-    host.run('umount %s' % POWER_DIR, ignore_status=True)
 
-    logging.debug('Restarting powerd to revert to old settings')
-    host.run('restart powerd')
+    def __init__(self, host):
+        """Set up powerd preferences so we will properly go into dark resume,
+        and still be able to communicate with the DUT.
+
+        @param host: the DUT to set up dark resume for
+
+        """
+        self._host = host
+        logging.info('Setting up dark resume preferences')
+
+        # Make temporary directory, which will be used to hold
+        # temporary preferences. We want to avoid writing into
+        # /var/lib so we don't have to save any state.
+        logging.debug('Creating temporary powerd prefs at %s', TMP_POWER_DIR)
+        host.run('mkdir -p %s' % TMP_POWER_DIR)
+
+        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))
+
+        # bind the tmp directory to the power preference directory
+        host.run('mount --bind %s %s' % (TMP_POWER_DIR, POWER_DIR))
+
+        logging.debug('Restarting powerd with new settings')
+        host.run('restart powerd')
+
+        logging.debug('Starting XMLRPC session to watch for dark resumes')
+        self._client_proxy = self._get_xmlrpc_proxy()
+
+
+    def teardown(self):
+        """Clean up changes made by DarkResumeUtils."""
+
+        logging.info('Tearing down dark resume preferences')
+
+        logging.debug('Cleaning up temporary powerd bind mounts')
+        self._host.run('umount %s' % POWER_DIR, ignore_status=True)
+
+        logging.debug('Restarting powerd to revert to old settings')
+        self._host.run('restart powerd')
+
+
+    def suspend(self):
+        """Returns a DarkResumeSuspend context manager that allows safe suspending
+        of the DUT."""
+        return DarkResumeSuspend(self._client_proxy, self._host)
+
+
+    def count_dark_resumes(self):
+        """Return the number of dark resumes that have occurred since the beginning
+        of the test. This will wake up the DUT, so make sure to put it back to
+        sleep if you need to keep it suspended for some reason.
+
+        This method will raise an error if the DUT does not wake up.
+
+        @return the number of dark resumes counted by this DarkResumeUtils
+
+        """
+        _wake_dut(self._host)
+
+        return self._client_proxy.get_dark_resume_count()
+
+
+    def _get_xmlrpc_proxy(self):
+        """Get a dark resume XMLRPC proxy for the host this DarkResumeUtils is
+        attached to.
+
+        The returned object has no particular type.  Instead, when you call
+        a method on the object, it marshalls the objects passed as arguments
+        and uses them to make RPCs on the remote server.  Thus, you should
+        read dark_resume_xmlrpc_server.py to find out what methods are supported.
+
+        @return proxy object for remote XMLRPC server.
+
+        """
+        # Make sure the client library is on the device so that the proxy
+        # code is there when we try to call it.
+        client_at = autotest.Autotest(self._host)
+        client_at.install()
+        # Start up the XMLRPC proxy on the client
+        proxy = self._host.xmlrpc_connect(
+                dark_resume_xmlrpc_server.SERVER_COMMAND,
+                dark_resume_xmlrpc_server.SERVER_PORT,
+                command_name=dark_resume_xmlrpc_server.CLEANUP_PATTERN,
+                ready_test_name=dark_resume_xmlrpc_server.READY_METHOD,
+                timeout_seconds=XMLRPC_BRINGUP_TIMEOUT_SECONDS)
+        return proxy
+
+
+def _wake_dut(host):
+    """Make sure |host| is up. If we can't wake it with normal keys, hit the
+    power button."""
+
+    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()
+
+        if host.wait_up(timeout=RESUME_GRACE_PERIOD):
+            woken = True
+            break
+        logging.debug('Wake attempt #%d failed', i+1)
+
+    if not woken:
+        logging.warning('DUT did not wake -- trouble ahead')
+        host.servo.power_key()
+        raise error.TestFail('DUT did not wake')