Bug 1299216 - Enhance handling of crashes in Marionette. draft
authorHenrik Skupin <mail@hskupin.info>
Wed, 02 Nov 2016 13:49:32 +0100
changeset 435303 090f868fde153f479cdbf11c703fe73d8cd4b083
parent 435302 35934fe5e30763c08b0cad93c988f5f2be5401f0
child 435304 bc4f6e73b9d49cda6c251ba5360f984bb626daa4
push id34992
push userbmo:hskupin@gmail.com
push dateTue, 08 Nov 2016 11:06:47 +0000
bugs1299216
milestone52.0a1
Bug 1299216 - Enhance handling of crashes in Marionette. There are currently various issues in Marionette which cause extra and unnecessary logging output in case of chrome and content crashes. Especially the latter ones are not easy to discover. This patch makes the following improvements: * Correctly mark process crashes as IOError test failure. Until now we have only used the "Process has been closed" message, which does not correctly cover it, especially not if the process shutdown without a crash. * Allow changing the socket_timeout for the socket client. This was mainly necessary to allow unit tests to run faster. * Collect the number of crashes so it's known even later if the process has been crashed during the current session. It also fixes the case when check_for_crash() gets called twice, and for the second time False is returned which will trigger an invalid code path. * Reduce code duplication when destroying a session. * Adding a unit test to verify the correct behavior for chrome and content crashes. MozReview-Commit-ID: KdUQuJqFRli
testing/marionette/client/marionette_driver/decorators.py
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/client/marionette_driver/transport.py
testing/marionette/client/requirements.txt
testing/marionette/harness/marionette/__init__.py
testing/marionette/harness/marionette/marionette_test/__init__.py
testing/marionette/harness/marionette/marionette_test/decorators.py
testing/marionette/harness/marionette/marionette_test/testcases.py
testing/marionette/harness/marionette/runner/base.py
testing/marionette/harness/marionette/tests/unit/test_crash.py
testing/marionette/harness/marionette/tests/unit/unit-tests.ini
testing/marionette/harness/requirements.txt
--- a/testing/marionette/client/marionette_driver/decorators.py
+++ b/testing/marionette/client/marionette_driver/decorators.py
@@ -28,41 +28,44 @@ def do_process_check(func, always=False)
                    was raised. If True, always checks for crashes.
     """
     @wraps(func)
     def _(*args, **kwargs):
         m = _find_marionette_in_args(*args, **kwargs)
 
         def check_for_crash():
             try:
-                m.check_for_crash()
-            except:
+                return m.check_for_crash()
+            except Exception:
                 # don't want to lose the original exception
                 traceback.print_exc()
 
+                return False
+
         try:
             return func(*args, **kwargs)
         except (MarionetteException, IOError) as e:
             exc, val, tb = sys.exc_info()
+            crashed = False
 
             # In case of no Marionette failures ensure to check for possible crashes.
             # Do it before checking for port disconnects, to avoid reporting of unrelated
             # crashes due to a forced shutdown of the application.
             if not isinstance(e, MarionetteException) or type(e) is MarionetteException:
                 if not always:
-                    check_for_crash()
+                    crashed = check_for_crash()
 
             # In case of socket failures force a shutdown of the application
-            if type(e) in (socket.error, socket.timeout):
-                m.force_shutdown()
+            if type(e) in (socket.error, socket.timeout) or crashed:
+                m.handle_socket_failure(crashed)
 
             raise exc, val, tb
         finally:
             if always:
-                check_for_crash(m)
+                check_for_crash()
     return _
 
 
 def uses_marionette(func):
     """Decorator which creates a marionette session and deletes it
     afterwards if one doesn't already exist.
     """
     @wraps(func)
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -535,16 +535,17 @@ class Alert(object):
         self.marionette._send_message("sendKeysToDialog", body)
 
 
 class Marionette(object):
     """Represents a Marionette connection to a browser or device."""
 
     CONTEXT_CHROME = 'chrome'  # non-browser content: windows, dialogs, etc.
     CONTEXT_CONTENT = 'content'  # browser content: iframes, divs, etc.
+    DEFAULT_CRASH_TIMEOUT = 10
     DEFAULT_SOCKET_TIMEOUT = 60
     DEFAULT_STARTUP_TIMEOUT = 120
     DEFAULT_SHUTDOWN_TIMEOUT = 65  # Firefox will kill hanging threads after 60s
 
     def __init__(self, host='localhost', port=2828, app=None, bin=None,
                  baseurl=None, timeout=None, socket_timeout=DEFAULT_SOCKET_TIMEOUT,
                  startup_timeout=None, **instance_args):
         """
