2 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
3 # vim: set filetype=python:
5 # This Source Code Form is subject to the terms of the Mozilla Public
6 # License, v. 2.0. If a copy of the MPL was not distributed with this
7 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 # The beginning of this script is both valid shell and valid python,
10 # such that the script starts with the shell and is reexecuted python
11 '''which' mach
> /dev
/null
2>&1 && exec mach python
"$0" "$@" ||
12 echo "mach not found, either add it to your \$PATH or run this script via ./mach python testing/profiles/profile"; exit # noqa
15 from __future__ import absolute_import, unicode_literals, print_function
17 """This script can be used to:
19 1) Show all preferences for a given suite
20 2) Diff preferences between two suites or profiles
21 3) Sort preference files alphabetically for a given profile
23 To use, either make sure that `mach` is on your $PATH, or run:
24 $ ./mach python testing/profiles/profile <args>
33 from argparse import ArgumentParser
34 from itertools import chain
36 from mozprofile import Profile
37 from mozprofile.prefs import Preferences
39 here = os.path.abspath(os.path.dirname(__file__))
44 from mozbuild.base import MozbuildObject
45 build = MozbuildObject.from_environment(cwd=here)
46 build.virtualenv_manager.install_pip_package("jsondiff")
57 '{pref
}: {value_a
} => {value_b
}'
62 def read_prefs(profile, pref_files=None):
63 """Read and return all preferences set in the given profile.
65 :param profile: Profile name relative to this `here`.
66 :returns: A dictionary of preferences set in the profile.
68 pref_files = pref_files or Profile.preference_file_names
70 for name in pref_files:
71 path = os.path.join(here, profile, name)
72 if not os.path.isfile(path):
76 prefs.update(Preferences.read_json(path))
78 prefs.update(Preferences.read_prefs(path))
82 def get_profiles(key):
83 """Return a list of profile names for key."""
84 with open(os.path.join(here, 'profiles.json
'), 'r
') as fh:
85 profiles = json.load(fh)
95 names.update(profiles[key])
96 elif os.path.isdir(os.path.join(here, key)):
100 raise ValueError('{} is not a recognized suite or profile
'.format(key))
105 """Read preferences relevant to either a profile or suite.
107 :param key: Can either be the name of a profile, or the name of
108 a suite as defined in suites.json.
111 for profile in get_profiles(key):
112 prefs.update(read_prefs(profile))
116 def format_diff(diff, fmt, limit_key):
120 diff = {limit_key: diff[limit_key]}
124 print(json.dumps(diff, sort_keys=True, indent=2))
128 for key, prefs in sorted(diff.items()):
130 lines.append("{}:".format(key))
132 for pref, value in sorted(prefs.items()):
133 context = {'pref
': pref, 'value
': repr(value)}
135 if isinstance(value, list):
136 context['value_a
'] = repr(value[0])
137 context['value_b
'] = repr(value[1])
138 text = FORMAT_STRINGS[fmt][1].format(**context)
140 text = FORMAT_STRINGS[fmt][0].format(**context)
142 lines.append('{}{}'.format(indent, text))
144 print('\n'.join(lines).strip())
147 def diff(a, b, fmt, limit_key):
148 """Diff two profiles or suites.
150 :param a: The first profile or suite name.
151 :param b: The second profile or suite name.
155 res = jsondiff.diff(prefs_a, prefs_b, syntax='symmetric
')
159 if isinstance(res, list) and len(res) == 2:
161 jsondiff.Symbol('delete
'): res[0],
162 jsondiff.Symbol('insert
'): res[1],
165 # Post process results to make them JSON compatible and a
166 # bit more clear. Also calculate identical prefs.
168 results['change
'] = {k: v for k, v in res.items() if not isinstance(k, jsondiff.Symbol)}
170 symbols = [(k, v) for k, v in res.items() if isinstance(k, jsondiff.Symbol)]
171 results['insert
'] = {k: v for sym, pref in symbols for k, v in pref.items()
172 if sym.label == 'insert
'}
173 results['delete
'] = {k: v for sym, pref in symbols for k, v in pref.items()
174 if sym.label == 'delete
'}
176 same = set(prefs_a.keys()) - set(chain(*results.values()))
177 results['same
'] = {k: v for k, v in prefs_a.items() if k in same}
178 return format_diff(results, fmt, limit_key)
181 def read_with_comments(path):
182 with open(path, 'r
') as fh:
183 lines = fh.readlines()
192 if line.startswith('//'):
197 result.append(buf + [line])
201 result.append([line])
206 """Sort the given pref file alphabetically, preserving preceding comments
207 that start with '//'.
209 :param path: Path to the preference file to sort.
211 result = read_with_comments(path)
212 result = sorted(result, key=lambda x: x[-1])
213 result = chain(*result)
215 with open(path, 'w
') as fh:
216 fh.write('\n'.join(result) + '\n')
220 """Sort all prefs in the given profile alphabetically. This will preserve
221 comments on preceding lines.
223 :param profile: The name of the profile to sort.
225 pref_files = Profile.preference_file_names
227 for name in pref_files:
228 path = os.path.join(here, profile, name)
229 if os.path.isfile(path):
234 """Display all prefs set in profiles used by the given suite.
236 :param suite: The name of the suite to show preferences for. This must
237 be a key in suites.json.
239 for k, v in sorted(read(suite).items()):
240 print("{}: {}".format(k, repr(v)))
243 def rm(profile, pref_file):
245 lines = sys.stdin.readlines()
247 with open(pref_file, 'r
') as fh:
248 lines = fh.readlines()
250 lines = [l.strip() for l in lines if l.strip()]
254 def filter_line(content):
255 return not any(line in content[-1] for line in lines)
257 path = os.path.join(here, profile, 'user.js
')
258 contents = read_with_comments(path)
259 contents = filter(filter_line, contents)
260 contents = chain(*contents)
261 with open(path, 'w
') as fh:
262 fh.write('\n'.join(contents))
265 def cli(args=sys.argv[1:]):
266 parser = ArgumentParser()
267 subparsers = parser.add_subparsers(dest='func
')
268 subparsers.required = True
270 diff_parser = subparsers.add_parser('diff')
271 diff_parser.add_argument('a
', metavar='A
',
272 help="Path to the first profile or suite name to diff.")
273 diff_parser.add_argument('b
', metavar='B
',
274 help="Path to the second profile or suite name to diff.")
275 diff_parser.add_argument('-f', '--format', dest='fmt', default='pretty
',
276 choices=['pretty
', 'json
', 'names
'],
277 help="Format to dump diff in (default: pretty)")
278 diff_parser.add_argument('-k', '--limit-key', default=None,
279 choices=['change
', 'delete
', 'insert
', 'same
'],
280 help="Restrict diff to the specified key.")
281 diff_parser.set_defaults(func=diff)
283 sort_parser = subparsers.add_parser('sort')
284 sort_parser.add_argument('profile
', help="Path to profile to sort preferences.")
285 sort_parser.set_defaults(func=sort)
287 show_parser = subparsers.add_parser('show
')
288 show_parser.add_argument('suite
', help="Name of suite to show arguments for.")
289 show_parser.set_defaults(func=show)
291 rm_parser = subparsers.add_parser('rm')
292 rm_parser.add_argument('profile
', help="Name of the profile to remove prefs from.")
293 rm_parser.add_argument('--pref-file', default='-', help="File containing a list of pref "
294 "substrings to delete (default: stdin)")
295 rm_parser.set_defaults(func=rm)
297 args = vars(parser.parse_args(args))
298 func = args.pop('func
')
302 if __name__ == '__main__
':