--- a/testing/marionette/harness/MANIFEST.in
+++ b/testing/marionette/harness/MANIFEST.in
@@ -1,6 +1,8 @@
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,18 +69,22 @@ class JSTest:
class CommonTestCase(unittest.TestCase):
__metaclass__ = MetaParameterized
match_re = None
failureException = AssertionError
pydebugger = None
- def __init__(self, methodName, **kwargs):
+ def __init__(self, methodName, marionette_weakref, fixtures, **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):
@@ -220,17 +224,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,
- httpd, testvars):
+ fixtures, testvars, **kwargs):
"""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:
@@ -247,17 +251,16 @@ 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()
@@ -416,31 +419,27 @@ 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, httpd_weakref, methodName='runTest',
+ def __init__(self, marionette_weakref, fixtures, 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)
- self.marionette = None
-
- super(MarionetteTestCase, self).__init__(methodName, **kwargs)
+ super(MarionetteTestCase, self).__init__(
+ methodName, marionette_weakref=marionette_weakref, fixtures=fixtures, **kwargs)
@classmethod
def add_tests_to_suite(cls, mod_name, filepath, suite, testloader, marionette,
- httpd, testvars, **kwargs):
+ fixtures, 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
#
@@ -454,17 +453,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),
- weakref.ref(httpd),
+ fixtures,
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
@@ -6,34 +6,30 @@ import json
import os
import random
import re
import socket
import sys
import time
import traceback
import unittest
-import warnings
-
from argparse import ArgumentParser
from copy import deepcopy
import mozinfo
-import moznetwork
import mozprofile
+from marionette_driver.marionette import Marionette
+
import mozversion
-
+import serve
from manifestparser import TestManifest
from manifestparser.filters import tags
-from marionette_driver.marionette import Marionette
-from moztest.adapters.unit import StructuredTestRunner, StructuredTestResult
-from moztest.results import TestResultCollection, TestResult, relevant_line
-
-import httpd
-
+from moztest.adapters.unit import StructuredTestResult, StructuredTestRunner
+from moztest.results import TestResult, TestResultCollection, relevant_line
+from serve import iter_proc, iter_url
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()
@@ -484,16 +480,21 @@ 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,
@@ -501,34 +502,34 @@ 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._appinfo = None
self._appName = None
self._capabilities = None
self._filename_pattern = None
self._version_info = {}
+ 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.shuffle = shuffle
self.shuffle_seed = shuffle_seed
self.server_root = server_root
@@ -667,17 +668,16 @@ 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()
@property
def version_info(self):
@@ -770,40 +770,16 @@ 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 "
@@ -822,29 +798,43 @@ class BaseMarionetteTestRunner(object):
'SKIP',
message=test['disabled'])
self.todo += 1
def run_tests(self, tests):
start_time = time.time()
self._initialize_test_run(tests)
- need_external_ip = self._start_marionette()
- self._set_baseurl(need_external_ip)
+ 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("/")
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.')
+ self.logger.warning('Could not get device info', exc_info=True)
- self.logger.info("running with e10s: {}".format(self.e10s))
+ if self.e10s:
+ self.logger.info("e10s is enabled")
+ else:
+ self.logger.info("e10s is disabled")
self.logger.suite_start(self.tests,
version_info=self.version_info,
device_info=device_info)
self._log_skipped_tests()
interrupted = None
@@ -870,19 +860,18 @@ 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 seed where seed is:{}".format(self.shuffle_seed))
+ self.logger.info("Using shuffle seed: %d" % 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()
@@ -904,29 +893,19 @@ 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_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()
+ def start_fixture_servers(self):
root = self.server_root or os.path.join(os.path.dirname(here), "www")
- rv = httpd.FixtureServer(root, host=host)
- rv.start()
- return rv
+ return serve.start(root)
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'):
@@ -976,29 +955,28 @@ 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.httpd,
+ self.fixtures,
self.testvars,
**self.test_kwargs)
break
if suite.countTestCases():
runner = self.textrunnerclass(logger=self.logger,
marionette=self.marionette,
capabilities=self.capabilities,
@@ -1054,21 +1032,22 @@ 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):
- if hasattr(self, 'httpd') and self.httpd:
- self.httpd.stop()
- self.httpd = None
+ for proc in iter_proc(self.fixture_servers):
+ proc.stop()
+ proc.kill()
+ self.fixture_servers = {}
if hasattr(self, 'marionette') and self.marionette:
- if self.marionette.instance:
+ if self.marionette.instance is not None:
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,82 +1,142 @@
+#!/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
-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
+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")
@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__":
- 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
+ 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()
new file mode 100755
--- /dev/null
+++ b/testing/marionette/harness/marionette/runner/serve.py
@@ -0,0 +1,227 @@
+#!/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.
+
+"""
+
+import argparse
+import multiprocessing
+import os
+import sys
+from collections import defaultdict
+
+import moznetwork
+
+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://%s:0/" % moznetwork.get_ip())
+
+
+def https_server(doc_root, ssl_config, **kwargs):
+ return httpd.FixtureServer(doc_root,
+ url="https://%s:0/" % moznetwork.get_ip(),
+ 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:])
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/runner/test.cert
@@ -0,0 +1,86 @@
+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
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/runner/test.key
@@ -0,0 +1,28 @@
+-----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
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/tests/harness_unit/test_httpd.py
@@ -0,0 +1,89 @@
+# 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 ['_set_baseurl', 'run_test_set', '_capabilities']:
+ for attr in ['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.httpd is None
+ assert mock_runner.fixture_servers == {}
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
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/tests/harness_unit/test_serve.py
@@ -0,0 +1,67 @@
+# 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__]))
deleted file mode 100644
--- a/testing/marionette/harness/marionette/tests/unit/test_httpd.py
+++ /dev/null
@@ -1,32 +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 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
@@ -118,10 +118,9 @@ 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,14 +1,14 @@
-marionette-driver >= 2.1.0
browsermob-proxy >= 0.6.0
manifestparser >= 1.1
-wptserve >= 1.3.0
+marionette-driver >= 2.1.0
+mozcrash >= 0.5
+mozdevice >= 0.44
mozinfo >= 0.8
-mozprocess >= 0.9
-mozrunner >= 6.13
-mozdevice >= 0.44
mozlog >= 3.0
moznetwork >= 0.21
-mozcrash >= 0.5
+mozprocess >= 0.9
mozprofile >= 0.7
+mozrunner >= 6.13
moztest >= 0.7
mozversion >= 1.1
+wptserve >= 1.3.0