Bug 803254 - Detect emulator crashes, r=ahal, DONTBUILD(NPOTB)
authorJonathan Griffin <jgriffin@mozilla.com>
Mon, 22 Oct 2012 15:00:54 -0700
changeset 111195 b1e54bf5bd6f5c2f81088874949252491d9c3287
parent 111194 b06ee7ea6ba03a6f89d79b5bae6d3fa86f0c87ba
child 111196 ad5279dd2037791d4d9bee7f58c6b6deb7f19bf1
push id23733
push userjgriffin@mozilla.com
push dateWed, 24 Oct 2012 14:01:35 +0000
treeherdermozilla-central@b1e54bf5bd6f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersahal, DONTBUILD
bugs803254
milestone19.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 803254 - Detect emulator crashes, r=ahal, DONTBUILD(NPOTB)
testing/marionette/client/marionette/emulator.py
testing/marionette/client/marionette/marionette.py
testing/marionette/client/marionette/runtests.py
testing/mozbase/mozdevice/mozdevice/emulator.py
--- a/testing/marionette/client/marionette/emulator.py
+++ b/testing/marionette/client/marionette/emulator.py
@@ -147,16 +147,30 @@ class Emulator(object):
 
     @property
     def is_running(self):
         if self._emulator_launched:
             return self.proc is not None and self.proc.poll() is None
         else:
             return self.port is not None
 
