Bug 1451159 - [mozprofile] Implement ability to merge other profile directories into the current one r=gbrown
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Thu, 19 Apr 2018 15:31:43 -0400
changeset 416925 05cf749971979f41084cd6e1d501035e329d9d9d
parent 416924 2a5a941c2db9c64d18a7b56cb7112ae7ac14fc52
child 416926 9a2ba1c7b0ec32f81e067acd2610e47b9be7d764
push id33943
push usercsabou@mozilla.com
push dateFri, 04 May 2018 17:19:55 +0000
treeherdermozilla-central@ef1db4e8bf06 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgbrown
bugs1451159
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1451159 - [mozprofile] Implement ability to merge other profile directories into the current one r=gbrown MozReview-Commit-ID: EHOFU58Ipa2
testing/mozbase/mozprofile/mozprofile/profile.py
testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js
testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences
testing/mozbase/mozprofile/tests/files/dummy-profile/extensions/empty.xpi
testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js
testing/mozbase/mozprofile/tests/files/dummy-profile/user.js
testing/mozbase/mozprofile/tests/test_profile.py
--- 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()