Bug 1669129 - [devtools] Enable devtools.overflow.debugging.enabled. r=jdescottes
[gecko.git] / testing / profiles / profile
blobd946f155e6c497d8d4d9bbbbb84805b6963e1b3a
1 #!/bin/sh
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
13 '''
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>
26 For more details run:
27 $ ./profile -- --help
28 """
30 import json
31 import os
32 import sys
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__))
41 try:
42 import jsondiff
43 except ImportError:
44 from mozbuild.base import MozbuildObject
45 build = MozbuildObject.from_environment(cwd=here)
46 build.virtualenv_manager.install_pip_package("jsondiff")
47 import jsondiff
50 FORMAT_STRINGS = {
51 'names': (
52 '{pref}',
53 '{pref}',
55 'pretty': (
56 '{pref}: {value}',
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.
67 """
68 pref_files = pref_files or Profile.preference_file_names
69 prefs = {}
70 for name in pref_files:
71 path = os.path.join(here, profile, name)
72 if not os.path.isfile(path):
73 continue
75 try:
76 prefs.update(Preferences.read_json(path))
77 except ValueError:
78 prefs.update(Preferences.read_prefs(path))
79 return prefs
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)
87 if '+' in key:
88 keys = key.split('+')
89 else:
90 keys = [key]
92 names = set()
93 for key in keys:
94 if key in profiles:
95 names.update(profiles[key])
96 elif os.path.isdir(os.path.join(here, key)):
97 names.add(key)
99 if not names:
100 raise ValueError('{} is not a recognized suite or profile'.format(key))
101 return names
104 def read(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.
110 prefs = {}
111 for profile in get_profiles(key):
112 prefs.update(read_prefs(profile))
113 return prefs
116 def format_diff(diff, fmt, limit_key):
117 """Format a diff."""
118 indent = ' '
119 if limit_key:
120 diff = {limit_key: diff[limit_key]}
121 indent = ''
123 if fmt == 'json':
124 print(json.dumps(diff, sort_keys=True, indent=2))
125 return 0
127 lines = []
128 for key, prefs in sorted(diff.items()):
129 if not limit_key:
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)
139 else:
140 text = FORMAT_STRINGS[fmt][0].format(**context)
142 lines.append('{}{}'.format(indent, text))
143 lines.append('')
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.
153 prefs_a = read(a)
154 prefs_b = read(b)
155 res = jsondiff.diff(prefs_a, prefs_b, syntax='symmetric')
156 if not res:
157 return 0
159 if isinstance(res, list) and len(res) == 2:
160 res = {
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.
167 results = {}
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()
185 result = []
186 buf = []
187 for line in lines:
188 line = line.strip()
189 if not line:
190 continue
192 if line.startswith('//'):
193 buf.append(line)
194 continue
196 if buf:
197 result.append(buf + [line])
198 buf = []
199 continue
201 result.append([line])
202 return result
205 def sort_file(path):
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')
219 def sort(profile):
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):
230 sort_file(path)
233 def show(suite):
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):
244 if pref_file == '-':
245 lines = sys.stdin.readlines()
246 else:
247 with open(pref_file, 'r') as fh:
248 lines = fh.readlines()
250 lines = [l.strip() for l in lines if l.strip()]
251 if not lines:
252 return
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')
299 func(**args)
302 if __name__ == '__main__':
303 sys.exit(cli())