Backed out changeset 4c42ee29c468 (bug 1103196) for broken Marionette tests on Windows (error loading pages). r=backout
authorSebastian Hengst <archaeopteryx@coole-files.de>
Fri, 25 Nov 2016 00:20:30 +0100
changeset 324224 602dfbdc77eac57cbb512c103467c37c1907699c
parent 324223 bd0f63832f4e9de0daab3cadba42330da7a6657e
child 324225 eb4be9ed211e684429f13296f587347b7972ba0d
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewersbackout
bugs1103196
milestone53.0a1
backs out4c42ee29c468905d25c65c8aac52f44abb2cb922
Backed out changeset 4c42ee29c468 (bug 1103196) for broken Marionette tests on Windows (error loading pages). r=backout
testing/marionette/harness/MANIFEST.in
testing/marionette/harness/marionette/marionette_test/testcases.py
testing/marionette/harness/marionette/runner/base.py
testing/marionette/harness/marionette/runner/httpd.py
testing/marionette/harness/marionette/runner/serve.py
testing/marionette/harness/marionette/runner/test.cert
testing/marionette/harness/marionette/runner/test.key
testing/marionette/harness/marionette/tests/harness_unit/test_httpd.py
testing/marionette/harness/marionette/tests/harness_unit/test_marionette_runner.py
testing/marionette/harness/marionette/tests/harness_unit/test_serve.py
testing/marionette/harness/marionette/tests/unit/test_httpd.py
testing/marionette/harness/marionette/tests/unit/unit-tests.ini
testing/marionette/harness/requirements.txt
--- a/testing/marionette/harness/MANIFEST.in
+++ b/testing/marionette/harness/MANIFEST.in
@@ -1,8 +1,6 @@
 recursive-include marionette/touch *.js
 recursive-include marionette/www *
 recursive-include marionette/chrome *
 recursive-include marionette/runner/mixins/resources *
 exclude MANIFEST.in
 include requirements.txt
-include marionette/runner/test.cert
-include marionette/runner/test.key
--- a/testing/marionette/harness/marionette/marionette_test/testcases.py
+++ b/testing/marionette/harness/marionette/marionette_test/testcases.py
@@ -69,22 +69,18 @@ class JSTest:
 
 class CommonTestCase(unittest.TestCase):
 
     __metaclass__ = MetaParameterized
     match_re = None
     failureException = AssertionError
     pydebugger = None
 
-    def __init__(self, methodName, marionette_weakref, fixtures, **kwargs):
+    def __init__(self, methodName, **kwargs):
         super(CommonTestCase, self).__init__(methodName)
-        self.methodName = methodName
-
-        self._marionette_weakref = marionette_weakref
-        self.fixtures = fixtures
 
         self.loglines = []
         self.duration = 0
         self.start_time = 0
         self.expected = kwargs.pop('expected', 'pass')
         self.logger = get_default_logger()
 
     def _enter_pm(self):
@@ -224,17 +220,17 @@ class CommonTestCase(unittest.TestCase):
         """
         if not cls.match_re:
             return False
         m = cls.match_re.match(filename)
         return m is not None
 
     @classmethod
     def add_tests_to_suite(cls, mod_name, filepath, suite, testloader, marionette,
-                           fixtures, testvars, **kwargs):
+                           httpd, testvars):
         """Add all the tests in the specified file to the specified suite."""
         raise NotImplementedError
 
     @property
     def test_name(self):
         if hasattr(self, 'jsFile'):
             return os.path.basename(self.jsFile)
         else:
@@ -251,16 +247,17 @@ class CommonTestCase(unittest.TestCase):
 
     def setUp(self):
         # Convert the marionette weakref to an object, just for the
         # duration of the test; this is deleted in tearDown() to prevent
         # a persistent circular reference which in turn would prevent
         # proper garbage collection.
         self.start_time = time.time()
         self.marionette = self._marionette_weakref()
+        self.httpd = self._httpd_weakref()
         if self.marionette.session is None:
             self.marionette.start_session()
         self.marionette.timeout.reset()
 
         super(CommonTestCase, self).setUp()
 
     def cleanTest(self):
         self._deleteSession()
@@ -419,27 +416,31 @@ if (!testUtils.hasOwnProperty("specialPo
                 raise
         self.marionette.test_name = original_test_name
 
 
 class MarionetteTestCase(CommonTestCase):
 
     match_re = re.compile(r"test_(.*)\.py$")
 
-    def __init__(self, marionette_weakref, fixtures, methodName='runTest',
+    def __init__(self, marionette_weakref, httpd_weakref, methodName='runTest',
                  filepath='', **kwargs):
+        self._marionette_weakref = marionette_weakref
+        self._httpd_weakref = httpd_weakref
+        self.methodName = methodName
         self.filepath = filepath
         self.testvars = kwargs.pop('testvars', None)
 
-        super(MarionetteTestCase, self).__init__(
-            methodName, marionette_weakref=marionette_weakref, fixtures=fixtures, **kwargs)
+        self.marionette = None
+
+        super(MarionetteTestCase, self).__init__(methodName, **kwargs)
 
     @classmethod
     def add_tests_to_suite(cls, mod_name, filepath, suite, testloader, marionette,
-                           fixtures, testvars, **kwargs):
+                           httpd, testvars, **kwargs):
         # since we use imp.load_source to load test modules, if a module
         # is loaded with the same name as another one the module would just be
         # reloaded.
         #
         # We may end up by finding too many test in a module then since
         # reload() only update the module dict (so old keys are still there!)
         # see https://docs.python.org/2/library/functions.html#reload
         #
@@ -453,17 +454,17 @@ class MarionetteTestCase(CommonTestCase)
 
         for name in dir(test_mod):
             obj = getattr(test_mod, name)
             if (isinstance(obj, (type, types.ClassType)) and
                     issubclass(obj, unittest.TestCase)):
                 testnames = testloader.getTestCaseNames(obj)
                 for testname in testnames:
                     suite.addTest(obj(weakref.ref(marionette),
-                                      fixtures,
+                                      weakref.ref(httpd),
                                       methodName=testname,
                                       filepath=filepath,
                                       testvars=testvars,
                                       **kwargs))
 
     def setUp(self):
         super(MarionetteTestCase, self).setUp()
         self.marionette.test_name = self.test_name
--- a/testing/marionette/harness/marionette/runner/base.py
+++ b/testing/marionette/harness/marionette/runner/base.py
@@ -1,35 +1,39 @@
 # 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/.
 
+from argparse import ArgumentParser
+
+from copy import deepcopy
 import json
+import mozinfo
+import moznetwork
 import os
 import random
 import re
 import socket
 import sys
 import time
 import traceback
 import unittest
-from argparse import ArgumentParser
-from copy import deepcopy
-
-import mozinfo
+import warnings
 import mozprofile
-from marionette_driver.marionette import Marionette
 
-import mozversion
-import serve
+
 from manifestparser import TestManifest
 from manifestparser.filters import tags
-from moztest.adapters.unit import StructuredTestResult, StructuredTestRunner
-from moztest.results import TestResult, TestResultCollection, relevant_line
-from serve import iter_proc, iter_url
+from marionette_driver.marionette import Marionette
+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__))
 
 
 def update_mozinfo(path=None):
     """Walk up directories to find mozinfo.json and update the info."""
     path = path or here
     dirs = set()
@@ -480,21 +484,16 @@ class RemoteMarionetteArguments(object):
         [['--package'],
          {'help': 'Name of Android package, e.g. org.mozilla.fennec',
           'dest': 'package_name',
           }],
 
     ]
 
 
-class Fixtures(object):
-    def where_is(self, uri, on="http"):
-        return serve.where_is(uri, on)
-
-
 class BaseMarionetteTestRunner(object):
 
     textrunnerclass = MarionetteTextTestRunner
     driverclass = Marionette
 
     def __init__(self, address=None,
                  app=None, app_args=None, binary=None, profile=None,
                  logger=None, logdir=None,
@@ -502,28 +501,27 @@ class BaseMarionetteTestRunner(object):
                  symbols_path=None,
                  shuffle=False, shuffle_seed=random.randint(0, sys.maxint), this_chunk=1,
                  total_chunks=1,
                  server_root=None, gecko_log=None, result_callbacks=None,
                  prefs=None, test_tags=None,
                  socket_timeout=BaseMarionetteArguments.socket_timeout_default,
                  startup_timeout=None, addons=None, workspace=None,
                  verbose=0, e10s=True, emulator=False, **kwargs):
-        self.fixture_servers = {}
-        self.fixtures = Fixtures()
         self.extra_kwargs = kwargs
         self.test_kwargs = deepcopy(kwargs)
         self.address = address
         self.app = app
         self.app_args = app_args or []
         self.bin = binary
         self.emulator = emulator
         self.profile = profile
         self.addons = addons
         self.logger = logger
+        self.httpd = None
         self.marionette = None
         self.logdir = logdir
         self.repeat = repeat
         self.symbols_path = symbols_path
         self.socket_timeout = socket_timeout
         self._capabilities = None
         self._appinfo = None
         self._appName = None
@@ -666,16 +664,17 @@ class BaseMarionetteTestRunner(object):
 
     @property
     def bin(self):
         return self._bin
 
     @bin.setter
     def bin(self, path):
         """Set binary and reset parts of runner accordingly.
