summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozhttpd
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozhttpd')
-rw-r--r--testing/mozbase/mozhttpd/mozhttpd/__init__.py48
-rw-r--r--testing/mozbase/mozhttpd/mozhttpd/handlers.py16
-rwxr-xr-xtesting/mozbase/mozhttpd/mozhttpd/mozhttpd.py330
-rw-r--r--testing/mozbase/mozhttpd/setup.py29
-rw-r--r--testing/mozbase/mozhttpd/tests/api.py266
-rw-r--r--testing/mozbase/mozhttpd/tests/baseurl.py19
-rw-r--r--testing/mozbase/mozhttpd/tests/basic.py46
-rw-r--r--testing/mozbase/mozhttpd/tests/filelisting.py43
-rw-r--r--testing/mozbase/mozhttpd/tests/manifest.ini6
-rw-r--r--testing/mozbase/mozhttpd/tests/paths.py77
-rw-r--r--testing/mozbase/mozhttpd/tests/requestlog.py41
11 files changed, 921 insertions, 0 deletions
diff --git a/testing/mozbase/mozhttpd/mozhttpd/__init__.py b/testing/mozbase/mozhttpd/mozhttpd/__init__.py
new file mode 100644
index 000000000..c15b0d028
--- /dev/null
+++ b/testing/mozbase/mozhttpd/mozhttpd/__init__.py
@@ -0,0 +1,48 @@
+# 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
+
+__all__ = ['MozHttpd', 'Request', 'RequestHandler', 'main', 'json_response']
diff --git a/testing/mozbase/mozhttpd/mozhttpd/handlers.py b/testing/mozbase/mozhttpd/mozhttpd/handlers.py
new file mode 100644
index 000000000..1b0a86a40
--- /dev/null
+++ b/testing/mozbase/mozhttpd/mozhttpd/handlers.py
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import 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
diff --git a/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
new file mode 100755
index 000000000..4ca0847d2
--- /dev/null
+++ b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
@@ -0,0 +1,330 @@
+#!/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()
diff --git a/testing/mozbase/mozhttpd/setup.py b/testing/mozbase/mozhttpd/setup.py
new file mode 100644
index 000000000..b7799dddd
--- /dev/null
+++ b/testing/mozbase/mozhttpd/setup.py
@@ -0,0 +1,29 @@
+# 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
+ """,
+ )
diff --git a/testing/mozbase/mozhttpd/tests/api.py b/testing/mozbase/mozhttpd/tests/api.py
new file mode 100644
index 000000000..b785ac5ef
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/api.py
@@ -0,0 +1,266 @@
+#!/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 as 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 as 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 as 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 as 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 as e:
+ exc = e
+ self.assertNotEqual(exc, None)
+ self.assertEqual(exc.code, 404)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozhttpd/tests/baseurl.py b/testing/mozbase/mozhttpd/tests/baseurl.py
new file mode 100644
index 000000000..0e971e6b2
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/baseurl.py
@@ -0,0 +1,19 @@
+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()
diff --git a/testing/mozbase/mozhttpd/tests/basic.py b/testing/mozbase/mozhttpd/tests/basic.py
new file mode 100644
index 000000000..8d64b4332
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/basic.py
@@ -0,0 +1,46 @@
+#!/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()
diff --git a/testing/mozbase/mozhttpd/tests/filelisting.py b/testing/mozbase/mozhttpd/tests/filelisting.py
new file mode 100644
index 000000000..6abea757f
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/filelisting.py
@@ -0,0 +1,43 @@
+#!/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()
diff --git a/testing/mozbase/mozhttpd/tests/manifest.ini b/testing/mozbase/mozhttpd/tests/manifest.ini
new file mode 100644
index 000000000..3f3d42d9b
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/manifest.ini
@@ -0,0 +1,6 @@
+[api.py]
+[baseurl.py]
+[basic.py]
+[filelisting.py]
+[paths.py]
+[requestlog.py]
diff --git a/testing/mozbase/mozhttpd/tests/paths.py b/testing/mozbase/mozhttpd/tests/paths.py
new file mode 100644
index 000000000..45ae40144
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/paths.py
@@ -0,0 +1,77 @@
+#!/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()
diff --git a/testing/mozbase/mozhttpd/tests/requestlog.py b/testing/mozbase/mozhttpd/tests/requestlog.py
new file mode 100644
index 000000000..bf2c59ec3
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/requestlog.py
@@ -0,0 +1,41 @@
+# 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()