Bug 1451159 - [mozprofile] Implement ability to merge other profile directories into the current one draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Thu, 19 Apr 2018 15:31:43 -0400
changeset 791460 24b6fa9ae84fb2bfb212789197a82c29747d4522
parent 791409 8994f35fe5fc89f4e8f4e09579a6962f8a4a3e65
child 791461 161baa18ce4bb8d32e33f4325bdbd6969a0ce257
push id108824
push userahalberstadt@mozilla.com
push dateFri, 04 May 2018 13:44:02 +0000
bugs1451159
milestone61.0a1
Bug 1451159 - [mozprofile] Implement ability to merge other profile directories into the current one 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()