+
         Intended use: to change binary between calls to run_tests
         """
         self._bin = path
         self.tests = []
         self.cleanup()
 
     def reset_test_stats(self):
         self.passed = 0
@@ -757,16 +756,40 @@ class BaseMarionetteTestRunner(object):
             traceback.print_exc()
         return crash
 
     def _initialize_test_run(self, tests):
         assert len(tests) > 0
         assert len(self.test_handlers) > 0
         self.reset_test_stats()
 
+    def _start_marionette(self):
+        need_external_ip = True
+        if not self.marionette:
+            self.marionette = self.driverclass(**self._build_kwargs())
+            # if we're working against a desktop version, we usually don't need
+            # an external ip
+            if self.appName != 'fennec':
+                need_external_ip = False
+        self.logger.info('Initial Profile Destination is '
+                         '"{}"'.format(self.marionette.profile_path))
+        return need_external_ip
+
+    def _set_baseurl(self, need_external_ip):
+        # Gaia sets server_root and that means we shouldn't spin up our own httpd
+        if not self.httpd:
+            if self.server_root is None or os.path.isdir(self.server_root):
+                self.logger.info("starting httpd")
+                self.start_httpd(need_external_ip)
+                self.marionette.baseurl = self.httpd.get_url()
+                self.logger.info("running httpd on {}".format(self.marionette.baseurl))
+            else:
+                self.marionette.baseurl = self.server_root
+                self.logger.info("using remote content from {}".format(self.marionette.baseurl))
+
     def _add_tests(self, tests):
         for test in tests:
             self.add_test(test)
 
         invalid_tests = [t['filepath'] for t in self.tests
                          if not self._is_filename_valid(t['filepath'])]
         if invalid_tests:
             raise Exception("Test file names must be of the form "
@@ -785,48 +808,34 @@ class BaseMarionetteTestRunner(object):
                                  'SKIP',
                                  message=test['disabled'])
             self.todo += 1
 
     def run_tests(self, tests):
         start_time = time.time()
         self._initialize_test_run(tests)
 
-        if self.marionette is None:
-            self.marionette = self.driverclass(**self._build_kwargs())
-            self.logger.info("Profile path is %s" % self.marionette.profile_path)
-
-        if len(self.fixture_servers) == 0 or \
-                any(not server.is_alive for _, server in self.fixture_servers):
-            self.logger.info("Starting fixture servers")
-            self.fixture_servers = self.start_fixture_servers()
-            for url in iter_url(self.fixture_servers):
-                self.logger.info("Fixture server listening on %s" % url)
-
-            # backwards compatibility
-            self.marionette.baseurl = serve.where_is("/")
+        need_external_ip = self._start_marionette()
+        self._set_baseurl(need_external_ip)
 
         self._add_tests(tests)
 
         device_info = None
         if self.marionette.instance and self.emulator:
             try:
                 device_info = self.marionette.instance.runner.device.dm.getInfo()
             except Exception:
-                self.logger.warning('Could not get device info', exc_info=True)
+                self.logger.warning('Could not get device info.')
 
         # TODO: Get version_info in Fennec case
         version_info = None
         if self.bin:
             version_info = mozversion.get_version(binary=self.bin)
 
-        if self.e10s:
-            self.logger.info("e10s is enabled")
-        else:
-            self.logger.info("e10s is disabled")
+        self.logger.info("running with e10s: {}".format(self.e10s))
 
         self.logger.suite_start(self.tests,
                                 version_info=version_info,
                                 device_info=device_info)
 
         self._log_skipped_tests()
 
         interrupted = None
@@ -852,18 +861,19 @@ class BaseMarionetteTestRunner(object):
         try:
             self._print_summary(tests)
             self.record_crash()
             self.elapsedtime = time.time() - start_time
 
             for run_tests in self.mixin_run_tests:
                 run_tests(tests)
             if self.shuffle:
-                self.logger.info("Using shuffle seed: %d" % self.shuffle_seed)
+                self.logger.info("Using seed where seed is:{}".format(self.shuffle_seed))
 
+            self.logger.info('mode: {}'.format('e10s' if self.e10s else 'non-e10s'))
             self.logger.suite_end()
         except:
             # raise only the exception if we were not interrupted
             if not interrupted:
                 raise
         finally:
             self.cleanup()
 
@@ -885,19 +895,29 @@ class BaseMarionetteTestRunner(object):
         else:
             self.logger.info('todo: {0} (skipped: {1})'.format(self.todo, self.skipped))
 
         if self.failed > 0:
             self.logger.info('\nFAILED TESTS\n-------')
             for failed_test in self.failures:
                 self.logger.info('{}'.format(failed_test[0]))
 
-    def start_fixture_servers(self):
+    def start_httpd(self, need_external_ip):
+        warnings.warn("start_httpd has been deprecated in favour of create_httpd",
+                      DeprecationWarning)
+        self.httpd = self.create_httpd(need_external_ip)
+
+    def create_httpd(self, need_external_ip):
+        host = "127.0.0.1"
+        if need_external_ip:
+            host = moznetwork.get_ip()
         root = self.server_root or os.path.join(os.path.dirname(here), "www")
-        return serve.start(root)
+        rv = httpd.FixtureServer(root, host=host)
+        rv.start()
+        return rv
 
     def add_test(self, test, expected='pass'):
         filepath = os.path.abspath(test)
 
         if os.path.isdir(filepath):
             for root, dirs, files in os.walk(filepath):
                 for filename in files:
                     if filename.endswith('.ini'):
@@ -947,28 +967,29 @@ class BaseMarionetteTestRunner(object):
                 file_ext = os.path.splitext(os.path.split(i['path'])[-1])[-1]
 
                 self.add_test(i["path"], i["expected"])
             return
 
         self.tests.append({'filepath': filepath, 'expected': expected})
 
     def run_test(self, filepath, expected):
+
         testloader = unittest.TestLoader()
         suite = unittest.TestSuite()
         self.test_kwargs['expected'] = expected
         mod_name = os.path.splitext(os.path.split(filepath)[-1])[0]
         for handler in self.test_handlers:
             if handler.match(os.path.basename(filepath)):
                 handler.add_tests_to_suite(mod_name,
                                            filepath,
                                            suite,
                                            testloader,
                                            self.marionette,
-                                           self.fixtures,
+                                           self.httpd,
                                            self.testvars,
                                            **self.test_kwargs)
                 break
 
         if suite.countTestCases():
             runner = self.textrunnerclass(logger=self.logger,
                                           marionette=self.marionette,
                                           capabilities=self.capabilities,
@@ -1024,22 +1045,21 @@ class BaseMarionetteTestRunner(object):
                              'total of {3})'.format(self.this_chunk, self.total_chunks,
                                                     len(chunks[self.this_chunk - 1]),
                                                     len(self.tests)))
             self.tests = chunks[self.this_chunk - 1]
 
         self.run_test_set(self.tests)
 
     def cleanup(self):
-        for proc in iter_proc(self.fixture_servers):
-            proc.stop()
-            proc.kill()
-        self.fixture_servers = {}
+        if hasattr(self, 'httpd') and self.httpd:
+            self.httpd.stop()
+            self.httpd = None
 
         if hasattr(self, 'marionette') and self.marionette:
-            if self.marionette.instance is not None:
+            if self.marionette.instance:
                 self.marionette.instance.close()
                 self.marionette.instance = None
 
             self.marionette.cleanup()
             self.marionette = None
 
     __del__ = cleanup
--- a/testing/marionette/harness/marionette/runner/httpd.py
+++ b/testing/marionette/harness/marionette/runner/httpd.py
@@ -1,142 +1,82 @@
-#!/usr/bin/env python
-
 # 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/.
 
-"""Specialisation of wptserver.server.WebTestHttpd for testing
-Marionette.
-
-"""
-
-import argparse
 import os
-import select
-import sys
 import time
-import urlparse
 
 from wptserve import server, handlers, routes as default_routes
 
 
-here = os.path.abspath(os.path.dirname(__file__))
-default_doc_root = os.path.join(os.path.dirname(here), "www")
-default_ssl_cert = os.path.join(here, "test.cert")
-default_ssl_key = os.path.join(here, "test.key")
+class FixtureServer(object):
+
+    def __init__(self, root, host="127.0.0.1", port=0):
+        if not os.path.isdir(root):
+            raise IOError("Server root is not a valid path: {}".format(root))
+        self.root = root
+        self.host = host
+        self.port = port
+        self._server = None
+
+    def start(self, block=False):
+        if self.alive:
+            return
+        routes = [("POST", "/file_upload", upload_handler),
+                  ("GET", "/slow", slow_loading_document)]
+        routes.extend(default_routes.routes)
+        self._server = server.WebTestHttpd(
+            port=self.port,
+            doc_root=self.root,
+            routes=routes,
+            host=self.host,
+        )
+        self._server.start(block=block)
+        self.port = self._server.httpd.server_port
+        self.base_url = self.get_url()
+
+    def stop(self):
+        if not self.alive:
+            return
+        self._server.stop()
+        self._server = None
+
+    @property
+    def alive(self):
+        return self._server is not None
+
+    def get_url(self, path="/"):
+        if not self.alive:
+            raise Exception("Server not started")
+        return self._server.get_url(path)
+
+    @property
+    def router(self):
+        return self._server.router
+
+    @property
+    def routes(self):
+        return self._server.router.routes
 
 
 @handlers.handler
 def upload_handler(request, response):
     return 200, [], [request.headers.get("Content-Type")] or []
 
 
 @handlers.handler
 def slow_loading_document(request, response):
     time.sleep(5)
     return """<!doctype html>
 <title>ok</title>
 <p>ok"""
 
 
-class NotAliveError(Exception):
-    """Occurs when attempting to run a function that requires the HTTPD
-    to have been started, and it has not.
-
-    """
-    pass
-
-
-class FixtureServer(object):
-
-    def __init__(self, doc_root, url="http://127.0.0.1:0", use_ssl=False,
-                 ssl_cert=None, ssl_key=None):
-        if not os.path.isdir(doc_root):
-            raise ValueError("Server root is not a directory: %s" % doc_root)
-
-        url = urlparse.urlparse(url)
-        if url.scheme is None:
-            raise ValueError("Server scheme not provided")
-
-        scheme, host, port = url.scheme, url.hostname, url.port
-        if host is None:
-            host = "127.0.0.1"
-        if port is None:
-            port = 0
-
-        routes = [("POST", "/file_upload", upload_handler),
-                  ("GET", "/slow", slow_loading_document)]
-        routes.extend(default_routes.routes)
-
-        self._httpd = server.WebTestHttpd(host=host,
-                                          port=port,
-                                          bind_hostname=True,
-                                          doc_root=doc_root,
-                                          routes=routes,
-                                          use_ssl=True if scheme == "https" else False,
-                                          certificate=ssl_cert,
-                                          key_file=ssl_key)
-
-    def start(self, block=False):
-        if self.is_alive:
-            return
-        self._httpd.start(block=block)
-
-    def wait(self):
-        if not self.is_alive:
-            return
-        try:
-            select.select([], [], [])
-        except KeyboardInterrupt:
-            self.stop()
-
-    def stop(self):
-        if not self.is_alive:
-            return
-        self._httpd.stop()
-
-    def get_url(self, path):
-        if not self.is_alive:
-            raise NotAliveError()
-        return self._httpd.get_url(path)
-
-    @property
-    def doc_root(self):
-        return self._httpd.router.doc_root
-
-    @property
-    def router(self):
-        return self._httpd.router
-
-    @property
-    def routes(self):
-        return self._httpd.router.routes
-
-    @property
-    def is_alive(self):
-        return self._httpd.started
-
-
 if __name__ == "__main__":
-    parser = argparse.ArgumentParser(
-        description="Specialised HTTP server for testing Marionette.")
-    parser.add_argument("url", help="""
-service address including scheme, hostname, port, and prefix for document root,
-e.g. \"https://0.0.0.0:0/base/\"""")
-    parser.add_argument(
-        "-r", dest="doc_root", default=default_doc_root,
-        help="path to document root (default %(default)s)")
-    parser.add_argument(
-        "-c", dest="ssl_cert", default=default_ssl_cert,
-        help="path to SSL certificate (default %(default)s)")
-    parser.add_argument(
-        "-k", dest="ssl_key", default=default_ssl_key,
-        help="path to SSL certificate key (default %(default)s)")
-    args = parser.parse_args()
-
-    httpd = FixtureServer(args.doc_root, args.url,
-                          ssl_cert=args.ssl_cert,
-                          ssl_key=args.ssl_key)
-    httpd.start()
-    print >>sys.stderr, "%s: started fixture server on %s" % \
-        (sys.argv[0], httpd.get_url("/"))
-    httpd.wait()
+    here = os.path.abspath(os.path.dirname(__file__))
+    doc_root = os.path.join(os.path.dirname(here), "www")
+    httpd = FixtureServer(doc_root, port=2829)
+    print "Started fixture server on http://{0}:{1}/".format(httpd.host, httpd.port)
+    try:
+        httpd.start(True)
+    except KeyboardInterrupt:
+        pass
deleted file mode 100755
--- a/testing/marionette/harness/marionette/runner/serve.py
+++ /dev/null
@@ -1,226 +0,0 @@
-#!/usr/bin/env python
-
-# 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/.
-
-"""Spawns necessary HTTP servers for testing Marionette in child
-processes.
-
-"""
-
-from collections import defaultdict
-import argparse
-import multiprocessing
-import os
-import sys
-
-import httpd
-
-
-__all__ = ["default_doc_root",
-           "iter_proc",
-           "iter_url",
-           "registered_servers",
-           "servers",
-           "start",
-           "where_is"]
-here = os.path.abspath(os.path.dirname(__file__))
-
-
-class BlockingChannel(object):
-
-    def __init__(self, channel):
-        self.chan = channel
-        self.lock = multiprocessing.Lock()
-
-    def call(self, func, args=()):
-        self.send((func, args))
-        return self.recv()
-
-    def send(self, *args):
-        try:
-            self.lock.acquire()
-            self.chan.send(args)
-        finally:
-            self.lock.release()
-
-    def recv(self):
-        try:
-            self.lock.acquire()
-            payload = self.chan.recv()
-            if isinstance(payload, tuple) and len(payload) == 1:
-                return payload[0]
-            return payload
-        except KeyboardInterrupt:
-            return ("stop", ())
-        finally:
-            self.lock.release()
-
-
-class ServerProxy(multiprocessing.Process, BlockingChannel):
-
-    def __init__(self, channel, init_func, *init_args, **init_kwargs):
-        multiprocessing.Process.__init__(self)
-        BlockingChannel.__init__(self, channel)
-        self.init_func = init_func
-        self.init_args = init_args
-        self.init_kwargs = init_kwargs
-
-    def run(self):
-        server = self.init_func(*self.init_args, **self.init_kwargs)
-        server.start(block=False)
-
-        try:
-            while True:
-                # ["func", ("arg", ...)]
-                # ["prop", ()]
-                sattr, fargs = self.recv()
-                attr = getattr(server, sattr)
-
-                # apply fargs to attr if it is a function
-                if callable(attr):
-                    rv = attr(*fargs)
-
-                # otherwise attr is a property
-                else:
-                    rv = attr
-
-                self.send(rv)
-
-                if sattr == "stop":
-                    return
-
-        except KeyboardInterrupt:
-            server.stop()
-
-
-class ServerProc(BlockingChannel):
-
-    def __init__(self, init_func):
-        self._init_func = init_func
-        self.proc = None
-
-        parent_chan, self.child_chan = multiprocessing.Pipe()
-        BlockingChannel.__init__(self, parent_chan)
-
-    def start(self, doc_root, ssl_config, **kwargs):
-        self.proc = ServerProxy(
-            self.child_chan, self._init_func, doc_root, ssl_config, **kwargs)
-        self.proc.daemon = True
-        self.proc.start()
-
-    def get_url(self, url):
-        return self.call("get_url", (url,))
-
-    @property
-    def doc_root(self):
-        return self.call("doc_root", ())
-
-    def stop(self):
-        self.call("stop")
-        if not self.is_alive:
-            return
-        self.proc.join()
-
-    def kill(self):
-        if not self.is_alive:
-            return
-        self.proc.terminate()
-        self.proc.join(0)
-
-    @property
-    def is_alive(self):
-        if self.proc is not None:
-            return self.proc.is_alive()
-        return False
-
-
-def http_server(doc_root, ssl_config, **kwargs):
-    return httpd.FixtureServer(doc_root, url="http://0.0.0.0:0/")
-
-
-def https_server(doc_root, ssl_config, **kwargs):
-    return httpd.FixtureServer(doc_root,
-                               url="https://0.0.0.0:0/",
-                               ssl_key=ssl_config["key_path"],
-                               ssl_cert=ssl_config["cert_path"])
-
-
-def start_servers(doc_root, ssl_config, **kwargs):
-    servers = defaultdict()
-    for schema, builder_fn in registered_servers:
-        proc = ServerProc(builder_fn)
-        proc.start(doc_root, ssl_config, **kwargs)
-        servers[schema] = (proc.get_url("/"), proc)
-    return servers
-
-
-def start(doc_root=None, **kwargs):
-    """Start all relevant test servers.
-
-    If no `doc_root` is given the default
-    testing/marionette/harness/marionette/www directory will be used.
-
-    Additional keyword arguments can be given which will be passed on
-    to the individual ``FixtureServer``'s in httpd.py.
-
-    """
-    doc_root = doc_root or default_doc_root
-    ssl_config = {"cert_path": httpd.default_ssl_cert,
-                  "key_path": httpd.default_ssl_key}
-
-    global servers
-    servers = start_servers(doc_root, ssl_config, **kwargs)
-
-    return servers
-
-
-def where_is(uri, on="http"):
-    """Returns the full URL, including scheme, hostname, and port, for
-    a fixture resource from the server associated with the ``on`` key.
-    It will by default look for the resource in the "http" server.
-
-    """
-    return servers.get(on)[1].get_url(uri)
-
-
-def iter_proc(servers):
-    for _, (_, proc) in servers.iteritems():
-        yield proc
-
-
-def iter_url(servers):
-    for _, (url, _) in servers.iteritems():
-        yield url
-
-
-default_doc_root = os.path.join(os.path.dirname(here), "www")
-registered_servers = [("http", http_server),
-                      ("https", https_server)]
-servers = defaultdict()
-
-
-def main(args):
-    global servers
-
-    parser = argparse.ArgumentParser()
-    parser.add_argument("-r", dest="doc_root",
-                        help="Path to document root.  Overrides default.")
-    args = parser.parse_args()
-
-    servers = start(args.doc_root)
-    for url in iter_url(servers):
-        print >>sys.stderr, "%s: listening on %s" % (sys.argv[0], url)
-
-    try:
-        while any(proc.is_alive for proc in iter_proc(servers)):
-            for proc in iter_proc(servers):
-                proc.proc.join(1)
-    except KeyboardInterrupt:
-        for proc in iter_proc(servers):
-            proc.kill()
-
-
-if __name__ == "__main__":
-    main(sys.argv[1:])
deleted file mode 100644
--- a/testing/marionette/harness/marionette/runner/test.cert
+++ /dev/null
@@ -1,86 +0,0 @@
-Certificate:
-    Data:
-        Version: 3 (0x2)
-        Serial Number: 2 (0x2)
-    Signature Algorithm: sha256WithRSAEncryption
-        Issuer: CN=web-platform-tests
-        Validity
-            Not Before: Dec 22 12:09:16 2014 GMT
-            Not After : Dec 21 12:09:16 2024 GMT
-        Subject: CN=web-platform.test
-        Subject Public Key Info:
-            Public Key Algorithm: rsaEncryption
-                Public-Key: (2048 bit)
-                Modulus:
-                    00:b3:84:d6:8b:01:59:18:85:d1:dc:32:df:38:f7:
-                    90:85:1b:3e:a5:5e:81:3e:2f:fc:3a:5f:7f:77:ef:
-                    23:bb:3a:88:27:0f:be:25:46:cd:63:7d:cb:95:d8:
-                    a5:50:10:d2:a2:d2:b7:97:d1:0d:6c:fb:f9:05:e8:
-                    6f:a8:4b:bd:95:67:9e:7b:94:58:a9:6d:93:fd:e0:
-                    12:c5:cd:b4:8a:64:52:31:5f:0e:e3:89:84:71:da:
-                    98:dd:4b:ec:02:25:a5:7d:35:fe:63:da:b3:ac:ec:
-                    a5:46:0f:0d:64:23:5c:6d:f3:ec:cc:28:63:23:c0:
-                    4b:9a:ec:8f:c1:ee:b1:a2:3e:72:4d:70:b5:09:c1:
-                    eb:b4:10:55:3c:8b:ea:1b:94:7e:4b:74:e6:f4:9f:
-                    4f:a6:45:30:b5:f0:b8:b4:d1:59:50:65:0a:86:53:
-                    ea:4c:9f:9e:f4:58:6c:31:f5:17:3a:6f:57:8b:cb:
-                    5f:f0:28:0b:45:92:8d:30:20:49:ff:52:e6:2c:cb:
-                    18:9a:d7:e6:ee:3e:4f:34:35:15:13:c5:02:da:c5:
-                    5f:be:fb:5b:ce:8d:bf:b5:35:76:3c:7c:e6:9c:3b:
-                    26:87:4d:8d:80:e6:16:c6:27:f2:50:49:b6:72:74:
-                    43:49:49:44:38:bb:78:43:23:ee:16:3e:d9:62:e6:
-                    a5:d7
-                Exponent: 65537 (0x10001)
-        X509v3 extensions:
-            X509v3 Basic Constraints: 
-                CA:FALSE
-            X509v3 Subject Key Identifier: 
-                2D:98:A3:99:39:1C:FE:E9:9A:6D:17:94:D2:3A:96:EE:C8:9E:04:22
-            X509v3 Authority Key Identifier: 
-                keyid:6A:AB:53:64:92:36:87:23:34:B3:1D:6F:85:4B:F5:DF:5A:5C:74:8F
-
-            X509v3 Key Usage: 
-                Digital Signature, Non Repudiation, Key Encipherment
-            X509v3 Extended Key Usage: 
-                TLS Web Server Authentication
-            X509v3 Subject Alternative Name: 
-                DNS:web-platform.test, DNS:www.web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--lve-6lad.web-platform.test, DNS:www2.web-platform.test, DNS:www1.web-platform.test
-    Signature Algorithm: sha256WithRSAEncryption
-         33:db:f7:f0:f6:92:16:4f:2d:42:bc:b8:aa:e6:ab:5e:f9:b9:
-         b0:48:ae:b5:8d:cc:02:7b:e9:6f:4e:75:f7:17:a0:5e:7b:87:
-         06:49:48:83:c5:bb:ca:95:07:37:0e:5d:e3:97:de:9e:0c:a4:
-         82:30:11:81:49:5d:50:29:72:92:a5:ca:17:b1:7c:f1:32:11:
-         17:57:e6:59:c1:ac:e3:3b:26:d2:94:97:50:6a:b9:54:88:84:
-         9b:6f:b1:06:f5:80:04:22:10:14:b1:f5:97:25:fc:66:d6:69:
-         a3:36:08:85:23:ff:8e:3c:2b:e0:6d:e7:61:f1:00:8f:61:3d:
-         b0:87:ad:72:21:f6:f0:cc:4f:c9:20:bf:83:11:0f:21:f4:b8:
-         c0:dd:9c:51:d7:bb:27:32:ec:ab:a4:62:14:28:32:da:f2:87:
-         80:68:9c:ea:ac:eb:f5:7f:f5:de:f4:c0:39:91:c8:76:a4:ee:
-         d0:a8:50:db:c1:4b:f9:c4:3d:d9:e8:8e:b6:3f:c0:96:79:12:
-         d8:fa:4d:0a:b3:36:76:aa:4e:b2:82:2f:a2:d4:0d:db:fd:64:
-         77:6f:6e:e9:94:7f:0f:c8:3a:3c:96:3d:cd:4d:6c:ba:66:95:
-         f7:b4:9d:a4:94:9f:97:b3:9a:0d:dc:18:8c:11:0b:56:65:8e:
-         46:4c:e6:5e
------BEGIN CERTIFICATE-----
-MIID2jCCAsKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJ3ZWIt
-cGxhdGZvcm0tdGVzdHMwHhcNMTQxMjIyMTIwOTE2WhcNMjQxMjIxMTIwOTE2WjAc
-MRowGAYDVQQDExF3ZWItcGxhdGZvcm0udGVzdDCCASIwDQYJKoZIhvcNAQEBBQAD
-ggEPADCCAQoCggEBALOE1osBWRiF0dwy3zj3kIUbPqVegT4v/Dpff3fvI7s6iCcP
-viVGzWN9y5XYpVAQ0qLSt5fRDWz7+QXob6hLvZVnnnuUWKltk/3gEsXNtIpkUjFf
-DuOJhHHamN1L7AIlpX01/mPas6zspUYPDWQjXG3z7MwoYyPAS5rsj8HusaI+ck1w
-tQnB67QQVTyL6huUfkt05vSfT6ZFMLXwuLTRWVBlCoZT6kyfnvRYbDH1FzpvV4vL
-X/AoC0WSjTAgSf9S5izLGJrX5u4+TzQ1FRPFAtrFX777W86Nv7U1djx85pw7JodN
-jYDmFsYn8lBJtnJ0Q0lJRDi7eEMj7hY+2WLmpdcCAwEAAaOCASQwggEgMAkGA1Ud
-EwQCMAAwHQYDVR0OBBYEFC2Yo5k5HP7pmm0XlNI6lu7IngQiMB8GA1UdIwQYMBaA
-FGqrU2SSNocjNLMdb4VL9d9aXHSPMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAKBggr
-BgEFBQcDATCBsAYDVR0RBIGoMIGlghF3ZWItcGxhdGZvcm0udGVzdIIVd3d3Lndl
-Yi1wbGF0Zm9ybS50ZXN0gil4bi0tbjhqNmRzNTNsd3drcnFodjI4YS53ZWItcGxh
-dGZvcm0udGVzdIIeeG4tLWx2ZS02bGFkLndlYi1wbGF0Zm9ybS50ZXN0ghZ3d3cy
-LndlYi1wbGF0Zm9ybS50ZXN0ghZ3d3cxLndlYi1wbGF0Zm9ybS50ZXN0MA0GCSqG
-SIb3DQEBCwUAA4IBAQAz2/fw9pIWTy1CvLiq5qte+bmwSK61jcwCe+lvTnX3F6Be
-e4cGSUiDxbvKlQc3Dl3jl96eDKSCMBGBSV1QKXKSpcoXsXzxMhEXV+ZZwazjOybS
-lJdQarlUiISbb7EG9YAEIhAUsfWXJfxm1mmjNgiFI/+OPCvgbedh8QCPYT2wh61y
-IfbwzE/JIL+DEQ8h9LjA3ZxR17snMuyrpGIUKDLa8oeAaJzqrOv1f/Xe9MA5kch2
-pO7QqFDbwUv5xD3Z6I62P8CWeRLY+k0KszZ2qk6ygi+i1A3b/WR3b27plH8PyDo8
-lj3NTWy6ZpX3tJ2klJ+Xs5oN3BiMEQtWZY5GTOZe
------END CERTIFICATE-----
\ No newline at end of file
deleted file mode 100644
--- a/testing/marionette/harness/marionette/runner/test.key
+++ /dev/null
@@ -1,28 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzhNaLAVkYhdHc
-Mt8495CFGz6lXoE+L/w6X3937yO7OognD74lRs1jfcuV2KVQENKi0reX0Q1s+/kF
-6G+oS72VZ557lFipbZP94BLFzbSKZFIxXw7jiYRx2pjdS+wCJaV9Nf5j2rOs7KVG
-Dw1kI1xt8+zMKGMjwEua7I/B7rGiPnJNcLUJweu0EFU8i+oblH5LdOb0n0+mRTC1
-8Li00VlQZQqGU+pMn570WGwx9Rc6b1eLy1/wKAtFko0wIEn/UuYsyxia1+buPk80
-NRUTxQLaxV+++1vOjb+1NXY8fOacOyaHTY2A5hbGJ/JQSbZydENJSUQ4u3hDI+4W
-Ptli5qXXAgMBAAECggEBAIcwDQSnIjo2ZECHytQykpG6X6XXEksLhc1Lp0lhPC49
-uNR5pX6a4AcBb3PLr0opMQZO2tUoKA0ff3t0e8loKD+/xXhY0Z/dlioEOP7elwv0
-2nS1mhe9spCuxpk4GGXRhdtR8t2tj8s0do3YvgPgITXoEDX6YBZHNGhZpzSrFPgQ
-/c3eGCVmzWYuLFfdj5OPQ9bwTaY4JSvDLZT0/WTgiica7VySwfz3HP1fFqNykTiK
-ACQREvtxfk5Ym2nT6oni7CM2zOEJL9SXicXI5HO4bERH0ZYh//F3g6mwGiFXUJPd
-NKgaTM1oT9kRGkUaEYsRWrddwR8d5mXLvBuTJbgIsSECgYEA1+2uJSYRW1OqbhYP
-ms59YQHSs3VjpJpnCV2zNa2Wixs57KS2cOH7B6KrQCogJFLtgCDVLtyoErfVkD7E
-FivTgYr1pVCRppJddQzXik31uOINOBVffr7/09g3GcRN+ubHPZPq3K+dD6gHa3Aj
-0nH1EjEEV0QpSTQFn87OF2mc9wcCgYEA1NVqMbbzd+9Xft5FXuSbX6E+S02dOGat
-SgpnkTM80rjqa6eHdQzqk3JqyteHPgdi1vdYRlSPOj/X+6tySY0Ej9sRnYOfddA2
-kpiDiVkmiqVolyJPY69Utj+E3TzJ1vhCQuYknJmB7zP9tDcTxMeq0l/NaWvGshEK
-yC4UTQog1rECgYASOFILfGzWgfbNlzr12xqlRtwanHst9oFfPvLSQrWDQ2bd2wAy
-Aj+GY2mD3oobxouX1i1m6OOdwLlalJFDNauBMNKNgoDnx03vhIfjebSURy7KXrNS
-JJe9rm7n07KoyzRgs8yLlp3wJkOKA0pihY8iW9R78JpzPNqEo5SsURMXnQKBgBlV
-gfuC9H4tPjP6zzUZbyk1701VYsaI6k2q6WMOP0ox+q1v1p7nN7DvaKjWeOG4TVqb
-PKW6gQYE/XeWk9cPcyCQigs+1KdYbnaKsvWRaBYO1GFREzQhdarv6qfPCZOOH40J
-Cgid+Sp4/NULzU2aGspJ3xCSZKdjge4MFhyJfRkxAoGBAJlwqY4nue0MBLGNpqcs
-WwDtSasHvegKAcxGBKL5oWPbLBk7hk+hdqc8f6YqCkCNqv/ooBspL15ESItL+6yT
-zt0YkK4oH9tmLDb+rvqZ7ZdXbWSwKITMoCyyHUtT6OKt/RtA0Vdy9LPnP27oSO/C
-dk8Qf7KgKZLWo0ZNkvw38tEC
------END PRIVATE KEY-----
\ No newline at end of file
deleted file mode 100644
--- a/testing/marionette/harness/marionette/tests/harness_unit/test_httpd.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# 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 json
-import os
-import types
-import urllib2
-
-import pytest
-
-from marionette.runner import httpd
-from wptserve.handlers import json_handler
-
-here = os.path.abspath(os.path.dirname(__file__))
-parent = os.path.dirname(here)
-default_doc_root = os.path.join(os.path.dirname(parent), "www")
-
-
-@pytest.yield_fixture
-def server():
-    server = httpd.FixtureServer(default_doc_root)
-    yield server
-    server.stop()
-
-
-def test_ctor():
-    with pytest.raises(ValueError):
-        httpd.FixtureServer("foo")
-    httpd.FixtureServer(default_doc_root)
-
-
-def test_start_stop(server):
-    server.start()
-    server.stop()
-
-
-def test_get_url(server):
-    server.start()
-    url = server.get_url("/")
-    assert isinstance(url, types.StringTypes)
-    assert "http://" in url
-
-    server.stop()
-    with pytest.raises(httpd.NotAliveError):
-        server.get_url("/")
-
-
-def test_doc_root(server):
-    server.start()
-    assert isinstance(server.doc_root, types.StringTypes)
-    server.stop()
-    assert isinstance(server.doc_root, types.StringTypes)
-
-
-def test_router(server):
-    assert server.router is not None
-
-
-def test_routes(server):
-    assert server.routes is not None
-
-
-def test_is_alive(server):
-    assert server.is_alive == False
-    server.start()
-    assert server.is_alive == True
-
-
-def test_handler(server):
-    counter = 0
-
-    @json_handler
-    def handler(request, response):
-        return {"count": counter}
-
-    route = ("GET", "/httpd/test_handler", handler)
-    server.router.register(*route)
-    server.start()
-
-    url = server.get_url("/httpd/test_handler")
-    body = urllib2.urlopen(url).read()
-    res = json.loads(body)
-    assert res["count"] == counter
-
-
-if __name__ == "__main__":
-    import sys
-    sys.exit(pytest.main(["--verbose", __file__]))
--- a/testing/marionette/harness/marionette/tests/harness_unit/test_marionette_runner.py
+++ b/testing/marionette/harness/marionette/tests/harness_unit/test_marionette_runner.py
@@ -20,17 +20,17 @@ def runner(mach_parsed_kwargs):
 @pytest.fixture
 def mock_runner(runner, mock_marionette, monkeypatch):
     """
     MarionetteTestRunner instance with mocked-out
     self.marionette and other properties,
     to enable testing runner.run_tests().
     """
     runner.driverclass = mock_marionette