@@ -567,16 +568,17 @@ class Marionette(object):
         self.session = None
         self.session_id = None
         self.window = None
         self.chrome_window = None
         self.baseurl = baseurl
         self._test_name = None
         self.timeout = timeout
         self.socket_timeout = socket_timeout
+        self.crashed = 0
 
         startup_timeout = startup_timeout or self.DEFAULT_STARTUP_TIMEOUT
         if self.bin:
             self.instance = self._create_instance(app, instance_args)
             self.instance.start()
             self.raise_for_port(timeout=startup_timeout)
 
     def _create_instance(self, app, instance_args):
@@ -616,17 +618,16 @@ class Marionette(object):
         if self.session:
             try:
                 self.delete_session()
             except (errors.MarionetteException, IOError):
                 # These exceptions get thrown if the Marionette server
                 # hit an exception/died or the connection died. We can
                 # do no further server-side cleanup in this case.
                 pass
-            self.session = None
         if self.instance:
             self.instance.close()
 
     def __del__(self):
         self.cleanup()
 
     @staticmethod
     def is_port_available(port, host=''):
@@ -716,21 +717,18 @@ class Marionette(object):
                 if params:
                     data["parameters"] = params
                 self.client.send(data)
                 msg = self.client.receive()
 
             else:
                 msg = self.client.request(name, params)
 
-        except socket.timeout:
-            self.session = None
-            self.window = None
-            self.client.close()
-
+        except IOError:
+            self.delete_session(send_request=False)
             raise
 
         res, err = msg.result, msg.error
         if err:
             self._handle_error(err)
 
         if key is not None:
             return self._unwrap_response(res.get(key))
@@ -778,46 +776,64 @@ class Marionette(object):
 
         if self.timeout is not None:
             for typ, ms in self.timeout:
                 timeout_types[typ](ms)
         else:
             self.set_page_load_timeout(30000)
 
     def check_for_crash(self):
-        returncode = None
-        name = None
-        crashed = False
+        """Check if the process crashed.
+
+        :returns: True, if a crash happened since the method has been called the last time.
+        """
+        crash_count = 0
+
         if self.instance:
-            if self.instance.runner.check_for_crashes(
-                    test_name=self.test_name or os.path.basename(__file__)):
-                crashed = True
-        if returncode is not None:
-            print ('PROCESS-CRASH | {0} | abnormal termination with exit code {1}'
-                   .format(name, returncode))
-        return crashed
+            name = self.test_name or 'marionette.py'
+            crash_count = self.instance.runner.check_for_crashes(test_name=name)
+            self.crashed = self.crashed + crash_count
+
+        return crash_count > 0
 
-    def force_shutdown(self):
-        """Force a shutdown of the running instance.
+    def handle_socket_failure(self, crashed=False):
+        """Handle socket failures for the currently running instance.
 
-        If we've launched the binary we are connected to, wait for it to shut down.
-        In the case when it doesn't happen, force its shut down.
+        :param crashed: Optional flag which indicates that the process has been crashed,
+            and no further socket checks have to be performed. Defaults to False.
+
+        If the application crashed then clean-up internal states, or in case of a content
+        crash also kill the process. If there are other reasons for a socket failure,
+        wait for the process to shutdown itself, or force kill it.
 
         """
         if self.instance:
             exc, val, tb = sys.exc_info()
 
-            # Give the application some time to shutdown
-            returncode = self.instance.runner.wait(timeout=self.DEFAULT_SHUTDOWN_TIMEOUT)
-            if returncode is None:
-                self.cleanup()
-                message = ('Process killed because the connection to Marionette server is lost.'
-                           ' Check gecko.log for errors')
+            # If the content process crashed, Marionette forces the application to shutdown.
+            if crashed:
+                returncode = self.instance.runner.wait(timeout=self.DEFAULT_CRASH_TIMEOUT)
+
+                if returncode == 0:
+                    message = 'Content process crashed'
+                else:
+                    message = 'Process crashed (Exit code: {returncode})'
+                self.delete_session(send_request=False, reset_session_id=True)
+
             else:
-                message = 'Process has been closed (Exit code: {returncode})'
+                # Somehow the socket disconnected. Give the application some time to shutdown
+                # itself before killing the process.
+                returncode = self.instance.runner.wait(timeout=self.DEFAULT_SHUTDOWN_TIMEOUT)
+                if returncode is None:
+                    self.quit()
+                    message = ('Process killed because the connection to Marionette server is '
+                               'lost. Check gecko.log for errors')
+                else:
+                    message = 'Process has been closed (Exit code: {returncode})'
+                    self.delete_session(send_request=False, reset_session_id=True)
 
             if exc:
                 message += ' (Reason: {reason})'
 
             raise IOError, message.format(returncode=returncode, reason=val), tb
 
     @staticmethod
     def convert_keys(*string):
@@ -1130,22 +1146,24 @@ class Marionette(object):
         self.reset_timeouts()
 
         if in_app:
             if callable(callback):
                 self._send_message("acceptConnections", {"value": False})
                 callback()
             else:
                 self._request_in_app_shutdown()
-            self.delete_session(in_app=True)
+
+            # Ensure to explicitely mark the session as deleted
+            self.delete_session(send_request=False, reset_session_id=True)
 
             # Give the application some time to shutdown
             self.instance.runner.wait(timeout=self.DEFAULT_SHUTDOWN_TIMEOUT)
         else:
-            self.delete_session()
+            self.delete_session(reset_session_id=True)
             self.instance.close()
 
     @do_process_check
     def restart(self, clean=False, in_app=False, callback=None):
         """
         This will terminate the currently running instance, and spawn a new instance
         with the same profile and then reuse the session id when creating a session again.
 
