Bug 803254 - Detect emulator crashes, r=ahal, DONTBUILD(NPOTB)
authorJonathan Griffin <jgriffin@mozilla.com>
Mon, 22 Oct 2012 15:00:54 -0700
changeset 111327 b1e54bf5bd6f5c2f81088874949252491d9c3287
parent 111326 b06ee7ea6ba03a6f89d79b5bae6d3fa86f0c87ba
child 111328 ad5279dd2037791d4d9bee7f58c6b6deb7f19bf1
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewersahal, DONTBUILD
bugs803254
milestone19.0a1
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