-    for attr in ['run_test_set', '_capabilities']:
+    for attr in ['_set_baseurl', 'run_test_set', '_capabilities']:
         setattr(runner, attr, Mock())
     runner._appName = 'fake_app'
     monkeypatch.setattr('marionette.runner.base.mozversion', Mock())
     return runner
 
 
 @pytest.fixture
 def build_kwargs_using(mach_parsed_kwargs):
@@ -347,17 +347,17 @@ def test_cleanup_with_manifest(mock_runn
     monkeypatch.setattr('marionette.runner.base.TestManifest', manifest_with_tests.manifest_class)
     if manifest_with_tests.n_enabled > 0:
         context = patch('marionette.runner.base.os.path.exists', return_value=True)
     else:
         context = pytest.raises(Exception)
     with context:
         mock_runner.run_tests([manifest_with_tests.filepath])
     assert mock_runner.marionette is None
-    assert mock_runner.fixture_servers == {}
+    assert mock_runner.httpd is None
 
 
 def test_reset_test_stats(mock_runner):
     def reset_successful(runner):
         stats = ['passed', 'failed', 'unexpected_successes', 'todo', 'skipped', 'failures']
         return all([((s in vars(runner)) and (not vars(runner)[s])) for s in stats])
     assert reset_successful(mock_runner)
     mock_runner.passed = 1
deleted file mode 100644
--- a/testing/marionette/harness/marionette/tests/harness_unit/test_serve.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# 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 types
-
-import pytest
-
-from marionette.runner import serve
-from marionette.runner.serve import iter_proc, iter_url
-
-
-def teardown_function(func):
-    for server in [server for server in iter_proc(serve.servers) if server.is_alive]:
-        server.stop()
-        server.kill()
-
-
-def test_registered_servers():
-    # [(name, factory), ...]
-    assert serve.registered_servers[0][0] == "http"
-    assert serve.registered_servers[1][0] == "https"
-
-
-def test_globals():
-    assert serve.default_doc_root is not None
-    assert serve.registered_servers is not None
-    assert serve.servers is not None
-
-
-def test_start():
-    serve.start()
-    assert len(serve.servers) == 2
-    assert "http" in serve.servers
-    assert "https" in serve.servers
-    for url in iter_url(serve.servers):
-        assert isinstance(url, types.StringTypes)
-
-
-def test_start_with_custom_root(tmpdir_factory):
-    tdir = tmpdir_factory.mktemp("foo")
-    serve.start(str(tdir))
-    for server in iter_proc(serve.servers):
-        assert server.doc_root == tdir
-
-
-def test_iter_proc():
-    serve.start()
-    for server in iter_proc(serve.servers):
-        server.stop()
-
-
-def test_iter_url():
-    serve.start()
-    for url in iter_url(serve.servers):
-        assert isinstance(url, types.StringTypes)
-
-
-def test_where_is():
-    serve.start()
-    assert serve.where_is("/") == serve.servers["http"][1].get_url("/")
-    assert serve.where_is("/", on="https") == serve.servers["https"][1].get_url("/")
-
-
-if __name__ == "__main__":
-    import sys
-    sys.exit(pytest.main(["-s", "--verbose", __file__]))
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/tests/unit/test_httpd.py
@@ -0,0 +1,32 @@
+# 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/.
+
+from marionette import MarionetteTestCase
+from marionette_driver import By
+
+
+class TestHttpdServer(MarionetteTestCase):
+
+    def test_handler(self):
+        status = {"count": 0}
+
+        def handler(request, response):
+            status["count"] += 1
+
+            response.headers.set("Content-Type", "text/html")
+            response.content = "<html><body><p id=\"count\">{}</p></body></html>".format(
+                status["count"])
+
+            return ()
+
+        route = ("GET", "/httpd/test_handler", handler)
+        self.httpd.router.register(*route)
+
+        url = self.marionette.absolute_url("httpd/test_handler")
+
+        for counter in range(0, 5):
+            self.marionette.navigate(url)
+            self.assertEqual(status["count"], counter + 1)
+            elem = self.marionette.find_element(By.ID, "count")
+            self.assertEqual(elem.text, str(counter + 1))
--- a/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
@@ -117,9 +117,10 @@ skip-if = appname == 'fennec' || os == "
 
 [test_chrome.py]
 skip-if = appname == 'fennec'
 
 [test_addons.py]
 
 [test_select.py]
 [test_crash.py]
+[test_httpd.py]
 [test_localization.py]
--- a/testing/marionette/harness/requirements.txt
+++ b/testing/marionette/harness/requirements.txt
@@ -1,13 +1,14 @@
+marionette-driver >= 2.1.0
 browsermob-proxy >= 0.6.0
 manifestparser >= 1.1
-marionette-driver >= 2.1.0
-mozcrash >= 0.5
-mozdevice >= 0.44
+wptserve >= 1.3.0
 mozinfo >= 0.8
+mozprocess >= 0.9
+mozrunner >= 6.13
+mozdevice >= 0.44
 mozlog >= 3.0
-mozprocess >= 0.9
+moznetwork >= 0.21
+mozcrash >= 0.5
 mozprofile >= 0.7
-mozrunner >= 6.13
 moztest >= 0.7
 mozversion >= 1.1
-wptserve >= 1.3.0