@@ -1169,17 +1187,19 @@ class Marionette(object):
             if clean:
                 raise ValueError("An in_app restart cannot be triggered with the clean flag set")
 
             if callable(callback):
                 self._send_message("acceptConnections", {"value": False})
                 callback()
             else:
                 self._request_in_app_shutdown("eRestart")
-            self.delete_session(in_app=True)
+
+            # Ensure to explicitely mark the session as deleted
+            self.delete_session(send_request=False, reset_session_id=True)
 
             try:
                 self.raise_for_port()
             except socket.timeout:
                 if self.instance.runner.returncode is not None:
                     exc, val, tb = sys.exc_info()
                     self.cleanup()
                     raise exc, "Requested restart of the application was aborted", tb
@@ -1216,17 +1236,21 @@ class Marionette(object):
         This method must be called before performing any other action.
 
         :param desired_capabilities: An optional dict of desired
             capabilities.  This is currently ignored.
         :param timeout: Timeout in seconds for the server to be ready.
         :param session_id: unique identifier for the session. If no session id is
             passed in then one will be generated by the marionette server.
 
-        :returns: A dict of the capabilities offered."""
+        :returns: A dict of the capabilities offered.
+
+        """
+        self.crashed = 0
+
         if self.instance:
             returncode = self.instance.runner.returncode
             if returncode is not None:
                 # We're managing a binary which has terminated, so restart it.
                 self.instance.restart()
 
         self.client = transport.TcpTransport(
             self.host,
@@ -1250,29 +1274,35 @@ class Marionette(object):
     def test_name(self):
         return self._test_name
 
     @test_name.setter
     def test_name(self, test_name):
         self._send_message("setTestName", {"value": test_name})
         self._test_name = test_name
 
-    def delete_session(self, in_app=False):
+    def delete_session(self, send_request=True, reset_session_id=False):
         """Close the current session and disconnect from the server.
 
-        :param in_app: False, if the session should be closed from the client.
-                       Otherwise a request to quit or restart the instance from
-                       within the application itself is used.
+        :param send_request: Optional, if `True` a request to close the session on
+            the server side will be send. Use `False` in case of eg. in_app restart()
+            or quit(), which trigger a deletion themselves. Defaults to `True`.
+        :param reset_session_id: Optional, if `True` the current session id will
+            be reset, which will require an explicit call to `start_session()` before
+            the test can continue. Defaults to `False`.
         """
-        if not in_app:
-            self._send_message("deleteSession")
-        self.session_id = None
-        self.session = None
-        self.window = None
-        self.client.close()
+        try:
+            if send_request:
+                self._send_message("deleteSession")
+        finally:
+            if reset_session_id:
+                self.session_id = None
+            self.session = None
+            self.window = None
+            self.client.close()
 
     @property
     def session_capabilities(self):
         """A JSON dictionary representing the capabilities of the
         current session.
 
         """
         return self.session
--- a/testing/marionette/client/marionette_driver/transport.py
+++ b/testing/marionette/client/marionette_driver/transport.py
@@ -120,24 +120,34 @@ class TcpTransport(object):
 
     def __init__(self, addr, port, socket_timeout=60.0):
         """If `socket_timeout` is `0` or `0.0`, non-blocking socket mode
         will be used.  Setting it to `1` or `None` disables timeouts on
         socket operations altogether.
         """
         self.addr = addr
         self.port = port
-        self.socket_timeout = socket_timeout
+        self._socket_timeout = socket_timeout
 
         self.protocol = 1
         self.application_type = None
         self.last_id = 0
         self.expected_response = None
         self.sock = None
 
+    @property
+    def socket_timeout(self):
+        return self._socket_timeout
+
+    @socket_timeout.setter
+    def socket_timeout(self, value):
+        if self.sock:
+            self.sock.settimeout(value)
+        self._socket_timeout = value
+
     def _unmarshal(self, packet):
         msg = None
 
         # protocol 3 and above
         if self.protocol >= 3:
             typ = int(packet[1])
             if typ == Command.TYPE:
                 msg = Command.from_msg(packet)
--- a/testing/marionette/client/requirements.txt
+++ b/testing/marionette/client/requirements.txt
@@ -1,1 +1,1 @@
-mozrunner >= 6.12
+mozrunner >= 6.13
--- a/testing/marionette/harness/marionette/__init__.py
+++ b/testing/marionette/harness/marionette/__init__.py
@@ -4,16 +4,17 @@
 
 __version__ = '3.2.0'
 
 from .marionette_test import (
     CommonTestCase,
     expectedFailure,
     MarionetteJSTestCase,
     MarionetteTestCase,
+    run_if_e10s,
     skip,
     skip_if_chrome,
     skip_if_desktop,
     skip_if_mobile,
     SkipTest,
     skip_unless_protocol,
 )
 from .runner import (
--- a/testing/marionette/harness/marionette/marionette_test/__init__.py
+++ b/testing/marionette/harness/marionette/marionette_test/__init__.py
@@ -3,16 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 __version__ = '3.1.0'
 
 
 from .decorators import (
     expectedFailure,
     parameterized,
+    run_if_e10s,
     skip,
     skip_if_chrome,
     skip_if_desktop,
     skip_if_e10s,
     skip_if_mobile,
     skip_unless_browser_pref,
     skip_unless_protocol,
     with_parameters,
--- a/testing/marionette/harness/marionette/marionette_test/decorators.py
+++ b/testing/marionette/harness/marionette/marionette_test/decorators.py
@@ -56,16 +56,33 @@ def parameterized(func_suffix, *args, **
     def wrapped(func):
         if not hasattr(func, 'metaparameters'):
             func.metaparameters = []
         func.metaparameters.append((func_suffix, args, kwargs))
         return func
     return wrapped
 
 
+def run_if_e10s(target):
+    """Decorator which runs a test if e10s mode is active."""
+    def wrapper(self, *args, **kwargs):
+        with self.marionette.using_context('chrome'):
+            multi_process_browser = self.marionette.execute_script("""
+            try {
+              return Services.appinfo.browserTabsRemoteAutostart;
+            } catch (e) {
+              return false;
+            }""")
+
+        if not multi_process_browser:
+            raise SkipTest('skipping due to e10s is disabled')
+        return target(self, *args, **kwargs)
+    return wrapper
+
+
 def skip(reason):
     """Decorator which unconditionally skips a test."""
     def decorator(test_item):
         if not isinstance(test_item, (type, types.ClassType)):
             @functools.wraps(test_item)
             def skip_wrapper(*args, **kwargs):
                 raise SkipTest(reason)
             test_item = skip_wrapper
--- a/testing/marionette/harness/marionette/marionette_test/testcases.py
+++ b/testing/marionette/harness/marionette/marionette_test/testcases.py
@@ -1,17 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import imp
 import os
 import re
 import sys
-import socket
 import time
 import types
 import unittest
 import weakref
 import warnings
 
 from marionette.marionette_test.errors import (
     _ExpectedFailure,
@@ -269,23 +268,19 @@ class CommonTestCase(unittest.TestCase):
         if hasattr(self.marionette, 'session'):
             if self.marionette.session is not None:
                 try:
                     self.loglines.extend(self.marionette.get_logs())
                 except Exception, inst:
                     self.loglines = [['Error getting log: {}'.format(inst)]]
                 try:
                     self.marionette.delete_session()
-                except (socket.error, MarionetteException, IOError):
+                except IOError:
                     # Gecko has crashed?
-                    self.marionette.session = None
-                    try:
-                        self.marionette.client.close()
-                    except socket.error:
-                        pass
+                    pass
         self.marionette = None
 
     def setup_SpecialPowers_observer(self):
         self.marionette.set_context("chrome")
         self.marionette.execute_script("""
 let SECURITY_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer";
 Components.utils.import("resource://gre/modules/Preferences.jsm");
 Preferences.set(SECURITY_PREF, true);
@@ -479,17 +474,17 @@ class MarionetteTestCase(CommonTestCase)
                                        sandbox="simpletest")
 
     def tearDown(self):
         # In the case no session is active (eg. the application was quit), start
         # a new session for clean-up steps.
         if not self.marionette.session:
             self.marionette.start_session()
 
-        if not self.marionette.check_for_crash():
+        if not self.marionette.crashed:
             try:
                 self.marionette.clear_imported_scripts()
                 self.marionette.execute_script("log('TEST-END: {0}:{1}')"
                                                .format(self.filepath.replace('\\', '\\\\'),
                                                        self.methodName),
                                                sandbox="simpletest")
                 self.marionette.test_name = None
             except (MarionetteException, IOError):
--- a/testing/marionette/harness/marionette/runner/base.py
+++ b/testing/marionette/harness/marionette/runner/base.py
@@ -18,17 +18,16 @@ import traceback
 import unittest
 import warnings
 import mozprofile
 
 
 from manifestparser import TestManifest
 from manifestparser.filters import tags
 from marionette_driver.marionette import Marionette
-from mozlog import get_default_logger
 from moztest.adapters.unit import StructuredTestRunner, StructuredTestResult
 from moztest.results import TestResultCollection, TestResult, relevant_line
 import mozversion
 
 import httpd
 
 
 here = os.path.abspath(os.path.dirname(__file__))
@@ -562,19 +561,18 @@ class BaseMarionetteTestRunner(object):
             # In the event we're gathering debug without starting a session,
             # skip marionette commands
             if marionette.session is not None:
                 try:
                     with marionette.using_context(marionette.CONTEXT_CHROME):
                         rv['screenshot'] = marionette.screenshot()
                     with marionette.using_context(marionette.CONTEXT_CONTENT):
                         rv['source'] = marionette.page_source
-                except Exception:
-                    logger = get_default_logger()
-                    logger.warning('Failed to gather test failure debug.', exc_info=True)
+                except Exception as exc:
+                    self.logger.warning('Failed to gather test failure debug: {}'.format(exc))
             return rv
 
         self.result_callbacks.append(gather_debug)
 
         # testvars are set up in self.testvars property
         self._testvars = None
         self.testvars_paths = testvars
 
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/tests/unit/test_crash.py
@@ -0,0 +1,155 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import glob
+import shutil
+
+from marionette import MarionetteTestCase, expectedFailure, run_if_e10s
+from marionette_driver.errors import MarionetteException
+
+# Import runner module to monkey patch mozcrash module
+from mozrunner.base import runner
+
+
+class MockMozCrash(object):
+    """Mock object to replace original mozcrash methods."""
+
+    def __init__(self, marionette):
+        self.marionette = marionette
+
+        with self.marionette.using_context('chrome'):
+            self.crash_reporter_enabled = self.marionette.execute_script("""
+                try {
+                  Components.classes["@mozilla.org/toolkit/crash-reporter;1"].
+                    getService(Components.interfaces.nsICrashReporter);
+                  return true;
+                } catch (exc) {
+                  return false;
+                }
+            """)
+
+    def check_for_crashes(self, dump_directory, *args, **kwargs):
+        minidump_files = glob.glob('{}/*.dmp'.format(dump_directory))
+        shutil.rmtree(dump_directory, ignore_errors=True)
+
+        if self.crash_reporter_enabled:
+            return len(minidump_files)
+        else:
+            return len(minidump_files) == 0
+
+    def log_crashes(self, logger, dump_directory, *args, **kwargs):
+        return self.check_for_crashes(dump_directory, *args, **kwargs)
+
+
+class BaseCrashTestCase(MarionetteTestCase):
+
+    # Reduce the timeout for faster processing of the tests
+    socket_timeout = 10
+
+    def setUp(self):
+        super(BaseCrashTestCase, self).setUp()
+
+        self.mozcrash_mock = MockMozCrash(self.marionette)
+        self.crash_count = self.marionette.crashed
+        self.pid = self.marionette.session["processId"]
+        self.remote_uri = self.marionette.absolute_url("javascriptPage.html")
+
+    def tearDown(self):
+        self.marionette.crashed = self.crash_count
+
+        super(BaseCrashTestCase, self).tearDown()
+
+    def crash(self, chrome=True):
+        context = 'chrome' if chrome else 'content'
+        sandbox = None if chrome else 'system'
+
+        # Monkey patch mozcrash to avoid crash info output only for our triggered crashes.
+        mozcrash = runner.mozcrash
+        runner.mozcrash = self.mozcrash_mock
+
+        socket_timeout = self.marionette.client.socket_timeout
+
+        self.marionette.set_context(context)
+        try:
+            self.marionette.client.socket_timeout = self.socket_timeout
+            self.marionette.execute_script("""
+              // Copied from crash me simple
+              Components.utils.import("resource://gre/modules/ctypes.jsm");
+
+              // ctypes checks for NULL pointer derefs, so just go near-NULL.
+              var zero = new ctypes.intptr_t(8);
+              var badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
+              var crash = badptr.contents;
+            """, sandbox=sandbox)
+        finally:
+            runner.mozcrash = mozcrash
+            self.marionette.client.socket_timeout = socket_timeout
+
+
+class TestCrash(BaseCrashTestCase):
+
+    def test_crash_chrome_process(self):
+        self.assertRaisesRegexp(IOError, 'Process crashed',
+                                self.crash, chrome=True)
+        self.assertEqual(self.marionette.crashed, 1)
+        self.assertIsNone(self.marionette.session)
+        self.assertRaisesRegexp(MarionetteException, 'Please start a session',
+                                self.marionette.get_url)
+
+        self.marionette.start_session()
+        self.assertNotEqual(self.marionette.session['processId'], self.pid)
+
+        # TODO: Bug 1314594 - Causes a hang for the communication between the
+        # chrome and frame script.
+        # self.marionette.get_url()
+
+    @run_if_e10s
+    def test_crash_content_process(self):
+        # If e10s is disabled the chrome process crashes
+        self.marionette.navigate(self.remote_uri)
+
+        self.assertRaisesRegexp(IOError, 'Content process crashed',
+                                self.crash, chrome=False)
+        self.assertEqual(self.marionette.crashed, 1)
+        self.assertIsNone(self.marionette.session)
+        self.assertRaisesRegexp(MarionetteException, 'Please start a session',
+                                self.marionette.get_url)
+
+        self.marionette.start_session()
+        self.assertNotEqual(self.marionette.session['processId'], self.pid)
+        self.marionette.get_url()
+
+    @expectedFailure
+    def test_unexpected_crash(self):
+        self.crash(chrome=True)
+
+
+class TestCrashInSetUp(BaseCrashTestCase):
+
+    def setUp(self):
+        super(TestCrashInSetUp, self).setUp()
+
+        self.assertRaisesRegexp(IOError, 'Process crashed',
+                                self.crash, chrome=True)
+        self.assertEqual(self.marionette.crashed, 1)
+        self.assertIsNone(self.marionette.session)
+
+    def test_crash_in_setup(self):
+        self.marionette.start_session()
+        self.assertNotEqual(self.marionette.session['processId'], self.pid)
+
+
+class TestCrashInTearDown(BaseCrashTestCase):
+
+    def tearDown(self):
+        try:
+            self.assertRaisesRegexp(IOError, 'Process crashed',
+                                    self.crash, chrome=True)
+        finally:
+            self.assertEqual(self.marionette.crashed, 1)
+            self.assertIsNone(self.marionette.session)
+            super(TestCrashInTearDown, self).tearDown()
+
+    def test_crash_in_teardown(self):
+        pass
--- a/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
@@ -135,9 +135,10 @@ skip-if = buildapp == 'b2g' || appname =
 [test_shadow_dom.py]
 
 [test_chrome.py]
 skip-if = buildapp == 'b2g' || appname == 'fennec'
 
 [test_addons.py]
 
 [test_select.py]
+[test_crash.py]
 [test_httpd.py]
--- a/testing/marionette/harness/requirements.txt
+++ b/testing/marionette/harness/requirements.txt
@@ -1,14 +1,14 @@
 marionette-driver >= 2.1.0
 browsermob-proxy >= 0.6.0
 manifestparser >= 1.1
 wptserve >= 1.3.0
 mozinfo >= 0.8
 mozprocess >= 0.9
-mozrunner >= 6.9
+mozrunner >= 6.13
 mozdevice >= 0.44
 mozlog >= 3.0
 moznetwork >= 0.21
 mozcrash >= 0.5
 mozprofile >= 0.7
 moztest >= 0.7
 mozversion >= 1.1