build/mobile/b2gautomation.py

Wed, 31 Dec 2014 07:16:47 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:16:47 +0100
branch
TOR_BUG_9701
changeset 3
141e0f1194b1
permissions
-rw-r--r--

Revert simplistic fix pending revisit of Mozilla integration attempt.

     1 # This Source Code Form is subject to the terms of the Mozilla Public
     2 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3 # You can obtain one at http://mozilla.org/MPL/2.0/.
     5 import mozcrash
     6 import threading
     7 import os
     8 import Queue
     9 import re
    10 import shutil
    11 import tempfile
    12 import time
    13 import traceback
    15 from automation import Automation
    16 from mozprocess import ProcessHandlerMixin
    19 class StdOutProc(ProcessHandlerMixin):
    20     """Process handler for b2g which puts all output in a Queue.
    21     """
    23     def __init__(self, cmd, queue, **kwargs):
    24         self.queue = queue
    25         kwargs.setdefault('processOutputLine', []).append(self.handle_output)
    26         ProcessHandlerMixin.__init__(self, cmd, **kwargs)
    28     def handle_output(self, line):
    29         self.queue.put_nowait(line)
    32 class B2GRemoteAutomation(Automation):
    33     _devicemanager = None
    35     def __init__(self, deviceManager, appName='', remoteLog=None,
    36                  marionette=None, context_chrome=True):
    37         self._devicemanager = deviceManager
    38         self._appName = appName
    39         self._remoteProfile = None
    40         self._remoteLog = remoteLog
    41         self.marionette = marionette
    42         self.context_chrome = context_chrome
    43         self._is_emulator = False
    44         self.test_script = None
    45         self.test_script_args = None
    47         # Default our product to b2g
    48         self._product = "b2g"
    49         self.lastTestSeen = "b2gautomation.py"
    50         # Default log finish to mochitest standard
    51         self.logFinish = 'INFO SimpleTest FINISHED'
    52         Automation.__init__(self)
    54     def setEmulator(self, is_emulator):
    55         self._is_emulator = is_emulator
    57     def setDeviceManager(self, deviceManager):
    58         self._devicemanager = deviceManager
    60     def setAppName(self, appName):
    61         self._appName = appName
    63     def setRemoteProfile(self, remoteProfile):
    64         self._remoteProfile = remoteProfile
    66     def setProduct(self, product):
    67         self._product = product
    69     def setRemoteLog(self, logfile):
    70         self._remoteLog = logfile
    72     def installExtension(self, extensionSource, profileDir, extensionID=None):
    73         # Bug 827504 - installing special-powers extension separately causes problems in B2G
    74         if extensionID != "special-powers@mozilla.org":
    75             Automation.installExtension(self, extensionSource, profileDir, extensionID)
    77     # Set up what we need for the remote environment
    78     def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False):
    79         # Because we are running remote, we don't want to mimic the local env
    80         # so no copying of os.environ
    81         if env is None:
    82             env = {}
    84         if crashreporter:
    85             env['MOZ_CRASHREPORTER'] = '1'
    86             env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
    88         # We always hide the results table in B2G; it's much slower if we don't.
    89         env['MOZ_HIDE_RESULTS_TABLE'] = '1'
    90         return env
    92     def waitForNet(self):
    93         active = False
    94         time_out = 0
    95         while not active and time_out < 40:
    96             data = self._devicemanager._runCmd(['shell', '/system/bin/netcfg']).stdout.readlines()
    97             data.pop(0)
    98             for line in data:
    99                 if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)):
   100                     active = True
   101                     break
   102             time_out += 1
   103             time.sleep(1)
   104         return active
   106     def checkForCrashes(self, directory, symbolsPath):
   107         crashed = False
   108         remote_dump_dir = self._remoteProfile + '/minidumps'
   109         print "checking for crashes in '%s'" % remote_dump_dir
   110         if self._devicemanager.dirExists(remote_dump_dir):
   111             local_dump_dir = tempfile.mkdtemp()
   112             self._devicemanager.getDirectory(remote_dump_dir, local_dump_dir)
   113             try:
   114                 crashed = mozcrash.check_for_crashes(local_dump_dir, symbolsPath, test_name=self.lastTestSeen)
   115             except:
   116                 traceback.print_exc()
   117             finally:
   118                 shutil.rmtree(local_dump_dir)
   119                 self._devicemanager.removeDir(remote_dump_dir)
   120         return crashed
   122     def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
   123         # if remote profile is specified, use that instead
   124         if (self._remoteProfile):
   125             profileDir = self._remoteProfile
   127         cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
   129         return app, args
   131     def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime,
   132                       debuggerInfo, symbolsPath):
   133         """ Wait for tests to finish (as evidenced by a signature string
   134             in logcat), or for a given amount of time to elapse with no
   135             output.
   136         """
   137         timeout = timeout or 120
   138         while True:
   139             currentlog = proc.getStdoutLines(timeout)
   140             if currentlog:
   141                 print currentlog
   142                 # Match the test filepath from the last TEST-START line found in the new
   143                 # log content. These lines are in the form:
   144                 # ... INFO TEST-START | /filepath/we/wish/to/capture.html\n
   145                 testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", currentlog)
   146                 if testStartFilenames:
   147                     self.lastTestSeen = testStartFilenames[-1]
   148                 if hasattr(self, 'logFinish') and self.logFinish in currentlog:
   149                     return 0
   150             else:
   151                 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed "
   152                               "out after %d seconds with no output",
   153                               self.lastTestSeen, int(timeout))
   154                 return 1
   156     def getDeviceStatus(self, serial=None):
   157         # Get the current status of the device.  If we know the device
   158         # serial number, we look for that, otherwise we use the (presumably
   159         # only) device shown in 'adb devices'.
   160         serial = serial or self._devicemanager._deviceSerial
   161         status = 'unknown'
   163         for line in self._devicemanager._runCmd(['devices']).stdout.readlines():
   164             result = re.match('(.*?)\t(.*)', line)
   165             if result:
   166                 thisSerial = result.group(1)
   167                 if not serial or thisSerial == serial:
   168                     serial = thisSerial
   169                     status = result.group(2)
   171         return (serial, status)
   173     def restartB2G(self):
   174         # TODO hangs in subprocess.Popen without this delay
   175         time.sleep(5)
   176         self._devicemanager._checkCmd(['shell', 'stop', 'b2g'])
   177         # Wait for a bit to make sure B2G has completely shut down.
   178         time.sleep(10)
   179         self._devicemanager._checkCmd(['shell', 'start', 'b2g'])
   180         if self._is_emulator:
   181             self.marionette.emulator.wait_for_port()
   183     def rebootDevice(self):
   184         # find device's current status and serial number
   185         serial, status = self.getDeviceStatus()
   187         # reboot!
   188         self._devicemanager._runCmd(['shell', '/system/bin/reboot'])
   190         # The above command can return while adb still thinks the device is
   191         # connected, so wait a little bit for it to disconnect from adb.
   192         time.sleep(10)
   194         # wait for device to come back to previous status
   195         print 'waiting for device to come back online after reboot'
   196         start = time.time()
   197         rserial, rstatus = self.getDeviceStatus(serial)
   198         while rstatus != 'device':
   199             if time.time() - start > 120:
   200                 # device hasn't come back online in 2 minutes, something's wrong
   201                 raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus))
   202             time.sleep(5)
   203             rserial, rstatus = self.getDeviceStatus(serial)
   204         print 'device:', serial, 'status:', rstatus
   206     def Process(self, cmd, stdout=None, stderr=None, env=None, cwd=None):
   207         # On a desktop or fennec run, the Process method invokes a gecko
   208         # process in which to the tests.  For B2G, we simply
   209         # reboot the device (which was configured with a test profile
   210         # already), wait for B2G to start up, and then navigate to the
   211         # test url using Marionette.  There doesn't seem to be any way
   212         # to pass env variables into the B2G process, but this doesn't
   213         # seem to matter.
   215         # reboot device so it starts up with the mochitest profile
   216         # XXX:  We could potentially use 'stop b2g' + 'start b2g' to achieve
   217         # a similar effect; will see which is more stable while attempting
   218         # to bring up the continuous integration.
   219         if not self._is_emulator:
   220             self.rebootDevice()
   221             time.sleep(5)
   222             #wait for wlan to come up
   223             if not self.waitForNet():
   224                 raise Exception("network did not come up, please configure the network" +
   225                                 " prior to running before running the automation framework")
   227         # stop b2g
   228         self._devicemanager._runCmd(['shell', 'stop', 'b2g'])
   229         time.sleep(5)
   231         # relaunch b2g inside b2g instance
   232         instance = self.B2GInstance(self._devicemanager, env=env)
   234         time.sleep(5)
   236         # Set up port forwarding again for Marionette, since any that
   237         # existed previously got wiped out by the reboot.
   238         if not self._is_emulator:
   239             self._devicemanager._checkCmd(['forward',
   240                                            'tcp:%s' % self.marionette.port,
   241                                            'tcp:%s' % self.marionette.port])
   243         if self._is_emulator:
   244             self.marionette.emulator.wait_for_port()
   245         else:
   246             time.sleep(5)
   248         # start a marionette session
   249         session = self.marionette.start_session()
   250         if 'b2g' not in session:
   251             raise Exception("bad session value %s returned by start_session" % session)
   253         if self._is_emulator:
   254             # Disable offline status management (bug 777145), otherwise the network
   255             # will be 'offline' when the mochitests start.  Presumably, the network
   256             # won't be offline on a real device, so we only do this for emulators.
   257             self.marionette.set_context(self.marionette.CONTEXT_CHROME)
   258             self.marionette.execute_script("""
   259                 Components.utils.import("resource://gre/modules/Services.jsm");
   260                 Services.io.manageOfflineStatus = false;
   261                 Services.io.offline = false;
   262                 """)
   264         if self.context_chrome:
   265             self.marionette.set_context(self.marionette.CONTEXT_CHROME)
   266         else:
   267             self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
   269         # run the script that starts the tests
   270         if self.test_script:
   271             if os.path.isfile(self.test_script):
   272                 script = open(self.test_script, 'r')
   273                 self.marionette.execute_script(script.read(), script_args=self.test_script_args)
   274                 script.close()
   275             elif isinstance(self.test_script, basestring):
   276                 self.marionette.execute_script(self.test_script, script_args=self.test_script_args)
   277         else:
   278             # assumes the tests are started on startup automatically
   279             pass
   281         return instance
   283     # be careful here as this inner class doesn't have access to outer class members
   284     class B2GInstance(object):
   285         """Represents a B2G instance running on a device, and exposes
   286            some process-like methods/properties that are expected by the
   287            automation.
   288         """
   290         def __init__(self, dm, env=None):
   291             self.dm = dm
   292             self.env = env or {}
   293             self.stdout_proc = None
   294             self.queue = Queue.Queue()
   296             # Launch b2g in a separate thread, and dump all output lines
   297             # into a queue.  The lines in this queue are
   298             # retrieved and returned by accessing the stdout property of
   299             # this class.
   300             cmd = [self.dm._adbPath]
   301             if self.dm._deviceSerial:
   302                 cmd.extend(['-s', self.dm._deviceSerial])
   303             cmd.append('shell')
   304             for k, v in self.env.iteritems():
   305                 cmd.append("%s=%s" % (k, v))
   306             cmd.append('/system/bin/b2g.sh')
   307             proc = threading.Thread(target=self._save_stdout_proc, args=(cmd, self.queue))
   308             proc.daemon = True
   309             proc.start()
   311         def _save_stdout_proc(self, cmd, queue):
   312             self.stdout_proc = StdOutProc(cmd, queue)
   313             self.stdout_proc.run()
   314             if hasattr(self.stdout_proc, 'processOutput'):
   315                 self.stdout_proc.processOutput()
   316             self.stdout_proc.wait()
   317             self.stdout_proc = None
   319         @property
   320         def pid(self):
   321             # a dummy value to make the automation happy
   322             return 0
   324         def getStdoutLines(self, timeout):
   325             # Return any lines in the queue used by the
   326             # b2g process handler.
   327             lines = []
   328             # get all of the lines that are currently available
   329             while True:
   330                 try:
   331                     lines.append(self.queue.get_nowait())
   332                 except Queue.Empty:
   333                     break
   335             # wait 'timeout' for any additional lines
   336             try:
   337                 lines.append(self.queue.get(True, timeout))
   338             except Queue.Empty:
   339                 pass
   340             return '\n'.join(lines)
   342         def wait(self, timeout=None):
   343             # this should never happen
   344             raise Exception("'wait' called on B2GInstance")
   346         def kill(self):
   347             # this should never happen
   348             raise Exception("'kill' called on B2GInstance")

mercurial