Bug 1230862 - replace mozhttpd with wptserve. r=wlach draft
authorJulien Pagès <j.parkouss@gmail.com>
Thu, 11 Feb 2016 12:22:13 +0100
changeset 330306 1cee887de1184c3d43f07afe372b5bc9782123aa
parent 330305 fe35ae46b5376e64a7d4e64c7856357580bddddf
child 514144 ca848ef11b1536b44233c8645ad027118811524e
push id10726
push userj.parkouss@gmail.com
push dateThu, 11 Feb 2016 11:24:35 +0000
reviewerswlach
bugs1230862
milestone47.0a1
Bug 1230862 - replace mozhttpd with wptserve. r=wlach MozReview-Commit-ID: ShMIajZm1p
build/mach_bootstrap.py
build/pgo/profileserver.py
build/valgrind/mach_commands.py
dom/media/test/external/requirements.txt
testing/config/mozbase_requirements.txt
testing/mozbase/docs/index.rst
testing/mozbase/docs/mozhttpd.rst
testing/mozbase/moz.build
testing/mozbase/mozcrash/tests/test.py
testing/mozbase/mozfile/setup.py
testing/mozbase/mozfile/tests/test_load.py
testing/mozbase/mozhttpd/mozhttpd/__init__.py
testing/mozbase/mozhttpd/mozhttpd/handlers.py
testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
testing/mozbase/mozhttpd/setup.py
testing/mozbase/mozhttpd/tests/api.py
testing/mozbase/mozhttpd/tests/baseurl.py
testing/mozbase/mozhttpd/tests/basic.py
testing/mozbase/mozhttpd/tests/filelisting.py
testing/mozbase/mozhttpd/tests/manifest.ini
testing/mozbase/mozhttpd/tests/paths.py
testing/mozbase/mozhttpd/tests/requestlog.py
testing/mozbase/mozprofile/setup.py
testing/mozbase/mozprofile/tests/test_addons.py
testing/mozbase/mozprofile/tests/test_preferences.py
testing/mozbase/packages.txt
testing/mozbase/test-manifest.ini
testing/mozharness/mozharness/mozilla/mozbase.py
testing/mozharness/scripts/marionette.py
testing/mozharness/test/pip-freeze.example.txt
testing/mozharness/test/test_base_python.py
testing/tools/mach_test_package_bootstrap.py
testing/tps/setup.py
testing/tps/tps/testrunner.py
tools/docs/mach_commands.py
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -79,17 +79,16 @@ SEARCH_PATHS = [
     'testing/luciddream',
     'testing/marionette/client',
     'testing/marionette/client/marionette/runner/mixins/browsermob-proxy-py',
     'testing/marionette/driver',
     'testing/mozbase/mozcrash',
     'testing/mozbase/mozdebug',
     'testing/mozbase/mozdevice',
     'testing/mozbase/mozfile',
-    'testing/mozbase/mozhttpd',
     'testing/mozbase/mozinfo',
     'testing/mozbase/mozinstall',
     'testing/mozbase/mozleak',
     'testing/mozbase/mozlog',
     'testing/mozbase/moznetwork',
     'testing/mozbase/mozprocess',
     'testing/mozbase/mozprofile',
     'testing/mozbase/mozrunner',
--- a/build/pgo/profileserver.py
+++ b/build/pgo/profileserver.py
@@ -2,17 +2,17 @@
 #
 # 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 mozprofile import FirefoxProfile, Profile, Preferences
 from mozprofile.permissions import ServerLocations
 from mozrunner import FirefoxRunner, CLI
-from mozhttpd import MozHttpd
+from wptserve.server import WebTestHttpd
 import json
 import socket
 import threading
 import os
 import sys
 import shutil
 import tempfile
 from datetime import datetime
@@ -21,18 +21,18 @@ from buildconfig import substs
 
 PORT = 8888
 
 if __name__ == '__main__':
   cli = CLI()
   debug_args, interactive = cli.debugger_arguments()
 
   build = MozbuildObject.from_environment()
-  httpd = MozHttpd(port=PORT,
-                   docroot=os.path.join(build.topsrcdir, "build", "pgo"))
+  httpd = WebTestHttpd(port=PORT,
+                       doc_root=os.path.join(build.topsrcdir, "build", "pgo"))
   httpd.start(block=False)
 
   locations = ServerLocations()
   locations.add_host(host='127.0.0.1',
                      port=PORT,
                      options='primary,privileged')
 
   #TODO: mozfile.TemporaryDirectory
--- a/build/valgrind/mach_commands.py
+++ b/build/valgrind/mach_commands.py
@@ -42,28 +42,28 @@ class MachCommands(MachCommandBase):
             'files.')
     def valgrind_test(self, suppressions):
         import json
         import sys
         import tempfile
 
         from mozbuild.base import MozbuildObject
         from mozfile import TemporaryDirectory
-        from mozhttpd import MozHttpd
+        from wptserve.server import WebTestHttpd
         from mozprofile import FirefoxProfile, Preferences
         from mozprofile.permissions import ServerLocations
         from mozrunner import FirefoxRunner
         from mozrunner.utils import findInPath
         from valgrind.output_handler import OutputHandler
 
         build_dir = os.path.join(self.topsrcdir, 'build')
 
         # XXX: currently we just use the PGO inputs for Valgrind runs.  This may
         # change in the future.
