Bug 1429759 - Allow Marionette tests to change the user profile.
If Marionette handles the instance of the application including the
user profile, the tests should be able to change the user profile.
This will enable tests to cover specific bugs in the application as
caused by the profile name and path.
To prevent dataloss changing the profile should only be allowed
when the application is not running.
MozReview-Commit-ID: JWQGV4JWP61
--- a/testing/marionette/client/marionette_driver/geckoinstance.py
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -14,16 +14,18 @@ from copy import deepcopy
import mozversion
from mozdevice import DMError
from mozprofile import Profile
from mozrunner import Runner, FennecEmulatorRunner
from six import reraise
+from . import errors
+
class GeckoInstance(object):
required_prefs = {
# Increase the APZ content response timeout in tests to 1 minute.
# This is to accommodate the fact that test environments tends to be slower
# than production environments (with the b2g emulator being the slowest of them
# all), resulting in the production timeout value sometimes being exceeded
# and causing false-positive test failures. See bug 1176798, bug 1177018,
@@ -122,37 +124,38 @@ class GeckoInstance(object):
self.runner_class = Runner
self.app_args = app_args or []
self.runner = None
self.symbols_path = symbols_path
self.binary = bin
self.marionette_host = host
self.marionette_port = port
- # Alternative to default temporary directory
- self.workspace = workspace
self.addons = addons
- # Check if it is a Profile object or a path to profile
- self.profile = None
- if isinstance(profile, Profile):
- self.profile = profile
- else:
- self.profile_path = profile
self.prefs = prefs
self.required_prefs = deepcopy(self.required_prefs)
if prefs:
self.required_prefs.update(prefs)
self._gecko_log_option = gecko_log
self._gecko_log = None
self.verbose = verbose
self.headless = headless
+
# keep track of errors to decide whether instance is unresponsive
self.unresponsive_count = 0
+ # Alternative to default temporary directory
+ self.workspace = workspace
+
+ # Don't use the 'profile' property here, because sub-classes could add
+ # further preferences and data, which would not be included in the new
+ # profile
+ self._profile = profile
+
@property
def gecko_log(self):
if self._gecko_log:
return self._gecko_log
path = self._gecko_log_option
if path != "-":
if path is None:
@@ -163,52 +166,110 @@ class GeckoInstance(object):
path = os.path.realpath(path)
if os.access(path, os.F_OK):
os.remove(path)
self._gecko_log = path
return self._gecko_log
- def _update_profile(self):
- profile_args = {"preferences": deepcopy(self.required_prefs)}
- profile_args["preferences"]["marionette.defaultPrefs.port"] = self.marionette_port
+ @property
+ def profile(self):
+ return self._profile
+
+ @profile.setter
+ def profile(self, value):
+ self._update_profile(value)
+
+ def _update_profile(self, profile=None, profile_name=None):
+ """Check if the profile has to be created, or replaced
+
+ :param profile: A Profile instance to be used.
+ :param name: Profile name to be used in the path.
+ """
+ if self.runner and self.runner.is_running():
+ raise errors.MarionetteException("The current profile can only be updated "
+ "when the instance is not running")
+
+ if isinstance(profile, Profile):
+ # Only replace the profile if it is not the current one
+ if hasattr(self, "_profile") and profile is self._profile:
+ return
+
+ else:
+ profile_args = self.profile_args
+ profile_path = profile
+
+ # If a path to a profile is given then clone it
+ if isinstance(profile_path, basestring):
+ profile_args["path_from"] = profile_path
+ profile_args["path_to"] = tempfile.mkdtemp(
+ suffix=".{}".format(profile_name or os.path.basename(profile_path)),
+ dir=self.workspace)
+ # The target must not exist yet
+ os.rmdir(profile_args["path_to"])
+
+ profile = Profile.clone(**profile_args)
+
+ # Otherwise create a new profile
+ else:
+ profile_args["profile"] = tempfile.mkdtemp(
+ suffix=".{}".format(profile_name or "mozrunner"),
+ dir=self.workspace)
+ profile = Profile(**profile_args)
+ profile.create_new = True
+
+ if isinstance(self.profile, Profile):
+ self.profile.cleanup()
+
+ self._profile = profile
+
+ def switch_profile(self, profile_name=None, clone_from=None):
+ """Switch the profile by using the given name, and optionally clone it.
+
+ Compared to :attr:`profile` this method allows to switch the profile
+ by giving control over the profile name as used for the new profile. It
+ also always creates a new blank profile, or as clone of an existent one.
+
+ :param profile_name: Optional, name of the profile, which will be used
+ as part of the profile path (folder name containing the profile).
+ :clone_from: Optional, if specified the new profile will be cloned
+ based on the given profile. This argument can be an instance of
+ ``mozprofile.Profile``, or the path of the profile.
+ """
+ if isinstance(clone_from, Profile):
+ clone_from = clone_from.profile
+
+ self._update_profile(clone_from, profile_name=profile_name)
+
+ @property
+ def profile_args(self):
+ args = {"preferences": deepcopy(self.required_prefs)}
+ args["preferences"]["marionette.defaultPrefs.port"] = self.marionette_port
+
if self.prefs:
- profile_args["preferences"].update(self.prefs)
+ args["preferences"].update(self.prefs)
+
if self.verbose:
level = "TRACE" if self.verbose >= 2 else "DEBUG"
- profile_args["preferences"]["marionette.logging"] = level
+ args["preferences"]["marionette.logging"] = level
+
if "-jsdebugger" in self.app_args:
- profile_args["preferences"].update({
+ args["preferences"].update({
"devtools.browsertoolbox.panel": "jsdebugger",
"devtools.debugger.remote-enabled": True,
"devtools.chrome.enabled": True,
"devtools.debugger.prompt-connection": False,
"marionette.debugging.clicktostart": True,
})
- if self.addons:
- profile_args["addons"] = self.addons
- if hasattr(self, "profile_path") and self.profile is None:
- if not self.profile_path:
- if self.workspace:
- profile_args["profile"] = tempfile.mkdtemp(
- suffix=".mozrunner-{:.0f}".format(time.time()),
- dir=self.workspace)
- self.profile = Profile(**profile_args)
- else:
- profile_args["path_from"] = self.profile_path
- profile_name = "{}-{:.0f}".format(
- os.path.basename(self.profile_path),
- time.time()
- )
- if self.workspace:
- profile_args["path_to"] = os.path.join(self.workspace,
- profile_name)
- self.profile = Profile.clone(**profile_args)
+ if self.addons:
+ args["addons"] = self.addons
+
+ return args
@classmethod
def create(cls, app=None, *args, **kwargs):
try:
if not app and kwargs["bin"] is not None:
app_id = mozversion.get_version(binary=kwargs["bin"])["application_id"]
app = app_ids[app_id]
@@ -216,17 +277,17 @@ class GeckoInstance(object):
except (IOError, KeyError):
exc, val, tb = sys.exc_info()
msg = 'Application "{0}" unknown (should be one of {1})'
reraise(NotImplementedError, msg.format(app, apps.keys()), tb)
return instance_class(*args, **kwargs)
def start(self):
- self._update_profile()
+ self._update_profile(self.profile)
self.runner = self.runner_class(**self._get_runner_args())
self.runner.start()
def _get_runner_args(self):
process_args = {
"processOutputLine": [NullOutput()],
}
@@ -266,33 +327,34 @@ class GeckoInstance(object):
:param clean: If True, also perform runner cleanup.
"""
if self.runner:
self.runner.stop()
if clean:
self.runner.cleanup()
- if clean and self.profile:
- self.profile.cleanup()
+ if clean:
+ if isinstance(self.profile, Profile):
+ self.profile.cleanup()
self.profile = None
def restart(self, prefs=None, clean=True):
"""
Close then start the managed Gecko process.
:param prefs: Dictionary of preference names and values.
:param clean: If True, reset the profile before starting.
"""
- self.close(clean=clean)
-
if prefs:
self.prefs = prefs
else:
self.prefs = None
+
+ self.close(clean=clean)
self.start()
class FennecInstance(GeckoInstance):
fennec_prefs = {
# Enable output of dump()
"browser.dom.window.dump.enabled": True,
@@ -345,17 +407,17 @@ class FennecInstance(GeckoInstance):
if self._package_name is None:
self._package_name = "org.mozilla.fennec"
user = os.getenv("USER")
if user:
self._package_name += "_" + user
return self._package_name
def start(self):
- self._update_profile()
+ self._update_profile(self.profile)
self.runner = self.runner_class(**self._get_runner_args())
try:
if self.connect_to_running_emulator:
self.runner.device.connect()
self.runner.start()
except Exception as e:
exc, val, tb = sys.exc_info()
message = "Error possibly due to runner or device args: {}"
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py
@@ -1,32 +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/.
from __future__ import absolute_import
+from marionette_driver import geckoinstance
from marionette_driver.errors import JavascriptException
-from marionette_harness import MarionetteTestCase
+from marionette_harness import (
+ MarionetteTestCase,
+ run_if_manage_instance,
+ skip_if_desktop,
+ skip_if_mobile
+)
class TestPreferences(MarionetteTestCase):
prefs = {
"bool": "marionette.test.bool",
"int": "marionette.test.int",
"string": "marionette.test.string",
}
def tearDown(self):
for pref in self.prefs.values():
self.marionette.clear_pref(pref)
super(TestPreferences, self).tearDown()
+ def test_gecko_instance_preferences(self):
+ required_prefs = geckoinstance.GeckoInstance.required_prefs
+
+ for key, value in required_prefs.iteritems():
+ self.assertEqual(self.marionette.get_pref(key), value,
+ "Preference {} hasn't been set to {}".format(key, value))
+
+ @skip_if_mobile("Only runnable with Firefox")
+ def test_desktop_instance_preferences(self):
+ required_prefs = geckoinstance.DesktopInstance.desktop_prefs
+
+ for key, value in required_prefs.iteritems():
+ if key in ["browser.tabs.remote.autostart"]:
+ return
+
+ self.assertEqual(self.marionette.get_pref(key), value,
+ "Preference {} hasn't been set to {}".format(key, value))
+
+ @skip_if_desktop("Only runnable with Fennec")
+ def test_fennec_instance_preferences(self):
+ required_prefs = geckoinstance.FennecInstance.required_prefs
+
+ for key, value in required_prefs.iteritems():
+ if key in ["browser.tabs.remote.autostart"]:
+ return
+
+ self.assertEqual(self.marionette.get_pref(key), value,
+ "Preference {} hasn't been set to {}".format(key, value))
+
def test_clear_pref(self):
self.assertIsNone(self.marionette.get_pref(self.prefs["bool"]))
self.marionette.set_pref(self.prefs["bool"], True)
self.assertTrue(self.marionette.get_pref(self.prefs["bool"]))
self.marionette.clear_pref(self.prefs["bool"])
self.assertIsNone(self.marionette.get_pref(self.prefs["bool"]))
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_prefs_enforce.py
@@ -0,0 +1,42 @@
+# 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 __future__ import absolute_import
+
+from marionette_harness import MarionetteTestCase
+
+
+class TestEnforcePreferences(MarionetteTestCase):
+
+ def setUp(self):
+ super(TestEnforcePreferences, self).setUp()
+
+ self.marionette.enforce_gecko_prefs({
+ "marionette.test.bool": True,
+ "marionette.test.int": 3,
+ "marionette.test.string": "testing",
+ })
+ self.marionette.set_context("chrome")
+
+ def tearDown(self):
+ self.marionette.quit(clean=True)
+
+ super(TestEnforcePreferences, self).tearDown()
+
+ def test_preferences_are_set(self):
+ self.assertTrue(self.marionette.get_pref("marionette.test.bool"))
+ self.assertEqual(self.marionette.get_pref("marionette.test.string"), "testing")
+ self.assertEqual(self.marionette.get_pref("marionette.test.int"), 3)
+
+ def test_change_preference(self):
+ self.assertTrue(self.marionette.get_pref("marionette.test.bool"))
+
+ self.marionette.enforce_gecko_prefs({"marionette.test.bool": False})
+
+ self.assertFalse(self.marionette.get_pref("marionette.test.bool"))
+
+ def test_restart_with_clean_profile(self):
+ self.marionette.restart(clean=True)
+
+ self.assertEqual(self.marionette.get_pref("marionette.test.bool"), None)
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_profile_management.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_profile_management.py
@@ -1,36 +1,235 @@
# 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 __future__ import absolute_import
+import os
+import shutil
+import tempfile
+
+import mozprofile
+
+from marionette_driver import errors
from marionette_harness import MarionetteTestCase
-class TestProfileManagement(MarionetteTestCase):
+class BaseProfileManagement(MarionetteTestCase):
+
+ def setUp(self):
+ super(BaseProfileManagement, self).setUp()
+
+ self.orig_profile_path = self.profile_path
+
+ def tearDown(self):
+ shutil.rmtree(self.orig_profile_path, ignore_errors=True)
+
+ self.marionette.profile = None
+
+ super(BaseProfileManagement, self).tearDown()
+
+ @property
+ def profile(self):
+ return self.marionette.instance.profile
+
+ @property
+ def profile_path(self):
+ return self.marionette.instance.profile.profile
+
+
+class WorkspaceProfileManagement(BaseProfileManagement):
+
+ def setUp(self):
+ super(WorkspaceProfileManagement, self).setUp()
+
+ # Set a new workspace for the instance, which will be used
+ # the next time a new profile is requested by a test.
+ self.workspace = tempfile.mkdtemp()
+ self.marionette.instance.workspace = self.workspace
+
+ def tearDown(self):
+ self.marionette.instance.workspace = None
+
+ shutil.rmtree(self.workspace, ignore_errors=True)
+
+ super(WorkspaceProfileManagement, self).tearDown()
+
+
+class ExternalProfileMixin(object):
def setUp(self):
- MarionetteTestCase.setUp(self)
- self.marionette.enforce_gecko_prefs(
- {"marionette.test.bool": True,
- "marionette.test.string": "testing",
- "marionette.test.int": 3
- })
- self.marionette.set_context("chrome")
+ super(ExternalProfileMixin, self).setUp()
+
+ # Create external profile
+ tmp_dir = tempfile.mkdtemp(suffix="external")
+ shutil.rmtree(tmp_dir, ignore_errors=True)
+
+ self.external_profile = mozprofile.Profile(profile=tmp_dir)
+ # Prevent profile from being removed during cleanup
+ self.external_profile.create_new = False
+
+ def tearDown(self):
+ shutil.rmtree(self.external_profile.profile, ignore_errors=True)
+
+ super(ExternalProfileMixin, self).tearDown()
+
+
+class TestQuitRestartWithoutWorkspace(BaseProfileManagement):
+
+ def test_quit_keeps_same_profile(self):
+ self.marionette.quit()
+ self.marionette.start_session()
+
+ self.assertEqual(self.profile_path, self.orig_profile_path)
+ self.assertTrue(os.path.exists(self.orig_profile_path))
+
+ def test_quit_clean_creates_new_profile(self):
+ self.marionette.quit(clean=True)
+ self.marionette.start_session()
+
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
+
+ def test_restart_keeps_same_profile(self):
+ self.marionette.restart()
- def test_preferences_are_set(self):
- self.assertTrue(self.marionette.get_pref("marionette.test.bool"))
- self.assertEqual(self.marionette.get_pref("marionette.test.string"), "testing")
- self.assertEqual(self.marionette.get_pref("marionette.test.int"), 3)
+ self.assertEqual(self.profile_path, self.orig_profile_path)
+ self.assertTrue(os.path.exists(self.orig_profile_path))
+
+ def test_restart_clean_creates_new_profile(self):
+ self.marionette.restart(clean=True)
+
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
+
+
+class TestQuitRestartWithWorkspace(WorkspaceProfileManagement):
+
+ def test_quit_keeps_same_profile(self):
+ self.marionette.quit()
+ self.marionette.start_session()
- def test_change_preference(self):
- self.assertTrue(self.marionette.get_pref("marionette.test.bool"))
+ self.assertEqual(self.profile_path, self.orig_profile_path)
+ self.assertNotIn(self.workspace, self.profile_path)
+ self.assertTrue(os.path.exists(self.orig_profile_path))
+
+ def test_quit_clean_creates_new_profile(self):
+ self.marionette.quit(clean=True)
+ self.marionette.start_session()
- self.marionette.enforce_gecko_prefs({"marionette.test.bool": False})
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertIn(self.workspace, self.profile_path)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
- self.assertFalse(self.marionette.get_pref("marionette.test.bool"))
+ def test_restart_keeps_same_profile(self):
+ self.marionette.restart()
- def test_clean_profile(self):
+ self.assertEqual(self.profile_path, self.orig_profile_path)
+ self.assertNotIn(self.workspace, self.profile_path)
+ self.assertTrue(os.path.exists(self.orig_profile_path))
+
+ def test_restart_clean_creates_new_profile(self):
self.marionette.restart(clean=True)
- self.assertEqual(self.marionette.get_pref("marionette.test.bool"), None)
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertIn(self.workspace, self.profile_path)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
+
+
+class TestSwitchProfileFailures(BaseProfileManagement):
+
+ def test_raise_for_switching_profile_while_instance_is_running(self):
+ with self.assertRaisesRegexp(errors.MarionetteException, "instance is not running"):
+ self.marionette.instance.switch_profile()
+
+
+class TestSwitchProfileWithoutWorkspace(ExternalProfileMixin, BaseProfileManagement):
+
+ def setUp(self):
+ super(TestSwitchProfileWithoutWorkspace, self).setUp()
+
+ self.marionette.quit()
+
+ def test_do_not_call_cleanup_of_profile_for_path_only(self):
+ # If a path to a profile has been given (eg. via the --profile command
+ # line argument) and the profile hasn't been created yet, switching the
+ # profile should not try to call `cleanup()` on a string.
+ self.marionette.instance._profile = self.external_profile.profile
+ self.marionette.instance.switch_profile()
+
+ def test_new_random_profile_name(self):
+ self.marionette.instance.switch_profile()
+ self.marionette.start_session()
+
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
+
+ def test_new_named_profile(self):
+ self.marionette.instance.switch_profile("foobar")
+ self.marionette.start_session()
+
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertIn("foobar", self.profile_path)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
+
+ def test_clone_existing_profile(self):
+ self.marionette.instance.switch_profile(clone_from=self.external_profile)
+ self.marionette.start_session()
+
+ self.assertIn(os.path.basename(self.external_profile.profile), self.profile_path)
+ self.assertTrue(os.path.exists(self.external_profile.profile))
+
+ def test_replace_with_current_profile(self):
+ self.marionette.instance.profile = self.profile
+ self.marionette.start_session()
+
+ self.assertEqual(self.profile_path, self.orig_profile_path)
+ self.assertTrue(os.path.exists(self.orig_profile_path))
+
+ def test_replace_with_external_profile(self):
+ self.marionette.instance.profile = self.external_profile
+ self.marionette.start_session()
+
+ self.assertEqual(self.profile_path, self.external_profile.profile)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
+
+ # Set a new profile and ensure the external profile has not been deleted
+ self.marionette.quit()
+ self.marionette.instance.profile = None
+
+ self.assertNotEqual(self.profile_path, self.external_profile.profile)
+ self.assertTrue(os.path.exists(self.external_profile.profile))
+
+
+class TestSwitchProfileWithWorkspace(ExternalProfileMixin, WorkspaceProfileManagement):
+
+ def setUp(self):
+ super(TestSwitchProfileWithWorkspace, self).setUp()
+
+ self.marionette.quit()
+
+ def test_new_random_profile_name(self):
+ self.marionette.instance.switch_profile()
+ self.marionette.start_session()
+
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertIn(self.workspace, self.profile_path)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
+
+ def test_new_named_profile(self):
+ self.marionette.instance.switch_profile("foobar")
+ self.marionette.start_session()
+
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertIn("foobar", self.profile_path)
+ self.assertIn(self.workspace, self.profile_path)
+ self.assertFalse(os.path.exists(self.orig_profile_path))
+
+ def test_clone_existing_profile(self):
+ self.marionette.instance.switch_profile(clone_from=self.external_profile)
+ self.marionette.start_session()
+
+ self.assertNotEqual(self.profile_path, self.orig_profile_path)
+ self.assertIn(self.workspace, self.profile_path)
+ self.assertIn(os.path.basename(self.external_profile.profile), self.profile_path)
+ self.assertTrue(os.path.exists(self.external_profile.profile))
--- a/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
@@ -1,11 +1,11 @@
[test_marionette.py]
[test_cli_arguments.py]
-skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921 and bug 1322993
+skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921
[test_geckoinstance.py]
[test_data_driven.py]
[test_session.py]
[test_capabilities.py]
[test_proxy.py]
[test_accessibility.py]
[test_expectedfail.py]
expected = fail
@@ -88,38 +88,40 @@ skip-if = appname == 'fennec'
[test_getactiveframe_oop.py]
skip-if = true # Bug 925688
[test_screen_orientation.py]
[test_errors.py]
[test_execute_isolate.py]
[test_click_scrolling.py]
[test_profile_management.py]
-skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921 and bug 1322993
+skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921
[test_quit_restart.py]
-skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921 and bug 1322993
+skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921
[test_with_using_context.py]
[test_modal_dialogs.py]
skip-if = appname == 'fennec' # Bug 1325738
[test_key_actions.py]
[test_mouse_action.py]
skip-if = appname == 'fennec'
[test_teardown_context_preserved.py]
[test_file_upload.py]
skip-if = appname == 'fennec' || os == "win" # http://bugs.python.org/issue14574
[test_execute_sandboxes.py]
[test_prefs.py]
+[test_prefs_enforce.py]
+skip-if = manage_instance == false || appname == 'fennec' # Bug 1298921
[test_shadow_dom.py]
skip-if = true # Bug 1293844, bug 1387678
[test_chrome.py]
skip-if = appname == 'fennec'
[test_addons.py]
skip-if = appname == 'fennec' # Bug 1330598
[test_select.py]
[test_crash.py]
-skip-if = asan || manage_instance == false || appname == 'fennec' || (os == 'mac' && !debug) # Bug 1298921, bug 1322993, bug 1376773
+skip-if = asan || manage_instance == false || appname == 'fennec' || (os == 'mac' && !debug) # Bug 1298921, bug 1376773
[test_localization.py]