Bug 1494437 - Support timed screenshots in AndroidMixin; r=bc
authorGeoff Brown <gbrown@mozilla.com>
Wed, 24 Oct 2018 10:33:02 -0600
changeset 491107 1aa7374de7ee8e5810e89adfeb38f6d39f316bfa
parent 491106 79ef182cc44ef9bd252fb8c5b9c9f924690db75b
child 491108 be944769416fd05768e14019b519b011ce4fff23
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersbc
bugs1494437
milestone65.0a1
Bug 1494437 - Support timed screenshots in AndroidMixin; r=bc
testing/mozbase/mozscreenshot/mozscreenshot/__init__.py
testing/mozharness/configs/android/android_common.py
testing/mozharness/mozharness/mozilla/testing/android.py
--- a/testing/mozbase/mozscreenshot/mozscreenshot/__init__.py
+++ b/testing/mozbase/mozscreenshot/mozscreenshot/__init__.py
@@ -16,17 +16,17 @@ def printstatus(name, returncode):
     print the status of a command exit code, formatted for tbpl.
 
     Note that mozlog structured action "process_exit" should be used
     instead of that in new code.
     """
     print("TEST-INFO | %s: %s" % (name, strstatus(returncode)))
 
 
-def dump_screen(utilityPath, log):
+def dump_screen(utilityPath, log, prefix='mozilla-test-fail-screenshot_'):
     """dumps a screenshot of the entire screen to a directory specified by
     the MOZ_UPLOAD_DIR environment variable.
 
     :param utilityPath: Path of utility programs. This is typically a path
         to either the objdir's bin directory or a path to the host utilities.
     :param log: Reference to logger.
     """
 
@@ -47,33 +47,33 @@ def dump_screen(utilityPath, log):
     parent_dir = os.environ.get('MOZ_UPLOAD_DIR', None)
     if not parent_dir:
         log.info('Failed to retrieve MOZ_UPLOAD_DIR env var')
         return
 
     # Run the capture
     try:
         tmpfd, imgfilename = tempfile.mkstemp(
-            prefix='mozilla-test-fail-screenshot_',
+            prefix=prefix,
             suffix='.png', dir=parent_dir
         )
         os.close(tmpfd)
         if is_structured_log:
             log.process_start(utilityname)
         returncode = subprocess.call(utility + [imgfilename])
         if is_structured_log:
             log.process_exit(utilityname, returncode)
         else:
             printstatus(utilityname, returncode)
     except OSError as err:
         log.info("Failed to start %s for screenshot: %s"
                  % (utility[0], err.strerror))
 
 
-def dump_device_screen(device, log):
+def dump_device_screen(device, log, prefix='mozilla-test-fail-screenshot_'):
     """dumps a screenshot of a real device's entire screen to a directory
     specified by the MOZ_UPLOAD_DIR environment variable. Cloned from
     mozscreenshot.dump_screen.
 
     :param device: Reference to an ADBAndroid object which provides the
         interface to interact with Android devices.
     :param log: Reference to logger.
     """
@@ -90,18 +90,18 @@ def dump_device_screen(device, log):
     # Run the capture
     try:
         # Android 6.0 and later support mktemp.  See
         # https://android.googlesource.com/platform/system/core/
         # +/master/shell_and_utilities/README.md#android-6_0-marshmallow
         # We can use mktemp on real devices since we do not test on
         # real devices older than Android 6.0. Note we must create the
         # file without an extension due to limitations in mktemp.
-        filename = device.shell_output('mktemp -p %s mozilla-test-fail-screenshot_XXXXXX' %
-                                       device.test_root)
+        filename = device.shell_output('mktemp -p %s %sXXXXXX' %
+                                       (device.test_root, prefix))
         pngfilename = filename + '.png'
         device.mv(filename, pngfilename)
         if is_structured_log:
             log.process_start(utilityname)
         device.shell_output('%s -p %s' % (utilityname, pngfilename))
         if is_structured_log:
             log.process_exit(utilityname, 0)
         else:
--- a/testing/mozharness/configs/android/android_common.py
+++ b/testing/mozharness/configs/android/android_common.py
@@ -47,16 +47,20 @@ config = {
     "hostutils_manifest_path": "testing/config/tooltool-manifests/linux64/hostutils.manifest",
     "avds_dir": "/builds/worker/workspace/build/.android",
     # "log_format": "%(levelname)8s - %(message)s",
     "log_tbpl_level": "info",
     "log_raw_level": "info",
     "minidump_stackwalk_path": "/usr/local/bin/linux64-minidump_stackwalk",
     "marionette_address": "localhost:2828",
     "marionette_test_manifest": "unit-tests.ini",
+    # To take device screenshots at timed intervals (each time in seconds, relative
+    # to the start of the run-tests step) specify screenshot_times. For example, to
+    # take 4 screenshots at one minute intervals you could specify:
+    # "screenshot_times": [60, 120, 180, 240],
 
     "suite_definitions": {
         "mochitest": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
             "options": [
                 "--app=%(app)s",
                 "--remote-webserver=%(remote_webserver)s",
--- a/testing/mozharness/mozharness/mozilla/testing/android.py
+++ b/testing/mozharness/mozharness/mozilla/testing/android.py
@@ -8,18 +8,19 @@
 import datetime
 import glob
 import os
 import re
 import signal
 import subprocess
 import time
 import tempfile
+from threading import Timer
 from mozharness.mozilla.automation import TBPL_RETRY, EXIT_STATUS_DICT
-from mozharness.base.script import PostScriptAction
+from mozharness.base.script import PreScriptAction, PostScriptAction
 
 
 class AndroidMixin(object):
     """
        Mixin class used by Android test scripts.
     """
 
     def __init__(self, **kwargs):
@@ -189,17 +190,17 @@ class AndroidMixin(object):
         if not boot_ok:
             self.warning('Unable to verify Android boot completion')
             return False
         return True
 
     def _verify_emulator_and_restart_on_fail(self):
         emulator_ok = self._verify_emulator()
         if not emulator_ok:
-            self.device_screenshot()
+            self.device_screenshot("screenshot-emulator-start")
             self.kill_processes(self.config["emulator_process_name"])
             subprocess.check_call(['ps', '-ef'])
             # remove emulator tmp files
             for dir in glob.glob("/tmp/android-*"):
                 self.rmtree(dir)
             time.sleep(5)
             self.emulator_proc = self._launch_emulator()
         return emulator_ok
@@ -330,31 +331,33 @@ class AndroidMixin(object):
         out = self.device.get_prop('sys.boot_completed', timeout=30)
         if out.strip() == '1':
             return True
         return False
 
     def shell_output(self, cmd):
         return self.device.shell_output(cmd, timeout=30)
 
-    def device_screenshot(self):
+    def device_screenshot(self, prefix):
         """
            On emulator, save a screenshot of the entire screen to the upload directory;
            otherwise, save a screenshot of the device to the upload directory.
