Bug 1451159 - [mozprofile] Implement ability to merge other profile directories into the current one
MozReview-Commit-ID: EHOFU58Ipa2
--- a/testing/mozbase/mozprofile/mozprofile/profile.py
+++ b/testing/mozbase/mozprofile/mozprofile/profile.py
@@ -5,17 +5,17 @@
from __future__ import absolute_import
import json
import os
import platform
import tempfile
import time
import uuid
-from abc import ABCMeta, abstractmethod
+from abc import ABCMeta, abstractmethod, abstractproperty
from shutil import copytree
import mozfile
from six import string_types
from .addons import AddonManager
from .permissions import Permissions
from .prefs import Preferences
@@ -27,17 +27,28 @@ from .prefs import Preferences
'ThunderbirdProfile',
'create_profile']
class BaseProfile(object):
__metaclass__ = ABCMeta
def __init__(self, profile=None, addons=None, preferences=None, restore=True):
- self._addons = addons
+ """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
@@ -78,16 +89,50 @@ class BaseProfile(object):
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')
+ 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:
@@ -123,31 +168,32 @@ class Profile(BaseProfile):
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):
+ 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)
+ profile=profile, addons=addons, preferences=preferences, restore=restore, **kwargs)
self._locations = locations
self._proxy = proxy
self._whitelistpaths = whitelistpaths
# Initialize all class members
self._reset()
@@ -221,37 +267,32 @@ class Profile(BaseProfile):
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"""
-
- # append to the file
prefs_file = os.path.join(self.profile, filename)
- f = open(prefs_file, 'a')
-
- if preferences:
+ 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])
- # write the preferences
Preferences.write(f, preferences)
# closing delimeter
f.write('%s\n' % self.delimeters[1])
- f.close()
-
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):
@@ -440,49 +481,61 @@ class ThunderbirdProfile(Profile):
'browser.warnOnQuit': False,
'browser.sessionstore.resume_from_crash': False,
# prevents the 'new e-mail address' wizard on new profile
'mail.provider.enabled': False,
}
class ChromeProfile(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):
+ # TODO Implement this properly
+ return os.path.exists(addon)
+
def __init__(self, **kwargs):
super(ChromeProfile, 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:
- pref_file = os.path.join(self.profile, 'Preferences')
-
- prefs = {}
- if os.path.isfile(pref_file):
- with open(pref_file, 'r') as fh:
- prefs.update(json.load(fh))
-
- prefs.update(self._preferences)
- with open(pref_file, 'w') as fh:
- json.dump(prefs, fh)
+ 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)
+
profile_class = {
'chrome': ChromeProfile,
'firefox': FirefoxProfile,
'thunderbird': ThunderbirdProfile,
}
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ globals: {
+ user_pref: true,
+ }
+};
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences
@@ -0,0 +1,1 @@
+{"Preferences": 1}
new file mode 100644
index 0000000000000000000000000000000000000000..26f28f099d24bec509e3abd991ff453c667543d6
GIT binary patch
literal 530
zc$^FHW@Zs#U|`^2a4J``_nh^LHI0#hVG9!j12=;VLuOuaNn%cpUQtR~Xb2|*^KFSV
zzmq^*TEWf0$nq7a60Es&lD~hmfyA-%pER}Nt}NMdpu{QSs!RE$=I*NZA5zU0vU;og
zJT9L;^<1u7{@c%g=IyJ$^k&m!)hhP89bUeF3>N%T{k-YsS&^@8S!(}Q8<u)`+}`FW
z%|115f`O-yld_O&#D0+-k(!6C*UVC`Ke0sNd(x&~0;wl+%@&ooIIL@SxBJYuNjjrQ
zH|z3r_nJ7}vq$2yrtb353Q4YrOgcGLV}ar8GmdOVqWWjPl=EydTfKU_s(x$a+i9mx
zI9T56J&_?0m0n!F?UB%p<o^o0-tw&2+S>Q_wz}B%eaXeVcS2^}<v)7i@K#r|O^f5S
zIZfjaTz=EHGxg8)r+Tk0rG+wgugUJe7iE7$!nd_;(JO&t_8Ql@4X&5QcFmDCzkDxg
z?S`AT)Srl(EITUkT<~c{iJew~(ej4FGmhIdrpUzi>?_-5uCg@Xd|ql{f!Lg=1ot~l
zdv0g&-QwD2_-Xa##1r;yH`E0`8D0AC{j8Qbz?+dtju}^ENicu_kjv1}2x6f`9V;a2
W(4sBCo0ScsiIE`?NUsLzW&i+p&C!|w
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js
@@ -0,0 +1,1 @@
+user_pref("prefs.js", 1);
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/user.js
@@ -0,0 +1,1 @@
+user_pref("user.js", 1);
--- a/testing/mozbase/mozprofile/tests/test_profile.py
+++ b/testing/mozbase/mozprofile/tests/test_profile.py
@@ -6,25 +6,28 @@
from __future__ import absolute_import
import os
import mozunit
import pytest
+from mozprofile.prefs import Preferences
from mozprofile import (
BaseProfile,
Profile,
ChromeProfile,
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)
@@ -54,10 +57,42 @@ def test_create_profile(tmpdir, app, cls
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,
+])
+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)
+
+ assert len(profile._addons) == 1
+ assert profile._addons[0].endswith('empty.xpi')
+ assert os.path.exists(profile._addons[0])
+
+
if __name__ == '__main__':
mozunit.main()