-        httpd = MozHttpd(docroot=os.path.join(build_dir, 'pgo'))
+        httpd = WebTestHttpd(doc_root=os.path.join(build_dir, 'pgo'))
         httpd.start(block=False)
 
         with TemporaryDirectory() as profilePath:
             #TODO: refactor this into mozprofile
             prefpath = os.path.join(self.topsrcdir, 'testing', 'profiles', 'prefs_general.js')
             prefs = {}
             prefs.update(Preferences.read_prefs(prefpath))
             interpolation = { 'server': '%s:%d' % httpd.httpd.server_address,
--- a/dom/media/test/external/requirements.txt
+++ b/dom/media/test/external/requirements.txt
@@ -1,14 +1,13 @@
 browsermob-proxy==0.7.1
 manifestparser==1.1
 mozcrash==0.16
 mozdevice==0.48
 mozfile==1.2
-mozhttpd==0.7
 mozinfo==0.9
 # optional - mozharness install step
 mozInstall==1.12
 mozlog==3.1
 moznetwork==0.27
 mozprocess==0.22
 mozprofile==0.28
 mozrunner==6.11
--- a/testing/config/mozbase_requirements.txt
+++ b/testing/config/mozbase_requirements.txt
@@ -1,14 +1,13 @@
 ../mozbase/manifestparser
 ../mozbase/mozcrash
 ../mozbase/mozdebug
 ../mozbase/mozdevice
 ../mozbase/mozfile
-../mozbase/mozhttpd
 ../mozbase/mozinfo
 ../mozbase/mozinstall
 ../mozbase/mozleak
 ../mozbase/mozlog
 ../mozbase/moznetwork
 ../mozbase/mozprocess
 ../mozbase/mozprofile
 ../mozbase/mozrunner
--- a/testing/mozbase/docs/index.rst
+++ b/testing/mozbase/docs/index.rst
@@ -39,17 +39,16 @@ The documentation is organized by catego
 want to do then dive in!
 
 .. toctree::
    :maxdepth: 2
 
    manifestparser
    gettinginfo
    setuprunning
-   mozhttpd
    loggingreporting
    devicemanagement
 
 Indices and tables
 ==================
 
 * :ref:`genindex`
 * :ref:`modindex`
deleted file mode 100644
--- a/testing/mozbase/docs/mozhttpd.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Serving up content to be consumed by the browser
-================================================
-
-
-.. warning:: The mozhttpd module is considered obsolete. For new code,
-             please use wptserve_ which can do everything mozhttpd does
-             and more.
-
-.. _wptserve: https://pypi.python.org/pypi/wptserve
-
-:mod:`mozhttpd` --- Simple webserver
-------------------------------------
-
-.. automodule:: mozhttpd
-   :members:
-
-Interface
-`````````
-
-.. autoclass:: MozHttpd
-   :members:
--- a/testing/mozbase/moz.build
+++ b/testing/mozbase/moz.build
@@ -9,17 +9,16 @@ PYTHON_UNIT_TESTS += [
 ]
 
 python_modules = [
     'manifestparser',
     'mozcrash',
     'mozdebug',
     'mozdevice',
     'mozfile',
-    'mozhttpd',
     'mozinfo',
     'mozinstall',
     'mozleak',
     'mozlog',
     'moznetwork',
     'mozprocess',
     'mozprofile',
     'mozrunner',
--- a/testing/mozbase/mozcrash/tests/test.py
+++ b/testing/mozbase/mozcrash/tests/test.py
@@ -1,17 +1,18 @@
 #!/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/.
 
 import os, unittest, subprocess, tempfile, shutil, urlparse, zipfile, StringIO
 import mozcrash
-import mozhttpd
+from wptserve.server import WebTestHttpd
+from wptserve import handlers
 import mozlog.unstructured as mozlog
 
 # Make logs go away
 log = mozlog.getLogger("mozcrash", handler=mozlog.FileHandler(os.devnull))
 
 def popen_factory(stdouts):
     """
     Generate a class that can mock subprocess.Popen. |stdouts| is an iterable that
@@ -160,21 +161,22 @@ class TestCrash(unittest.TestCase):
         self.stdouts.append(["this is some output"])
 
         def make_zipfile():
             data = StringIO.StringIO()
             z = zipfile.ZipFile(data, 'w')
             z.writestr("symbols.txt", "abc/xyz")
             z.close()
             return data.getvalue()
-        def get_symbols(req):
-            headers = {}
-            return (200, headers, make_zipfile())
-        httpd = mozhttpd.MozHttpd(port=0,
-                                  urlhandlers=[{'method':'GET', 'path':'/symbols', 'function':get_symbols}])
+
+        @handlers.handler
+        def get_symbols(request, response):
+            return (200, [], [make_zipfile()])
+        httpd = WebTestHttpd(port=0,
+                             routes=[("GET", "/symbols", get_symbols)])
         httpd.start()
         symbol_url = urlparse.urlunsplit(('http', '%s:%d' % httpd.httpd.server_address,
                                         '/symbols','',''))
         self.assert_(mozcrash.check_for_crashes(self.tempdir,
                                                 symbol_url,
                                                 stackwalk_binary=self.stackwalk,
                                                 quiet=True))
 
--- a/testing/mozbase/mozfile/setup.py
+++ b/testing/mozbase/mozfile/setup.py
@@ -16,10 +16,10 @@ setup(name=PACKAGE_NAME,
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
       url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['mozfile'],
       include_package_data=True,
       zip_safe=False,
       install_requires=[],
-      tests_require=['mozhttpd']
+      tests_require=['wptserve']
       )
--- a/testing/mozbase/mozfile/tests/test_load.py
+++ b/testing/mozbase/mozfile/tests/test_load.py
@@ -1,39 +1,39 @@
 #!/usr/bin/env python
 
 """
 tests for mozfile.load
 """
 
-import mozhttpd
+from wptserve import server, handlers
 import os
 import tempfile
 import unittest
 from mozfile import load
 
 
 class TestLoad(unittest.TestCase):
     """test the load function"""
 
     def test_http(self):
-        """test with mozhttpd and a http:// URL"""
+        """test with wptserve and a http:// URL"""
 
-        def example(request):
+        @handlers.handler
+        def example(request, reponse):
             """example request handler"""
             body = 'example'
-            return (200, {'Content-type': 'text/plain',
-                          'Content-length': len(body)
-                          }, body)
+            return (200, [{'Content-type': 'text/plain',
+                           'Content-length': len(body)
+                          }], [body])
 
         host = '127.0.0.1'
-        httpd = mozhttpd.MozHttpd(host=host,
-                                  urlhandlers=[{'method': 'GET',
-                                                'path': '.*',
-                                                'function': example}])
+        httpd = server.WebTestHttpd(
+            host=host,
+            routes=[('GET', "*", example)])
         try:
             httpd.start(block=False)
             content = load(httpd.get_url()).read()
             self.assertEqual(content, 'example')
         finally:
             httpd.stop()
 
     def test_file_path(self):
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/mozhttpd/__init__.py
+++ /dev/null
@@ -1,46 +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/.
-
-"""
-Mozhttpd is a simple http webserver written in python, designed expressly
-for use in automated testing scenarios. It is designed to both serve static
-content and provide simple web services.
-
-The server is based on python standard library modules such as
-SimpleHttpServer, urlparse, etc. The ThreadingMixIn is used to
-serve each request on a discrete thread.
-
-Some existing uses of mozhttpd include Peptest_, Eideticker_, and Talos_.
-
-.. _Peptest: https://github.com/mozilla/peptest/
-
-.. _Eideticker: https://github.com/mozilla/eideticker/
-
-.. _Talos: http://hg.mozilla.org/build/
-
-The following simple example creates a basic HTTP server which serves
-content from the current directory, defines a single API endpoint
-`/api/resource/<resourceid>` and then serves requests indefinitely:
-
-::
-
-  import mozhttpd
-
-  @mozhttpd.handlers.json_response
-  def resource_get(request, objid):
-      return (200, { 'id': objid,
-                     'query': request.query })
-
-
-  httpd = mozhttpd.MozHttpd(port=8080, docroot='.',
-                            urlhandlers = [ { 'method': 'GET',
-                                              'path': '/api/resources/([^/]+)/?',
-                                              'function': resource_get } ])
-  print "Serving '%s' at %s:%s" % (httpd.docroot, httpd.host, httpd.port)
-  httpd.start(block=True)
-
-"""
-
-from mozhttpd import MozHttpd, Request, RequestHandler, main
-from handlers import json_response
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/mozhttpd/handlers.py
+++ /dev/null
@@ -1,15 +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
-
-def json_response(func):
-    """ Translates results of 'func' into a JSON response. """
-    def wrap(*a, **kw):
-        (code, data) = func(*a, **kw)
-        json_data = json.dumps(data)
-        return (code, { 'Content-type': 'application/json',
-                        'Content-Length': len(json_data) }, json_data)
-
-    return wrap
deleted file mode 100755
--- a/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
+++ /dev/null
@@ -1,329 +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/.
-
-import BaseHTTPServer
-import SimpleHTTPServer
-import errno
-import logging
-import threading
-import posixpath
-import socket
-import sys
-import os
-import urllib
-import urlparse
-import re
-import moznetwork
-import time
-from SocketServer import ThreadingMixIn
-
-class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
-    allow_reuse_address = True
-    acceptable_errors = (errno.EPIPE, errno.ECONNABORTED)
-
-    def handle_error(self, request, client_address):
-        error = sys.exc_value
-
-        if ((isinstance(error, socket.error) and
-             isinstance(error.args, tuple) and
-             error.args[0] in self.acceptable_errors)
-            or
-            (isinstance(error, IOError) and
-             error.errno in self.acceptable_errors)):
-            pass  # remote hang up before the result is sent
-        else:
-            logging.error(error)
-
-
-class Request(object):
-    """Details of a request."""
-
-    # attributes from urlsplit that this class also sets
-    uri_attrs = ('scheme', 'netloc', 'path', 'query', 'fragment')
-
-    def __init__(self, uri, headers, rfile=None):
-        self.uri = uri
-        self.headers = headers
-        parsed = urlparse.urlsplit(uri)
-        for i, attr in enumerate(self.uri_attrs):
-            setattr(self, attr, parsed[i])
-        try:
-            body_len = int(self.headers.get('Content-length', 0))
-        except ValueError:
-            body_len = 0
-        if body_len and rfile:
-            self.body = rfile.read(body_len)
-        else:
-            self.body = None
-
-
-class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
-
-    docroot = os.getcwd() # current working directory at time of import
-    proxy_host_dirs = False
-    request_log = []
-    log_requests = False
-    request = None
-
-    def __init__(self, *args, **kwargs):
-        SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs)
-        self.extensions_map['.svg'] = 'image/svg+xml'
-
-    def _try_handler(self, method):
-        if self.log_requests:
-            self.request_log.append({ 'method': method,
-                                      'path': self.request.path,
-                                      'time': time.time() })
-
-        handlers = [handler for handler in self.urlhandlers
-                    if handler['method'] == method]
-        for handler in handlers:
-            m = re.match(handler['path'], self.request.path)
-            if m:
-                (response_code, headerdict, data) = \
-                    handler['function'](self.request, *m.groups())
-                self.send_response(response_code)
-                for (keyword, value) in headerdict.iteritems():
-                    self.send_header(keyword, value)
-                self.end_headers()
-                self.wfile.write(data)
-
-                return True
-
-        return False
-
-    def _find_path(self):
-        """Find the on-disk path to serve this request from,
-        using self.path_mappings and self.docroot.
-        Return (url_path, disk_path)."""
-        path_components = filter(None, self.request.path.split('/'))
-        for prefix, disk_path in self.path_mappings.iteritems():
-            prefix_components = filter(None, prefix.split('/'))
-            if len(path_components) < len(prefix_components):
-                continue
-            if path_components[:len(prefix_components)] == prefix_components:
-                return ('/'.join(path_components[len(prefix_components):]),
-                        disk_path)
-        if self.docroot:
-            return self.request.path, self.docroot
-        return None
-
-    def parse_request(self):
-        retval = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self)
-        self.request = Request(self.path, self.headers, self.rfile)
-        return retval
-
-    def do_GET(self):
-        if not self._try_handler('GET'):
-            res = self._find_path()
-            if res:
-                self.path, self.disk_root = res
-                # don't include query string and fragment, and prepend
-                # host directory if required.
-                if self.request.netloc and self.proxy_host_dirs:
-                    self.path = '/' + self.request.netloc + \
-                        self.path
-                SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
-            else:
-                self.send_response(404)
-                self.end_headers()
-                self.wfile.write('')
-
-    def do_POST(self):
-        # if we don't have a match, we always fall through to 404 (this may
-        # not be "technically" correct if we have a local file at the same
-        # path as the resource but... meh)
-        if not self._try_handler('POST'):
-            self.send_response(404)
-            self.end_headers()
-            self.wfile.write('')
-
-    def do_DEL(self):
-        # if we don't have a match, we always fall through to 404 (this may
-        # not be "technically" correct if we have a local file at the same
-        # path as the resource but... meh)
-        if not self._try_handler('DEL'):
-            self.send_response(404)
-            self.end_headers()
-            self.wfile.write('')
-
-    def translate_path(self, path):
-        # this is taken from SimpleHTTPRequestHandler.translate_path(),
-        # except we serve from self.docroot instead of os.getcwd(), and
-        # parse_request()/do_GET() have already stripped the query string and
-        # fragment and mangled the path for proxying, if required.
-        path = posixpath.normpath(urllib.unquote(self.path))
-        words = path.split('/')
-        words = filter(None, words)
-        path = self.disk_root
-        for word in words:
-            drive, word = os.path.splitdrive(word)
-            head, word = os.path.split(word)
-            if word in (os.curdir, os.pardir): continue
-            path = os.path.join(path, word)
-        return path
-
-
-    # I found on my local network that calls to this were timing out
-    # I believe all of these calls are from log_message
-    def address_string(self):
-        return "a.b.c.d"
-
-    # This produces a LOT of noise
-    def log_message(self, format, *args):
-        pass
-
-
-class MozHttpd(object):
-    """
-    :param host: Host from which to serve (default 127.0.0.1)
-    :param port: Port from which to serve (default 8888)
-    :param docroot: Server root (default os.getcwd())
-    :param urlhandlers: Handlers to specify behavior against method and path match (default None)
-    :param path_mappings: A dict mapping URL prefixes to additional on-disk paths.
-    :param proxy_host_dirs: Toggle proxy behavior (default False)
-    :param log_requests: Toggle logging behavior (default False)
-
-    Very basic HTTP server class. Takes a docroot (path on the filesystem)
-    and a set of urlhandler dictionaries of the form:
-
-    ::
-
-      {
-        'method': HTTP method (string): GET, POST, or DEL,
-        'path': PATH_INFO (regular expression string),
-        'function': function of form fn(arg1, arg2, arg3, ..., request)
-      }
-
-    and serves HTTP. For each request, MozHttpd will either return a file
-    off the docroot, or dispatch to a handler function (if both path and
-    method match).
-
-    Note that one of docroot or urlhandlers may be None (in which case no
-    local files or handlers, respectively, will be used). If both docroot or
-    urlhandlers are None then MozHttpd will default to serving just the local
-    directory.
-
-    MozHttpd also handles proxy requests (i.e. with a full URI on the request
-    line).  By default files are served from docroot according to the request
-    URI's path component, but if proxy_host_dirs is True, files are served
-    from <self.docroot>/<host>/.
-
-    For example, the request "GET http://foo.bar/dir/file.html" would
-    (assuming no handlers match) serve <docroot>/dir/file.html if
-    proxy_host_dirs is False, or <docroot>/foo.bar/dir/file.html if it is
-    True.
-    """
-
-    def __init__(self,
-                 host="127.0.0.1",
-                 port=0,
-                 docroot=None,
-                 urlhandlers=None,
-                 path_mappings=None,
-                 proxy_host_dirs=False,
-                 log_requests=False):
-        self.host = host
-        self.port = int(port)
-        self.docroot = docroot
-        if not (urlhandlers or docroot or path_mappings):
-            self.docroot = os.getcwd()
-        self.proxy_host_dirs = proxy_host_dirs
-        self.httpd = None
-        self.urlhandlers = urlhandlers or []
-        self.path_mappings = path_mappings or {}
-        self.log_requests = log_requests
-        self.request_log = []
-
-        class RequestHandlerInstance(RequestHandler):
-            docroot = self.docroot
-            urlhandlers = self.urlhandlers
-            path_mappings = self.path_mappings
-            proxy_host_dirs = self.proxy_host_dirs
-            request_log = self.request_log
-            log_requests = self.log_requests
-
-        self.handler_class = RequestHandlerInstance
-
-    def start(self, block=False):
-        """
-        Starts the server.
-
-        If `block` is True, the call will not return. If `block` is False, the
-        server will be started on a separate thread that can be terminated by
-        a call to stop().
-        """
-        self.httpd = EasyServer((self.host, self.port), self.handler_class)
-        if block:
-            self.httpd.serve_forever()
-        else:
-            self.server = threading.Thread(target=self.httpd.serve_forever)
-            self.server.setDaemon(True) # don't hang on exit
-            self.server.start()
-
-    def stop(self):
-        """
-        Stops the server.
-
-        If the server is not running, this method has no effect.
-        """
-        if self.httpd:
-            ### FIXME: There is no shutdown() method in Python 2.4...
-            try:
-                self.httpd.shutdown()
-            except AttributeError:
-                pass
-        self.httpd = None
-
-    def get_url(self, path="/"):
-        """
-        Returns a URL that can be used for accessing the server (e.g. http://192.168.1.3:4321/)
-
-        :param path: Path to append to URL (e.g. if path were /foobar.html you would get a URL like
-                     http://192.168.1.3:4321/foobar.html). Default is `/`.
-        """
-        if not self.httpd:
-            return None
-
-        return "http://%s:%s%s" % (self.host, self.httpd.server_port, path)
-
-    __del__ = stop
-
-
-def main(args=sys.argv[1:]):
-
-    # parse command line options
-    from optparse import OptionParser
-    parser = OptionParser()
-    parser.add_option('-p', '--port', dest='port',
-                      type="int", default=8888,
-                      help="port to run the server on [DEFAULT: %default]")
-    parser.add_option('-H', '--host', dest='host',
-                      default='127.0.0.1',
-                      help="host [DEFAULT: %default]")
-    parser.add_option('-i', '--external-ip', action="store_true",
-                      dest='external_ip', default=False,
-                      help="find and use external ip for host")
-    parser.add_option('-d', '--docroot', dest='docroot',
-                      default=os.getcwd(),
-                      help="directory to serve files from [DEFAULT: %default]")
-    options, args = parser.parse_args(args)
-    if args:
-        parser.error("mozhttpd does not take any arguments")
-
-    if options.external_ip:
-        host = moznetwork.get_lan_ip()
-    else:
-        host = options.host
-
-    # create the server
-    server = MozHttpd(host=host, port=options.port, docroot=options.docroot)
-
-    print "Serving '%s' at %s:%s" % (server.docroot, server.host, server.port)
-    server.start(block=True)
-
-if __name__ == '__main__':
-    main()
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/setup.py
+++ /dev/null
@@ -1,30 +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/.
-
-from setuptools import setup
-
-PACKAGE_VERSION = '0.7'
-deps = ['moznetwork >= 0.24']
-
-setup(name='mozhttpd',
-      version=PACKAGE_VERSION,
-      description="Python webserver intended for use with Mozilla testing",
-      long_description="see http://mozbase.readthedocs.org/",
-      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
-      keywords='mozilla',
-      author='Mozilla Automation and Testing Team',
-      author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
-      license='MPL',
-      packages=['mozhttpd'],
-      include_package_data=True,
-      zip_safe=False,
-      install_requires=deps,
-      entry_points="""
-      # -*- Entry points: -*-
-      [console_scripts]
-      mozhttpd = mozhttpd:main
-      """,
-      )
-
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/tests/api.py
+++ /dev/null
@@ -1,262 +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/.
-
-import mozfile
-import mozhttpd
-import urllib2
-import os
-import unittest
-import json
-import tempfile
-
-here = os.path.dirname(os.path.abspath(__file__))
-
-class ApiTest(unittest.TestCase):
-    resource_get_called = 0
-    resource_post_called = 0
-    resource_del_called = 0
-
-    @mozhttpd.handlers.json_response
-    def resource_get(self, request, objid):
-        self.resource_get_called += 1
-        return (200, { 'called': self.resource_get_called,
-                       'id': objid,
-                       'query': request.query })
-
-    @mozhttpd.handlers.json_response
-    def resource_post(self, request):
-        self.resource_post_called += 1
-        return (201, { 'called': self.resource_post_called,
-                       'data': json.loads(request.body),
-                       'query': request.query })
-
-    @mozhttpd.handlers.json_response
-    def resource_del(self, request, objid):
-        self.resource_del_called += 1
-        return (200, { 'called': self.resource_del_called,
-                       'id': objid,
-                       'query': request.query })
-
-    def get_url(self, path, server_port, querystr):
-        url = "http://127.0.0.1:%s%s" % (server_port, path)
-        if querystr:
-            url += "?%s" % querystr
-        return url
-
-    def try_get(self, server_port, querystr):
-        self.resource_get_called = 0
-
-        f = urllib2.urlopen(self.get_url('/api/resource/1', server_port, querystr))
-        try:
-            self.assertEqual(f.getcode(), 200)
-        except AttributeError:
-            pass  # python 2.4
-        self.assertEqual(json.loads(f.read()), { 'called': 1, 'id': str(1), 'query': querystr })
-        self.assertEqual(self.resource_get_called, 1)
-
-    def try_post(self, server_port, querystr):
-        self.resource_post_called = 0
-
-        postdata = { 'hamburgers': '1234' }
-        try:
-            f = urllib2.urlopen(self.get_url('/api/resource/', server_port, querystr),
-                                data=json.dumps(postdata))
-        except urllib2.HTTPError, e:
-            # python 2.4
-            self.assertEqual(e.code, 201)
-            body = e.fp.read()
-        else:
-            self.assertEqual(f.getcode(), 201)
-            body = f.read()
-        self.assertEqual(json.loads(body), { 'called': 1,
-                                             'data': postdata,
-                                             'query': querystr })
-        self.assertEqual(self.resource_post_called, 1)
-
-    def try_del(self, server_port, querystr):
-        self.resource_del_called = 0
-
-        opener = urllib2.build_opener(urllib2.HTTPHandler)
-        request = urllib2.Request(self.get_url('/api/resource/1', server_port, querystr))
-        request.get_method = lambda: 'DEL'
-        f = opener.open(request)
-
-        try:
-            self.assertEqual(f.getcode(), 200)
-        except AttributeError:
-            pass  # python 2.4
-        self.assertEqual(json.loads(f.read()), { 'called': 1, 'id': str(1), 'query': querystr })
-        self.assertEqual(self.resource_del_called, 1)
-
-    def test_api(self):
-        httpd = mozhttpd.MozHttpd(port=0,
-                                  urlhandlers = [ { 'method': 'GET',
-                                                    'path': '/api/resource/([^/]+)/?',
-                                                    'function': self.resource_get },
-                                                  { 'method': 'POST',
-                                                    'path': '/api/resource/?',
-                                                    'function': self.resource_post },
-                                                  { 'method': 'DEL',
-                                                    'path': '/api/resource/([^/]+)/?',
-                                                    'function': self.resource_del }
-                                                  ])
-        httpd.start(block=False)
-
-        server_port = httpd.httpd.server_port
-
-        # GET
-        self.try_get(server_port, '')
-        self.try_get(server_port, '?foo=bar')
-
-        # POST
-        self.try_post(server_port, '')
-        self.try_post(server_port, '?foo=bar')
-
-        # DEL
-        self.try_del(server_port, '')
-        self.try_del(server_port, '?foo=bar')
-
-        # GET: By default we don't serve any files if we just define an API
-        exception_thrown = False
-        try:
-            urllib2.urlopen(self.get_url('/', server_port, None))
-        except urllib2.HTTPError, e:
-            self.assertEqual(e.code, 404)
-            exception_thrown = True
-        self.assertTrue(exception_thrown)
-
-    def test_nonexistent_resources(self):
-        # Create a server with a placeholder handler so we don't fall back
-        # to serving local files
-        httpd = mozhttpd.MozHttpd(port=0)
-        httpd.start(block=False)
-        server_port = httpd.httpd.server_port
-
-        # GET: Return 404 for non-existent endpoint
-        exception_thrown = False
-        try:
-            urllib2.urlopen(self.get_url('/api/resource/', server_port, None))
-        except urllib2.HTTPError, e:
-            self.assertEqual(e.code, 404)
-            exception_thrown = True
-        self.assertTrue(exception_thrown)
-
-        # POST: POST should also return 404
-        exception_thrown = False
-        try:
-            urllib2.urlopen(self.get_url('/api/resource/', server_port, None),
-                            data=json.dumps({}))
-        except urllib2.HTTPError, e:
-            self.assertEqual(e.code, 404)
-            exception_thrown = True
-        self.assertTrue(exception_thrown)
-
-        # DEL: DEL should also return 404
-        exception_thrown = False
-        try:
-            opener = urllib2.build_opener(urllib2.HTTPHandler)
-            request = urllib2.Request(self.get_url('/api/resource/', server_port,
-                                                   None))
-            request.get_method = lambda: 'DEL'
-            opener.open(request)
-        except urllib2.HTTPError:
-            self.assertEqual(e.code, 404)
-            exception_thrown = True
-        self.assertTrue(exception_thrown)
-
-    def test_api_with_docroot(self):
-        httpd = mozhttpd.MozHttpd(port=0, docroot=here,
-                                  urlhandlers = [ { 'method': 'GET',
-                                                    'path': '/api/resource/([^/]+)/?',
-                                                    'function': self.resource_get } ])
-        httpd.start(block=False)
-        server_port = httpd.httpd.server_port
-
-        # We defined a docroot, so we expect a directory listing
-        f = urllib2.urlopen(self.get_url('/', server_port, None))
-        try:
-            self.assertEqual(f.getcode(), 200)
-        except AttributeError:
-            pass  # python 2.4
-        self.assertTrue('Directory listing for' in f.read())
-
-        # Make sure API methods still work
-        self.try_get(server_port, '')
-        self.try_get(server_port, '?foo=bar')
-
-class ProxyTest(unittest.TestCase):
-
-    def tearDown(self):
-        # reset proxy opener in case it changed
-        urllib2.install_opener(None)
-
-    def test_proxy(self):
-        docroot = tempfile.mkdtemp()
-        self.addCleanup(mozfile.remove, docroot)
-        hosts = ('mozilla.com', 'mozilla.org')
-        unproxied_host = 'notmozilla.org'
-        def url(host): return 'http://%s/' % host
-
-        index_filename = 'index.html'
-        def index_contents(host): return '%s index' % host
-
-        index = file(os.path.join(docroot, index_filename), 'w')
-        index.write(index_contents('*'))
-        index.close()
-
-        httpd = mozhttpd.MozHttpd(port=0, docroot=docroot)
-        httpd.start(block=False)
-        server_port = httpd.httpd.server_port
-
-        proxy_support = urllib2.ProxyHandler({'http': 'http://127.0.0.1:%d' %
-                                              server_port})
-        urllib2.install_opener(urllib2.build_opener(proxy_support))
-
-        for host in hosts:
-            f = urllib2.urlopen(url(host))
-            try:
-                self.assertEqual(f.getcode(), 200)
-            except AttributeError:
-                pass  # python 2.4
-            self.assertEqual(f.read(), index_contents('*'))
-
-        httpd.stop()
-
-        # test separate directories per host
-
-        httpd = mozhttpd.MozHttpd(port=0, docroot=docroot, proxy_host_dirs=True)
-        httpd.start(block=False)
-        server_port = httpd.httpd.server_port
-
-        proxy_support = urllib2.ProxyHandler({'http': 'http://127.0.0.1:%d' %
-                                              server_port})
-        urllib2.install_opener(urllib2.build_opener(proxy_support))
-
-        # set up dirs
-        for host in hosts:
-            os.mkdir(os.path.join(docroot, host))
-            file(os.path.join(docroot, host, index_filename), 'w') \
-                .write(index_contents(host))
-
-        for host in hosts:
-            f = urllib2.urlopen(url(host))
-            try:
-                self.assertEqual(f.getcode(), 200)
-            except AttributeError:
-                pass  # python 2.4
-            self.assertEqual(f.read(), index_contents(host))
-
-        exc = None
-        try:
-            urllib2.urlopen(url(unproxied_host))
-        except urllib2.HTTPError, e:
-            exc = e
-        self.assertNotEqual(exc, None)
-        self.assertEqual(exc.code, 404)
-
-
-if __name__ == '__main__':
-    unittest.main()
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/tests/baseurl.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import mozhttpd
-import unittest
-
-class BaseUrlTest(unittest.TestCase):
-
-    def test_base_url(self):
-        httpd = mozhttpd.MozHttpd(port=0)
-        self.assertEqual(httpd.get_url(), None)
-        httpd.start(block=False)
-        self.assertEqual("http://127.0.0.1:%s/" % httpd.httpd.server_port,
-                         httpd.get_url())
-        self.assertEqual("http://127.0.0.1:%s/cheezburgers.html" % \
-                             httpd.httpd.server_port,
-                         httpd.get_url(path="/cheezburgers.html"))
-        httpd.stop()
-
-if __name__ == '__main__':
-    unittest.main()
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/tests/basic.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env python
-
-import mozhttpd
-import mozfile
-import os
-import tempfile
-import unittest
-
-
-class TestBasic(unittest.TestCase):
-    """ Test basic Mozhttpd capabilites """
-
-    def test_basic(self):
-        """ Test mozhttpd can serve files """
-
-        tempdir = tempfile.mkdtemp()
-
-        # sizes is a dict of the form: name -> [size, binary_string, filepath]
-        sizes = {'small': [128], 'large': [16384]}
-
-        for k in sizes.keys():
-            # Generate random binary string
-            sizes[k].append(os.urandom(sizes[k][0]))
-
-            # Add path of file with binary string to list
-            fpath = os.path.join(tempdir, k)
-            sizes[k].append(fpath)
-
-            # Write binary string to file
-            with open(fpath, 'wb') as f:
-                f.write(sizes[k][1])
-
-        server = mozhttpd.MozHttpd(docroot=tempdir)
-        server.start()
-        server_url = server.get_url()
-
-        # Retrieve file and check contents matchup
-        for k in sizes.keys():
-            retrieved_content = mozfile.load(server_url + k).read()
-            self.assertEqual(retrieved_content, sizes[k][1])
-
-        # Cleanup tempdir and related files
-        mozfile.rmtree(tempdir)
-
-if __name__ == '__main__':
-    unittest.main()
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/tests/filelisting.py
+++ /dev/null
@@ -1,41 +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/.
-
-import mozhttpd
-import urllib2
-import os
-import unittest
-import re
-
-here = os.path.dirname(os.path.abspath(__file__))
-
-class FileListingTest(unittest.TestCase):
-
-    def check_filelisting(self, path=''):
-        filelist = os.listdir(here)
-
-        httpd = mozhttpd.MozHttpd(port=0, docroot=here)
-        httpd.start(block=False)
-        f = urllib2.urlopen("http://%s:%s/%s" % ('127.0.0.1', httpd.httpd.server_port, path))
-        for line in f.readlines():
-            webline = re.sub('\<[a-zA-Z0-9\-\_\.\=\"\'\/\\\%\!\@\#\$\^\&\*\(\) ]*\>', '', line.strip('\n')).strip('/').strip().strip('@')
-
-            if webline and not webline.startswith("Directory listing for"):
-                self.assertTrue(webline in filelist,
-                                "File %s in dir listing corresponds to a file" % webline)
-                filelist.remove(webline)
-        self.assertFalse(filelist, "Should have no items in filelist (%s) unaccounted for" % filelist)
-
-
-    def test_filelist(self):
-        self.check_filelisting()
-
-    def test_filelist_params(self):
-        self.check_filelisting('?foo=bar&fleem=&foo=fleem')
-
-
-if __name__ == '__main__':
-    unittest.main()
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/tests/manifest.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[api.py]
-[baseurl.py]
-[basic.py]
-[filelisting.py]
-[paths.py]
-[requestlog.py]
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/tests/paths.py
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/env python
-
-# Any copyright is dedicated to the Public Domain.
-# http://creativecommons.org/publicdomain/zero/1.0/
-
-from mozfile import TemporaryDirectory
-import mozhttpd
-import os
-import unittest
-import urllib2
-
-class PathTest(unittest.TestCase):
-    def try_get(self, url, expected_contents):
-        f = urllib2.urlopen(url)
-        self.assertEqual(f.getcode(), 200)
-        self.assertEqual(f.read(), expected_contents)
-
-    def try_get_expect_404(self, url):
-        with self.assertRaises(urllib2.HTTPError) as cm:
-            urllib2.urlopen(url)
-        self.assertEqual(404, cm.exception.code)
-
-    def test_basic(self):
-        """Test that requests to docroot and a path mapping work as expected."""
-        with TemporaryDirectory() as d1, TemporaryDirectory() as d2:
-            open(os.path.join(d1, "test1.txt"), "w").write("test 1 contents")
-            open(os.path.join(d2, "test2.txt"), "w").write("test 2 contents")
-            httpd = mozhttpd.MozHttpd(port=0,
-                                      docroot=d1,
-                                      path_mappings={'/files': d2}
-                                      )
-            httpd.start(block=False)
-            self.try_get(httpd.get_url("/test1.txt"), "test 1 contents")
-            self.try_get(httpd.get_url("/files/test2.txt"), "test 2 contents")
-            self.try_get_expect_404(httpd.get_url("/files/test2_nope.txt"))
-            httpd.stop()
-
-    def test_substring_mappings(self):
-        """Test that a path mapping that's a substring of another works."""
-        with TemporaryDirectory() as d1, TemporaryDirectory() as d2:
-            open(os.path.join(d1, "test1.txt"), "w").write("test 1 contents")
-            open(os.path.join(d2, "test2.txt"), "w").write("test 2 contents")
-            httpd = mozhttpd.MozHttpd(port=0,
-                                      path_mappings={'/abcxyz': d1,
-                                                     '/abc': d2,}
-                                      )
-            httpd.start(block=False)
-            self.try_get(httpd.get_url("/abcxyz/test1.txt"), "test 1 contents")
-            self.try_get(httpd.get_url("/abc/test2.txt"), "test 2 contents")
-            httpd.stop()
-
-    def test_multipart_path_mapping(self):
-        """Test that a path mapping with multiple directories works."""
-        with TemporaryDirectory() as d1:
-            open(os.path.join(d1, "test1.txt"), "w").write("test 1 contents")
-            httpd = mozhttpd.MozHttpd(port=0,
-                                      path_mappings={'/abc/def/ghi': d1}
-                                      )
-            httpd.start(block=False)
-            self.try_get(httpd.get_url("/abc/def/ghi/test1.txt"), "test 1 contents")
-            self.try_get_expect_404(httpd.get_url("/abc/test1.txt"))
-            self.try_get_expect_404(httpd.get_url("/abc/def/test1.txt"))
-            httpd.stop()
-
-    def test_no_docroot(self):
-        """Test that path mappings with no docroot work."""
-        with TemporaryDirectory() as d1:
-            httpd = mozhttpd.MozHttpd(port=0,
-                                      path_mappings={'/foo': d1})
-            httpd.start(block=False)
-            self.try_get_expect_404(httpd.get_url())
-            httpd.stop()
-
-if __name__ == '__main__':
-    unittest.main()
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/tests/requestlog.py
+++ /dev/null
@@ -1,40 +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 mozhttpd
-import urllib2
-import os
-import unittest
-
-here = os.path.dirname(os.path.abspath(__file__))
-
-class RequestLogTest(unittest.TestCase):
-
-    def check_logging(self, log_requests=False):
-
-        httpd = mozhttpd.MozHttpd(port=0, docroot=here, log_requests=log_requests)
-        httpd.start(block=False)
-        url = "http://%s:%s/" % ('127.0.0.1', httpd.httpd.server_port)
-        f = urllib2.urlopen(url)
-        f.read()
-
-        return httpd.request_log
-
-    def test_logging_enabled(self):
-        request_log = self.check_logging(log_requests=True)
-
-        self.assertEqual(len(request_log), 1)
-
-        log_entry = request_log[0]
-        self.assertEqual(log_entry['method'], 'GET')
-        self.assertEqual(log_entry['path'], '/')
-        self.assertEqual(type(log_entry['time']), float)
-
-    def test_logging_disabled(self):
-        request_log = self.check_logging(log_requests=False)
-
-        self.assertEqual(len(request_log), 0)
-
-if __name__ == '__main__':
-    unittest.main()
--- a/testing/mozbase/mozprofile/setup.py
+++ b/testing/mozbase/mozprofile/setup.py
@@ -30,17 +30,17 @@ setup(name=PACKAGE_NAME,
       author_email='tools@lists.mozilla.org',
       url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL 2.0',
       packages=['mozprofile'],
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       extras_require={'manifest': ['manifestparser >= 0.6']},
-      tests_require=['mozhttpd'],
+      tests_require=['wptserve'],
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       mozprofile = mozprofile:cli
       view-profile = mozprofile:view_profile
       diff-profiles = mozprofile:diff_profiles
       """,
     )
--- a/testing/mozbase/mozprofile/tests/test_addons.py
+++ b/testing/mozbase/mozprofile/tests/test_addons.py
@@ -7,17 +7,17 @@
 import os
 import shutil
 import tempfile
 import unittest
 import urllib2
 
 from manifestparser import ManifestParser
 import mozfile
-import mozhttpd
+from wptserve.server import WebTestHttpd
 import mozlog.unstructured as mozlog
 import mozprofile
 
 from addon_stubs import generate_addon, generate_manifest
 
 
 here = os.path.dirname(os.path.abspath(__file__))
 
@@ -58,17 +58,17 @@ class TestAddonsManager(unittest.TestCas
         # specified via XPI and folder
         self.am.install_addons([addon_folder, addon_xpi])
         self.assertEqual(len(self.am.installed_addons), 2)
         self.assertIn(addon_folder, self.am.installed_addons)
         self.assertIn(addon_xpi, self.am.installed_addons)
         self.am.clean()
 
     def test_download(self):
-        server = mozhttpd.MozHttpd(docroot=os.path.join(here, 'addons'))
+        server = WebTestHttpd(doc_root=os.path.join(here, 'addons'))
         server.start()
 
         # Download a valid add-on without a class instance to the general
         # tmp folder and clean-up
         try:
             addon = server.get_url() + 'empty.xpi'
             xpi_file = mozprofile.addons.AddonManager.download(addon)
             self.assertTrue(os.path.isfile(xpi_file))
@@ -168,17 +168,17 @@ class TestAddonsManager(unittest.TestCas
         self.am.clean()
 
         # Test forcing unpack an add-on
         self.am.install_from_path(addon_no_unpack, unpack=True)
         self.assertEqual(self.am.installed_addons, [addon_no_unpack])
         self.am.clean()
 
     def test_install_from_path_url(self):
-        server = mozhttpd.MozHttpd(docroot=os.path.join(here, 'addons'))
+        server = WebTestHttpd(doc_root=os.path.join(here, 'addons'))
         server.start()
 
         addon = server.get_url() + 'empty.xpi'
         self.am.install_from_path(addon)
 
         server.stop()
 
         self.assertEqual(len(self.am.downloaded_addons), 1)
@@ -340,17 +340,17 @@ class TestAddonsManager(unittest.TestCas
         addons_after_cleanup = [unicode(x[:-len('.xpi')]) for x in os.listdir(os.path.join(
                                 duplicate_profile.profile, 'extensions', 'staged'))]
         # New addons installed should be removed by clean_addons()
         self.assertEqual(installed_addons, addons_after_cleanup)
 
     def test_noclean(self):
         """test `restore=True/False` functionality"""
 
-        server = mozhttpd.MozHttpd(docroot=os.path.join(here, 'addons'))
+        server = WebTestHttpd(doc_root=os.path.join(here, 'addons'))
         server.start()
 
         profile = tempfile.mkdtemp()
         tmpdir = tempfile.mkdtemp()
 
         try:
             # empty initially
             self.assertFalse(bool(os.listdir(profile)))
--- a/testing/mozbase/mozprofile/tests/test_preferences.py
+++ b/testing/mozbase/mozprofile/tests/test_preferences.py
@@ -1,16 +1,16 @@
 #!/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/.
 
 import mozfile
-import mozhttpd
+from wptserve.server import WebTestHttpd
 import os
 import shutil
 import tempfile
 import unittest
 from mozprofile.cli import MozProfileCLI
 from mozprofile.prefs import Preferences
 from mozprofile.profile import Profile
 
@@ -347,31 +347,30 @@ user_pref("webgl.force-enabled", true);
             "server": "server-name",
             "abc": "something"
             }
         path = os.path.join(here, 'files', 'prefs_with_interpolation.js')
         read_prefs = Preferences.read_prefs(path, interpolation=values)
         self.assertEqual(dict(read_prefs), expected_prefs)
 
     def test_read_prefs_ttw(self):
-        """test reading preferences through the web via mozhttpd"""
+        """test reading preferences through the web via wptserve"""
 
-        # create a MozHttpd instance
-        docroot = os.path.join(here, 'files')
-        host = '127.0.0.1'
-        port = 8888
-        httpd = mozhttpd.MozHttpd(host=host, port=port, docroot=docroot)
+        # create a server instance
+        httpd = WebTestHttpd(host='127.0.0.1', port=0,
+                             doc_root=os.path.join(here, 'files'))
 
         # create a preferences instance
         prefs = Preferences()
 
         try:
             # start server
             httpd.start(block=False)
 
             # read preferences through the web
-            read = prefs.read_prefs('http://%s:%d/prefs_with_comments.js' % (host, port))
+            read = prefs.read_prefs('http://%s:%d/prefs_with_comments.js' %
+                                    httpd.httpd.server_address)
             self.assertEqual(dict(read), self._prefs_with_comments)
         finally:
             httpd.stop()
 
 if __name__ == '__main__':
     unittest.main()
--- a/testing/mozbase/packages.txt
+++ b/testing/mozbase/packages.txt
@@ -1,15 +1,14 @@
 manifestparser.pth:testing/mozbase/manifestparser
 mozb2g.pth:testing/mozbase/mozb2g
 mozcrash.pth:testing/mozbase/mozcrash
 mozdebug.pth:testing/mozbase/mozdebug
 mozdevice.pth:testing/mozbase/mozdevice
 mozfile.pth:testing/mozbase/mozfile
-mozhttpd.pth:testing/mozbase/mozhttpd
 mozinfo.pth:testing/mozbase/mozinfo
 mozinstall.pth:testing/mozbase/mozinstall
 mozleak.pth:testing/mozbase/mozleak
 mozlog.pth:testing/mozbase/mozlog
 moznetwork.pth:testing/mozbase/moznetwork
 mozprocess.pth:testing/mozbase/mozprocess
 mozprofile.pth:testing/mozbase/mozprofile
 mozrunner.pth:testing/mozbase/mozrunner
--- a/testing/mozbase/test-manifest.ini
+++ b/testing/mozbase/test-manifest.ini
@@ -7,17 +7,16 @@
 
 # run with
 # https://github.com/mozilla/mozbase/blob/master/test.py
 
 [include:manifestparser/tests/manifest.ini]
 [include:mozcrash/tests/manifest.ini]
 [include:mozdevice/tests/manifest.ini]
 [include:mozfile/tests/manifest.ini]
-[include:mozhttpd/tests/manifest.ini]
 [include:mozinfo/tests/manifest.ini]
 [include:mozinstall/tests/manifest.ini]
 [include:mozlog/tests/manifest.ini]
 [include:moznetwork/tests/manifest.ini]
 [include:mozprocess/tests/manifest.ini]
 [include:mozprofile/tests/manifest.ini]
 [include:mozrunner/tests/manifest.ini]
 [include:moztest/tests/manifest.ini]
--- a/testing/mozharness/mozharness/mozilla/mozbase.py
+++ b/testing/mozharness/mozharness/mozilla/mozbase.py
@@ -26,14 +26,14 @@ class MozbaseMixin(object):
         # XXX Bug 908356: This block can be removed as soon as the
         # in-tree requirements files propagate to all active trees.
         mozbase_dir = os.path.join('tests', 'mozbase')
         self.register_virtualenv_module(
             'manifestparser',
             url=os.path.join(mozbase_dir, 'manifestdestiny')
         )
 
-        for m in ('mozfile', 'mozlog', 'mozinfo', 'moznetwork', 'mozhttpd',
+        for m in ('mozfile', 'mozlog', 'mozinfo', 'moznetwork',
                   'mozcrash', 'mozinstall', 'mozdevice', 'mozprofile',
                   'mozprocess', 'mozrunner'):
             self.register_virtualenv_module(
                 m, url=os.path.join(mozbase_dir, m)
             )
--- a/testing/mozharness/scripts/marionette.py
+++ b/testing/mozharness/scripts/marionette.py
@@ -238,17 +238,17 @@ class MarionetteTest(TestingMixin, Mercu
         else:
             # XXX Bug 879765: Dependent modules need to be listed before parent
             # modules, otherwise they will get installed from the pypi server.
             # XXX Bug 908356: This block can be removed as soon as the
             # in-tree requirements files propagate to all active trees.
             mozbase_dir = os.path.join('tests', 'mozbase')
             self.register_virtualenv_module(
                 'manifestparser', os.path.join(mozbase_dir, 'manifestdestiny'))
-            for m in ('mozfile', 'mozlog', 'mozinfo', 'moznetwork', 'mozhttpd',
+            for m in ('mozfile', 'mozlog', 'mozinfo', 'moznetwork',
                       'mozcrash', 'mozinstall', 'mozdevice', 'mozprofile',
                       'mozprocess', 'mozrunner'):
                 self.register_virtualenv_module(
                     m, os.path.join(mozbase_dir, m))
 
             self.register_virtualenv_module(
                 'marionette', os.path.join('tests', 'marionette'))
 
--- a/testing/mozharness/test/pip-freeze.example.txt
+++ b/testing/mozharness/test/pip-freeze.example.txt
@@ -4,16 +4,15 @@ Tempita==0.5.1
 WebOb==1.2b3
 -e hg+http://k0s.org/mozilla/hg/configuration@35416ad140982c11eba0a2d6b96d683f53429e94#egg=configuration-dev
 coverage==3.5.1
 -e hg+http://k0s.org/mozilla/hg/jetperf@4645ae34d2c41a353dcdbd856b486b6d3faabb99#egg=jetperf-dev
 logilab-astng==0.23.1
 logilab-common==0.57.1
 mozdevice==0.2
 -e hg+https://hg.mozilla.org/build/mozharness@df6b7f1e14d8c472125ef7a77b8a3b40c96ae181#egg=mozharness-jetperf
-mozhttpd==0.3
 mozinfo==0.3.3
 nose==1.1.2
 pyflakes==0.5.0
 pylint==0.25.1
 -e hg+https://hg.mozilla.org/build/talos@ee5c0b090d808e81a8fc5ba5f96b012797b3e785#egg=talos-dev
 virtualenv==1.7.1.2
 wsgiref==0.1.2
--- a/testing/mozharness/test/test_base_python.py
+++ b/testing/mozharness/test/test_base_python.py
@@ -17,17 +17,16 @@ class TestVirtualenvMixin(unittest.TestC
         expected = {'MakeItSo': '0.2.6',
                     'PyYAML': '3.10',
                     'Tempita': '0.5.1',
                     'WebOb': '1.2b3',
                     'coverage': '3.5.1',
                     'logilab-astng': '0.23.1',
                     'logilab-common': '0.57.1',
                     'mozdevice': '0.2',
-                    'mozhttpd': '0.3',
                     'mozinfo': '0.3.3',
                     'nose': '1.1.2',
                     'pyflakes': '0.5.0',
                     'pylint': '0.25.1',
                     'virtualenv': '1.7.1.2',
                     'wsgiref': '0.1.2'}
 
         self.assertEqual(packages, expected)
--- a/testing/tools/mach_test_package_bootstrap.py
+++ b/testing/tools/mach_test_package_bootstrap.py
@@ -11,17 +11,16 @@ import time
 
 
 SEARCH_PATHS = [
     'mochitest',
     'mozbase/mozcrash',
     'mozbase/mozdebug',
     'mozbase/mozdevice',
     'mozbase/mozfile',
-    'mozbase/mozhttpd',
     'mozbase/mozleak',
     'mozbase/mozlog',
     'mozbase/moznetwork',
     'mozbase/mozprocess',
     'mozbase/mozprofile',
     'mozbase/mozrunner',
     'mozbase/mozsystemmonitor',
     'mozbase/mozinfo',
--- a/testing/tps/setup.py
+++ b/testing/tps/setup.py
@@ -4,17 +4,17 @@
 
 from setuptools import setup, find_packages
 import sys
 
 version = '0.5'
 
 deps = ['httplib2 == 0.7.3',
         'mozfile == 1.1',
-        'mozhttpd == 0.7',
+        'wptserve == 1.3.0',
         'mozinfo == 0.7',
         'mozinstall == 1.10',
         'mozprocess == 0.19',
         'mozprofile == 0.27',
         'mozrunner == 6.0',
         'mozversion == 1.4',
        ]
 
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -6,17 +6,17 @@ import json
 import os
 import platform
 import random
 import re
 import tempfile
 import time
 import traceback
 
-from mozhttpd import MozHttpd
+from wptserve.server import WebTestHttpd
 import mozinfo
 from mozprofile import Profile
 import mozversion
 
 from .firefoxrunner import TPSFirefoxRunner
 from .phase import TPSTestPhase
 
 
@@ -430,18 +430,18 @@ class TPSTestRunner(object):
             jsondata = f.read()
             f.close()
             testfiles = json.loads(jsondata)
             testlist = testfiles['tests']
         except ValueError:
             testlist = [os.path.basename(self.testfile)]
         testdir = os.path.dirname(self.testfile)
 
-        self.mozhttpd = MozHttpd(port=4567, docroot=testdir)
-        self.mozhttpd.start()
+        self.httpd = WebTestHttpd(port=4567, doc_root=testdir)
+        self.httpd.start()
 
         # run each test, and save the results
         for test in testlist:
             result = self.run_single_test(testdir, test)
 
             if not self.productversion:
                 self.productversion = result['productversion']
             if not self.addonversion:
@@ -454,17 +454,17 @@ class TPSTestRunner(object):
             if result['state'] == 'TEST-PASS':
                 self.numpassed += 1
             else:
                 self.numfailed += 1
                 if self.stop_on_error:
                     print '\nTest failed with --stop-on-error specified; not running any more tests.\n'
                     break
 
-        self.mozhttpd.stop()
+        self.httpd.stop()
 
         # generate the postdata we'll use to post the results to the db
         self.postdata = { 'tests': self.results,
                           'os': '%s %sbit' % (mozinfo.version, mozinfo.bits),
                           'testtype': 'crossweave',
                           'productversion': self.productversion,
                           'addonversion': self.addonversion,
                           'synctype': self.synctype,
--- a/tools/docs/mach_commands.py
+++ b/tools/docs/mach_commands.py
@@ -8,17 +8,17 @@ import os
 import sys
 
 from mach.decorators import (
     Command,
     CommandArgument,
     CommandProvider,
 )
 
-import mozhttpd
+from wptserve.server import WebTestHttpd
 
 from mozbuild.base import MachCommandBase
 
 
 @CommandProvider
 class Documentation(MachCommandBase):
     """Helps manage in-tree documentation."""
 
@@ -85,17 +85,17 @@ class Documentation(MachCommandBase):
             return die('failed to generate documentation:\n%s' % '\n'.join(failed))
 
         if http is not None:
             host, port = http.split(':', 1)
             addr = (host, int(port))
             if len(addr) != 2:
                 return die('invalid address: %s' % http)
 
-            httpd = mozhttpd.MozHttpd(host=addr[0], port=addr[1], docroot=outdir)
+            httpd = WebTestHttpd(host=addr[0], port=addr[1], doc_root=outdir)
             print('listening on %s:%d' % addr)
             httpd.start(block=True)
 
     def _find_project_name(self, path):
         import imp
         path = os.path.join(path, 'conf.py')
         with open(path, 'r') as fh:
             conf = imp.load_module('doc_conf', fh, path,