+
+           :param prefix specifies a filename prefix for the screenshot
         """
         from mozscreenshot import dump_screen, dump_device_screen
         reset_dir = False
         if not os.environ.get("MOZ_UPLOAD_DIR", None):
             dirs = self.query_abs_dirs()
             os.environ["MOZ_UPLOAD_DIR"] = dirs['abs_blob_upload_dir']
             reset_dir = True
         if self.is_emulator:
-            dump_screen(self.xre_path, self)
+            dump_screen(self.xre_path, self, prefix=prefix)
         else:
-            dump_device_screen(self.device, self)
+            dump_device_screen(self.device, self, prefix=prefix)
         if reset_dir:
             del os.environ["MOZ_UPLOAD_DIR"]
 
     def download_hostutils(self, xre_dir):
         """
            Download and install hostutils from tooltool.
         """
         xre_path = None
@@ -489,19 +492,40 @@ class AndroidMixin(object):
                            % max_restarts, EXIT_STATUS_DICT[TBPL_RETRY])
 
         self.mkdir_p(self.query_abs_dirs()['abs_blob_upload_dir'])
         self.dump_perf_info()
         self.logcat_start()
         # Get a post-boot device process list for diagnostics
         self.info(self.shell_output('ps'))
 
+    @PreScriptAction('run-tests')
+    def timed_screenshots(self, action, success=None):
+        """
+        If configured, start screenshot timers.
+        """
+        if not self.is_android:
+            return
+
+        def take_screenshot(seconds):
+            self.device_screenshot("screenshot-%ss-" % str(seconds))
+            self.info("timed (%ss) screenshot complete" % str(seconds))
+
+        self.timers = []
+        for seconds in self.config.get("screenshot_times", []):
+            self.info("screenshot requested %s seconds from now" % str(seconds))
+            t = Timer(int(seconds), take_screenshot, [seconds])
+            t.start()
+            self.timers.append(t)
+
     @PostScriptAction('run-tests')
     def stop_device(self, action, success=None):
         """
         Stop logcat and kill the emulator, if necessary.
         """
         if not self.is_android:
             return
 
+        for t in self.timers:
+            t.cancel()
         self.logcat_stop()
         if self.is_emulator:
             self.kill_processes(self.config["emulator_process_name"])