Add test to take pictures while suspending

This adds a test that suspend the system while recording frames from
the camera. The only property this verifies about each frame is that
it is different from the previous frame.

BUG=chromium-os:36056
TEST=run_remote_tests.sh power_CameraSuspend [daisy, link]

Change-Id: Id27f8c3880af66f937f2c1ec4dec727b6f84ccc4
Reviewed-on: https://gerrit.chromium.org/gerrit/40457
Reviewed-by: Scott Zawalski <scottz@chromium.org>
Tested-by: Michael Spang <spang@chromium.org>
Commit-Queue: Michael Spang <spang@chromium.org>
diff --git a/client/cros/camera/camera_utils.py b/client/cros/camera/camera_utils.py
index ba45f03..919ae8d 100644
--- a/client/cros/camera/camera_utils.py
+++ b/client/cros/camera/camera_utils.py
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import glob, os
 import numpy as np
 
 # Dimension padding/unpadding function for converting points matrices to
@@ -24,3 +25,17 @@
         return (self.__class__.__name__ + '(' +
         ', '.join('%s=%s' % (k, v) for k, v in sorted(self.__dict__.items())
                   if not k.startswith('_')) + ')')
+
+
+def find_camera():
+    """
+    Find a V4L camera device.
+
+    @return (device_name, device_index). If no camera is found, (None, None).
+    """
+    cameras = [os.path.basename(camera) for camera in
+               glob.glob('/sys/bus/usb/drivers/uvcvideo/*/video4linux/video*')]
+    if not cameras:
+        return None, None
+    camera = cameras[0]
+    return camera, int(camera[5:])
diff --git a/client/site_tests/power_CameraSuspend/control b/client/site_tests/power_CameraSuspend/control
new file mode 100644
index 0000000..b51fcf0
--- /dev/null
+++ b/client/site_tests/power_CameraSuspend/control
@@ -0,0 +1,20 @@
+# Copyright (c) 2012 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.
+
+AUTHOR = "spang, chromeos-kernel"
+NAME = "power_CameraSuspend"
+SUITE = "kernel_per-build_regression"
+TIME = "SHORT"
+TEST_CATEGORY = "Functional"
+TEST_CLASS = "power"
+TEST_TYPE = "client"
+DEPENDENCIES = "webcam"
+EXPERIMENTAL = "True"
+
+DOC = """
+Suspend the system with the camera device open.
+"""
+
+job.add_sysinfo_logfile('/sys/kernel/debug/suspend_stats', on_every_test=True)
+job.run_test('power_CameraSuspend', save_images=False)
diff --git a/client/site_tests/power_CameraSuspend/power_CameraSuspend.py b/client/site_tests/power_CameraSuspend/power_CameraSuspend.py
new file mode 100644
index 0000000..3cb16cd
--- /dev/null
+++ b/client/site_tests/power_CameraSuspend/power_CameraSuspend.py
@@ -0,0 +1,87 @@
+# Copyright (c) 2013 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, multiprocessing, os, time
+import numpy
+from autotest_lib.client.bin import test
+from autotest_lib.client.cros import sys_power
+from autotest_lib.client.cros.camera import camera_utils
+from autotest_lib.client.common_lib import error
+
+try:
+    # HACK: We need to succeed if OpenCV is missing to allow "emerge
+    # autotest-tests" to succeed, as OpenCV is not available in the build
+    # environment. It is available on the target where the test actually runs.
+    import cv2
+except ImportError:
+    pass
+
+
+def async_suspend():
+    try:
+        time.sleep(5) # allow some time to start capturing
+        sys_power.do_suspend(seconds=10, method='kernel')
+    except:
+        # Any exception will be re-raised in main process, but the stack trace
+        # will be wrong. Log it here with the correct stack trace.
+        logging.exception('suspend raised exception')
+        raise
+
+
+class power_CameraSuspend(test.test):
+    """Test camera before & after suspend."""
+
+    version = 1
+
+    def run_once(self, save_images=False):
+        # open the camera via opencv
+        cam_name, cam_index = camera_utils.find_camera()
+        if cam_index is None:
+            raise error.TestError('no camera found')
+        cam = cv2.VideoCapture(cam_index)
+
+        # kick off async suspend
+        logging.info('starting subprocess to suspend system')
+        pool = multiprocessing.Pool(processes=1)
+        # TODO(spang): Move async suspend to library.
+        result = pool.apply_async(async_suspend)
+
+        # capture images concurrently with suspend
+        capture_start = time.time()
+        logging.info('start capturing at %d', capture_start)
+        image_count = 0
+        resume_count = None
+        last_image = None
+
+        while True:
+            # terminate if we've captured a few frames after resume
+            if result.ready() and resume_count is None:
+                result.get() # reraise exception, if any
+                resume_count = image_count
+                logging.info('suspend task finished')
+            if resume_count is not None and image_count - resume_count >= 10:
+                break
+
+            # capture one frame
+            image_ok, image = cam.read()
+            image_count += 1
+            if not image_ok:
+                logging.error('failed capture at image %d', image_count)
+                raise error.TestFail('image capture failed from %s', cam_name)
+
+            # write image to disk, if requested
+            if save_images and image_count <= 200:
+                path = os.path.join(self.outputdir, '%03d.jpg' % image_count)
+                cv2.imwrite(path, image)
+
+            # verify camera produces a unique image on each capture
+            if last_image is not None and numpy.array_equal(image, last_image):
+                raise error.TestFail('camera produced two identical images')
+            last_image = image
+
+        capture_end = time.time()
+        logging.info('done capturing at %d', capture_end)
+
+        logging.info('captured %d frames in %d seconds',
+                     image_count, capture_end - capture_start)