author | David Burns <dburns@mozilla.com> |
Thu, 02 Jun 2016 21:33:03 +0100 | |
changeset 300437 | ac4bcc6aba083f42e58916811a792511a2f6d720 |
parent 300436 | 27d3e50446a83e569b8192eb7c8c5dca025a741c |
child 300438 | f19d56c111d97f08611c15d42627958ee2de30ef |
push id | 30313 |
push user | cbook@mozilla.com |
push date | Mon, 06 Jun 2016 09:56:25 +0000 |
treeherder | mozilla-central@0a3b6e2df656 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | maja_zf |
bugs | 1277672 |
milestone | 49.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/testing/marionette/client/marionette_driver/addons.py +++ b/testing/marionette/client/marionette_driver/addons.py @@ -49,17 +49,18 @@ class Addons(object): :param temp: Install a temporary addon. Temporary addons will automatically be uninstalled on shutdown and do not need to be signed, though they must be restartless. :returns: The addon ID string of the newly installed addon. :raises: :exc:`AddonInstallException` """ with self._mn.using_context('chrome'): addon_id, status = self._mn.execute_async_script(""" - let FileUtils = Components.utils.import("resource://gre/modules/FileUtils.jsm").FileUtils; + let fileUtils = Components.utils.import("resource://gre/modules/FileUtils.jsm"); + let FileUtils = fileUtils.FileUtils; Components.utils.import("resource://gre/modules/AddonManager.jsm"); let listener = { onInstallEnded: function(install, addon) { marionetteScriptFinished([addon.id, 0]); }, onInstallFailed: function(install) { marionetteScriptFinished([null, install.error]);
--- a/testing/marionette/client/marionette_driver/date_time_value.py +++ b/testing/marionette/client/marionette_driver/date_time_value.py @@ -1,12 +1,13 @@ # 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/. + class DateTimeValue(object): """ Interface for setting the value of HTML5 "date" and "time" input elements. Simple usage example: :: @@ -41,9 +42,8 @@ class DateTimeValue(object): return self.element.get_attribute('value') # As per the W3C "time" element specification # (http://dev.w3.org/html5/markup/input.time.html), this value is formatted # according to RFC 3339: http://tools.ietf.org/html/rfc3339#section-5.6 @time.setter def time(self, time_value): self.element.send_keys(time_value.strftime('%H:%M:%S')) -
--- a/testing/marionette/client/marionette_driver/decorators.py +++ b/testing/marionette/client/marionette_driver/decorators.py @@ -3,24 +3,26 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from errors import MarionetteException from functools import wraps import socket import sys import traceback + def _find_marionette_in_args(*args, **kwargs): try: m = [a for a in args + tuple(kwargs.values()) if hasattr(a, 'session')][0] except IndexError: print("Can only apply decorator to function using a marionette object") raise return m + def do_crash_check(func, always=False): """Decorator which checks for crashes after the function has run. :param always: If False, only checks for crashes if an exception was raised. If True, always checks for crashes. """ @wraps(func) def _(*args, **kwargs): @@ -40,16 +42,17 @@ def do_crash_check(func, always=False): if not always: check() raise exc, val, tb finally: if always: check() return _ + def uses_marionette(func): """Decorator which creates a marionette session and deletes it afterwards if one doesn't already exist. """ @wraps(func) def _(*args, **kwargs): m = _find_marionette_in_args(*args, **kwargs) delete_session = False @@ -61,23 +64,24 @@ def uses_marionette(func): ret = func(*args, **kwargs) if delete_session: m.delete_session() return ret return _ + def using_context(context): """Decorator which allows a function to execute in certain scope using marionette.using_context functionality and returns to old scope once the function exits. :param context: Either 'chrome' or 'content'. """ def wrap(func): - @wraps(func) - def inner(*args, **kwargs): - m = _find_marionette_in_args(*args, **kwargs) - with m.using_context(context): - return func(*args, **kwargs) + @wraps(func) + def inner(*args, **kwargs): + m = _find_marionette_in_args(*args, **kwargs) + with m.using_context(context): + return func(*args, **kwargs) - return inner + return inner return wrap
--- a/testing/marionette/client/marionette_driver/errors.py +++ b/testing/marionette/client/marionette_driver/errors.py @@ -1,14 +1,13 @@ # 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 traceback -import types class InstallGeckoError(Exception): pass class MarionetteException(Exception): @@ -59,16 +58,17 @@ class ElementNotSelectableException(Mari class InvalidArgumentException(MarionetteException): status = "invalid argument" class InvalidSessionIdException(MarionetteException): status = "invalid session id" + class TimeoutException(MarionetteException): code = (21,) status = "timeout" class JavascriptException(MarionetteException): code = (17,) status = "javascript error" @@ -93,18 +93,18 @@ class ScriptTimeoutException(MarionetteE code = (28,) status = "script timeout" class ElementNotVisibleException(MarionetteException): code = (11,) status = "element not visible" - def __init__( - self, message="Element is not currently visible and may not be manipulated", + def __init__(self, + message="Element is not currently visible and may not be manipulated", stacktrace=None, cause=None): super(ElementNotVisibleException, self).__init__( message, cause=cause, stacktrace=stacktrace) class ElementNotAccessibleException(MarionetteException): code = (56,) status = "element not accessible"
--- a/testing/marionette/client/marionette_driver/expected.py +++ b/testing/marionette/client/marionette_driver/expected.py @@ -10,68 +10,73 @@ from marionette import HTMLElement """This file provides a set of expected conditions for common use cases when writing Marionette tests. The conditions rely on explicit waits that retries conditions a number of times until they are either successfully met, or they time out. """ + class element_present(object): """Checks that a web element is present in the DOM of the current context. This does not necessarily mean that the element is visible. You can select which element to be checked for presence by supplying a locator:: el = Wait(marionette).until(expected.element_present(By.ID, "foo")) Or by using a function/lambda returning an element:: - el = Wait(marionette).until(expected.element_present(lambda m: m.find_element(By.ID, "foo"))) + el = Wait(marionette).\ + until(expected.element_present(lambda m: m.find_element(By.ID, "foo"))) :param args: locator or function returning web element :returns: the web element once it is located, or False """ def __init__(self, *args): if len(args) == 1 and isinstance(args[0], types.FunctionType): self.locator = args[0] else: self.locator = lambda m: m.find_element(*args) def __call__(self, marionette): return _find(marionette, self.locator) + class element_not_present(element_present): """Checks that a web element is not present in the DOM of the current context. You can select which element to be checked for lack of presence by supplying a locator:: r = Wait(marionette).until(expected.element_not_present(By.ID, "foo")) Or by using a function/lambda returning an element:: - r = Wait(marionette).until(expected.element_present(lambda m: m.find_element(By.ID, "foo"))) + r = Wait(marionette).\ + until(expected.element_present(lambda m: m.find_element(By.ID, "foo"))) :param args: locator or function returning web element :returns: True if element is not present, or False if it is present """ def __init__(self, *args): super(element_not_present, self).__init__(*args) def __call__(self, marionette): return not super(element_not_present, self).__call__(marionette) + class element_stale(object): """Check that the given element is no longer attached to DOM of the current context. This can be useful for waiting until an element is no longer present. Sample usage:: @@ -92,69 +97,74 @@ class element_stale(object): def __call__(self, marionette): try: # Calling any method forces a staleness check self.el.is_enabled() return False except errors.StaleElementException: return True + class elements_present(object): """Checks that web elements are present in the DOM of the current context. This does not necessarily mean that the elements are visible. You can select which elements to be checked for presence by supplying a locator:: els = Wait(marionette).until(expected.elements_present(By.TAG_NAME, "a")) Or by using a function/lambda returning a list of elements:: - els = Wait(marionette).until(expected.elements_present(lambda m: m.find_elements(By.TAG_NAME, "a"))) + els = Wait(marionette).\ + until(expected.elements_present(lambda m: m.find_elements(By.TAG_NAME, "a"))) :param args: locator or function returning a list of web elements :returns: list of web elements once they are located, or False """ def __init__(self, *args): if len(args) == 1 and isinstance(args[0], types.FunctionType): self.locator = args[0] else: self.locator = lambda m: m.find_elements(*args) def __call__(self, marionette): return _find(marionette, self.locator) + class elements_not_present(elements_present): """Checks that web elements are not present in the DOM of the current context. You can select which elements to be checked for not being present by supplying a locator:: r = Wait(marionette).until(expected.elements_not_present(By.TAG_NAME, "a")) Or by using a function/lambda returning a list of elements:: - r = Wait(marionette).until(expected.elements_not_present(lambda m: m.find_elements(By.TAG_NAME, "a"))) + r = Wait(marionette).\ + until(expected.elements_not_present(lambda m: m.find_elements(By.TAG_NAME, "a"))) :param args: locator or function returning a list of web elements :returns: True if elements are missing, False if one or more are present """ def __init__(self, *args): super(elements_not_present, self).__init__(*args) def __call__(self, marionette): return not super(elements_not_present, self).__call__(marionette) + class element_displayed(object): """An expectation for checking that an element is visible. Visibility means that the element is not only displayed, but also has a height and width that is greater than 0 pixels. Stale elements, meaning elements that have been detached from the DOM of the current context are treated as not being displayed, @@ -188,16 +198,17 @@ class element_displayed(object): self.el = _find(marionette, self.locator) if not self.el: return False try: return self.el.is_displayed() except errors.StaleElementException: return False + class element_not_displayed(element_displayed): """An expectation for checking that an element is not visible. Visibility means that the element is not only displayed, but also has a height and width that is greater than 0 pixels. Stale elements, meaning elements that have been detached fom the DOM of the current context are treated as not being displayed, @@ -220,73 +231,78 @@ class element_not_displayed(element_disp """ def __init__(self, *args): super(element_not_displayed, self).__init__(*args) def __call__(self, marionette): return not super(element_not_displayed, self).__call__(marionette) + class element_selected(object): """An expectation for checking that the given element is selected. :param element: the element to be selected :returns: True if element is selected, False otherwise """ def __init__(self, element): self.el = element def __call__(self, marionette): return self.el.is_selected() + class element_not_selected(element_selected): """An expectation for checking that the given element is not selected. :param element: the element to not be selected :returns: True if element is not selected, False if selected """ def __init__(self, element): super(element_not_selected, self).__init__(element) def __call__(self, marionette): return not super(element_not_selected, self).__call__(marionette) + class element_enabled(object): """An expectation for checking that the given element is enabled. :param element: the element to check if enabled :returns: True if element is enabled, False otherwise """ def __init__(self, element): self.el = element def __call__(self, marionette): return self.el.is_enabled() + class element_not_enabled(element_enabled): """An expectation for checking that the given element is disabled. :param element: the element to check if disabled :returns: True if element is disabled, False if enabled """ def __init__(self, element): super(element_not_enabled, self).__init__(element) def __call__(self, marionette): return not super(element_not_enabled, self).__call__(marionette) + def _find(marionette, func): el = None try: el = func(marionette) except errors.NoSuchElementException: pass
--- a/testing/marionette/client/marionette_driver/geckoinstance.py +++ b/testing/marionette/client/marionette_driver/geckoinstance.py @@ -126,18 +126,18 @@ class GeckoInstance(object): process_args['stream'] = sys.stdout else: process_args['logfile'] = self.gecko_log env = os.environ.copy() # environment variables needed for crashreporting # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting - env.update({ 'MOZ_CRASHREPORTER': '1', - 'MOZ_CRASHREPORTER_NO_REPORT': '1', }) + env.update({'MOZ_CRASHREPORTER': '1', + 'MOZ_CRASHREPORTER_NO_REPORT': '1'}) self.runner = Runner( binary=self.bin, profile=self.profile, cmdargs=['-no-remote', '-marionette'] + self.app_args, env=env, symbols_path=self.symbols_path, process_args=process_args) self.runner.start() @@ -158,16 +158,17 @@ class GeckoInstance(object): self.profile = None if prefs: self.prefs = prefs else: self.prefs = None self.start() + class B2GDesktopInstance(GeckoInstance): def __init__(self, host, port, bin, **kwargs): # Pass a profile and change the binary to -bin so that # the built-in gaia profile doesn't get touched. if kwargs.get('profile', None) is None: # GeckoInstance.start will clone the profile. kwargs['profile'] = os.path.join(os.path.dirname(bin), 'gaia',
--- a/testing/marionette/client/marionette_driver/gestures.py +++ b/testing/marionette/client/marionette_driver/gestures.py @@ -1,21 +1,25 @@ # 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 MultiActions, Actions -#axis is y or x -#direction is 0 for positive, and -1 for negative -#length is the total length we want to scroll -#increments is how much we want to move per scrolling -#wait_period is the seconds we wait between scrolling -#scroll_back is whether we want to scroll back to original view -def smooth_scroll(marionette_session, start_element, axis, direction, length, increments=None, wait_period=None, scroll_back=None): + +def smooth_scroll(marionette_session, start_element, axis, direction, + length, increments=None, wait_period=None, scroll_back=None): + """ + :param axis: y or x + :param direction: 0 for positive, and -1 for negative + :param length: total length of scroll scroll + :param increments: Amount to be moved per scrolling + :param wait_period: Seconds to wait between scrolling + :param scroll_back: Scroll back to original view? + """ if axis not in ["x", "y"]: raise Exception("Axis must be either 'x' or 'y'") if direction not in [-1, 0]: raise Exception("Direction must either be -1 negative or 0 positive") increments = increments or 100 wait_period = wait_period or 0.05 scroll_back = scroll_back or False current = 0 @@ -37,23 +41,26 @@ def smooth_scroll(marionette_session, st if scroll_back: offset = [-value for value in offset] while (current > 0): current -= increments action.move_by_offset(*offset).wait(wait_period) action.release() action.perform() -#element is the target -#x1,x2 are 1st finger starting position relative to the target -#x3,y3 are 1st finger ending position relative to the target -#x2,y2 are 2nd finger starting position relative to the target -#x4,y4 are 2nd finger ending position relative to the target -#duration is the amount of time in milliseconds we want to complete the pinch. + def pinch(marionette_session, element, x1, y1, x2, y2, x3, y3, x4, y4, duration=200): + """ + :param element: target + :param x1, y1: 1st finger starting position relative to the target + :param x3, y3: 1st finger ending position relative to the target + :param x2, y2: 2nd finger starting position relative to the target + :param x4, y4: 2nd finger ending position relative to the target + :param duration: Amount of time in milliseconds to complete the pinch. + """ time = 0 time_increment = 10 if time_increment >= duration: time_increment = duration move_x1 = time_increment*1.0/duration * (x3 - x1) move_y1 = time_increment*1.0/duration * (y3 - y1) move_x2 = time_increment*1.0/duration * (x4 - x2) move_y2 = time_increment*1.0/duration * (y4 - y2) @@ -65,19 +72,22 @@ def pinch(marionette_session, element, x while (time < duration): time += time_increment action1.move_by_offset(move_x1, move_y1).wait(time_increment/1000) action2.move_by_offset(move_x2, move_y2).wait(time_increment/1000) action1.release() action2.release() multiAction.add(action1).add(action2).perform() -#element: The element to press. -#time_in_seconds: Time in seconds to wait before releasing the press. -#x: Optional, x-coordinate to tap, relative to the top-left corner of the element. -#y: Optional, y-coordinate to tap, relative to the top-leftcorner of the element. + def long_press_without_contextmenu(marionette_session, element, time_in_seconds, x=None, y=None): + """ + :param element: The element to press. + :param time_in_seconds: Time in seconds to wait before releasing the press. + #x: Optional, x-coordinate to tap, relative to the top-left corner of the element. + #y: Optional, y-coordinate to tap, relative to the top-leftcorner of the element. + """ action = Actions(marionette_session) action.press(element, x, y) action.move_by_offset(0, 0) action.wait(time_in_seconds) action.release() action.perform()
--- a/testing/marionette/client/marionette_driver/keys.py +++ b/testing/marionette/client/marionette_driver/keys.py @@ -11,74 +11,74 @@ # distributed under the License is distributed on an "AS IS" BASIS # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied. # See the License for the specific language governing permissions and # limitations under the License. class Keys(object): - NULL = u'\ue000' - CANCEL = u'\ue001' # ^break - HELP = u'\ue002' - BACK_SPACE = u'\ue003' - TAB = u'\ue004' - CLEAR = u'\ue005' - RETURN = u'\ue006' - ENTER = u'\ue007' - SHIFT = u'\ue008' - LEFT_SHIFT = u'\ue008' # alias - CONTROL = u'\ue009' - LEFT_CONTROL = u'\ue009' # alias - ALT = u'\ue00a' - LEFT_ALT = u'\ue00a' # alias - PAUSE = u'\ue00b' - ESCAPE = u'\ue00c' - SPACE = u'\ue00d' - PAGE_UP = u'\ue00e' - PAGE_DOWN = u'\ue00f' - END = u'\ue010' - HOME = u'\ue011' - LEFT = u'\ue012' - ARROW_LEFT = u'\ue012' # alias - UP = u'\ue013' - ARROW_UP = u'\ue013' # alias - RIGHT = u'\ue014' - ARROW_RIGHT = u'\ue014' # alias - DOWN = u'\ue015' - ARROW_DOWN = u'\ue015' # alias - INSERT = u'\ue016' - DELETE = u'\ue017' - SEMICOLON = u'\ue018' - EQUALS = u'\ue019' + NULL = u'\ue000' + CANCEL = u'\ue001' # ^break + HELP = u'\ue002' + BACK_SPACE = u'\ue003' + TAB = u'\ue004' + CLEAR = u'\ue005' + RETURN = u'\ue006' + ENTER = u'\ue007' + SHIFT = u'\ue008' + LEFT_SHIFT = u'\ue008' # alias + CONTROL = u'\ue009' + LEFT_CONTROL = u'\ue009' # alias + ALT = u'\ue00a' + LEFT_ALT = u'\ue00a' # alias + PAUSE = u'\ue00b' + ESCAPE = u'\ue00c' + SPACE = u'\ue00d' + PAGE_UP = u'\ue00e' + PAGE_DOWN = u'\ue00f' + END = u'\ue010' + HOME = u'\ue011' + LEFT = u'\ue012' + ARROW_LEFT = u'\ue012' # alias + UP = u'\ue013' + ARROW_UP = u'\ue013' # alias + RIGHT = u'\ue014' + ARROW_RIGHT = u'\ue014' # alias + DOWN = u'\ue015' + ARROW_DOWN = u'\ue015' # alias + INSERT = u'\ue016' + DELETE = u'\ue017' + SEMICOLON = u'\ue018' + EQUALS = u'\ue019' - NUMPAD0 = u'\ue01a' # numbe pad keys - NUMPAD1 = u'\ue01b' - NUMPAD2 = u'\ue01c' - NUMPAD3 = u'\ue01d' - NUMPAD4 = u'\ue01e' - NUMPAD5 = u'\ue01f' - NUMPAD6 = u'\ue020' - NUMPAD7 = u'\ue021' - NUMPAD8 = u'\ue022' - NUMPAD9 = u'\ue023' - MULTIPLY = u'\ue024' - ADD = u'\ue025' - SEPARATOR = u'\ue026' - SUBTRACT = u'\ue027' - DECIMAL = u'\ue028' - DIVIDE = u'\ue029' + NUMPAD0 = u'\ue01a' # numbe pad keys + NUMPAD1 = u'\ue01b' + NUMPAD2 = u'\ue01c' + NUMPAD3 = u'\ue01d' + NUMPAD4 = u'\ue01e' + NUMPAD5 = u'\ue01f' + NUMPAD6 = u'\ue020' + NUMPAD7 = u'\ue021' + NUMPAD8 = u'\ue022' + NUMPAD9 = u'\ue023' + MULTIPLY = u'\ue024' + ADD = u'\ue025' + SEPARATOR = u'\ue026' + SUBTRACT = u'\ue027' + DECIMAL = u'\ue028' + DIVIDE = u'\ue029' - F1 = u'\ue031' # function keys - F2 = u'\ue032' - F3 = u'\ue033' - F4 = u'\ue034' - F5 = u'\ue035' - F6 = u'\ue036' - F7 = u'\ue037' - F8 = u'\ue038' - F9 = u'\ue039' - F10 = u'\ue03a' - F11 = u'\ue03b' - F12 = u'\ue03c' + F1 = u'\ue031' # function keys + F2 = u'\ue032' + F3 = u'\ue033' + F4 = u'\ue034' + F5 = u'\ue035' + F6 = u'\ue036' + F7 = u'\ue037' + F8 = u'\ue038' + F9 = u'\ue039' + F10 = u'\ue03a' + F11 = u'\ue03b' + F12 = u'\ue03c' - META = u'\ue03d' - COMMAND = u'\ue03d' + META = u'\ue03d' + COMMAND = u'\ue03d'
--- a/testing/marionette/client/marionette_driver/marionette.py +++ b/testing/marionette/client/marionette_driver/marionette.py @@ -2,17 +2,16 @@ # 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 base64 import ConfigParser import json import os import socket -import StringIO import traceback import warnings from contextlib import contextmanager from decorators import do_crash_check from keys import Keys @@ -223,17 +222,17 @@ class Actions(object): relative to the top-left corner of the element. :param element: The element to press on. :param x: Optional, x-coordinate to tap, relative to the top-left corner of the element. :param y: Optional, y-coordinate to tap, relative to the top-left corner of the element. ''' - element=element.id + element = element.id self.action_chain.append(['press', element, x, y]) return self def release(self): ''' Sends a 'touchend' event to this element. May only be called if press() has already be called on this element. @@ -252,39 +251,43 @@ class Actions(object): def move(self, element): ''' Sends a 'touchmove' event at the center of the target element. :param element: Element to move towards. May only be called if press() has already be called. ''' - element=element.id + element = element.id self.action_chain.append(['move', element]) return self def move_by_offset(self, x, y): ''' - Sends 'touchmove' event to the given x, y coordinates relative to the top-left of the currently touched element. + Sends 'touchmove' event to the given x, y coordinates relative to the + top-left of the currently touched element. May only be called if press() has already be called. :param x: Specifies x-coordinate of move event, relative to the top-left corner of the element. :param y: Specifies y-coordinate of move event, relative to the top-left corner of the element. ''' self.action_chain.append(['moveByOffset', x, y]) return self def wait(self, time=None): ''' Waits for specified time period. - :param time: Time in seconds to wait. If time is None then this has no effect for a single action chain. If used inside a multi-action chain, then time being None indicates that we should wait for all other currently executing actions that are part of the chain to complete. + :param time: Time in seconds to wait. If time is None then this has no effect + for a single action chain. If used inside a multi-action chain, + then time being None indicates that we should wait for all other + currently executing actions that are part of the chain to complete. ''' self.action_chain.append(['wait', time]) return self def cancel(self): ''' Sends 'touchcancel' event to the target of the original 'touchstart' event. @@ -306,32 +309,32 @@ class Actions(object): element. This is equivalent to calling: :: action.press(element, x, y).release() ''' - element=element.id + element = element.id self.action_chain.append(['press', element, x, y]) self.action_chain.append(['release']) return self def double_tap(self, element, x=None, y=None): ''' Performs a double tap on the target element. :param element: The element to double tap. :param x: Optional, x-coordinate of double tap, relative to the top-left corner of the element. :param y: Optional, y-coordinate of double tap, relative to the top-left corner of the element. ''' - element=element.id + element = element.id self.action_chain.append(['press', element, x, y]) self.action_chain.append(['release']) self.action_chain.append(['press', element, x, y]) self.action_chain.append(['release']) return self def click(self, element, button=MouseButton.LEFT, count=1): ''' @@ -448,16 +451,17 @@ class Actions(object): def perform(self): """Sends the action chain built so far to the server side for execution and clears the current chain of actions.""" body = {"chain": self.action_chain, "nextId": self.current_id} self.current_id = self.marionette._send_message("actionChain", body, key="value") self.action_chain = [] return self + class MultiActions(object): ''' A MultiActions object represents a sequence of actions that may be performed at the same time. Its intent is to allow the simulation of multi-touch gestures. Usage example: :: @@ -482,17 +486,17 @@ class MultiActions(object): def add(self, action): ''' Adds a set of actions to perform. :param action: An Actions object. ''' self.multi_actions.append(action.action_chain) if len(action.action_chain) > self.max_length: - self.max_length = len(action.action_chain) + self.max_length = len(action.action_chain) return self def perform(self): """Perform all the actions added to this object.""" body = {"value": self.multi_actions, "max_length": self.max_length} self.marionette._send_message("multiAction", body) @@ -526,18 +530,18 @@ class Alert(object): tab modal dialog.""" body = {"value": Marionette.convert_keys(*string)} self.marionette._send_message("sendKeysToDialog", body) class Marionette(object): """Represents a Marionette connection to a browser or device.""" - CONTEXT_CHROME = 'chrome' # non-browser content: windows, dialogs, etc. - CONTEXT_CONTENT = 'content' # browser content: iframes, divs, etc. + CONTEXT_CHROME = 'chrome' # non-browser content: windows, dialogs, etc. + CONTEXT_CONTENT = 'content' # browser content: iframes, divs, etc. TIMEOUT_SEARCH = 'implicit' TIMEOUT_SCRIPT = 'script' TIMEOUT_PAGE = 'page load' DEFAULT_SOCKET_TIMEOUT = 360 DEFAULT_STARTUP_TIMEOUT = 120 def __init__(self, host='localhost', port=2828, app=None, app_args=None, bin=None, profile=None, addons=None, @@ -633,17 +637,16 @@ class Marionette(object): timeout = timeout or self.DEFAULT_STARTUP_TIMEOUT return transport.wait_for_port(self.host, self.port, timeout=timeout) @do_crash_check def raise_for_port(self, port_obtained): if not port_obtained: raise IOError("Timed out waiting for port!") - @do_crash_check def _send_message(self, name, params=None, key=None): """Send a blocking message to the server. Marionette provides an asynchronous, non-blocking interface and this attempts to paper over this by providing a synchronous API to the user. @@ -688,18 +691,18 @@ class Marionette(object): self._handle_error(err) if key is not None: return self._unwrap_response(res.get(key)) else: return self._unwrap_response(res) def _unwrap_response(self, value): - if isinstance(value, dict) and \ - (WEBELEMENT_KEY in value or W3C_WEBELEMENT_KEY in value): + if isinstance(value, dict) and (WEBELEMENT_KEY in value or + W3C_WEBELEMENT_KEY in value): if value.get(WEBELEMENT_KEY): return HTMLElement(self, value.get(WEBELEMENT_KEY)) else: return HTMLElement(self, value.get(W3C_WEBELEMENT_KEY)) elif isinstance(value, list): return list(self._unwrap_response(item) for item in value) else: return value @@ -733,17 +736,17 @@ class Marionette(object): name = None crashed = False if self.instance: if self.instance.runner.check_for_crashes( test_name=self.test_name): crashed = True if returncode is not None: print ('PROCESS-CRASH | %s | abnormal termination with exit code %d' % - (name, returncode)) + (name, returncode)) return crashed @staticmethod def convert_keys(*string): typing = [] for val in string: if isinstance(val, Keys): typing.append(val) @@ -752,97 +755,103 @@ class Marionette(object): for i in range(len(val)): typing.append(val[i]) else: for i in range(len(val)): typing.append(val[i]) return typing def get_permission(self, perm): + script = """ + let value = { + 'url': document.nodePrincipal.URI.spec, + 'appId': document.nodePrincipal.appId, + 'isInIsolatedMozBrowserElement': document.nodePrincipal.isInIsolatedMozBrowserElement, + 'type': arguments[0] + }; + return value;""" with self.using_context('content'): - value = self.execute_script(""" - let value = { - 'url': document.nodePrincipal.URI.spec, - 'appId': document.nodePrincipal.appId, - 'isInIsolatedMozBrowserElement': document.nodePrincipal.isInIsolatedMozBrowserElement, - 'type': arguments[0] - }; - return value; - """, script_args=[perm], sandbox='system') + value = self.execute_script(script, script_args=[perm], sandbox='system') with self.using_context('chrome'): permission = self.execute_script(""" Components.utils.import("resource://gre/modules/Services.jsm"); let perm = arguments[0]; let secMan = Services.scriptSecurityManager; - let attrs = {appId: perm.appId, inIsolatedMozBrowser: perm.isInIsolatedMozBrowserElement}; + let attrs = {appId: perm.appId, + inIsolatedMozBrowser: perm.isInIsolatedMozBrowserElement}; let principal = secMan.createCodebasePrincipal( Services.io.newURI(perm.url, null, null), attrs); let testPerm = Services.perms.testPermissionFromPrincipal( principal, perm.type); return testPerm; """, script_args=[value]) return permission def push_permission(self, perm, allow): - with self.using_context('content'): - perm = self.execute_script(""" - let allow = arguments[0]; - if (typeof(allow) == "boolean") { - if (allow) { - allow = Components.interfaces.nsIPermissionManager.ALLOW_ACTION; - } - else { - allow = Components.interfaces.nsIPermissionManager.DENY_ACTION; - } - } - let perm_type = arguments[1]; + script = """ + let allow = arguments[0]; + if (typeof(allow) == "boolean") { + if (allow) { + allow = Components.interfaces.nsIPermissionManager.ALLOW_ACTION; + } + else { + allow = Components.interfaces.nsIPermissionManager.DENY_ACTION; + } + } + let perm_type = arguments[1]; - Components.utils.import("resource://gre/modules/Services.jsm"); - window.wrappedJSObject.permChanged = false; - window.wrappedJSObject.permObserver = function(subject, topic, data) { - if (topic == "perm-changed") { - let permission = subject.QueryInterface(Components.interfaces.nsIPermission); - if (perm_type == permission.type) { - Services.obs.removeObserver(window.wrappedJSObject.permObserver, "perm-changed"); - window.wrappedJSObject.permChanged = true; - } - } - }; - Services.obs.addObserver(window.wrappedJSObject.permObserver, - "perm-changed", false); + Components.utils.import("resource://gre/modules/Services.jsm"); + window.wrappedJSObject.permChanged = false; + window.wrappedJSObject.permObserver = function(subject, topic, data) { + if (topic == "perm-changed") { + let permission = subject.QueryInterface(Components.interfaces.nsIPermission); + if (perm_type == permission.type) { + Services.obs.removeObserver(window.wrappedJSObject.permObserver, + "perm-changed"); + window.wrappedJSObject.permChanged = true; + } + } + }; + Services.obs.addObserver(window.wrappedJSObject.permObserver, + "perm-changed", false); - let value = { - 'url': document.nodePrincipal.URI.spec, - 'appId': document.nodePrincipal.appId, - 'isInIsolatedMozBrowserElement': document.nodePrincipal.isInIsolatedMozBrowserElement, - 'type': perm_type, - 'action': allow - }; - return value; - """, script_args=[allow, perm], sandbox='system') + let value = { + 'url': document.nodePrincipal.URI.spec, + 'appId': document.nodePrincipal.appId, + 'isInIsolatedMozBrowserElement': document.nodePrincipal.isInIsolatedMozBrowserElement, + 'type': perm_type, + 'action': allow + }; + return value; + """ + with self.using_context('content'): + perm = self.execute_script(script, script_args=[allow, perm], sandbox='system') current_perm = self.get_permission(perm['type']) if current_perm == perm['action']: with self.using_context('content'): self.execute_script(""" Components.utils.import("resource://gre/modules/Services.jsm"); - Services.obs.removeObserver(window.wrappedJSObject.permObserver, "perm-changed"); + Services.obs.removeObserver(window.wrappedJSObject.permObserver, + "perm-changed"); """, sandbox='system') return with self.using_context('chrome'): self.execute_script(""" Components.utils.import("resource://gre/modules/Services.jsm"); let perm = arguments[0]; let secMan = Services.scriptSecurityManager; - let attrs = {appId: perm.appId, inIsolatedMozBrowser: perm.isInIsolatedMozBrowserElement}; - let principal = secMan.createCodebasePrincipal(Services.io.newURI(perm.url, null, null), - attrs); + let attrs = {appId: perm.appId, + inIsolatedMozBrowser: perm.isInIsolatedMozBrowserElement}; + let principal = secMan.createCodebasePrincipal(Services.io.newURI(perm.url, + null, null), + attrs); Services.perms.addFromPrincipal(principal, perm.type, perm.action); return true; """, script_args=[perm]) with self.using_context("content"): self.execute_async_script(""" let start = new Date(); let end = new Date(start.valueOf() + 5000); @@ -957,17 +966,17 @@ class Marionette(object): def enforce_gecko_prefs(self, prefs): """ Checks if the running instance has the given prefs. If not, it will kill the currently running instance, and spawn a new instance with the requested preferences. : param prefs: A dictionary whose keys are preference names. """ if not self.instance: - raise errors.MarionetteException("enforce_gecko_prefs can only be called " \ + raise errors.MarionetteException("enforce_gecko_prefs can only be called " "on gecko instances launched by Marionette") pref_exists = True self.set_context(self.CONTEXT_CHROME) for pref, value in prefs.iteritems(): if type(value) is not str: value = json.dumps(value) pref_exists = self.execute_script(""" let prefInterface = Components.classes["@mozilla.org/preferences-service;1"] @@ -1004,24 +1013,24 @@ class Marionette(object): : param clean: If False the same profile will be used after the restart. Note that the in app initiated restart always maintains the same profile. : param in_app: If True, marionette will cause a restart from within the browser. Otherwise the browser will be restarted immediately by killing the process. """ if not self.instance: - raise errors.MarionetteException("restart can only be called " \ + raise errors.MarionetteException("restart can only be called " "on gecko instances launched by Marionette") if in_app: if clean: raise ValueError # Values here correspond to constants in nsIAppStartup. - # See https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIAppStartup + # See http://mzl.la/1X0JZsC restart_flags = [ "eForceQuit", "eRestart", ] self._send_message("quitApplication", {"flags": restart_flags}) self.client.close() # The instance is restarting itself; we will no longer be able to # track it by pid, so mark it as 'detached'. @@ -1556,17 +1565,17 @@ class Marionette(object): result = self.marionette.execute_script("return global.test1;", new_sandbox=False) assert result == "foo" """ if script_args is None: script_args = [] args = self.wrapArguments(script_args) stack = traceback.extract_stack() - frame = stack[-2:-1][0] # grab the second-to-last frame + frame = stack[-2:-1][0] # grab the second-to-last frame body = {"script": script, "args": args, "newSandbox": new_sandbox, "sandbox": sandbox, "scriptTimeout": script_timeout, "line": int(frame[1]), "filename": os.path.basename(frame[0])} rv = self._send_message("executeScript", body, key="value") @@ -1606,17 +1615,17 @@ class Marionette(object): }, 5000); ''') assert result == 1 """ if script_args is None: script_args = [] args = self.wrapArguments(script_args) stack = traceback.extract_stack() - frame = stack[-2:-1][0] # grab the second-to-last frame + frame = stack[-2:-1][0] # grab the second-to-last frame body = {"script": script, "args": args, "newSandbox": new_sandbox, "sandbox": sandbox, "scriptTimeout": script_timeout, "line": int(frame[1]), "filename": os.path.basename(frame[0]), "debug_script": debug_script}
--- a/testing/marionette/client/marionette_driver/transport.py +++ b/testing/marionette/client/marionette_driver/transport.py @@ -2,17 +2,16 @@ # 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 datetime import errno import json import socket import time -import types class SocketTimeout(object): def __init__(self, socket, timeout): self.sock = socket self.timeout = timeout self.old_timeout = None @@ -62,18 +61,18 @@ class Response(Message): Message.__init__(self, msgid) self.error = error self.result = result def __str__(self): return "<Response id=%s, error=%s, result=%s>" % (self.id, self.error, self.result) def to_msg(self): - msg = [Response.TYPE, self.id, self.error, self.result] - return json.dumps(msg) + msg = [Response.TYPE, self.id, self.error, self.result] + return json.dumps(msg) @staticmethod def from_msg(payload): data = json.loads(payload) assert data[0] == Response.TYPE return Response(data[1], data[2], data[3]) @@ -112,17 +111,17 @@ class TcpTransport(object): 7:MESSAGE On top of this protocol it uses a Marionette message format, that depending on the protocol level offered by the remote server, varies. Supported protocol levels are 1 and above. """ max_packet_length = 4096 - connection_lost_msg = "Connection to Marionette server is lost. Check gecko.log (desktop firefox) or logcat (b2g) for errors." + connection_lost_msg = "Connection to Marionette server is lost. Check gecko.log for errors." def __init__(self, addr, port, socket_timeout=360.0): """If `socket_timeout` is `0` or `0.0`, non-blocking socket mode will be used. Setting it to `1` or `None` disables timeouts on socket operations altogether. """ self.addr = addr self.port = port @@ -238,26 +237,27 @@ class TcpTransport(object): data = json.dumps(obj) payload = "%s:%s" % (len(data), data) totalsent = 0 while totalsent < len(payload): try: sent = self.sock.send(payload[totalsent:]) if sent == 0: - raise IOError("socket error after sending %d of %d bytes" % \ - (totalsent, len(payload))) + raise IOError("socket error after sending %d of %d bytes" % + (totalsent, len(payload))) else: totalsent += sent except IOError as e: if e.errno == errno.EPIPE: raise IOError("%s: %s" % (str(e), self.connection_lost_msg)) else: raise e + def respond(self, obj): """Send a response to a command. This can be an arbitrary JSON serialisable object or an ``Exception``. """ res, err = None, None if isinstance(obj, Exception): err = obj else:
--- a/testing/marionette/client/marionette_driver/wait.py +++ b/testing/marionette/client/marionette_driver/wait.py @@ -5,16 +5,17 @@ import collections import errors import sys import time DEFAULT_TIMEOUT = 5 DEFAULT_INTERVAL = 0.1 + class Wait(object): """An explicit conditional utility class for waiting until a condition evaluates to true or not null. This will repeatedly evaluate a condition in anticipation for a truthy return value, or its timeout to expire, or its waiting predicate to become true. @@ -137,19 +138,21 @@ class Wait(object): if message: message = " with message: %s" % message raise errors.TimeoutException( "Timed out after %s seconds%s" % (round((self.clock.now - start), 1), message if message else ""), cause=last_exc) + def until_pred(clock, end): return clock.now >= end + class SystemClock(object): def __init__(self): self._time = time def sleep(self, duration): self._time.sleep(duration) @property