summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozprofile
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozprofile')
-rw-r--r--testing/mozbase/mozprofile/mozprofile/__init__.py22
-rw-r--r--testing/mozbase/mozprofile/mozprofile/addons.py340
-rwxr-xr-xtesting/mozbase/mozprofile/mozprofile/cli.py206
-rw-r--r--testing/mozbase/mozprofile/mozprofile/diff.py88
-rw-r--r--testing/mozbase/mozprofile/mozprofile/permissions.py432
-rw-r--r--testing/mozbase/mozprofile/mozprofile/prefs.py239
-rw-r--r--testing/mozbase/mozprofile/mozprofile/profile.py589
-rw-r--r--testing/mozbase/mozprofile/mozprofile/view.py48
-rw-r--r--testing/mozbase/mozprofile/setup.cfg2
-rw-r--r--testing/mozbase/mozprofile/setup.py51
-rw-r--r--testing/mozbase/mozprofile/tests/addon_stubs.py69
-rw-r--r--testing/mozbase/mozprofile/tests/addons/apply-css-id-via-browser-specific-settings.xpibin0 -> 6444 bytes
-rw-r--r--testing/mozbase/mozprofile/tests/addons/apply-css-sans-id.xpibin0 -> 3371 bytes
-rw-r--r--testing/mozbase/mozprofile/tests/addons/apply-css.xpibin0 -> 3412 bytes
-rw-r--r--testing/mozbase/mozprofile/tests/addons/empty.xpibin0 -> 530 bytes
-rw-r--r--testing/mozbase/mozprofile/tests/addons/empty/install.rdf20
-rw-r--r--testing/mozbase/mozprofile/tests/addons/invalid.xpibin0 -> 564 bytes
-rw-r--r--testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js7
-rw-r--r--testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences1
-rw-r--r--testing/mozbase/mozprofile/tests/files/dummy-profile/extensions/empty.xpibin0 -> 530 bytes
-rw-r--r--testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js1
-rw-r--r--testing/mozbase/mozprofile/tests/files/dummy-profile/user.js1
-rw-r--r--testing/mozbase/mozprofile/tests/files/not_an_addon.txt0
-rw-r--r--testing/mozbase/mozprofile/tests/files/prefs_with_comments.js6
-rw-r--r--testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js5
-rw-r--r--testing/mozbase/mozprofile/tests/install_manifests/test_addon_1.rdf21
-rw-r--r--testing/mozbase/mozprofile/tests/install_manifests/test_addon_2.rdf21
-rw-r--r--testing/mozbase/mozprofile/tests/install_manifests/test_addon_3.rdf22
-rw-r--r--testing/mozbase/mozprofile/tests/install_manifests/test_addon_4.rdf22
-rw-r--r--testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_no_id.rdf22
-rw-r--r--testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_not_wellformed.rdf23
-rw-r--r--testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_version.rdf23
-rw-r--r--testing/mozbase/mozprofile/tests/install_manifests/test_addon_unpack.rdf22
-rw-r--r--testing/mozbase/mozprofile/tests/manifest.ini13
-rwxr-xr-xtesting/mozbase/mozprofile/tests/test_addonid.py161
-rw-r--r--testing/mozbase/mozprofile/tests/test_addons.py376
-rwxr-xr-xtesting/mozbase/mozprofile/tests/test_bug758250.py47
-rw-r--r--testing/mozbase/mozprofile/tests/test_chrome_profile.py75
-rw-r--r--testing/mozbase/mozprofile/tests/test_clone_cleanup.py81
-rwxr-xr-xtesting/mozbase/mozprofile/tests/test_nonce.py44
-rwxr-xr-xtesting/mozbase/mozprofile/tests/test_permissions.py225
-rwxr-xr-xtesting/mozbase/mozprofile/tests/test_preferences.py422
-rw-r--r--testing/mozbase/mozprofile/tests/test_profile.py114
-rw-r--r--testing/mozbase/mozprofile/tests/test_profile_view.py78
-rw-r--r--testing/mozbase/mozprofile/tests/test_server_locations.py151
45 files changed, 4090 insertions, 0 deletions
diff --git a/testing/mozbase/mozprofile/mozprofile/__init__.py b/testing/mozbase/mozprofile/mozprofile/__init__.py
new file mode 100644
index 0000000000..fa1353d6f6
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/__init__.py
@@ -0,0 +1,22 @@
+# flake8: noqa
+# 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/.
+
+"""
+To use mozprofile as an API you can import mozprofile.profile_ and/or the AddonManager_.
+
+``mozprofile.profile`` features a generic ``Profile`` class. In addition,
+subclasses ``FirefoxProfile`` and ``ThundebirdProfile`` are available
+with preset preferences for those applications.
+"""
+
+from __future__ import absolute_import
+
+from mozprofile.addons import *
+from mozprofile.cli import *
+from mozprofile.diff import *
+from mozprofile.permissions import *
+from mozprofile.prefs import *
+from mozprofile.profile import *
+from mozprofile.view import *
diff --git a/testing/mozbase/mozprofile/mozprofile/addons.py b/testing/mozbase/mozprofile/mozprofile/addons.py
new file mode 100644
index 0000000000..a99a8ecefb
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/addons.py
@@ -0,0 +1,340 @@
+# 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 json
+import os
+import sys
+import shutil
+import tempfile
+import zipfile
+import hashlib
+import binascii
+from xml.dom import minidom
+from six import reraise, string_types
+
+import mozfile
+from mozlog.unstructured import getLogger
+
+_SALT = binascii.hexlify(os.urandom(32))
+_TEMPORARY_ADDON_SUFFIX = "@temporary-addon"
+
+# Logger for 'mozprofile.addons' module
+module_logger = getLogger(__name__)
+
+
+class AddonFormatError(Exception):
+ """Exception for not well-formed add-on manifest files"""
+
+
+class AddonManager(object):
+ """
+ Handles all operations regarding addons in a profile including:
+ installing and cleaning addons
+ """
+
+ def __init__(self, profile, restore=True):
+ """
+ :param profile: the path to the profile for which we install addons
+ :param restore: whether to reset to the previous state on instance garbage collection
+ """
+ self.profile = profile
+ self.restore = restore
+
+ # Initialize all class members
+ self._internal_init()
+
+ def _internal_init(self):
+ """Internal: Initialize all class members to their default value"""
+
+ # Add-ons installed; needed for cleanup
+ self._addons = []
+
+ # Backup folder for already existing addons
+ self.backup_dir = None
+
+ # Information needed for profile reset (see http://bit.ly/17JesUf)
+ self.installed_addons = []
+
+ def __del__(self):
+ # reset to pre-instance state
+ if self.restore:
+ self.clean()
+
+ def clean(self):
+ """Clean up addons in the profile."""
+
+ # Remove all add-ons installed
+ for addon in self._addons:
+ # TODO (bug 934642)
+ # Once we have a proper handling of add-ons we should kill the id
+ # from self._addons once the add-on is removed. For now lets forget
+ # about the exception
+ try:
+ self.remove_addon(addon)
+ except IOError:
+ pass
+
+ # restore backups
+ if self.backup_dir and os.path.isdir(self.backup_dir):
+ extensions_path = os.path.join(self.profile, "extensions")
+
+ for backup in os.listdir(self.backup_dir):
+ backup_path = os.path.join(self.backup_dir, backup)
+ shutil.move(backup_path, extensions_path)
+
+ if not os.listdir(self.backup_dir):
+ mozfile.remove(self.backup_dir)
+
+ # reset instance variables to defaults
+ self._internal_init()
+
+ def get_addon_path(self, addon_id):
+ """Returns the path to the installed add-on
+
+ :param addon_id: id of the add-on to retrieve the path from
+ """
+ # By default we should expect add-ons being located under the
+ # extensions folder.
+ extensions_path = os.path.join(self.profile, "extensions")
+ paths = [
+ os.path.join(extensions_path, addon_id),
+ os.path.join(extensions_path, addon_id + ".xpi"),
+ ]
+ for path in paths:
+ if os.path.exists(path):
+ return path
+
+ raise IOError("Add-on not found: %s" % addon_id)
+
+ @classmethod
+ def is_addon(self, addon_path):
+ """
+ Checks if the given path is a valid addon
+
+ :param addon_path: path to the add-on directory or XPI
+ """
+ try:
+ self.addon_details(addon_path)
+ return True
+ except AddonFormatError:
+ return False
+
+ def _install_addon(self, path, unpack=False):
+ addons = [path]
+
+ # if path is not an add-on, try to install all contained add-ons
+ try:
+ self.addon_details(path)
+ except AddonFormatError as e:
+ module_logger.warning("Could not install %s: %s" % (path, str(e)))
+
+ # If the path doesn't exist, then we don't really care, just return
+ if not os.path.isdir(path):
+ return
+
+ addons = [
+ os.path.join(path, x)
+ for x in os.listdir(path)
+ if self.is_addon(os.path.join(path, x))
+ ]
+ addons.sort()
+
+ # install each addon
+ for addon in addons:
+ # determine the addon id
+ addon_details = self.addon_details(addon)
+ addon_id = addon_details.get("id")
+
+ # if the add-on has to be unpacked force it now
+ # note: we might want to let Firefox do it in case of addon details
+ orig_path = None
+ if os.path.isfile(addon) and (unpack or addon_details["unpack"]):
+ orig_path = addon
+ addon = tempfile.mkdtemp()
+ mozfile.extract(orig_path, addon)
+
+ # copy the addon to the profile
+ extensions_path = os.path.join(self.profile, "extensions")
+ addon_path = os.path.join(extensions_path, addon_id)
+
+ if os.path.isfile(addon):
+ addon_path += ".xpi"
+
+ # move existing xpi file to backup location to restore later
+ if os.path.exists(addon_path):
+ self.backup_dir = self.backup_dir or tempfile.mkdtemp()
+ shutil.move(addon_path, self.backup_dir)
+
+ # copy new add-on to the extension folder
+ if not os.path.exists(extensions_path):
+ os.makedirs(extensions_path)
+ shutil.copy(addon, addon_path)
+ else:
+ # move existing folder to backup location to restore later
+ if os.path.exists(addon_path):
+ self.backup_dir = self.backup_dir or tempfile.mkdtemp()
+ shutil.move(addon_path, self.backup_dir)
+
+ # copy new add-on to the extension folder
+ shutil.copytree(addon, addon_path, symlinks=True)
+
+ # if we had to extract the addon, remove the temporary directory
+ if orig_path:
+ mozfile.remove(addon)
+ addon = orig_path
+
+ self._addons.append(addon_id)
+ self.installed_addons.append(addon)
+
+ def install(self, addons, **kwargs):
+ """
+ Installs addons from a filepath or directory of addons in the profile.
+
+ :param addons: paths to .xpi or addon directories
+ :param unpack: whether to unpack unless specified otherwise in the install.rdf
+ """
+ if not addons:
+ return
+
+ # install addon paths
+ if isinstance(addons, string_types):
+ addons = [addons]
+ for addon in set(addons):
+ self._install_addon(addon, **kwargs)
+
+ @classmethod
+ def _gen_iid(cls, addon_path):
+ hash = hashlib.sha1(_SALT)
+ hash.update(addon_path.encode())
+ return hash.hexdigest() + _TEMPORARY_ADDON_SUFFIX
+
+ @classmethod
+ def addon_details(cls, addon_path):
+ """
+ Returns a dictionary of details about the addon.
+
+ :param addon_path: path to the add-on directory or XPI
+
+ Returns::
+
+ {'id': u'rainbow@colors.org', # id of the addon
+ 'version': u'1.4', # version of the addon
+ 'name': u'Rainbow', # name of the addon
+ 'unpack': False } # whether to unpack the addon
+ """
+
+ details = {"id": None, "unpack": False, "name": None, "version": None}
+
+ def get_namespace_id(doc, url):
+ attributes = doc.documentElement.attributes
+ namespace = ""
+ for i in range(attributes.length):
+ if attributes.item(i).value == url:
+ if ":" in attributes.item(i).name:
+ # If the namespace is not the default one remove 'xlmns:'
+ namespace = attributes.item(i).name.split(":")[1] + ":"
+ break
+ return namespace
+
+ def get_text(element):
+ """Retrieve the text value of a given node"""
+ rc = []
+ for node in element.childNodes:
+ if node.nodeType == node.TEXT_NODE:
+ rc.append(node.data)
+ return "".join(rc).strip()
+
+ if not os.path.exists(addon_path):
+ raise IOError("Add-on path does not exist: %s" % addon_path)
+
+ is_webext = False
+ try:
+ if zipfile.is_zipfile(addon_path):
+ # Bug 944361 - We cannot use 'with' together with zipFile because
+ # it will cause an exception thrown in Python 2.6.
+ try:
+ compressed_file = zipfile.ZipFile(addon_path, "r")
+ filenames = [f.filename for f in (compressed_file).filelist]
+ if "install.rdf" in filenames:
+ manifest = compressed_file.read("install.rdf")
+ elif "manifest.json" in filenames:
+ is_webext = True
+ manifest = compressed_file.read("manifest.json").decode()
+ manifest = json.loads(manifest)
+ else:
+ raise KeyError("No manifest")
+ finally:
+ compressed_file.close()
+ elif os.path.isdir(addon_path):
+ try:
+ with open(os.path.join(addon_path, "install.rdf")) as f:
+ manifest = f.read()
+ except IOError:
+ with open(os.path.join(addon_path, "manifest.json")) as f:
+ manifest = json.loads(f.read())
+ is_webext = True
+ else:
+ raise IOError(
+ "Add-on path is neither an XPI nor a directory: %s" % addon_path
+ )
+ except (IOError, KeyError) as e:
+ reraise(AddonFormatError, AddonFormatError(str(e)), sys.exc_info()[2])
+
+ if is_webext:
+ details["version"] = manifest["version"]
+ details["name"] = manifest["name"]
+ # Bug 1572404 - we support two locations for gecko-specific
+ # metadata.
+ for location in ("applications", "browser_specific_settings"):
+ try:
+ details["id"] = manifest[location]["gecko"]["id"]
+ break
+ except KeyError:
+ pass
+ if details["id"] is None:
+ details["id"] = cls._gen_iid(addon_path)
+ details["unpack"] = False
+ else:
+ try:
+ doc = minidom.parseString(manifest)
+
+ # Get the namespaces abbreviations
+ em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
+ rdf = get_namespace_id(
+ doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ )
+
+ description = doc.getElementsByTagName(rdf + "Description").item(0)
+ for entry, value in description.attributes.items():
+ # Remove the namespace prefix from the tag for comparison
+ entry = entry.replace(em, "")
+ if entry in details.keys():
+ details.update({entry: value})
+ for node in description.childNodes:
+ # Remove the namespace prefix from the tag for comparison
+ entry = node.nodeName.replace(em, "")
+ if entry in details.keys():
+ details.update({entry: get_text(node)})
+ except Exception as e:
+ reraise(AddonFormatError, AddonFormatError(str(e)), sys.exc_info()[2])
+
+ # turn unpack into a true/false value
+ if isinstance(details["unpack"], string_types):
+ details["unpack"] = details["unpack"].lower() == "true"
+
+ # If no ID is set, the add-on is invalid
+ if details.get("id") is None and not is_webext:
+ raise AddonFormatError("Add-on id could not be found.")
+
+ return details
+
+ def remove_addon(self, addon_id):
+ """Remove the add-on as specified by the id
+
+ :param addon_id: id of the add-on to be removed
+ """
+ path = self.get_addon_path(addon_id)
+ mozfile.remove(path)
diff --git a/testing/mozbase/mozprofile/mozprofile/cli.py b/testing/mozbase/mozprofile/mozprofile/cli.py
new file mode 100755
index 0000000000..44bb3361c1
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/cli.py
@@ -0,0 +1,206 @@
+#!/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/.
+
+"""
+Creates and/or modifies a Firefox profile.
+The profile can be modified by passing in addons to install or preferences to set.
+If no profile is specified, a new profile is created and the path of the
+resulting profile is printed.
+"""
+from __future__ import absolute_import, print_function
+
+import sys
+from optparse import OptionParser
+from .prefs import Preferences
+from .profile import FirefoxProfile
+from .profile import Profile
+
+__all__ = [
+ "MozProfileCLI",
+ "cli",
+ "KeyValueParseError",
+ "parse_key_value",
+ "parse_preferences",
+]
+
+
+class KeyValueParseError(Exception):
+ """Error when parsing strings of serialized key-values."""
+
+ def __init__(self, msg, errors=()):
+ self.errors = errors
+ Exception.__init__(self, msg)
+
+
+def parse_key_value(strings, separator="=", context="key, value"):
+ """Parse string-serialized key-value pairs in the form of `key = value`.
+
+ Args:
+ strings (list): List of strings to parse.
+ separator (str): Identifier used to split the strings.
+
+ Returns:
+ list: A list of (<key>, <value>) tuples. Whitespace is not stripped.
+
+ Raises:
+ KeyValueParseError
+ """
+
+ # syntax check
+ missing = [string for string in strings if separator not in string]
+ if missing:
+ raise KeyValueParseError(
+ "Error: syntax error in %s: %s" % (context, ",".join(missing)),
+ errors=missing,
+ )
+ return [string.split(separator, 1) for string in strings]
+
+
+def parse_preferences(prefs, context="--setpref="):
+ """Parse preferences specified on the command line.
+
+ Args:
+ prefs (list): A list of strings, usually of the form "<pref>=<value>".
+
+ Returns:
+ dict: A dictionary of the form {<pref>: <value>} where values have been
+ cast.
+ """
+ try:
+ prefs = dict(parse_key_value(prefs, context=context))
+ except KeyValueParseError as e:
+ print(str(e))
+ sys.exit(1)
+
+ return {k: Preferences.cast(v) for k, v in prefs.items()}
+
+
+class MozProfileCLI(object):
+ """The Command Line Interface for ``mozprofile``."""
+
+ module = "mozprofile"
+ profile_class = Profile
+
+ def __init__(self, args=sys.argv[1:], add_options=None):
+ self.parser = OptionParser(description=__doc__)
+ self.add_options(self.parser)
+ if add_options:
+ add_options(self.parser)
+ (self.options, self.args) = self.parser.parse_args(args)
+
+ def add_options(self, parser):
+
+ parser.add_option(
+ "-p",
+ "--profile",
+ dest="profile",
+ help="The path to the profile to operate on. "
+ "If none, creates a new profile in temp directory",
+ )
+ parser.add_option(
+ "-a",
+ "--addon",
+ dest="addons",
+ action="append",
+ default=[],
+ help="Addon paths to install. Can be a filepath, "
+ "a directory containing addons, or a url",
+ )
+ parser.add_option(
+ "--pref",
+ dest="prefs",
+ action="append",
+ default=[],
+ help="A preference to set. " "Must be a key-value pair separated by a ':'",
+ )
+ parser.add_option(
+ "--preferences",
+ dest="prefs_files",
+ action="append",
+ default=[],
+ metavar="FILE",
+ help="read preferences from a JSON or INI file. "
+ "For INI, use 'file.ini:section' to specify a particular section.",
+ )
+
+ def profile_args(self):
+ """arguments to instantiate the profile class"""
+ return dict(
+ profile=self.options.profile,
+ addons=self.options.addons,
+ preferences=self.preferences(),
+ )
+
+ def preferences(self):
+ """profile preferences"""
+
+ # object to hold preferences
+ prefs = Preferences()
+
+ # add preferences files
+ for prefs_file in self.options.prefs_files:
+ prefs.add_file(prefs_file)
+
+ # change CLI preferences into 2-tuples
+ cli_prefs = parse_key_value(self.options.prefs, separator=":")
+
+ # string preferences
+ prefs.add(cli_prefs, cast=True)
+
+ return prefs()
+
+ def profile(self, restore=False):
+ """create the profile"""
+
+ kwargs = self.profile_args()
+ kwargs["restore"] = restore
+ return self.profile_class(**kwargs)
+
+
+def cli(args=sys.argv[1:]):
+ """ Handles the command line arguments for ``mozprofile`` via ``sys.argv``"""
+
+ # add a view method for this cli method only
+ def add_options(parser):
+ parser.add_option(
+ "--view",
+ dest="view",
+ action="store_true",
+ default=False,
+ help="view summary of profile following invocation",
+ )
+ parser.add_option(
+ "--firefox",
+ dest="firefox_profile",
+ action="store_true",
+ default=False,
+ help="use FirefoxProfile defaults",
+ )
+
+ # process the command line
+ cli = MozProfileCLI(args, add_options)
+
+ if cli.args:
+ cli.parser.error("Program doesn't support positional arguments.")
+
+ if cli.options.firefox_profile:
+ cli.profile_class = FirefoxProfile
+
+ # create the profile
+ profile = cli.profile()
+
+ if cli.options.view:
+ # view the profile, if specified
+ print(profile.summary())
+ return
+
+ # if no profile was passed in print the newly created profile
+ if not cli.options.profile:
+ print(profile.profile)
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/testing/mozbase/mozprofile/mozprofile/diff.py b/testing/mozbase/mozprofile/mozprofile/diff.py
new file mode 100644
index 0000000000..81dba50a15
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/diff.py
@@ -0,0 +1,88 @@
+#!/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/.
+
+
+"""
+diff two profile summaries
+"""
+
+from __future__ import absolute_import, print_function
+
+import difflib
+import profile
+import optparse
+import os
+import sys
+
+__all__ = ["diff", "diff_profiles"]
+
+
+def diff(profile1, profile2, diff_function=difflib.unified_diff):
+
+ profiles = (profile1, profile2)
+ parts = {}
+ parts_dict = {}
+ for index in (0, 1):
+ prof = profiles[index]
+
+ # first part, the path, isn't useful for diffing
+ parts[index] = prof.summary(return_parts=True)[1:]
+
+ parts_dict[index] = dict(parts[index])
+
+ # keys the first profile is missing
+ first_missing = [i for i in parts_dict[1] if i not in parts_dict[0]]
+ parts[0].extend([(i, "") for i in first_missing])
+
+ # diffs
+ retval = []
+ for key, value in parts[0]:
+ other = parts_dict[1].get(key, "")
+ value = value.strip()
+ other = other.strip()
+
+ if key == "Files":
+ # first line of files is the path; we don't care to diff that
+ value = "\n".join(value.splitlines()[1:])
+ if other:
+ other = "\n".join(other.splitlines()[1:])
+
+ value = value.splitlines()
+ other = other.splitlines()
+ section_diff = list(
+ diff_function(value, other, profile1.profile, profile2.profile)
+ )
+ if section_diff:
+ retval.append((key, "\n".join(section_diff)))
+
+ return retval
+
+
+def diff_profiles(args=sys.argv[1:]):
+
+ # parse command line
+ usage = "%prog [options] profile1 profile2"
+ parser = optparse.OptionParser(usage=usage, description=__doc__)
+ options, args = parser.parse_args(args)
+ if len(args) != 2:
+ parser.error("Must give two profile paths")
+ missing = [arg for arg in args if not os.path.exists(arg)]
+ if missing:
+ parser.error("Profile not found: %s" % (", ".join(missing)))
+
+ # get the profile differences
+ diffs = diff(*([profile.Profile(arg) for arg in args]))
+
+ # display them
+ while diffs:
+ key, value = diffs.pop(0)
+ print("[%s]:\n" % key)
+ print(value)
+ if diffs:
+ print("-" * 4)
+
+
+if __name__ == "__main__":
+ diff_profiles()
diff --git a/testing/mozbase/mozprofile/mozprofile/permissions.py b/testing/mozbase/mozprofile/mozprofile/permissions.py
new file mode 100644
index 0000000000..5d456bf93f
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/permissions.py
@@ -0,0 +1,432 @@
+# 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/.
+
+
+"""
+add permissions to the profile
+"""
+
+from __future__ import absolute_import
+
+import codecs
+import os
+import sqlite3
+
+from six import string_types
+from six.moves.urllib import parse
+import six
+
+__all__ = [
+ "MissingPrimaryLocationError",
+ "MultiplePrimaryLocationsError",
+ "DEFAULT_PORTS",
+ "DuplicateLocationError",
+ "BadPortLocationError",
+ "LocationsSyntaxError",
+ "Location",
+ "ServerLocations",
+ "Permissions",
+]
+
+# http://hg.mozilla.org/mozilla-central/file/b871dfb2186f/build/automation.py.in#l28
+DEFAULT_PORTS = {"http": "8888", "https": "4443", "ws": "4443", "wss": "4443"}
+
+
+class LocationError(Exception):
+ """Signifies an improperly formed location."""
+
+ def __str__(self):
+ s = "Bad location"
+ m = str(Exception.__str__(self))
+ if m:
+ s += ": %s" % m
+ return s
+
+
+class MissingPrimaryLocationError(LocationError):
+ """No primary location defined in locations file."""
+
+ def __init__(self):
+ LocationError.__init__(self, "missing primary location")
+
+
+class MultiplePrimaryLocationsError(LocationError):
+ """More than one primary location defined."""
+
+ def __init__(self):
+ LocationError.__init__(self, "multiple primary locations")
+
+
+class DuplicateLocationError(LocationError):
+ """Same location defined twice."""
+
+ def __init__(self, url):
+ LocationError.__init__(self, "duplicate location: %s" % url)
+
+
+class BadPortLocationError(LocationError):
+ """Location has invalid port value."""
+
+ def __init__(self, given_port):
+ LocationError.__init__(self, "bad value for port: %s" % given_port)
+
+
+class LocationsSyntaxError(Exception):
+ """Signifies a syntax error on a particular line in server-locations.txt."""
+
+ def __init__(self, lineno, err=None):
+ self.err = err
+ self.lineno = lineno
+
+ def __str__(self):
+ s = "Syntax error on line %s" % self.lineno
+ if self.err:
+ s += ": %s." % self.err
+ else:
+ s += "."
+ return s
+
+
+class Location(object):
+ """Represents a location line in server-locations.txt."""
+
+ attrs = ("scheme", "host", "port")
+
+ def __init__(self, scheme, host, port, options):
+ for attr in self.attrs:
+ setattr(self, attr, locals()[attr])
+ self.options = options
+ try:
+ int(self.port)
+ except ValueError:
+ raise BadPortLocationError(self.port)
+
+ def isEqual(self, location):
+ """compare scheme://host:port, but ignore options"""
+ return len(
+ [i for i in self.attrs if getattr(self, i) == getattr(location, i)]
+ ) == len(self.attrs)
+
+ __eq__ = isEqual
+
+ def __hash__(self):
+ # pylint --py3k: W1641
+ return hash(tuple(getattr(attr) for attr in self.attrs))
+
+ def url(self):
+ return "%s://%s:%s" % (self.scheme, self.host, self.port)
+
+ def __str__(self):
+ return "%s %s" % (self.url(), ",".join(self.options))
+
+
+class ServerLocations(object):
+ """Iterable collection of locations.
+ Use provided functions to add new locations, rather that manipulating
+ _locations directly, in order to check for errors and to ensure the
+ callback is called, if given.
+ """
+
+ def __init__(self, filename=None, add_callback=None):
+ self.add_callback = add_callback
+ self._locations = []
+ self.hasPrimary = False
+ if filename:
+ self.read(filename)
+
+ def __iter__(self):
+ return self._locations.__iter__()
+
+ def __len__(self):
+ return len(self._locations)
+
+ def add(self, location, suppress_callback=False):
+ if "primary" in location.options:
+ if self.hasPrimary:
+ raise MultiplePrimaryLocationsError()
+ self.hasPrimary = True
+
+ self._locations.append(location)
+ if self.add_callback and not suppress_callback:
+ self.add_callback([location])
+
+ def add_host(self, host, port="80", scheme="http", options="privileged"):
+ if isinstance(options, string_types):
+ options = options.split(",")
+ self.add(Location(scheme, host, port, options))
+
+ def read(self, filename, check_for_primary=True):
+ """
+ Reads the file and adds all valid locations to the ``self._locations`` array.
+
+ :param filename: in the format of server-locations.txt_
+ :param check_for_primary: if True, a ``MissingPrimaryLocationError`` exception is raised
+ if no primary is found
+
+ .. _server-locations.txt: http://searchfox.org/mozilla-central/source/build/pgo/server-locations.txt # noqa
+
+ The only exception is that the port, if not defined, defaults to 80 or 443.
+
+ FIXME: Shouldn't this default to the protocol-appropriate port? Is
+ there any reason to have defaults at all?
+ """
+
+ locationFile = codecs.open(filename, "r", "UTF-8")
+ lineno = 0
+ new_locations = []
+
+ for line in locationFile:
+ line = line.strip()
+ lineno += 1
+
+ # check for comments and blank lines
+ if line.startswith("#") or not line:
+ continue
+
+ # split the server from the options
+ try:
+ server, options = line.rsplit(None, 1)
+ options = options.split(",")
+ except ValueError:
+ server = line
+ options = []
+
+ # parse the server url
+ if "://" not in server:
+ server = "http://" + server
+ scheme, netloc, path, query, fragment = parse.urlsplit(server)
+ # get the host and port
+ try:
+ host, port = netloc.rsplit(":", 1)
+ except ValueError:
+ host = netloc
+ port = DEFAULT_PORTS.get(scheme, "80")
+
+ try:
+ location = Location(scheme, host, port, options)
+ self.add(location, suppress_callback=True)
+ except LocationError as e:
+ raise LocationsSyntaxError(lineno, e)
+
+ new_locations.append(location)
+
+ # ensure that a primary is found
+ if check_for_primary and not self.hasPrimary:
+ raise LocationsSyntaxError(lineno + 1, MissingPrimaryLocationError())
+
+ if self.add_callback:
+ self.add_callback(new_locations)
+
+
+class Permissions(object):
+ """Allows handling of permissions for ``mozprofile``"""
+
+ def __init__(self, profileDir, locations=None):
+ self._profileDir = profileDir
+ self._locations = ServerLocations(add_callback=self.write_db)
+ if locations:
+ if isinstance(locations, ServerLocations):
+ self._locations = locations
+ self._locations.add_callback = self.write_db
+ self.write_db(self._locations._locations)
+ elif isinstance(locations, list):
+ for l in locations:
+ self._locations.add_host(**l)
+ elif isinstance(locations, dict):
+ self._locations.add_host(**locations)
+ elif os.path.exists(locations):
+ self._locations.read(locations)
+
+ def write_db(self, locations):
+ """write permissions to the sqlite database"""
+
+ # Open database and create table
+ permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sqlite"))
+ cursor = permDB.cursor()
+
+ # SQL copied from
+ # http://searchfox.org/mozilla-central/source/extensions/permissions/PermissionManager.cpp
+ cursor.execute(
+ """CREATE TABLE IF NOT EXISTS moz_hosts (
+ id INTEGER PRIMARY KEY
+ ,origin TEXT
+ ,type TEXT
+ ,permission INTEGER
+ ,expireType INTEGER
+ ,expireTime INTEGER
+ ,modificationTime INTEGER
+ )"""
+ )
+
+ rows = cursor.execute("PRAGMA table_info(moz_hosts)")
+ count = len(rows.fetchall())
+
+ using_origin = False
+ # if the db contains 7 columns, we're using user_version 5
+ if count == 7:
+ statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0)"
+ cursor.execute("PRAGMA user_version=5;")
+ using_origin = True
+ # if the db contains 9 columns, we're using user_version 4
+ elif count == 9:
+ statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0, 0)"
+ cursor.execute("PRAGMA user_version=4;")
+ # if the db contains 8 columns, we're using user_version 3
+ elif count == 8:
+ statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)"
+ cursor.execute("PRAGMA user_version=3;")
+ else:
+ statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0)"
+ cursor.execute("PRAGMA user_version=2;")
+
+ for location in locations:
+ # set the permissions
+ permissions = {"allowXULXBL": "noxul" not in location.options}
+ for perm, allow in six.iteritems(permissions):
+ if allow:
+ permission_type = 1
+ else:
+ permission_type = 2
+
+ if using_origin:
+ # This is a crude approximation of the origin generation
+ # logic from ContentPrincipal and nsStandardURL. It should
+ # suffice for the permissions which the test runners will
+ # want to insert into the system.
+ origin = location.scheme + "://" + location.host
+ if (location.scheme != "http" or location.port != "80") and (
+ location.scheme != "https" or location.port != "443"
+ ):
+ origin += ":" + str(location.port)
+
+ cursor.execute(statement, (origin, perm, permission_type))
+ else:
+ # The database is still using a legacy system based on hosts
+ # We can insert the permission as a host
+ #
+ # XXX This codepath should not be hit, as tests are run with
+ # fresh profiles. However, if it was hit, permissions would
+ # not be added to the database correctly (bug 1183185).
+ cursor.execute(statement, (location.host, perm, permission_type))
+
+ # Commit and close
+ permDB.commit()
+ cursor.close()
+
+ def network_prefs(self, proxy=None):
+ """
+ take known locations and generate preferences to handle permissions and proxy
+ returns a tuple of prefs, user_prefs
+ """
+
+ prefs = []
+
+ if proxy:
+ user_prefs = self.pac_prefs(proxy)
+ else:
+ user_prefs = []
+
+ return prefs, user_prefs
+
+ def pac_prefs(self, user_proxy=None):
+ """
+ return preferences for Proxy Auto Config.
+ """
+ proxy = DEFAULT_PORTS.copy()
+
+ # We need to proxy every server but the primary one.
+ origins = ["'%s'" % l.url() for l in self._locations]
+ origins = ", ".join(origins)
+ proxy["origins"] = origins
+
+ for l in self._locations:
+ if "primary" in l.options:
+ proxy["remote"] = l.host
+ proxy[l.scheme] = l.port
+
+ # overwrite defaults with user specified proxy
+ if isinstance(user_proxy, dict):
+ proxy.update(user_proxy)
+
+ # TODO: this should live in a template!
+ # If you must escape things in this string with backslashes, be aware
+ # of the multiple layers of escaping at work:
+ #
+ # - Python will unescape backslashes;
+ # - Writing out the prefs will escape things via JSON serialization;
+ # - The prefs file reader will unescape backslashes;
+ # - The JS engine parser will unescape backslashes.
+ pacURL = (
+ """data:text/plain,
+var knownOrigins = (function () {
+ return [%(origins)s].reduce(function(t, h) { t[h] = true; return t; }, {})
+})();
+var uriRegex = new RegExp('^([a-z][-a-z0-9+.]*)' +
+ '://' +
+ '(?:[^/@]*@)?' +
+ '(.*?)' +
+ '(?::(\\\\d+))?/');
+var defaultPortsForScheme = {
+ 'http': 80,
+ 'ws': 80,
+ 'https': 443,
+ 'wss': 443
+};
+var originSchemesRemap = {
+ 'ws': 'http',
+ 'wss': 'https'
+};
+var proxyForScheme = {
+ 'http': 'PROXY %(remote)s:%(http)s',
+ 'https': 'PROXY %(remote)s:%(https)s',
+ 'ws': 'PROXY %(remote)s:%(ws)s',
+ 'wss': 'PROXY %(remote)s:%(wss)s'
+};
+
+function FindProxyForURL(url, host)
+{
+ var matches = uriRegex.exec(url);
+ if (!matches)
+ return 'DIRECT';
+ var originalScheme = matches[1];
+ var host = matches[2];
+ var port = matches[3];
+ if (!port && originalScheme in defaultPortsForScheme) {
+ port = defaultPortsForScheme[originalScheme];
+ }
+ var schemeForOriginChecking = originSchemesRemap[originalScheme] || originalScheme;
+
+ var origin = schemeForOriginChecking + '://' + host + ':' + port;
+ if (!(origin in knownOrigins))
+ return 'DIRECT';
+ return proxyForScheme[originalScheme] || 'DIRECT';
+}"""
+ % proxy
+ )
+ pacURL = "".join(pacURL.splitlines())
+
+ prefs = []
+ prefs.append(("network.proxy.type", 2))
+ prefs.append(("network.proxy.autoconfig_url", pacURL))
+
+ return prefs
+
+ def clean_db(self):
+ """Removed permissions added by mozprofile."""
+
+ sqlite_file = os.path.join(self._profileDir, "permissions.sqlite")
+ if not os.path.exists(sqlite_file):
+ return
+
+ # Open database and create table
+ permDB = sqlite3.connect(sqlite_file)
+ cursor = permDB.cursor()
+
+ # TODO: only delete values that we add, this would require sending
+ # in the full permissions object
+ cursor.execute("DROP TABLE IF EXISTS moz_hosts")
+
+ # Commit and close
+ permDB.commit()
+ cursor.close()
diff --git a/testing/mozbase/mozprofile/mozprofile/prefs.py b/testing/mozbase/mozprofile/mozprofile/prefs.py
new file mode 100644
index 0000000000..b7b80a3afa
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/prefs.py
@@ -0,0 +1,239 @@
+# 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/.
+
+"""
+user preferences
+"""
+from __future__ import absolute_import, print_function
+
+import json
+import mozfile
+import os
+import tokenize
+
+from six.moves.configparser import SafeConfigParser as ConfigParser
+from six import StringIO, string_types
+
+__all__ = ("PreferencesReadError", "Preferences")
+
+
+class PreferencesReadError(Exception):
+ """read error for prefrences files"""
+
+
+class Preferences(object):
+ """assembly of preferences from various sources"""
+
+ def __init__(self, prefs=None):
+ self._prefs = []
+ if prefs:
+ self.add(prefs)
+
+ def add(self, prefs, cast=False):
+ """
+ :param prefs:
+ :param cast: whether to cast strings to value, e.g. '1' -> 1
+ """
+ # wants a list of 2-tuples
+ if isinstance(prefs, dict):
+ prefs = prefs.items()
+ if cast:
+ prefs = [(i, self.cast(j)) for i, j in prefs]
+ self._prefs += prefs
+
+ def add_file(self, path):
+ """a preferences from a file
+
+ :param path:
+ """
+ self.add(self.read(path))
+
+ def __call__(self):
+ return self._prefs
+
+ @classmethod
+ def cast(cls, value):
+ """
+ interpolate a preference from a string
+ from the command line or from e.g. an .ini file, there is no good way to denote
+ what type the preference value is, as natively it is a string
+
+ - integers will get cast to integers
+ - true/false will get cast to True/False
+ - anything enclosed in single quotes will be treated as a string
+ with the ''s removed from both sides
+ """
+
+ if not isinstance(value, string_types):
+ return value # no op
+ quote = "'"
+ if value == "true":
+ return True
+ if value == "false":
+ return False
+ try:
+ return int(value)
+ except ValueError:
+ pass
+ if value.startswith(quote) and value.endswith(quote):
+ value = value[1:-1]
+ return value
+
+ @classmethod
+ def read(cls, path):
+ """read preferences from a file"""
+
+ section = None # for .ini files
+ basename = os.path.basename(path)
+ if ":" in basename:
+ # section of INI file
+ path, section = path.rsplit(":", 1)
+
+ if not os.path.exists(path) and not mozfile.is_url(path):
+ raise PreferencesReadError("'%s' does not exist" % path)
+
+ if section:
+ try:
+ return cls.read_ini(path, section)
+ except PreferencesReadError:
+ raise
+ except Exception as e:
+ raise PreferencesReadError(str(e))
+
+ # try both JSON and .ini format
+ try:
+ return cls.read_json(path)
+ except Exception as e:
+ try:
+ return cls.read_ini(path)
+ except Exception as f:
+ for exception in e, f:
+ if isinstance(exception, PreferencesReadError):
+ raise exception
+ raise PreferencesReadError("Could not recognize format of %s" % path)
+
+ @classmethod
+ def read_ini(cls, path, section=None):
+ """read preferences from an .ini file"""
+
+ parser = ConfigParser()
+ parser.optionxform = str
+ parser.readfp(mozfile.load(path))
+
+ if section:
+ if section not in parser.sections():
+ raise PreferencesReadError("No section '%s' in %s" % (section, path))
+ retval = parser.items(section, raw=True)
+ else:
+ retval = parser.defaults().items()
+
+ # cast the preferences since .ini is just strings
+ return [(i, cls.cast(j)) for i, j in retval]
+
+ @classmethod
+ def read_json(cls, path):
+ """read preferences from a JSON blob"""
+
+ prefs = json.loads(mozfile.load(path).read())
+
+ if type(prefs) not in [list, dict]:
+ raise PreferencesReadError("Malformed preferences: %s" % path)
+ if isinstance(prefs, list):
+ if [i for i in prefs if type(i) != list or len(i) != 2]:
+ raise PreferencesReadError("Malformed preferences: %s" % path)
+ values = [i[1] for i in prefs]
+ elif isinstance(prefs, dict):
+ values = prefs.values()
+ else:
+ raise PreferencesReadError("Malformed preferences: %s" % path)
+ types = (bool, string_types, int)
+ if [i for i in values if not any([isinstance(i, j) for j in types])]:
+ raise PreferencesReadError("Only bool, string, and int values allowed")
+ return prefs
+
+ @classmethod
+ def read_prefs(cls, path, pref_setter="user_pref", interpolation=None):
+ """
+ Read preferences from (e.g.) prefs.js
+
+ :param path: The path to the preference file to read.
+ :param pref_setter: The name of the function used to set preferences
+ in the preference file.
+ :param interpolation: If provided, a dict that will be passed
+ to str.format to interpolate preference values.
+ """
+
+ marker = "##//" # magical marker
+ lines = [i.strip() for i in mozfile.load(path).readlines()]
+ _lines = []
+ for line in lines:
+ # decode bytes in case of URL processing
+ if isinstance(line, bytes):
+ line = line.decode()
+ if not line.startswith(pref_setter):
+ continue
+ if "//" in line:
+ line = line.replace("//", marker)
+ _lines.append(line)
+ string = "\n".join(_lines)
+
+ # skip trailing comments
+ processed_tokens = []
+ f_obj = StringIO(string)
+ for token in tokenize.generate_tokens(f_obj.readline):
+ if token[0] == tokenize.COMMENT:
+ continue
+ processed_tokens.append(
+ token[:2]
+ ) # [:2] gets around http://bugs.python.org/issue9974
+ string = tokenize.untokenize(processed_tokens)
+
+ retval = []
+
+ def pref(a, b):
+ if interpolation and isinstance(b, string_types):
+ b = b.format(**interpolation)
+ retval.append((a, b))
+
+ lines = [i.strip().rstrip(";") for i in string.split("\n") if i.strip()]
+
+ _globals = {"retval": retval, "true": True, "false": False}
+ _globals[pref_setter] = pref
+ for line in lines:
+ try:
+ eval(line, _globals, {})
+ except SyntaxError:
+ print(line)
+ raise
+
+ # de-magic the marker
+ for index, (key, value) in enumerate(retval):
+ if isinstance(value, string_types) and marker in value:
+ retval[index] = (key, value.replace(marker, "//"))
+
+ return retval
+
+ @classmethod
+ def write(cls, _file, prefs, pref_string="user_pref(%s, %s);"):
+ """write preferences to a file"""
+
+ if isinstance(_file, string_types):
+ f = open(_file, "a")
+ else:
+ f = _file
+
+ if isinstance(prefs, dict):
+ # order doesn't matter
+ prefs = prefs.items()
+
+ # serialize -> JSON
+ _prefs = [(json.dumps(k), json.dumps(v)) for k, v in prefs]
+
+ # write the preferences
+ for _pref in _prefs:
+ print(pref_string % _pref, file=f)
+
+ # close the file if opened internally
+ if isinstance(_file, string_types):
+ f.close()
diff --git a/testing/mozbase/mozprofile/mozprofile/profile.py b/testing/mozbase/mozprofile/mozprofile/profile.py
new file mode 100644
index 0000000000..f38c72c45b
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/profile.py
@@ -0,0 +1,589 @@
+# 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 six
+import json
+import os
+import platform
+import tempfile
+import time
+import uuid
+from abc import ABCMeta, abstractmethod, abstractproperty
+from shutil import copytree
+
+import mozfile
+from six import string_types, python_2_unicode_compatible
+
+from .addons import AddonManager
+from .permissions import Permissions
+from .prefs import Preferences
+
+__all__ = [
+ "BaseProfile",
+ "ChromeProfile",
+ "ChromiumProfile",
+ "Profile",
+ "FirefoxProfile",
+ "ThunderbirdProfile",
+ "create_profile",
+]
+
+
+@six.add_metaclass(ABCMeta)
+class BaseProfile(object):
+ def __init__(self, profile=None, addons=None, preferences=None, restore=True):
+ """Create a new Profile.
+
+ All arguments are optional.
+
+ :param profile: Path to a profile. If not specified, a new profile
+ directory will be created.
+ :param addons: List of paths to addons which should be installed in the profile.
+ :param preferences: Dict of preferences to set in the profile.
+ :param restore: Whether or not to clean up any modifications made to this profile
+ (default True).
+ """
+ self._addons = addons or []
+
+ # Prepare additional preferences
+ if preferences:
+ if isinstance(preferences, dict):
+ # unordered
+ preferences = preferences.items()
+
+ # sanity check
+ assert not [i for i in preferences if len(i) != 2]
+ else:
+ preferences = []
+ self._preferences = preferences
+
+ # Handle profile creation
+ self.restore = restore
+ self.create_new = not profile
+ if profile:
+ # Ensure we have a full path to the profile
+ self.profile = os.path.abspath(os.path.expanduser(profile))
+ else:
+ self.profile = tempfile.mkdtemp(suffix=".mozrunner")
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.cleanup()
+
+ def __del__(self):
+ self.cleanup()
+
+ def cleanup(self):
+ """Cleanup operations for the profile."""
+
+ if self.restore:
+ # If it's a temporary profile we have to remove it
+ if self.create_new:
+ mozfile.remove(self.profile)
+
+ @abstractmethod
+ def _reset(self):
+ pass
+
+ def reset(self):
+ """
+ reset the profile to the beginning state
+ """
+ self.cleanup()
+ self._reset()
+
+ @abstractmethod
+ def set_preferences(self, preferences, filename="user.js"):
+ pass
+
+ @abstractproperty
+ def preference_file_names(self):
+ """A tuple of file basenames expected to contain preferences."""
+
+ def merge(self, other, interpolation=None):
+ """Merges another profile into this one.
+
+ This will handle pref files matching the profile's
+ `preference_file_names` property, and any addons in the
+ other/extensions directory.
+ """
+ for basename in os.listdir(other):
+ if basename not in self.preference_file_names:
+ continue
+
+ path = os.path.join(other, basename)
+ try:
+ prefs = Preferences.read_json(path)
+ except ValueError:
+ prefs = Preferences.read_prefs(path, interpolation=interpolation)
+ self.set_preferences(prefs, filename=basename)
+
+ extension_dir = os.path.join(other, "extensions")
+ if not os.path.isdir(extension_dir):
+ return
+
+ for basename in os.listdir(extension_dir):
+ path = os.path.join(extension_dir, basename)
+
+ if self.addons.is_addon(path):
+ self._addons.append(path)
+ self.addons.install(path)
+
+ @classmethod
+ def clone(cls, path_from, path_to=None, ignore=None, **kwargs):
+ """Instantiate a temporary profile via cloning
+ - path: path of the basis to clone
+ - ignore: callable passed to shutil.copytree
+ - kwargs: arguments to the profile constructor
+ """
+ if not path_to:
+ tempdir = tempfile.mkdtemp() # need an unused temp dir name
+ mozfile.remove(tempdir) # copytree requires that dest does not exist
+ path_to = tempdir
+ copytree(path_from, path_to, ignore=ignore)
+
+ c = cls(path_to, **kwargs)
+ c.create_new = True # deletes a cloned profile when restore is True
+ return c
+
+ def exists(self):
+ """returns whether the profile exists or not"""
+ return os.path.exists(self.profile)
+
+
+@python_2_unicode_compatible
+class Profile(BaseProfile):
+ """Handles all operations regarding profile.
+
+ Creating new profiles, installing add-ons, setting preferences and
+ handling cleanup.
+
+ The files associated with the profile will be removed automatically after
+ the object is garbage collected: ::
+
+ profile = Profile()
+ print profile.profile # this is the path to the created profile
+ del profile
+ # the profile path has been removed from disk
+
+ :meth:`cleanup` is called under the hood to remove the profile files. You
+ can ensure this method is called (even in the case of exception) by using
+ the profile as a context manager: ::
+
+ with Profile() as profile:
+ # do things with the profile
+ pass
+ # profile.cleanup() has been called here
+ """
+
+ preference_file_names = ("user.js", "prefs.js")
+
+ def __init__(
+ self,
+ profile=None,
+ addons=None,
+ preferences=None,
+ locations=None,
+ proxy=None,
+ restore=True,
+ whitelistpaths=None,
+ **kwargs
+ ):
+ """
+ :param profile: Path to the profile
+ :param addons: String of one or list of addons to install
+ :param preferences: Dictionary or class of preferences
+ :param locations: ServerLocations object
+ :param proxy: Setup a proxy
+ :param restore: Flag for removing all custom settings during cleanup
+ :param whitelistpaths: List of paths to pass to Firefox to allow read
+ access to from the content process sandbox.
+ """
+ super(Profile, self).__init__(
+ profile=profile,
+ addons=addons,
+ preferences=preferences,
+ restore=restore,
+ **kwargs
+ )
+
+ self._locations = locations
+ self._proxy = proxy
+ self._whitelistpaths = whitelistpaths
+
+ # Initialize all class members
+ self._reset()
+
+ def _reset(self):
+ """Internal: Initialize all class members to their default value"""
+
+ if not os.path.exists(self.profile):
+ os.makedirs(self.profile)
+
+ # Preferences files written to
+ self.written_prefs = set()
+
+ # Our magic markers
+ nonce = "%s %s" % (str(time.time()), uuid.uuid4())
+ self.delimeters = (
+ "#MozRunner Prefs Start %s" % nonce,
+ "#MozRunner Prefs End %s" % nonce,
+ )
+
+ # If sub-classes want to set default preferences
+ if hasattr(self.__class__, "preferences"):
+ self.set_preferences(self.__class__.preferences)
+ # Set additional preferences
+ self.set_preferences(self._preferences)
+
+ self.permissions = Permissions(self.profile, self._locations)
+ prefs_js, user_js = self.permissions.network_prefs(self._proxy)
+
+ if self._whitelistpaths:
+ # On macOS we don't want to support a generalized read whitelist,
+ # and the macOS sandbox policy language doesn't have support for
+ # lists, so we handle these specially.
+ if platform.system() == "Darwin":
+ assert len(self._whitelistpaths) <= 2
+ if len(self._whitelistpaths) == 2:
+ prefs_js.append(
+ (
+ "security.sandbox.content.mac.testing_read_path2",
+ self._whitelistpaths[1],
+ )
+ )
+ prefs_js.append(
+ (
+ "security.sandbox.content.mac.testing_read_path1",
+ self._whitelistpaths[0],
+ )
+ )
+ else:
+ prefs_js.append(
+ (
+ "security.sandbox.content.read_path_whitelist",
+ ",".join(self._whitelistpaths),
+ )
+ )
+ self.set_preferences(prefs_js, "prefs.js")
+ self.set_preferences(user_js)
+
+ # handle add-on installation
+ self.addons = AddonManager(self.profile, restore=self.restore)
+ self.addons.install(self._addons)
+
+ def cleanup(self):
+ """Cleanup operations for the profile."""
+
+ if self.restore:
+ # If copies of those class instances exist ensure we correctly
+ # reset them all (see bug 934484)
+ self.clean_preferences()
+ if getattr(self, "addons", None) is not None:
+ self.addons.clean()
+ if getattr(self, "permissions", None) is not None:
+ self.permissions.clean_db()
+ super(Profile, self).cleanup()
+
+ def clean_preferences(self):
+ """Removed preferences added by mozrunner."""
+ for filename in self.written_prefs:
+ if not os.path.exists(os.path.join(self.profile, filename)):
+ # file has been deleted
+ break
+ while True:
+ if not self.pop_preferences(filename):
+ break
+
+ # methods for preferences
+
+ def set_preferences(self, preferences, filename="user.js"):
+ """Adds preferences dict to profile preferences"""
+ prefs_file = os.path.join(self.profile, filename)
+ with open(prefs_file, "a") as f:
+ if not preferences:
+ return
+
+ # note what files we've touched
+ self.written_prefs.add(filename)
+
+ # opening delimeter
+ f.write("\n%s\n" % self.delimeters[0])
+
+ Preferences.write(f, preferences)
+
+ # closing delimeter
+ f.write("%s\n" % self.delimeters[1])
+
+ def set_persistent_preferences(self, preferences):
+ """
+ Adds preferences dict to profile preferences and save them during a
+ profile reset
+ """
+
+ # this is a dict sometimes, convert
+ if isinstance(preferences, dict):
+ preferences = preferences.items()
+
+ # add new prefs to preserve them during reset
+ for new_pref in preferences:
+ # if dupe remove item from original list
+ self._preferences = [
+ pref for pref in self._preferences if not new_pref[0] == pref[0]
+ ]
+ self._preferences.append(new_pref)
+
+ self.set_preferences(preferences, filename="user.js")
+
+ def pop_preferences(self, filename):
+ """
+ pop the last set of preferences added
+ returns True if popped
+ """
+
+ path = os.path.join(self.profile, filename)
+ with open(path) as f:
+ lines = f.read().splitlines()
+
+ def last_index(_list, value):
+ """
+ returns the last index of an item;
+ this should actually be part of python code but it isn't
+ """
+ for index in reversed(range(len(_list))):
+ if _list[index] == value:
+ return index
+
+ s = last_index(lines, self.delimeters[0])
+ e = last_index(lines, self.delimeters[1])
+
+ # ensure both markers are found
+ if s is None:
+ assert e is None, "%s found without %s" % (
+ self.delimeters[1],
+ self.delimeters[0],
+ )
+ return False # no preferences found
+ elif e is None:
+ assert s is None, "%s found without %s" % (
+ self.delimeters[0],
+ self.delimeters[1],
+ )
+
+ # ensure the markers are in the proper order
+ assert e > s, "%s found at %s, while %s found at %s" % (
+ self.delimeters[1],
+ e,
+ self.delimeters[0],
+ s,
+ )
+
+ # write the prefs
+ cleaned_prefs = "\n".join(lines[:s] + lines[e + 1 :])
+ with open(path, "w") as f:
+ f.write(cleaned_prefs)
+ return True
+
+ # methods for introspection
+
+ def summary(self, return_parts=False):
+ """
+ returns string summarizing profile information.
+ if return_parts is true, return the (Part_name, value) list
+ of tuples instead of the assembled string
+ """
+
+ parts = [("Path", self.profile)] # profile path
+
+ # directory tree
+ parts.append(("Files", "\n%s" % mozfile.tree(self.profile)))
+
+ # preferences
+ for prefs_file in ("user.js", "prefs.js"):
+ path = os.path.join(self.profile, prefs_file)
+ if os.path.exists(path):
+
+ # prefs that get their own section
+ # This is currently only 'network.proxy.autoconfig_url'
+ # but could be expanded to include others
+ section_prefs = ["network.proxy.autoconfig_url"]
+ line_length = 80
+ # buffer for 80 character display:
+ # length = 80 - len(key) - len(': ') - line_length_buffer
+ line_length_buffer = 10
+ line_length_buffer += len(": ")
+
+ def format_value(key, value):
+ if key not in section_prefs:
+ return value
+ max_length = line_length - len(key) - line_length_buffer
+ if len(value) > max_length:
+ value = "%s..." % value[:max_length]
+ return value
+
+ prefs = Preferences.read_prefs(path)
+ if prefs:
+ prefs = dict(prefs)
+ parts.append(
+ (
+ prefs_file,
+ "\n%s"
+ % (
+ "\n".join(
+ [
+ "%s: %s" % (key, format_value(key, prefs[key]))
+ for key in sorted(prefs.keys())
+ ]
+ )
+ ),
+ )
+ )
+
+ # Currently hardcorded to 'network.proxy.autoconfig_url'
+ # but could be generalized, possibly with a generalized (simple)
+ # JS-parser
+ network_proxy_autoconfig = prefs.get("network.proxy.autoconfig_url")
+ if network_proxy_autoconfig and network_proxy_autoconfig.strip():
+ network_proxy_autoconfig = network_proxy_autoconfig.strip()
+ lines = network_proxy_autoconfig.replace(
+ ";", ";\n"
+ ).splitlines()
+ lines = [line.strip() for line in lines]
+ origins_string = "var origins = ["
+ origins_end = "];"
+ if origins_string in lines[0]:
+ start = lines[0].find(origins_string)
+ end = lines[0].find(origins_end, start)
+ splitline = [
+ lines[0][:start],
+ lines[0][start : start + len(origins_string) - 1],
+ ]
+ splitline.extend(
+ lines[0][start + len(origins_string) : end]
+ .replace(",", ",\n")
+ .splitlines()
+ )
+ splitline.append(lines[0][end:])
+ lines[0:1] = [i.strip() for i in splitline]
+ parts.append(
+ (
+ "Network Proxy Autoconfig, %s" % (prefs_file),
+ "\n%s" % "\n".join(lines),
+ )
+ )
+
+ if return_parts:
+ return parts
+
+ retval = "%s\n" % (
+ "\n\n".join(["[%s]: %s" % (key, value) for key, value in parts])
+ )
+ return retval
+
+ def __str__(self):
+ return self.summary()
+
+
+class FirefoxProfile(Profile):
+ """Specialized Profile subclass for Firefox"""
+
+ preferences = {}
+
+
+class ThunderbirdProfile(Profile):
+ """Specialized Profile subclass for Thunderbird"""
+
+ preferences = {
+ "extensions.update.enabled": False,
+ "extensions.update.notifyUser": False,
+ "browser.shell.checkDefaultBrowser": False,
+ "browser.tabs.warnOnClose": False,
+ "browser.warnOnQuit": False,
+ "browser.sessionstore.resume_from_crash": False,
+ # prevents the 'new e-mail address' wizard on new profile
+ "mail.provider.enabled": False,
+ }
+
+
+class ChromiumProfile(BaseProfile):
+ preference_file_names = ("Preferences",)
+
+ class AddonManager(list):
+ def install(self, addons):
+ if isinstance(addons, string_types):
+ addons = [addons]
+ self.extend(addons)
+
+ @classmethod
+ def is_addon(self, addon):
+ # Don't include testing/profiles on Google Chrome
+ return False
+
+ def __init__(self, **kwargs):
+ super(ChromiumProfile, self).__init__(**kwargs)
+
+ if self.create_new:
+ self.profile = os.path.join(self.profile, "Default")
+ self._reset()
+
+ def _reset(self):
+ if not os.path.isdir(self.profile):
+ os.makedirs(self.profile)
+
+ if self._preferences:
+ self.set_preferences(self._preferences)
+
+ self.addons = self.AddonManager()
+ if self._addons:
+ self.addons.install(self._addons)
+
+ def set_preferences(self, preferences, filename="Preferences", **values):
+ pref_file = os.path.join(self.profile, filename)
+
+ prefs = {}
+ if os.path.isfile(pref_file):
+ with open(pref_file, "r") as fh:
+ prefs.update(json.load(fh))
+
+ prefs.update(preferences)
+ with open(pref_file, "w") as fh:
+ prefstr = json.dumps(prefs)
+ prefstr % values # interpolate prefs with values
+ fh.write(prefstr)
+
+
+class ChromeProfile(ChromiumProfile):
+ # update this if Google Chrome requires more
+ # specific profiles
+ pass
+
+
+profile_class = {
+ "chrome": ChromeProfile,
+ "chromium": ChromiumProfile,
+ "firefox": FirefoxProfile,
+ "thunderbird": ThunderbirdProfile,
+}
+
+
+def create_profile(app, **kwargs):
+ """Create a profile given an application name.
+
+ :param app: String name of the application to create a profile for, e.g 'firefox'.
+ :param kwargs: Same as the arguments for the Profile class (optional).
+ :returns: An application specific Profile instance
+ :raises: NotImplementedError
+ """
+ cls = profile_class.get(app)
+
+ if not cls:
+ raise NotImplementedError(
+ "Profiles not supported for application '{}'".format(app)
+ )
+
+ return cls(**kwargs)
diff --git a/testing/mozbase/mozprofile/mozprofile/view.py b/testing/mozbase/mozprofile/mozprofile/view.py
new file mode 100644
index 0000000000..4ee617e09f
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/view.py
@@ -0,0 +1,48 @@
+#!/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/.
+
+
+"""
+script to view mozilla profiles
+"""
+from __future__ import absolute_import, print_function
+
+import mozprofile
+import optparse
+import os
+import sys
+
+__all__ = ["view_profile"]
+
+
+def view_profile(args=sys.argv[1:]):
+
+ usage = "%prog [options] profile_path <...>"
+ parser = optparse.OptionParser(usage=usage, description=__doc__)
+ options, args = parser.parse_args(args)
+ if not args:
+ parser.print_usage()
+ parser.exit()
+
+ # check existence
+ missing = [i for i in args if not os.path.exists(i)]
+ if missing:
+ if len(missing) > 1:
+ missing_string = "Profiles do not exist"
+ else:
+ missing_string = "Profile does not exist"
+ parser.error("%s: %s" % (missing_string, ", ".join(missing)))
+
+ # print summary for each profile
+ while args:
+ path = args.pop(0)
+ profile = mozprofile.Profile(path)
+ print(profile.summary())
+ if args:
+ print("-" * 4)
+
+
+if __name__ == "__main__":
+ view_profile()
diff --git a/testing/mozbase/mozprofile/setup.cfg b/testing/mozbase/mozprofile/setup.cfg
new file mode 100644
index 0000000000..3c6e79cf31
--- /dev/null
+++ b/testing/mozbase/mozprofile/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=1
diff --git a/testing/mozbase/mozprofile/setup.py b/testing/mozbase/mozprofile/setup.py
new file mode 100644
index 0000000000..82ff7fad59
--- /dev/null
+++ b/testing/mozbase/mozprofile/setup.py
@@ -0,0 +1,51 @@
+# 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 setuptools import setup
+
+PACKAGE_NAME = "mozprofile"
+PACKAGE_VERSION = "2.5.0"
+
+deps = [
+ "mozfile>=1.2",
+ "mozlog>=6.0",
+ "six>=1.13.0,<2",
+]
+
+setup(
+ name=PACKAGE_NAME,
+ version=PACKAGE_VERSION,
+ description="Library to create and modify Mozilla application profiles",
+ long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html",
+ classifiers=[
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
+ "Natural Language :: English",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3.5",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords="mozilla",
+ author="Mozilla Automation and Tools team",
+ author_email="tools@lists.mozilla.org",
+ url="https://wiki.mozilla.org/Auto-tools/Projects/Mozbase",
+ license="MPL 2.0",
+ packages=["mozprofile"],
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=deps,
+ extras_require={"manifest": ["manifestparser >= 0.6"]},
+ tests_require=["wptserve"],
+ entry_points="""
+ # -*- Entry points: -*-
+ [console_scripts]
+ mozprofile = mozprofile:cli
+ view-profile = mozprofile:view_profile
+ diff-profiles = mozprofile:diff_profiles
+ """,
+)
diff --git a/testing/mozbase/mozprofile/tests/addon_stubs.py b/testing/mozbase/mozprofile/tests/addon_stubs.py
new file mode 100644
index 0000000000..32b64070ea
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/addon_stubs.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+
+from __future__ import absolute_import
+
+import os
+import tempfile
+import zipfile
+
+import mozfile
+
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+# stubs is a dict of the form {'addon id': 'install manifest content'}
+stubs = {
+ "test-addon-1@mozilla.org": "test_addon_1.rdf",
+ "test-addon-2@mozilla.org": "test_addon_2.rdf",
+ "test-addon-3@mozilla.org": "test_addon_3.rdf",
+ "test-addon-4@mozilla.org": "test_addon_4.rdf",
+ "test-addon-invalid-no-id@mozilla.org": "test_addon_invalid_no_id.rdf",
+ "test-addon-invalid-version@mozilla.org": "test_addon_invalid_version.rdf",
+ "test-addon-invalid-no-manifest@mozilla.org": None,
+ "test-addon-invalid-not-wellformed@mozilla.org": "test_addon_invalid_not_wellformed.rdf",
+ "test-addon-unpack@mozilla.org": "test_addon_unpack.rdf",
+}
+
+
+def generate_addon(addon_id, path=None, name=None, xpi=True):
+ """
+ Method to generate a single addon.
+
+ :param addon_id: id of an addon to generate from the stubs dictionary
+ :param path: path where addon and .xpi should be generated
+ :param name: name for the addon folder or .xpi file
+ :param xpi: Flag if an XPI or folder should be generated
+
+ Returns the file-path of the addon's .xpi file
+ """
+
+ if addon_id not in stubs:
+ raise IOError('Requested addon stub "%s" does not exist' % addon_id)
+
+ # Generate directory structure for addon
+ try:
+ tmpdir = path or tempfile.mkdtemp()
+ addon_dir = os.path.join(tmpdir, name or addon_id)
+ os.mkdir(addon_dir)
+ except IOError:
+ raise IOError("Could not generate directory structure for addon stub.")
+
+ # Write install.rdf for addon
+ if stubs[addon_id]:
+ install_rdf = os.path.join(addon_dir, "install.rdf")
+ with open(install_rdf, "w") as f:
+ manifest = os.path.join(here, "install_manifests", stubs[addon_id])
+ f.write(open(manifest, "r").read())
+
+ if not xpi:
+ return addon_dir
+
+ # Generate the .xpi for the addon
+ xpi_file = os.path.join(tmpdir, (name or addon_id) + ".xpi")
+ with zipfile.ZipFile(xpi_file, "w") as x:
+ x.write(install_rdf, install_rdf[len(addon_dir) :])
+
+ # Ensure we remove the temporary folder to not install the addon twice
+ mozfile.rmtree(addon_dir)
+
+ return xpi_file
diff --git a/testing/mozbase/mozprofile/tests/addons/apply-css-id-via-browser-specific-settings.xpi b/testing/mozbase/mozprofile/tests/addons/apply-css-id-via-browser-specific-settings.xpi
new file mode 100644
index 0000000000..c9ad38f63b
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/addons/apply-css-id-via-browser-specific-settings.xpi
Binary files differ
diff --git a/testing/mozbase/mozprofile/tests/addons/apply-css-sans-id.xpi b/testing/mozbase/mozprofile/tests/addons/apply-css-sans-id.xpi
new file mode 100644
index 0000000000..fa721a4f76
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/addons/apply-css-sans-id.xpi
Binary files differ
diff --git a/testing/mozbase/mozprofile/tests/addons/apply-css.xpi b/testing/mozbase/mozprofile/tests/addons/apply-css.xpi
new file mode 100644
index 0000000000..0ed64f79ac
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/addons/apply-css.xpi
Binary files differ
diff --git a/testing/mozbase/mozprofile/tests/addons/empty.xpi b/testing/mozbase/mozprofile/tests/addons/empty.xpi
new file mode 100644
index 0000000000..26f28f099d
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/addons/empty.xpi
Binary files differ
diff --git a/testing/mozbase/mozprofile/tests/addons/empty/install.rdf b/testing/mozbase/mozprofile/tests/addons/empty/install.rdf
new file mode 100644
index 0000000000..70b9e13e44
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/addons/empty/install.rdf
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-empty@quality.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+ <em:name>Test Extension (empty)</em:name>
+ <em:creator>Mozilla QA</em:creator>
+ <em:homepageURL>http://quality.mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/testing/mozbase/mozprofile/tests/addons/invalid.xpi b/testing/mozbase/mozprofile/tests/addons/invalid.xpi
new file mode 100644
index 0000000000..2f222c7637
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/addons/invalid.xpi
Binary files differ
diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js b/testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js
new file mode 100644
index 0000000000..8d36cca287
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ globals: {
+ user_pref: true,
+ },
+};
diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences b/testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences
new file mode 100644
index 0000000000..697201c687
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences
@@ -0,0 +1 @@
+{"Preferences": 1}
diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/extensions/empty.xpi b/testing/mozbase/mozprofile/tests/files/dummy-profile/extensions/empty.xpi
new file mode 100644
index 0000000000..26f28f099d
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/extensions/empty.xpi
Binary files differ
diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js b/testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js
new file mode 100644
index 0000000000..c2e18261a8
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js
@@ -0,0 +1 @@
+user_pref("prefs.js", 1);
diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/user.js b/testing/mozbase/mozprofile/tests/files/dummy-profile/user.js
new file mode 100644
index 0000000000..66752d6397
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/user.js
@@ -0,0 +1 @@
+user_pref("user.js", 1);
diff --git a/testing/mozbase/mozprofile/tests/files/not_an_addon.txt b/testing/mozbase/mozprofile/tests/files/not_an_addon.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/not_an_addon.txt
diff --git a/testing/mozbase/mozprofile/tests/files/prefs_with_comments.js b/testing/mozbase/mozprofile/tests/files/prefs_with_comments.js
new file mode 100644
index 0000000000..06a56f2138
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/prefs_with_comments.js
@@ -0,0 +1,6 @@
+# A leading comment
+user_pref("browser.startup.homepage", "http://planet.mozilla.org"); # A trailing comment
+user_pref("zoom.minPercent", 30);
+// Another leading comment
+user_pref("zoom.maxPercent", 300); // Another trailing comment
+user_pref("webgl.verbose", "false");
diff --git a/testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js b/testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js
new file mode 100644
index 0000000000..52700df6d8
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js
@@ -0,0 +1,5 @@
+/* globals user_pref */
+user_pref("browser.foo", "http://{server}");
+user_pref("zoom.minPercent", 30);
+user_pref("webgl.verbose", "false");
+user_pref("browser.bar", "{abc}xyz");
diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_1.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_1.rdf
new file mode 100644
index 0000000000..839ea9fbd5
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_1.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-1@mozilla.org</em:id>
+ <em:version>0.1</em:version>
+ <em:name>Test Add-on 1</em:name>
+ <em:creator>Mozilla</em:creator>
+ <em:homepageURL>http://mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_2.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_2.rdf
new file mode 100644
index 0000000000..8303e862fc
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_2.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-2@mozilla.org</em:id>
+ <em:version>0.2</em:version>
+ <em:name>Test Add-on 2</em:name>
+ <em:creator>Mozilla</em:creator>
+ <em:homepageURL>http://mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_3.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_3.rdf
new file mode 100644
index 0000000000..5bd6d38043
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_3.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-3@mozilla.org</em:id>
+ <em:version>0.1</em:version>
+ <em:name>Test Add-on 3</em:name>
+ <em:creator>Mozilla</em:creator>
+ <em:homepageURL>http://mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
+
diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_4.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_4.rdf
new file mode 100644
index 0000000000..e0f99d3133
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_4.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-4@mozilla.org</em:id>
+ <em:version>0.1</em:version>
+ <em:name>Test Add-on 4</em:name>
+ <em:creator>Mozilla</em:creator>
+ <em:homepageURL>http://mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
+
diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_no_id.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_no_id.rdf
new file mode 100644
index 0000000000..23f60fece1
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_no_id.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <!-- Invalid because of a missing add-on id -->
+ <em:version>0.1</em:version>
+ <em:name>Test Invalid Extension (no id)</em:name>
+ <em:creator>Mozilla</em:creator>
+ <em:homepageURL>http://mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <!-- Invalid target application string -->
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_not_wellformed.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_not_wellformed.rdf
new file mode 100644
index 0000000000..690ec406cc
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_not_wellformed.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <!-- Invalid because it's not well-formed -->
+ <em:id>test-addon-invalid-not-wellformed@mozilla.org</em:id
+ <em:version>0.1</em:version>
+ <em:name>Test Invalid Extension (no id)</em:name>
+ <em:creator>Mozilla</em:creator>
+ <em:homepageURL>http://mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <!-- Invalid target application string -->
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_version.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_version.rdf
new file mode 100644
index 0000000000..c854bfcdb5
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_version.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-invalid-version@mozilla.org</em:id>
+ <!-- Invalid addon version -->
+ <em:version>0.NOPE</em:version>
+ <em:name>Test Invalid Extension (invalid version)</em:name>
+ <em:creator>Mozilla</em:creator>
+ <em:homepageURL>http://mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <!-- Invalid target application string -->
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_unpack.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_unpack.rdf
new file mode 100644
index 0000000000..cc85ea560f
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_unpack.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-unpack@mozilla.org</em:id>
+ <em:version>0.1</em:version>
+ <em:name>Test Add-on (unpack)</em:name>
+ <em:creator>Mozilla</em:creator>
+ <em:homepageURL>http://mozilla.org</em:homepageURL>
+ <em:type>2</em:type>
+ <em:unpack>true</em:unpack>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.*</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/testing/mozbase/mozprofile/tests/manifest.ini b/testing/mozbase/mozprofile/tests/manifest.ini
new file mode 100644
index 0000000000..d69f4649ce
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/manifest.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+subsuite = mozbase
+[test_addonid.py]
+[test_server_locations.py]
+[test_preferences.py]
+[test_permissions.py]
+[test_bug758250.py]
+[test_nonce.py]
+[test_clone_cleanup.py]
+[test_profile.py]
+[test_profile_view.py]
+[test_addons.py]
+[test_chrome_profile.py]
diff --git a/testing/mozbase/mozprofile/tests/test_addonid.py b/testing/mozbase/mozprofile/tests/test_addonid.py
new file mode 100755
index 0000000000..2390acf114
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_addonid.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python
+
+from __future__ import absolute_import
+
+import os
+
+import mozunit
+import pytest
+
+from mozprofile import addons
+
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+
+"""Test finding the addon id in a variety of install.rdf styles"""
+
+
+ADDON_ID_TESTS = [
+ """
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>winning</em:id>
+ <em:name>MozMill</em:name>
+ <em:version>2.0a</em:version>
+ <em:creator>Adam Christian</em:creator>
+ <em:description>A testing extension based on the
+ Windmill Testing Framework client source</em:description>
+ <em:unpack>true</em:unpack>
+ <em:targetApplication>
+ <!-- Firefox -->
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5</em:minVersion>
+ <em:maxVersion>8.*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <!-- Thunderbird -->
+ <Description>
+ <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id>
+ <em:minVersion>3.0a1pre</em:minVersion>
+ <em:maxVersion>3.2*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <!-- Sunbird -->
+ <Description>
+ <em:id>{718e30fb-e89b-41dd-9da7-e25a45638b28}</em:id>
+ <em:minVersion>0.6a1</em:minVersion>
+ <em:maxVersion>1.0pre</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <!-- SeaMonkey -->
+ <Description>
+ <em:id>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</em:id>
+ <em:minVersion>2.0a1</em:minVersion>
+ <em:maxVersion>2.1*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <!-- Songbird -->
+ <Description>
+ <em:id>songbird@songbirdnest.com</em:id>
+ <em:minVersion>0.3pre</em:minVersion>
+ <em:maxVersion>1.3.0a</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9.1</em:minVersion>
+ <em:maxVersion>2.0*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>""",
+ """
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:targetApplication>
+ <!-- Firefox -->
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5</em:minVersion>
+ <em:maxVersion>8.*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:id>winning</em:id>
+ <em:name>MozMill</em:name>
+ <em:version>2.0a</em:version>
+ <em:creator>Adam Christian</em:creator>
+ <em:description>A testing extension based on the
+ Windmill Testing Framework client source</em:description>
+ <em:unpack>true</em:unpack>
+ </Description>
+ </RDF>""",
+ """
+<RDF xmlns="http://www.mozilla.org/2004/em-rdf#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <rdf:Description about="urn:mozilla:install-manifest">
+ <id>winning</id>
+ <name>foo</name>
+ <version>42</version>
+ <description>A testing extension based on the
+ Windmill Testing Framework client source</description>
+ </rdf:Description>
+</RDF>""",
+ """
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:foobar="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <foobar:targetApplication>
+ <!-- Firefox -->
+ <Description>
+ <foobar:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</foobar:id>
+ <foobar:minVersion>3.5</foobar:minVersion>
+ <foobar:maxVersion>8.*</foobar:maxVersion>
+ </Description>
+ </foobar:targetApplication>
+ <foobar:id>winning</foobar:id>
+ <foobar:name>MozMill</foobar:name>
+ <foobar:version>2.0a</foobar:version>
+ <foobar:creator>Adam Christian</foobar:creator>
+ <foobar:description>A testing extension based on the
+ Windmill Testing Framework client source</foobar:description>
+ <foobar:unpack>true</foobar:unpack>
+ </Description>
+ </RDF>""",
+]
+
+
+@pytest.fixture(
+ params=ADDON_ID_TESTS, ids=[str(i) for i in range(0, len(ADDON_ID_TESTS))]
+)
+def profile(request, tmpdir):
+ test = request.param
+ path = tmpdir.mkdtemp().strpath
+
+ with open(os.path.join(path, "install.rdf"), "w") as fh:
+ fh.write(test)
+ return path
+
+
+def test_addonID(profile):
+ a = addons.AddonManager(os.path.join(profile, "profile"))
+ addon_id = a.addon_details(profile)["id"]
+ assert addon_id == "winning"
+
+
+def test_addonID_xpi():
+ a = addons.AddonManager("profile")
+ addon = a.addon_details(os.path.join(here, "addons", "empty.xpi"))
+ assert addon["id"] == "test-empty@quality.mozilla.org"
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_addons.py b/testing/mozbase/mozprofile/tests/test_addons.py
new file mode 100644
index 0000000000..bcff62f724
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_addons.py
@@ -0,0 +1,376 @@
+#!/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/.
+
+from __future__ import absolute_import, unicode_literals
+
+import os
+import zipfile
+
+import mozunit
+import pytest
+
+import mozprofile
+from addon_stubs import generate_addon
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+
+@pytest.fixture
+def profile():
+ return mozprofile.Profile()
+
+
+@pytest.fixture
+def am(profile):
+ return profile.addons
+
+
+def test_install_multiple_same_source(tmpdir, am):
+ path = tmpdir.strpath
+
+ # Generate installer stubs for all possible types of addons
+ addon_xpi = generate_addon("test-addon-1@mozilla.org", path=path)
+ addon_folder = generate_addon("test-addon-1@mozilla.org", path=path, xpi=False)
+
+ # The same folder should not be installed twice
+ am.install([addon_folder, addon_folder])
+ assert am.installed_addons == [addon_folder]
+ am.clean()
+
+ # The same XPI file should not be installed twice
+ am.install([addon_xpi, addon_xpi])
+ assert am.installed_addons == [addon_xpi]
+ am.clean()
+
+ # Even if it is the same id the add-on should be installed twice, if
+ # specified via XPI and folder
+ am.install([addon_folder, addon_xpi])
+ assert len(am.installed_addons) == 2
+ assert addon_folder in am.installed_addons
+ assert addon_xpi in am.installed_addons
+ am.clean()
+
+
+def test_install_webextension_from_dir(tmpdir, am):
+ tmpdir = tmpdir.strpath
+
+ addon = os.path.join(here, "addons", "apply-css.xpi")
+ zipped = zipfile.ZipFile(addon)
+ try:
+ zipped.extractall(tmpdir)
+ finally:
+ zipped.close()
+ am.install(tmpdir)
+ assert len(am.installed_addons) == 1
+ assert os.path.isdir(am.installed_addons[0])
+
+
+def test_install_webextension(am):
+ addon = os.path.join(here, "addons", "apply-css.xpi")
+
+ am.install(addon)
+ assert len(am.installed_addons) == 1
+ assert os.path.isfile(am.installed_addons[0])
+ assert "apply-css.xpi" == os.path.basename(am.installed_addons[0])
+
+ details = am.addon_details(am.installed_addons[0])
+ assert "test-webext@quality.mozilla.org" == details["id"]
+
+
+def test_install_webextension_id_via_browser_specific_settings(am):
+ # See Bug 1572404
+ addon = os.path.join(
+ here, "addons", "apply-css-id-via-browser-specific-settings.xpi"
+ )
+ am.install(addon)
+ assert len(am.installed_addons) == 1
+ assert os.path.isfile(am.installed_addons[0])
+ assert "apply-css-id-via-browser-specific-settings.xpi" == os.path.basename(
+ am.installed_addons[0]
+ )
+
+ details = am.addon_details(am.installed_addons[0])
+ assert "test-webext@quality.mozilla.org" == details["id"]
+
+
+def test_install_webextension_sans_id(am):
+ addon = os.path.join(here, "addons", "apply-css-sans-id.xpi")
+ am.install(addon)
+
+ assert len(am.installed_addons) == 1
+ assert os.path.isfile(am.installed_addons[0])
+ assert "apply-css-sans-id.xpi" == os.path.basename(am.installed_addons[0])
+
+ details = am.addon_details(am.installed_addons[0])
+ assert "@temporary-addon" in details["id"]
+
+
+def test_install_xpi(tmpdir, am):
+ tmpdir = tmpdir.strpath
+
+ addons_to_install = []
+ addons_installed = []
+
+ # Generate installer stubs and install them
+ for ext in ["test-addon-1@mozilla.org", "test-addon-2@mozilla.org"]:
+ temp_addon = generate_addon(ext, path=tmpdir)
+ addons_to_install.append(am.addon_details(temp_addon)["id"])
+ am.install(temp_addon)
+
+ # Generate a list of addons installed in the profile
+ addons_installed = [
+ str(x[: -len(".xpi")])
+ for x in os.listdir(os.path.join(am.profile, "extensions"))
+ ]
+ assert addons_to_install.sort() == addons_installed.sort()
+
+
+def test_install_folder(tmpdir, am):
+ tmpdir = tmpdir.strpath
+
+ # Generate installer stubs for all possible types of addons
+ addons = []
+ addons.append(generate_addon("test-addon-1@mozilla.org", path=tmpdir))
+ addons.append(generate_addon("test-addon-2@mozilla.org", path=tmpdir, xpi=False))
+ addons.append(
+ generate_addon("test-addon-3@mozilla.org", path=tmpdir, name="addon-3")
+ )
+ addons.append(
+ generate_addon(
+ "test-addon-4@mozilla.org", path=tmpdir, name="addon-4", xpi=False
+ )
+ )
+ addons.sort()
+
+ am.install(tmpdir)
+
+ assert am.installed_addons == addons
+
+
+def test_install_unpack(tmpdir, am):
+ tmpdir = tmpdir.strpath
+
+ # Generate installer stubs for all possible types of addons
+ addon_xpi = generate_addon("test-addon-unpack@mozilla.org", path=tmpdir)
+ addon_folder = generate_addon(
+ "test-addon-unpack@mozilla.org", path=tmpdir, xpi=False
+ )
+ addon_no_unpack = generate_addon("test-addon-1@mozilla.org", path=tmpdir)
+
+ # Test unpack flag for add-on as XPI
+ am.install(addon_xpi)
+ assert am.installed_addons == [addon_xpi]
+ am.clean()
+
+ # Test unpack flag for add-on as folder
+ am.install(addon_folder)
+ assert am.installed_addons == [addon_folder]
+ am.clean()
+
+ # Test forcing unpack an add-on
+ am.install(addon_no_unpack, unpack=True)
+ assert am.installed_addons == [addon_no_unpack]
+ am.clean()
+
+
+def test_install_after_reset(tmpdir, profile):
+ tmpdir = tmpdir.strpath
+ am = profile.addons
+
+ # Installing the same add-on after a reset should not cause a failure
+ addon = generate_addon("test-addon-1@mozilla.org", path=tmpdir, xpi=False)
+
+ # We cannot use am because profile.reset() creates a new instance
+ am.install(addon)
+ profile.reset()
+
+ am.install(addon)
+ assert am.installed_addons == [addon]
+
+
+def test_install_backup(tmpdir, am):
+ tmpdir = tmpdir.strpath
+
+ staged_path = os.path.join(am.profile, "extensions")
+
+ # Generate installer stubs for all possible types of addons
+ addon_xpi = generate_addon("test-addon-1@mozilla.org", path=tmpdir)
+ addon_folder = generate_addon("test-addon-1@mozilla.org", path=tmpdir, xpi=False)
+ addon_name = generate_addon(
+ "test-addon-1@mozilla.org", path=tmpdir, name="test-addon-1-dupe@mozilla.org"
+ )
+
+ # Test backup of xpi files
+ am.install(addon_xpi)
+ assert am.backup_dir is None
+
+ am.install(addon_xpi)
+ assert am.backup_dir is not None
+ assert os.listdir(am.backup_dir) == ["test-addon-1@mozilla.org.xpi"]
+
+ am.clean()
+ assert os.listdir(staged_path) == ["test-addon-1@mozilla.org.xpi"]
+ am.clean()
+
+ # Test backup of folders
+ am.install(addon_folder)
+ assert am.backup_dir is None
+
+ am.install(addon_folder)
+ assert am.backup_dir is not None
+ assert os.listdir(am.backup_dir) == ["test-addon-1@mozilla.org"]
+
+ am.clean()
+ assert os.listdir(staged_path) == ["test-addon-1@mozilla.org"]
+ am.clean()
+
+ # Test backup of xpi files with another file name
+ am.install(addon_name)
+ assert am.backup_dir is None
+
+ am.install(addon_xpi)
+ assert am.backup_dir is not None
+ assert os.listdir(am.backup_dir) == ["test-addon-1@mozilla.org.xpi"]
+
+ am.clean()
+ assert os.listdir(staged_path) == ["test-addon-1@mozilla.org.xpi"]
+ am.clean()
+
+
+def test_install_invalid_addons(tmpdir, am):
+ tmpdir = tmpdir.strpath
+
+ # Generate installer stubs for all possible types of addons
+ addons = []
+ addons.append(
+ generate_addon(
+ "test-addon-invalid-no-manifest@mozilla.org", path=tmpdir, xpi=False
+ )
+ )
+ addons.append(generate_addon("test-addon-invalid-no-id@mozilla.org", path=tmpdir))
+
+ am.install(tmpdir)
+
+ assert am.installed_addons == []
+
+
+@pytest.mark.xfail(reason="feature not implemented as part of AddonManger")
+def test_install_error(am):
+ """ Check install raises an error with an invalid addon"""
+ temp_addon = generate_addon("test-addon-invalid-version@mozilla.org")
+ # This should raise an error here
+ with pytest.raises(Exception):
+ am.install(temp_addon)
+
+
+def test_addon_details(tmpdir, am):
+ tmpdir = tmpdir.strpath
+
+ # Generate installer stubs for a valid and invalid add-on manifest
+ valid_addon = generate_addon("test-addon-1@mozilla.org", path=tmpdir)
+ invalid_addon = generate_addon(
+ "test-addon-invalid-not-wellformed@mozilla.org", path=tmpdir
+ )
+
+ # Check valid add-on
+ details = am.addon_details(valid_addon)
+ assert details["id"] == "test-addon-1@mozilla.org"
+ assert details["name"] == "Test Add-on 1"
+ assert not details["unpack"]
+ assert details["version"] == "0.1"
+
+ # Check invalid add-on
+ with pytest.raises(mozprofile.addons.AddonFormatError):
+ am.addon_details(invalid_addon)
+
+ # Check invalid path
+ with pytest.raises(IOError):
+ am.addon_details("")
+
+ # Check invalid add-on format
+ addon_path = os.path.join(os.path.join(here, "files"), "not_an_addon.txt")
+ with pytest.raises(mozprofile.addons.AddonFormatError):
+ am.addon_details(addon_path)
+
+
+def test_clean_addons(am):
+ addon_one = generate_addon("test-addon-1@mozilla.org")
+ addon_two = generate_addon("test-addon-2@mozilla.org")
+
+ am.install(addon_one)
+ installed_addons = [
+ str(x[: -len(".xpi")])
+ for x in os.listdir(os.path.join(am.profile, "extensions"))
+ ]
+
+ # Create a new profile based on an existing profile
+ # Install an extra addon in the new profile
+ # Cleanup addons
+ duplicate_profile = mozprofile.profile.Profile(profile=am.profile, addons=addon_two)
+ duplicate_profile.addons.clean()
+
+ addons_after_cleanup = [
+ str(x[: -len(".xpi")])
+ for x in os.listdir(os.path.join(duplicate_profile.profile, "extensions"))
+ ]
+ # New addons installed should be removed by clean_addons()
+ assert installed_addons == addons_after_cleanup
+
+
+def test_noclean(tmpdir):
+ """test `restore=True/False` functionality"""
+ profile = tmpdir.mkdtemp().strpath
+ tmpdir = tmpdir.mkdtemp().strpath
+
+ # empty initially
+ assert not bool(os.listdir(profile))
+
+ # make an addon
+ addons = [
+ generate_addon("test-addon-1@mozilla.org", path=tmpdir),
+ os.path.join(here, "addons", "empty.xpi"),
+ ]
+
+ # install it with a restore=True AddonManager
+ am = mozprofile.addons.AddonManager(profile, restore=True)
+
+ for addon in addons:
+ am.install(addon)
+
+ # now its there
+ assert os.listdir(profile) == ["extensions"]
+ staging_folder = os.path.join(profile, "extensions")
+ assert os.path.exists(staging_folder)
+ assert len(os.listdir(staging_folder)) == 2
+
+ del am
+
+ assert os.listdir(profile) == ["extensions"]
+ assert os.path.exists(staging_folder)
+ assert os.listdir(staging_folder) == []
+
+
+def test_remove_addon(tmpdir, am):
+ tmpdir = tmpdir.strpath
+
+ addons = []
+ addons.append(generate_addon("test-addon-1@mozilla.org", path=tmpdir))
+ addons.append(generate_addon("test-addon-2@mozilla.org", path=tmpdir))
+
+ am.install(tmpdir)
+
+ extensions_path = os.path.join(am.profile, "extensions")
+ staging_path = os.path.join(extensions_path)
+
+ for addon in am._addons:
+ am.remove_addon(addon)
+
+ assert os.listdir(staging_path) == []
+ assert os.listdir(extensions_path) == []
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_bug758250.py b/testing/mozbase/mozprofile/tests/test_bug758250.py
new file mode 100755
index 0000000000..e9d7862bd5
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_bug758250.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+
+from __future__ import absolute_import
+
+import mozprofile
+import os
+import shutil
+
+import mozunit
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+"""
+use of --profile in mozrunner just blows away addon sources:
+https://bugzilla.mozilla.org/show_bug.cgi?id=758250
+"""
+
+
+def test_profile_addon_cleanup(tmpdir):
+ tmpdir = tmpdir.mkdtemp().strpath
+ addon = os.path.join(here, "addons", "empty")
+
+ # sanity check: the empty addon should be here
+ assert os.path.exists(addon)
+ assert os.path.isdir(addon)
+ assert os.path.exists(os.path.join(addon, "install.rdf"))
+
+ # because we are testing data loss, let's make sure we make a copy
+ shutil.rmtree(tmpdir)
+ shutil.copytree(addon, tmpdir)
+ assert os.path.exists(os.path.join(tmpdir, "install.rdf"))
+
+ # make a starter profile
+ profile = mozprofile.FirefoxProfile()
+ path = profile.profile
+
+ # make a new profile based on the old
+ newprofile = mozprofile.FirefoxProfile(profile=path, addons=[tmpdir])
+ newprofile.cleanup()
+
+ # the source addon *should* still exist
+ assert os.path.exists(tmpdir)
+ assert os.path.exists(os.path.join(tmpdir, "install.rdf"))
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_chrome_profile.py b/testing/mozbase/mozprofile/tests/test_chrome_profile.py
new file mode 100644
index 0000000000..95f3b01baa
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_chrome_profile.py
@@ -0,0 +1,75 @@
+# 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 json
+import os
+
+import mozunit
+
+from mozprofile import ChromeProfile
+
+
+def test_chrome_profile_pre_existing(tmpdir):
+ path = tmpdir.strpath
+ profile = ChromeProfile(profile=path)
+ assert not profile.create_new
+ assert os.path.isdir(profile.profile)
+ assert profile.profile == path
+
+
+def test_chrome_profile_create_new():
+ profile = ChromeProfile()
+ assert profile.create_new
+ assert os.path.isdir(profile.profile)
+ assert profile.profile.endswith("Default")
+
+
+def test_chrome_preferences(tmpdir):
+ prefs = {"foo": "bar"}
+ profile = ChromeProfile(preferences=prefs)
+ prefs_file = os.path.join(profile.profile, "Preferences")
+
+ assert os.path.isfile(prefs_file)
+
+ with open(prefs_file) as fh:
+ assert json.load(fh) == prefs
+
+ # test with existing prefs
+ prefs_file = tmpdir.join("Preferences").strpath
+ with open(prefs_file, "w") as fh:
+ json.dump({"num": "1"}, fh)
+
+ profile = ChromeProfile(profile=tmpdir.strpath, preferences=prefs)
+
+ def assert_prefs():
+ with open(prefs_file) as fh:
+ data = json.load(fh)
+
+ assert len(data) == 2
+ assert data.get("foo") == "bar"
+ assert data.get("num") == "1"
+
+ assert_prefs()
+ profile.reset()
+ assert_prefs()
+
+
+def test_chrome_addons():
+ addons = ["foo", "bar"]
+ profile = ChromeProfile(addons=addons)
+
+ assert isinstance(profile.addons, list)
+ assert profile.addons == addons
+
+ profile.addons.install("baz")
+ assert profile.addons == addons + ["baz"]
+
+ profile.reset()
+ assert profile.addons == addons
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_clone_cleanup.py b/testing/mozbase/mozprofile/tests/test_clone_cleanup.py
new file mode 100644
index 0000000000..40b00175d5
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_clone_cleanup.py
@@ -0,0 +1,81 @@
+#!/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/.
+
+from __future__ import absolute_import
+
+import os
+import mozfile
+
+import mozunit
+import pytest
+
+from mozprofile.profile import Profile
+
+"""
+test cleanup logic for the clone functionality
+see https://bugzilla.mozilla.org/show_bug.cgi?id=642843
+"""
+
+
+@pytest.fixture
+def profile(tmpdir):
+ # make a profile with one preference
+ path = tmpdir.mkdtemp().strpath
+ profile = Profile(path, preferences={"foo": "bar"}, restore=False)
+ user_js = os.path.join(profile.profile, "user.js")
+ assert os.path.exists(user_js)
+ return profile
+
+
+def test_restore_true(profile):
+ counter = [0]
+
+ def _feedback(dir, content):
+ # Called by shutil.copytree on each visited directory.
+ # Used here to display info.
+ #
+ # Returns the items that should be ignored by
+ # shutil.copytree when copying the tree, so always returns
+ # an empty list.
+ counter[0] += 1
+ return []
+
+ # make a clone of this profile with restore=True
+ clone = Profile.clone(profile.profile, restore=True, ignore=_feedback)
+ try:
+ clone.cleanup()
+
+ # clone should be deleted
+ assert not os.path.exists(clone.profile)
+ assert counter[0] > 0
+ finally:
+ mozfile.remove(clone.profile)
+
+
+def test_restore_false(profile):
+ # make a clone of this profile with restore=False
+ clone = Profile.clone(profile.profile, restore=False)
+ try:
+ clone.cleanup()
+
+ # clone should still be around on the filesystem
+ assert os.path.exists(clone.profile)
+ finally:
+ mozfile.remove(clone.profile)
+
+
+def test_cleanup_on_garbage_collected(profile):
+ clone = Profile.clone(profile.profile)
+ profile_dir = clone.profile
+ assert os.path.exists(profile_dir)
+ del clone
+
+ # clone should be deleted
+ assert not os.path.exists(profile_dir)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_nonce.py b/testing/mozbase/mozprofile/tests/test_nonce.py
new file mode 100755
index 0000000000..1d412d079e
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_nonce.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+"""
+test nonce in prefs delimeters
+see https://bugzilla.mozilla.org/show_bug.cgi?id=722804
+"""
+
+from __future__ import absolute_import
+
+import os
+
+import mozunit
+
+from mozprofile.prefs import Preferences
+from mozprofile.profile import Profile
+
+
+def test_nonce(tmpdir):
+ # make a profile with one preference
+ path = tmpdir.strpath
+ profile = Profile(path, preferences={"foo": "bar"}, restore=False)
+ user_js = os.path.join(profile.profile, "user.js")
+ assert os.path.exists(user_js)
+
+ # ensure the preference is correct
+ prefs = Preferences.read_prefs(user_js)
+ assert dict(prefs) == {"foo": "bar"}
+
+ del profile
+
+ # augment the profile with a second preference
+ profile = Profile(path, preferences={"fleem": "baz"}, restore=True)
+ prefs = Preferences.read_prefs(user_js)
+ assert dict(prefs) == {"foo": "bar", "fleem": "baz"}
+
+ # cleanup the profile;
+ # this should remove the new preferences but not the old
+ profile.cleanup()
+ prefs = Preferences.read_prefs(user_js)
+ assert dict(prefs) == {"foo": "bar"}
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_permissions.py b/testing/mozbase/mozprofile/tests/test_permissions.py
new file mode 100755
index 0000000000..d943bab66e
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_permissions.py
@@ -0,0 +1,225 @@
+#!/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/.
+
+from __future__ import absolute_import
+
+import os
+import sqlite3
+
+import mozunit
+import pytest
+
+from mozprofile.permissions import Permissions
+
+LOCATIONS = """http://mochi.test:8888 primary,privileged
+http://127.0.0.1:80 noxul
+http://127.0.0.1:8888 privileged
+"""
+
+
+@pytest.fixture
+def locations_file(tmpdir):
+ locations_file = tmpdir.join("locations.txt")
+ locations_file.write(LOCATIONS)
+ return locations_file.strpath
+
+
+@pytest.fixture
+def perms(tmpdir, locations_file):
+ return Permissions(tmpdir.mkdir("profile").strpath, locations_file)
+
+
+def test_create_permissions_db(perms):
+ profile_dir = perms._profileDir
+ perms_db_filename = os.path.join(profile_dir, "permissions.sqlite")
+
+ select_stmt = "select origin, type, permission from moz_hosts"
+
+ con = sqlite3.connect(perms_db_filename)
+ cur = con.cursor()
+ cur.execute(select_stmt)
+ entries = cur.fetchall()
+
+ assert len(entries) == 3
+
+ assert entries[0][0] == "http://mochi.test:8888"
+ assert entries[0][1] == "allowXULXBL"
+ assert entries[0][2] == 1
+
+ assert entries[1][0] == "http://127.0.0.1"
+ assert entries[1][1] == "allowXULXBL"
+ assert entries[1][2] == 2
+
+ assert entries[2][0] == "http://127.0.0.1:8888"
+ assert entries[2][1] == "allowXULXBL"
+ assert entries[2][2] == 1
+
+ perms._locations.add_host("a.b.c", port="8081", scheme="https", options="noxul")
+
+ cur.execute(select_stmt)
+ entries = cur.fetchall()
+
+ assert len(entries) == 4
+ assert entries[3][0] == "https://a.b.c:8081"
+ assert entries[3][1] == "allowXULXBL"
+ assert entries[3][2] == 2
+
+ # when creating a DB we should default to user_version==5
+ cur.execute("PRAGMA user_version")
+ entries = cur.fetchall()
+ assert entries[0][0] == 5
+
+ perms.clean_db()
+ # table should be removed
+ cur.execute("select * from sqlite_master where type='table'")
+ entries = cur.fetchall()
+ assert len(entries) == 0
+
+
+def test_nw_prefs(perms):
+ prefs, user_prefs = perms.network_prefs(False)
+
+ assert len(user_prefs) == 0
+ assert len(prefs) == 0
+
+ prefs, user_prefs = perms.network_prefs(True)
+ assert len(user_prefs) == 2
+ assert user_prefs[0] == ("network.proxy.type", 2)
+ assert user_prefs[1][0] == "network.proxy.autoconfig_url"
+
+ origins_decl = (
+ "var knownOrigins = (function () { return ['http://mochi.test:8888', "
+ "'http://127.0.0.1:80', 'http://127.0.0.1:8888'].reduce"
+ )
+ assert origins_decl in user_prefs[1][1]
+
+ proxy_check = (
+ "'http': 'PROXY mochi.test:8888'",
+ "'https': 'PROXY mochi.test:4443'",
+ "'ws': 'PROXY mochi.test:4443'",
+ "'wss': 'PROXY mochi.test:4443'",
+ )
+ assert all(c in user_prefs[1][1] for c in proxy_check)
+
+
+@pytest.fixture
+def perms_db_filename(tmpdir):
+ return tmpdir.join("permissions.sqlite").strpath
+
+
+@pytest.fixture
+def permDB(perms_db_filename):
+ permDB = sqlite3.connect(perms_db_filename)
+ yield permDB
+ permDB.cursor().close()
+
+
+# pylint: disable=W1638
+@pytest.fixture(params=range(2, 6))
+def version(request, perms_db_filename, permDB, locations_file):
+ version = request.param
+
+ cursor = permDB.cursor()
+ cursor.execute("PRAGMA user_version=%d;" % version)
+
+ if version == 5:
+ cursor.execute(
+ """CREATE TABLE IF NOT EXISTS moz_hosts (
+ id INTEGER PRIMARY KEY,
+ origin TEXT,
+ type TEXT,
+ permission INTEGER,
+ expireType INTEGER,
+ expireTime INTEGER,
+ modificationTime INTEGER)"""
+ )
+ elif version == 4:
+ cursor.execute(
+ """CREATE TABLE IF NOT EXISTS moz_hosts (
+ id INTEGER PRIMARY KEY,
+ host TEXT,
+ type TEXT,
+ permission INTEGER,
+ expireType INTEGER,
+ expireTime INTEGER,
+ modificationTime INTEGER,
+ appId INTEGER,
+ isInBrowserElement INTEGER)"""
+ )
+ elif version == 3:
+ cursor.execute(
+ """CREATE TABLE IF NOT EXISTS moz_hosts (
+ id INTEGER PRIMARY KEY,
+ host TEXT,
+ type TEXT,
+ permission INTEGER,
+ expireType INTEGER,
+ expireTime INTEGER,
+ appId INTEGER,
+ isInBrowserElement INTEGER)"""
+ )
+ elif version == 2:
+ cursor.execute(
+ """CREATE TABLE IF NOT EXISTS moz_hosts (
+ id INTEGER PRIMARY KEY,
+ host TEXT,
+ type TEXT,
+ permission INTEGER,
+ expireType INTEGER,
+ expireTime INTEGER)"""
+ )
+ else:
+ raise Exception("version must be 2, 3, 4 or 5")
+ permDB.commit()
+
+ # Create a permissions object to read the db
+ Permissions(os.path.dirname(perms_db_filename), locations_file)
+ return version
+
+
+def test_verify_user_version(version, permDB):
+ """Verifies that we call INSERT statements using the correct number
+ of columns for existing databases.
+ """
+ select_stmt = "select * from moz_hosts"
+
+ cur = permDB.cursor()
+ cur.execute(select_stmt)
+ entries = cur.fetchall()
+
+ assert len(entries) == 3
+
+ columns = {
+ 1: 6,
+ 2: 6,
+ 3: 8,
+ 4: 9,
+ 5: 7,
+ }[version]
+
+ assert len(entries[0]) == columns
+ for x in range(4, columns):
+ assert entries[0][x] == 0
+
+
+def test_schema_version(perms, locations_file):
+ profile_dir = perms._profileDir
+ perms_db_filename = os.path.join(profile_dir, "permissions.sqlite")
+ perms.write_db(open(locations_file, "w+b"))
+
+ stmt = "PRAGMA user_version;"
+
+ con = sqlite3.connect(perms_db_filename)
+ cur = con.cursor()
+ cur.execute(stmt)
+ entries = cur.fetchall()
+
+ schema_version = entries[0][0]
+ assert schema_version == 5
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_preferences.py b/testing/mozbase/mozprofile/tests/test_preferences.py
new file mode 100755
index 0000000000..261fe6f2a0
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_preferences.py
@@ -0,0 +1,422 @@
+#!/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/.
+
+from __future__ import absolute_import
+
+import mozfile
+import os
+import shutil
+import tempfile
+
+import mozunit
+import pytest
+from wptserve import server
+
+from mozprofile.cli import MozProfileCLI
+from mozprofile.prefs import Preferences, PreferencesReadError
+from mozprofile.profile import Profile
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+
+# preferences from files/prefs_with_comments.js
+_prefs_with_comments = {
+ "browser.startup.homepage": "http://planet.mozilla.org",
+ "zoom.minPercent": 30,
+ "zoom.maxPercent": 300,
+ "webgl.verbose": "false",
+}
+
+
+@pytest.fixture
+def run_command():
+ """
+ invokes mozprofile command line via the CLI factory
+ - args : command line arguments (equivalent of sys.argv[1:])
+ """
+
+ def inner(*args):
+ # instantiate the factory
+ cli = MozProfileCLI(list(args))
+
+ # create the profile
+ profile = cli.profile()
+
+ # return path to profile
+ return profile.profile
+
+ return inner
+
+
+@pytest.fixture
+def compare_generated(run_command):
+ """
+ writes out to a new profile with mozprofile command line
+ reads the generated preferences with prefs.py
+ compares the results
+ cleans up
+ """
+
+ def inner(prefs, commandline):
+ profile = run_command(*commandline)
+ prefs_file = os.path.join(profile, "user.js")
+ assert os.path.exists(prefs_file)
+ read = Preferences.read_prefs(prefs_file)
+ if isinstance(prefs, dict):
+ read = dict(read)
+ assert prefs == read
+ shutil.rmtree(profile)
+
+ return inner
+
+
+def test_basic_prefs(compare_generated):
+ """test setting a pref from the command line entry point"""
+
+ _prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"}
+ commandline = []
+ for pref, value in _prefs.items():
+ commandline += ["--pref", "%s:%s" % (pref, value)]
+ compare_generated(_prefs, commandline)
+
+
+def test_ordered_prefs(compare_generated):
+ """ensure the prefs stay in the right order"""
+ _prefs = [
+ ("browser.startup.homepage", "http://planet.mozilla.org/"),
+ ("zoom.minPercent", 30),
+ ("zoom.maxPercent", 300),
+ ("webgl.verbose", "false"),
+ ]
+ commandline = []
+ for pref, value in _prefs:
+ commandline += ["--pref", "%s:%s" % (pref, value)]
+ _prefs = [(i, Preferences.cast(j)) for i, j in _prefs]
+ compare_generated(_prefs, commandline)
+
+
+def test_ini(compare_generated):
+ # write the .ini file
+ _ini = """[DEFAULT]
+browser.startup.homepage = http://planet.mozilla.org/
+
+[foo]
+browser.startup.homepage = http://github.com/
+"""
+ try:
+ fd, name = tempfile.mkstemp(suffix=".ini", text=True)
+ os.write(fd, _ini.encode())
+ os.close(fd)
+ commandline = ["--preferences", name]
+
+ # test the [DEFAULT] section
+ _prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"}
+ compare_generated(_prefs, commandline)
+
+ # test a specific section
+ _prefs = {"browser.startup.homepage": "http://github.com/"}
+ commandline[-1] = commandline[-1] + ":foo"
+ compare_generated(_prefs, commandline)
+
+ finally:
+ # cleanup
+ os.remove(name)
+
+
+def test_ini_keep_case(compare_generated):
+ """
+ Read a preferences config file with a preference in camel-case style.
+ Check that the read preference name has not been lower-cased
+ """
+ # write the .ini file
+ _ini = """[DEFAULT]
+network.dns.disableIPv6 = True
+"""
+ try:
+ fd, name = tempfile.mkstemp(suffix=".ini", text=True)
+ os.write(fd, _ini.encode())
+ os.close(fd)
+ commandline = ["--preferences", name]
+
+ # test the [DEFAULT] section
+ _prefs = {"network.dns.disableIPv6": "True"}
+ compare_generated(_prefs, commandline)
+
+ finally:
+ # cleanup
+ os.remove(name)
+
+
+def test_reset_should_remove_added_prefs():
+ """Check that when we call reset the items we expect are updated"""
+ profile = Profile()
+ prefs_file = os.path.join(profile.profile, "user.js")
+
+ # we shouldn't have any initial preferences
+ initial_prefs = Preferences.read_prefs(prefs_file)
+ assert not initial_prefs
+ initial_prefs = open(prefs_file).read().strip()
+ assert not initial_prefs
+
+ # add some preferences
+ prefs1 = [("mr.t.quotes", "i aint getting on no plane!")]
+ profile.set_preferences(prefs1)
+ assert prefs1 == Preferences.read_prefs(prefs_file)
+ lines = open(prefs_file).read().strip().splitlines()
+ assert any(line.startswith("#MozRunner Prefs Start") for line in lines)
+ assert any(line.startswith("#MozRunner Prefs End") for line in lines)
+
+ profile.reset()
+ assert prefs1 != Preferences.read_prefs(os.path.join(profile.profile, "user.js"))
+
+
+def test_reset_should_keep_user_added_prefs():
+ """Check that when we call reset the items we expect are updated"""
+ profile = Profile()
+ prefs_file = os.path.join(profile.profile, "user.js")
+
+ # we shouldn't have any initial preferences
+ initial_prefs = Preferences.read_prefs(prefs_file)
+ assert not initial_prefs
+ initial_prefs = open(prefs_file).read().strip()
+ assert not initial_prefs
+
+ # add some preferences
+ prefs1 = [("mr.t.quotes", "i aint getting on no plane!")]
+ profile.set_persistent_preferences(prefs1)
+ assert prefs1 == Preferences.read_prefs(prefs_file)
+ lines = open(prefs_file).read().strip().splitlines()
+ assert any(line.startswith("#MozRunner Prefs Start") for line in lines)
+ assert any(line.startswith("#MozRunner Prefs End") for line in lines)
+
+ profile.reset()
+ assert prefs1 == Preferences.read_prefs(os.path.join(profile.profile, "user.js"))
+
+
+def test_magic_markers():
+ """ensure our magic markers are working"""
+
+ profile = Profile()
+ prefs_file = os.path.join(profile.profile, "user.js")
+
+ # we shouldn't have any initial preferences
+ initial_prefs = Preferences.read_prefs(prefs_file)
+ assert not initial_prefs
+ initial_prefs = open(prefs_file).read().strip()
+ assert not initial_prefs
+
+ # add some preferences
+ prefs1 = [
+ ("browser.startup.homepage", "http://planet.mozilla.org/"),
+ ("zoom.minPercent", 30),
+ ]
+ profile.set_preferences(prefs1)
+ assert prefs1 == Preferences.read_prefs(prefs_file)
+ lines = open(prefs_file).read().strip().splitlines()
+ assert bool([line for line in lines if line.startswith("#MozRunner Prefs Start")])
+ assert bool([line for line in lines if line.startswith("#MozRunner Prefs End")])
+
+ # add some more preferences
+ prefs2 = [("zoom.maxPercent", 300), ("webgl.verbose", "false")]
+ profile.set_preferences(prefs2)
+ assert prefs1 + prefs2 == Preferences.read_prefs(prefs_file)
+ lines = open(prefs_file).read().strip().splitlines()
+ assert (
+ len([line for line in lines if line.startswith("#MozRunner Prefs Start")]) == 2
+ )
+ assert len([line for line in lines if line.startswith("#MozRunner Prefs End")]) == 2
+
+ # now clean it up
+ profile.clean_preferences()
+ final_prefs = Preferences.read_prefs(prefs_file)
+ assert not final_prefs
+ lines = open(prefs_file).read().strip().splitlines()
+ assert "#MozRunner Prefs Start" not in lines
+ assert "#MozRunner Prefs End" not in lines
+
+
+def test_preexisting_preferences():
+ """ensure you don't clobber preexisting preferences"""
+
+ # make a pretend profile
+ tempdir = tempfile.mkdtemp()
+
+ try:
+ # make a user.js
+ contents = """
+user_pref("webgl.enabled_for_all_sites", true);
+user_pref("webgl.force-enabled", true);
+"""
+ user_js = os.path.join(tempdir, "user.js")
+ f = open(user_js, "w")
+ f.write(contents)
+ f.close()
+
+ # make sure you can read it
+ prefs = Preferences.read_prefs(user_js)
+ original_prefs = [
+ ("webgl.enabled_for_all_sites", True),
+ ("webgl.force-enabled", True),
+ ]
+ assert prefs == original_prefs
+
+ # now read this as a profile
+ profile = Profile(
+ tempdir, preferences={"browser.download.dir": "/home/jhammel"}
+ )
+
+ # make sure the new pref is now there
+ new_prefs = original_prefs[:] + [("browser.download.dir", "/home/jhammel")]
+ prefs = Preferences.read_prefs(user_js)
+ assert prefs == new_prefs
+
+ # clean up the added preferences
+ profile.cleanup()
+ del profile
+
+ # make sure you have the original preferences
+ prefs = Preferences.read_prefs(user_js)
+ assert prefs == original_prefs
+ finally:
+ shutil.rmtree(tempdir)
+
+
+def test_can_read_prefs_with_multiline_comments():
+ """
+ Ensure that multiple comments in the file header do not break reading
+ the prefs (https://bugzilla.mozilla.org/show_bug.cgi?id=1233534).
+ """
+ user_js = tempfile.NamedTemporaryFile(suffix=".js", delete=False)
+ try:
+ with user_js:
+ user_js.write(
+ """
+# Mozilla User Preferences
+
+/* Do not edit this file.
+*
+* If you make changes to this file while the application is running,
+* the changes will be overwritten when the application exits.
+*
+* To make a manual change to preferences, you can visit the URL about:config
+*/
+
+user_pref("webgl.enabled_for_all_sites", true);
+user_pref("webgl.force-enabled", true);
+""".encode()
+ )
+ assert Preferences.read_prefs(user_js.name) == [
+ ("webgl.enabled_for_all_sites", True),
+ ("webgl.force-enabled", True),
+ ]
+ finally:
+ mozfile.remove(user_js.name)
+
+
+def test_json(compare_generated):
+ _prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"}
+ json = '{"browser.startup.homepage": "http://planet.mozilla.org/"}'
+
+ # just repr it...could use the json module but we don't need it here
+ with mozfile.NamedTemporaryFile(suffix=".json") as f:
+ f.write(json.encode())
+ f.flush()
+
+ commandline = ["--preferences", f.name]
+ compare_generated(_prefs, commandline)
+
+
+def test_json_datatypes():
+ # minPercent is at 30.1 to test if non-integer data raises an exception
+ json = """{"zoom.minPercent": 30.1, "zoom.maxPercent": 300}"""
+
+ with mozfile.NamedTemporaryFile(suffix=".json") as f:
+ f.write(json.encode())
+ f.flush()
+
+ with pytest.raises(PreferencesReadError):
+ Preferences.read_json(f._path)
+
+
+def test_prefs_write():
+ """test that the Preferences.write() method correctly serializes preferences"""
+
+ _prefs = {
+ "browser.startup.homepage": "http://planet.mozilla.org",
+ "zoom.minPercent": 30,
+ "zoom.maxPercent": 300,
+ }
+
+ # make a Preferences manager with the testing preferences
+ preferences = Preferences(_prefs)
+
+ # write them to a temporary location
+ path = None
+ read_prefs = None
+ try:
+ with mozfile.NamedTemporaryFile(suffix=".js", delete=False, mode="w+t") as f:
+ path = f.name
+ preferences.write(f, _prefs)
+
+ # read them back and ensure we get what we put in
+ read_prefs = dict(Preferences.read_prefs(path))
+
+ finally:
+ # cleanup
+ if path and os.path.exists(path):
+ os.remove(path)
+
+ assert read_prefs == _prefs
+
+
+def test_read_prefs_with_comments():
+ """test reading preferences from a prefs.js file that contains comments"""
+
+ path = os.path.join(here, "files", "prefs_with_comments.js")
+ assert dict(Preferences.read_prefs(path)) == _prefs_with_comments
+
+
+def test_read_prefs_with_interpolation():
+ """test reading preferences from a prefs.js file whose values
+ require interpolation"""
+
+ expected_prefs = {
+ "browser.foo": "http://server-name",
+ "zoom.minPercent": 30,
+ "webgl.verbose": "false",
+ "browser.bar": "somethingxyz",
+ }
+ values = {"server": "server-name", "abc": "something"}
+ path = os.path.join(here, "files", "prefs_with_interpolation.js")
+ read_prefs = Preferences.read_prefs(path, interpolation=values)
+ assert dict(read_prefs) == expected_prefs
+
+
+def test_read_prefs_ttw():
+ """test reading preferences through the web via wptserve"""
+
+ # create a WebTestHttpd instance
+ docroot = os.path.join(here, "files")
+ host = "127.0.0.1"
+ port = 8888
+ httpd = server.WebTestHttpd(host=host, port=port, doc_root=docroot)
+
+ # create a preferences instance
+ prefs = Preferences()
+
+ try:
+ # start server
+ httpd.start(block=False)
+
+ # read preferences through the web
+ read = prefs.read_prefs("http://%s:%d/prefs_with_comments.js" % (host, port))
+ assert dict(read) == _prefs_with_comments
+ finally:
+ httpd.stop()
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_profile.py b/testing/mozbase/mozprofile/tests/test_profile.py
new file mode 100644
index 0000000000..20aaf959de
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_profile.py
@@ -0,0 +1,114 @@
+#!/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/.
+
+from __future__ import absolute_import, division
+
+import os
+
+import mozunit
+import pytest
+
+from mozprofile.prefs import Preferences
+from mozprofile import (
+ BaseProfile,
+ Profile,
+ ChromeProfile,
+ ChromiumProfile,
+ FirefoxProfile,
+ ThunderbirdProfile,
+ create_profile,
+)
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+def test_with_profile_should_cleanup():
+ with Profile() as profile:
+ assert os.path.exists(profile.profile)
+
+ # profile is cleaned
+ assert not os.path.exists(profile.profile)
+
+
+def test_with_profile_should_cleanup_even_on_exception():
+ with pytest.raises(ZeroDivisionError):
+ # pylint --py3k W1619
+ with Profile() as profile:
+ assert os.path.exists(profile.profile)
+ 1 / 0 # will raise ZeroDivisionError
+
+ # profile is cleaned
+ assert not os.path.exists(profile.profile)
+
+
+@pytest.mark.parametrize(
+ "app,cls",
+ [
+ ("chrome", ChromeProfile),
+ ("chromium", ChromiumProfile),
+ ("firefox", FirefoxProfile),
+ ("thunderbird", ThunderbirdProfile),
+ ("unknown", None),
+ ],
+)
+def test_create_profile(tmpdir, app, cls):
+ path = tmpdir.strpath
+
+ if cls is None:
+ with pytest.raises(NotImplementedError):
+ create_profile(app)
+ return
+
+ profile = create_profile(app, profile=path)
+ assert isinstance(profile, BaseProfile)
+ assert profile.__class__ == cls
+ assert profile.profile == path
+
+
+@pytest.mark.parametrize(
+ "cls",
+ [
+ Profile,
+ ChromeProfile,
+ ChromiumProfile,
+ ],
+)
+def test_merge_profile(cls):
+ profile = cls(preferences={"foo": "bar"})
+ assert profile._addons == []
+ assert os.path.isfile(
+ os.path.join(profile.profile, profile.preference_file_names[0])
+ )
+
+ other_profile = os.path.join(here, "files", "dummy-profile")
+ profile.merge(other_profile)
+
+ # make sure to add a pref file for each preference_file_names in the dummy-profile
+ prefs = {}
+ for name in profile.preference_file_names:
+ path = os.path.join(profile.profile, name)
+ assert os.path.isfile(path)
+
+ try:
+ prefs.update(Preferences.read_json(path))
+ except ValueError:
+ prefs.update(Preferences.read_prefs(path))
+
+ assert "foo" in prefs
+ assert len(prefs) == len(profile.preference_file_names) + 1
+ assert all(name in prefs for name in profile.preference_file_names)
+
+ # for Google Chrome currently we ignore webext in profile prefs
+ if cls == Profile:
+ assert len(profile._addons) == 1
+ assert profile._addons[0].endswith("empty.xpi")
+ assert os.path.exists(profile._addons[0])
+ else:
+ assert len(profile._addons) == 0
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_profile_view.py b/testing/mozbase/mozprofile/tests/test_profile_view.py
new file mode 100644
index 0000000000..948491c140
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_profile_view.py
@@ -0,0 +1,78 @@
+#!/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/.
+
+from __future__ import absolute_import
+
+import mozprofile
+import os
+import sys
+import pytest
+
+import mozunit
+from six import text_type
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+
+def test_profileprint(tmpdir):
+ """Test the summary function."""
+ keys = set(["Files", "Path"])
+
+ tmpdir = tmpdir.strpath
+ profile = mozprofile.FirefoxProfile(tmpdir)
+ parts = profile.summary(return_parts=True)
+ parts = dict(parts)
+
+ assert parts["Path"] == tmpdir
+ assert set(parts.keys()) == keys
+
+
+def test_str_cast():
+ """Test casting to a string."""
+ profile = mozprofile.Profile()
+ if sys.version_info[0] >= 3:
+ assert str(profile) == profile.summary()
+ else:
+ assert str(profile) == profile.summary().encode("utf-8")
+
+
+@pytest.mark.skipif(
+ sys.version_info[0] >= 3, reason="no unicode() operator starting from python3"
+)
+def test_unicode_cast():
+ """Test casting to a unicode string."""
+ profile = mozprofile.Profile()
+ assert text_type(profile) == profile.summary()
+
+
+def test_profile_diff():
+ profile1 = mozprofile.Profile()
+ profile2 = mozprofile.Profile(preferences=dict(foo="bar"))
+
+ # diff a profile against itself; no difference
+ assert mozprofile.diff(profile1, profile1) == []
+
+ # diff two profiles
+ diff = dict(mozprofile.diff(profile1, profile2))
+ assert list(diff.keys()) == ["user.js"]
+ lines = [line.strip() for line in diff["user.js"].splitlines()]
+ assert "+foo: bar" in lines
+
+ # diff a blank vs FirefoxProfile
+ ff_profile = mozprofile.FirefoxProfile()
+ diff = dict(mozprofile.diff(profile2, ff_profile))
+ assert list(diff.keys()) == ["user.js"]
+ lines = [line.strip() for line in diff["user.js"].splitlines()]
+ assert "-foo: bar" in lines
+ ff_pref_lines = [
+ "+%s: %s" % (key, value)
+ for key, value in mozprofile.FirefoxProfile.preferences.items()
+ ]
+ assert set(ff_pref_lines).issubset(lines)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/testing/mozbase/mozprofile/tests/test_server_locations.py b/testing/mozbase/mozprofile/tests/test_server_locations.py
new file mode 100644
index 0000000000..caa07196c5
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/test_server_locations.py
@@ -0,0 +1,151 @@
+#!/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/.
+
+from __future__ import absolute_import
+
+import mozunit
+import pytest
+
+from mozprofile.permissions import (
+ ServerLocations,
+ MissingPrimaryLocationError,
+ MultiplePrimaryLocationsError,
+ BadPortLocationError,
+ LocationsSyntaxError,
+)
+
+
+LOCATIONS = """# This is the primary location from which tests run.
+#
+http://mochi.test:8888 primary,privileged
+
+# a few test locations
+http://127.0.0.1:80 privileged
+http://127.0.0.1:8888 privileged
+https://test:80 privileged
+http://example.org:80 privileged
+http://test1.example.org privileged
+
+"""
+
+LOCATIONS_NO_PRIMARY = """http://secondary.test:80 privileged
+http://tertiary.test:8888 privileged
+"""
+
+LOCATIONS_BAD_PORT = """http://mochi.test:8888 primary,privileged
+http://127.0.0.1:80 privileged
+http://127.0.0.1:8888 privileged
+http://test:badport privileged
+http://example.org:80 privileged
+"""
+
+
+def compare_location(location, scheme, host, port, options):
+ assert location.scheme == scheme
+ assert location.host == host
+ assert location.port == port
+ assert location.options == options
+
+
+@pytest.fixture
+def create_temp_file(tmpdir):
+ def inner(contents):
+ f = tmpdir.mkdtemp().join("locations.txt")
+ f.write(contents)
+ return f.strpath
+
+ return inner
+
+
+def test_server_locations(create_temp_file):
+ # write a permissions file
+ f = create_temp_file(LOCATIONS)
+
+ # read the locations
+ locations = ServerLocations(f)
+
+ # ensure that they're what we expect
+ assert len(locations) == 6
+ i = iter(locations)
+ compare_location(next(i), "http", "mochi.test", "8888", ["primary", "privileged"])
+ compare_location(next(i), "http", "127.0.0.1", "80", ["privileged"])
+ compare_location(next(i), "http", "127.0.0.1", "8888", ["privileged"])
+ compare_location(next(i), "https", "test", "80", ["privileged"])
+ compare_location(next(i), "http", "example.org", "80", ["privileged"])
+ compare_location(next(i), "http", "test1.example.org", "8888", ["privileged"])
+
+ locations.add_host("mozilla.org")
+ assert len(locations) == 7
+ compare_location(next(i), "http", "mozilla.org", "80", ["privileged"])
+
+ # test some errors
+ with pytest.raises(MultiplePrimaryLocationsError):
+ locations.add_host("primary.test", options="primary")
+
+ # assert we don't throw DuplicateLocationError
+ locations.add_host("127.0.0.1")
+
+ with pytest.raises(BadPortLocationError):
+ locations.add_host("127.0.0.1", port="abc")
+
+ # test some errors in locations file
+ f = create_temp_file(LOCATIONS_NO_PRIMARY)
+
+ exc = None
+ try:
+ ServerLocations(f)
+ except LocationsSyntaxError as e:
+ exc = e
+ assert exc is not None
+ assert exc.err.__class__ == MissingPrimaryLocationError
+ assert exc.lineno == 3
+
+ # test bad port in a locations file to ensure lineno calculated
+ # properly.
+ f = create_temp_file(LOCATIONS_BAD_PORT)
+
+ exc = None
+ try:
+ ServerLocations(f)
+ except LocationsSyntaxError as e:
+ exc = e
+ assert exc is not None
+ assert exc.err.__class__ == BadPortLocationError
+ assert exc.lineno == 4
+
+
+def test_server_locations_callback(create_temp_file):
+ class CallbackTest(object):
+ last_locations = None
+
+ def callback(self, locations):
+ self.last_locations = locations
+
+ c = CallbackTest()
+ f = create_temp_file(LOCATIONS)
+ locations = ServerLocations(f, c.callback)
+
+ # callback should be for all locations in file
+ assert len(c.last_locations) == 6
+
+ # validate arbitrary one
+ compare_location(c.last_locations[2], "http", "127.0.0.1", "8888", ["privileged"])
+
+ locations.add_host("a.b.c")
+
+ # callback should be just for one location
+ assert len(c.last_locations) == 1
+ compare_location(c.last_locations[0], "http", "a.b.c", "80", ["privileged"])
+
+ # read a second file, which should generate a callback with both
+ # locations.
+ f = create_temp_file(LOCATIONS_NO_PRIMARY)
+ locations.read(f)
+ assert len(c.last_locations) == 2
+
+
+if __name__ == "__main__":
+ mozunit.main()