+    def check_for_crash(self):
+        """
+        Checks if the emulator has crashed or not.  Always returns False if
+        we've connected to an already-running emulator, since we can't track
+        the emulator's pid in that case.  Otherwise, returns True iff
+        self.proc is not None (meaning the emulator hasn't been explicitly
+        closed), and self.proc.poll() is also not None (meaning the emulator
+        process has terminated).
+        """
+        if (self._emulator_launched and self.proc is not None
+                                    and self.proc.poll() is not None):
+            return True
+        return False
+
     def create_sdcard(self, sdcard):
         self._tmp_sdcard = tempfile.mktemp(prefix='sdcard')
         sdargs = [self.mksdcard, "-l", "mySdCard", sdcard, self._tmp_sdcard]
         sd = subprocess.Popen(sdargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
         retcode = sd.wait()
         if retcode:
             raise Exception('unable to create sdcard : exit code %d: %s'
                             % (retcode, sd.stdout.read()))
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -261,16 +261,32 @@ class Marionette(object):
                  or status == ErrorCodes.INVALID_XPATH_SELECTOR_RETURN_TYPER:
                 raise InvalidSelectorException(message=message, status=status, stacktrace=stacktrace)
             elif status == ErrorCodes.MOVE_TARGET_OUT_OF_BOUNDS:
                 MoveTargetOutOfBoundsException(message=message, status=status, stacktrace=stacktrace)
             else:
                 raise MarionetteException(message=message, status=status, stacktrace=stacktrace)
         raise MarionetteException(message=response, status=500)
 
+    def check_for_crash(self):
+        returncode = None
+        name = None
+        if self.emulator:
+            if self.emulator.check_for_crash():
+                returncode = self.emulator.proc.returncode
+                name = 'emulator'
+        elif self.instance:
+            # In the future, a check for crashed Firefox processes
+            # should be here.
+            pass
+        if returncode is not None:
+            print ('TEST-UNEXPECTED-FAIL - PROCESS CRASH - %s has terminated with exit code %d' %
+                (name, returncode))
+        return returncode is not None
+
     def absolute_url(self, relative_url):
         return "%s%s" % (self.baseurl, relative_url)
 
     def status(self):
         return self._send_message('getStatus', 'value')
 
     def start_session(self, desired_capabilities=None):
         # We are ignoring desired_capabilities, at least for now.
--- a/testing/marionette/client/marionette/runtests.py
+++ b/testing/marionette/client/marionette/runtests.py
@@ -28,18 +28,20 @@ except ImportError:
     sys.exit(1)
 
 from marionette import Marionette
 from marionette_test import MarionetteJSTestCase, MarionetteTestCase
 
 
 class MarionetteTestResult(unittest._TextTestResult):
 
-    def __init__(self, *args):
-        super(MarionetteTestResult, self).__init__(*args)
+    def __init__(self, *args, **kwargs):
+        self.marionette = kwargs['marionette']
+        del kwargs['marionette']
+        super(MarionetteTestResult, self).__init__(*args, **kwargs)
         self.passed = 0
         self.perfdata = None
         self.tests_passed = []
 
     def addSuccess(self, test):
         super(MarionetteTestResult, self).addSuccess(test)
         self.passed += 1
         self.tests_passed.append(test)
@@ -83,28 +85,39 @@ class MarionetteTestResult(unittest._Tex
             self.stream.writeln(self.separator1)
             self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
             self.stream.writeln(self.separator2)
             errlines = err.strip().split('\n')
             for line in errlines[0:-1]:
                 self.stream.writeln("%s" % line)
             self.stream.writeln("TEST-UNEXPECTED-FAIL : %s" % errlines[-1])
 
+    def stopTest(self, *args, **kwargs):
+        unittest._TextTestResult.stopTest(self, *args, **kwargs)
+        if self.marionette.check_for_crash():
+            # this tells unittest.TestSuite not to continue running tests
+            self.shouldStop = True
+
 
 class MarionetteTextTestRunner(unittest.TextTestRunner):
 
     resultclass = MarionetteTestResult
 
     def __init__(self, **kwargs):
         self.perf = kwargs['perf']
         del kwargs['perf']
+        self.marionette = kwargs['marionette']
+        del kwargs['marionette']
         unittest.TextTestRunner.__init__(self, **kwargs)
 
     def _makeResult(self):
-        return self.resultclass(self.stream, self.descriptions, self.verbosity)
+        return self.resultclass(self.stream,
+                                self.descriptions,
+                                self.verbosity,
+                                marionette=self.marionette)
 
     def run(self, test):
         "Run the given test case or test suite."
         result = self._makeResult()
         if hasattr(self, 'failfast'):
             result.failfast = self.failfast
         if hasattr(self, 'buffer'):
             result.buffer = self.buffer
@@ -354,29 +367,31 @@ class MarionetteTestRunner(object):
             self.marionette.instance.close()
             self.marionette.instance = None
         del self.marionette
 
     def run_test(self, test, testtype):
         if not self.httpd:
             print "starting httpd"
             self.start_httpd()
-        
+
         if not self.marionette:
             self.start_marionette()
 
         filepath = os.path.abspath(test)
 
         if os.path.isdir(filepath):
             for root, dirs, files in os.walk(filepath):
                 for filename in files:
                     if ((filename.startswith('test_') or filename.startswith('browser_')) and 
                         (filename.endswith('.py') or filename.endswith('.js'))):
                         filepath = os.path.join(root, filename)
                         self.run_test(filepath, testtype)
+                        if self.marionette.check_for_crash():
+                            return
             return
 
         mod_name,file_ext = os.path.splitext(os.path.split(filepath)[-1])
 
         testloader = unittest.TestLoader()
         suite = unittest.TestSuite()
 
         if file_ext == '.ini':
@@ -417,27 +432,32 @@ class MarionetteTestRunner(object):
                              branch=manifest.get("branch")[0],
                              id=os.getenv('BUILD_ID'),
                              test_date=int(time.time()))
 
             manifest_tests = manifest.get(**testargs)
 
             for i in manifest_tests:
                 self.run_test(i["path"], testtype)
+                if self.marionette.check_for_crash():
+                    return
             return
 
         self.logger.info('TEST-START %s' % os.path.basename(test))
 
         for handler in self.test_handlers:
             if handler.match(os.path.basename(test)):
                 handler.add_tests_to_suite(mod_name, filepath, suite, testloader, self.marionette, self.testvars)
                 break
 
         if suite.countTestCases():
-            results = MarionetteTextTestRunner(verbosity=3, perf=self.perf).run(suite)
+            runner = MarionetteTextTestRunner(verbosity=3,
+                                              perf=self.perf,
+                                              marionette=self.marionette)
+            results = runner.run(suite)
             self.results.append(results)
 
             self.failed += len(results.failures) + len(results.errors)
             if results.perfdata and options.perf:
                 self.perfrequest.add_datazilla_result(results.perfdata)
             if hasattr(results, 'skipped'):
                 self.todo += len(results.skipped) + len(results.expectedFailures)
             self.passed += results.passed
--- a/testing/mozbase/mozdevice/mozdevice/emulator.py
+++ b/testing/mozbase/mozdevice/mozdevice/emulator.py
@@ -82,16 +82,30 @@ class Emulator(object):
 
     @property
     def is_running(self):
         if self._emulator_launched:
             return self.proc is not None and self.proc.poll() is None
         else:
             return self.port is not None
 
+    def check_for_crash(self):
+        """
+        Checks if the emulator has crashed or not.  Always returns False if
+        we've connected to an already-running emulator, since we can't track
+        the emulator's pid in that case.  Otherwise, returns True iff
+        self.proc is not None (meaning the emulator hasn't been explicitly
+        closed), and self.proc.poll() is also not None (meaning the emulator
+        process has terminated).
+        """
+        if (self._emulator_launched and self.proc is not None
+                                    and self.proc.poll() is not None):
+            return True
+        return False
+
     def _default_adb(self):
         adb = subprocess.Popen(['which', 'adb'],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT)
         retcode = adb.wait()
         if retcode == 0:
             self.adb = adb.stdout.read().strip() # remove trailing newline
         return retcode