import threading import SimpleHTTPServer import SocketServer import BaseHTTPServer import urllib import urlparse import os import posixpath from marionette_driver.errors import NoSuchElementException from marionette_harness import MarionetteTestCase DEBUG = True # Example taken from mozilla-central/browser/components/loop/ # XXX Once we're on a branch with bug 993478 landed, we may want to get # rid of this HTTP server and just use the built-in one from Marionette, # since there will less code to maintain, and it will be faster. We'll # need to consider whether this code wants to be shared with WebDriver tests # for other browsers, though. class ThreadingSimpleServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): pass class HttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler, object): def __init__(self, *args): # set root to toolkit/components/microformats/ self.root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.normpath(__file__)))) super(HttpRequestHandler, self).__init__(*args) def log_message(self, format, *args, **kwargs): if DEBUG: super(HttpRequestHandler, self).log_message(format, *args, **kwargs) def translate_path(self, path): """Translate a /-separated PATH to the local filename syntax. Components that mean special things to the local file system (e.g. drive or directory names) are ignored. (XXX They should probably be diagnosed.) """ # abandon query parameters path = path.split('?',1)[0] path = path.split('#',1)[0] # Don't forget explicit trailing slash when normalizing. Issue17324 trailing_slash = path.rstrip().endswith('/') path = posixpath.normpath(urllib.unquote(path)) words = path.split('/') words = filter(None, words) path = self.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) if trailing_slash: path += '/' return path class BaseTestFrontendUnits(MarionetteTestCase): @classmethod def setUpClass(cls): super(BaseTestFrontendUnits, cls).setUpClass() # Port 0 means to select an arbitrary unused port cls.server = ThreadingSimpleServer(('', 0), HttpRequestHandler) cls.ip, cls.port = cls.server.server_address cls.server_thread = threading.Thread(target=cls.server.serve_forever) cls.server_thread.daemon = False cls.server_thread.start() @classmethod def tearDownClass(cls): cls.server.shutdown() cls.server_thread.join() # make sure everything gets GCed so it doesn't interfere with the next # test class. Even though this is class-static, each subclass gets # its own instance of this stuff. cls.server_thread = None cls.server = None def setUp(self): super(BaseTestFrontendUnits, self).setUp() # Unfortunately, enforcing preferences currently comes with the side # effect of launching and restarting the browser before running the # real functional tests. Bug 1048554 has been filed to track this. # # Note: when e10s is enabled by default, this pref can go away. The automatic # restart will also go away if this is still the only pref set here. self.marionette.enforce_gecko_prefs({ "browser.tabs.remote.autostart": True }) # This extends the timeout for find_element. We need this as the tests # take an amount of time to run after loading, which we have to wait for. self.marionette.timeout.implicit = 120 self.marionette.timeout.page_load = 120 # srcdir_path should be the directory relative to this file. def set_server_prefix(self, srcdir_path): self.server_prefix = urlparse.urljoin("http://localhost:" + str(self.port), srcdir_path) def check_page(self, page): self.marionette.navigate(urlparse.urljoin(self.server_prefix, page)) try: self.marionette.find_element("id", 'complete') except NoSuchElementException: fullPageUrl = urlparse.urljoin(self.relPath, page) details = "%s: 1 failure encountered\n%s" % \ (fullPageUrl, self.get_failure_summary( fullPageUrl, "Waiting for Completion", "Could not find the test complete indicator")) raise AssertionError(details) fail_node = self.marionette.find_element("css selector", '.failures > em') if fail_node.text == "0": return # This may want to be in a more general place triggerable by an env # var some day if it ends up being something we need often: # # If you have browser-based unit tests which work when loaded manually # but not from marionette, uncomment the two lines below to break # on failing tests, so that the browsers won't be torn down, and you # can use the browser debugging facilities to see what's going on. #from ipdb import set_trace #set_trace() raise AssertionError(self.get_failure_details(page)) def get_failure_summary(self, fullPageUrl, testName, testError): return "TEST-UNEXPECTED-FAIL | %s | %s - %s" % (fullPageUrl, testName, testError) def get_failure_details(self, page): fail_nodes = self.marionette.find_elements("css selector", '.test.fail') fullPageUrl = urlparse.urljoin(self.relPath, page) details = ["%s: %d failure(s) encountered:" % (fullPageUrl, len(fail_nodes))] for node in fail_nodes: errorText = node.find_element("css selector", '.error').text # We have to work our own failure message here, as we could be reporting multiple failures. # XXX Ideally we'd also give the full test tree for - that requires walking # up the DOM tree. # Format: TEST-UNEXPECTED-FAIL | | - details.append( self.get_failure_summary(page, node.find_element("tag name", 'h2').text.split("\n")[0], errorText.split("\n")[0])) details.append( errorText) return "\n".join(details)