Bug 1859570 [wpt PR 42212] - Update wpt metadata, a=testonly
[gecko.git] / testing / profiles / profile
blob7ca300a0bdc174ff9a2ffa7d8d20527c1f064258
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 """This script can be used to:
17 1) Show all preferences for a given suite
18 2) Diff preferences between two suites or profiles
19 3) Sort preference files alphabetically for a given profile
21 To use, either make sure that `mach` is on your $PATH, or run:
22 $ ./mach python testing/profiles/profile <args>
24 For more details run:
25 $ ./profile -- --help
26 """
28 import json
29 import os
30 import sys
31 from argparse import ArgumentParser
32 from itertools import chain
34 from mozprofile import Profile
35 from mozprofile.prefs import Preferences
37 here = os.path.abspath(os.path.dirname(__file__))
39 try:
40 import jsondiff
41 except ImportError:
42 from mozbuild.base import MozbuildObject
43 build = MozbuildObject.from_environment(cwd=here)
44 build.virtualenv_manager.install_pip_package("jsondiff")
45 import jsondiff
48 FORMAT_STRINGS = {
49 'names': (
50 '{pref}',
51 '{pref}',
53 'pretty': (
54 '{pref}: {value}',
55 '{pref}: {value_a} => {value_b}'
60 def read_prefs(profile, pref_files=None):
61 """Read and return all preferences set in the given profile.
63 :param profile: Profile name relative to this `here`.
64 :returns: A dictionary of preferences set in the profile.
65 """
66 pref_files = pref_files or Profile.preference_file_names
67 prefs = {}
68 for name in pref_files:
69 path = os.path.join(here, profile, name)
70 if not os.path.isfile(path):
71 continue
73 try:
74 prefs.update(Preferences.read_json(path))
75 except ValueError:
76 prefs.update(Preferences.read_prefs(path))
77 return prefs
80 def get_profiles(key):
81 """Return a list of profile names for key."""
82 with open(os.path.join(here, 'profiles.json'), 'r') as fh:
83 profiles = json.load(fh)
85 if '+' in key:
86 keys = key.split('+')
87 else:
88 keys = [key]
90 names = set()
91 for key in keys:
92 if key in profiles:
93 names.update(profiles[key])
94 elif os.path.isdir(os.path.join(here, key)):
95 names.add(key)
97 if not names:
98 raise ValueError('{} is not a recognized suite or profile'.format(key))
99 return names
102 def read(key):
103 """Read preferences relevant to either a profile or suite.
105 :param key: Can either be the name of a profile, or the name of
106 a suite as defined in suites.json.
108 prefs = {}
109 for profile in get_profiles(key):
110 prefs.update(read_prefs(profile))
111 return prefs
114 def format_diff(diff, fmt, limit_key):
115 """Format a diff."""
116 indent = ' '
117 if limit_key:
118 diff = {limit_key: diff[limit_key]}
119 indent = ''
121 if fmt == 'json':
122 print(json.dumps(diff, sort_keys=True, indent=2))
123 return 0
125 lines = []
126 for key, prefs in sorted(diff.items()):
127 if not limit_key:
128 lines.append("{}:".format(key))
130 for pref, value in sorted(prefs.items()):
131 context = {'pref': pref, 'value': repr(value)}
133 if isinstance(value, list):
134 context['value_a'] = repr(value[0])
135 context['value_b'] = repr(value[1])
136 text = FORMAT_STRINGS[fmt][1].format(**context)
137 else:
138 text = FORMAT_STRINGS[fmt][0].format(**context)
140 lines.append('{}{}'.format(indent, text))
141 lines.append('')
142 print('\n'.join(lines).strip())
145 def diff(a, b, fmt, limit_key):
146 """Diff two profiles or suites.
148 :param a: The first profile or suite name.
149 :param b: The second profile or suite name.
151 prefs_a = read(a)
152 prefs_b = read(b)
153 res = jsondiff.diff(prefs_a, prefs_b, syntax='symmetric')
154 if not res:
155 return 0
157 if isinstance(res, list) and len(res) == 2:
158 res = {
159 jsondiff.Symbol('delete'): res[0],
160 jsondiff.Symbol('insert'): res[1],
163 # Post process results to make them JSON compatible and a
164 # bit more clear. Also calculate identical prefs.
165 results = {}
166 results['change'] = {k: v for k, v in res.items() if not isinstance(k, jsondiff.Symbol)}
168 symbols = [(k, v) for k, v in res.items() if isinstance(k, jsondiff.Symbol)]
169 results['insert'] = {k: v for sym, pref in symbols for k, v in pref.items()
170 if sym.label == 'insert'}
171 results['delete'] = {k: v for sym, pref in symbols for k, v in pref.items()
172 if sym.label == 'delete'}
174 same = set(prefs_a.keys()) - set(chain(*results.values()))
175 results['same'] = {k: v for k, v in prefs_a.items() if k in same}
176 return format_diff(results, fmt, limit_key)
179 def read_with_comments(path):
180 with open(path, 'r') as fh:
181 lines = fh.readlines()
183 result = []
184 buf = []
185 for line in lines:
186 line = line.strip()
187 if not line:
188 continue
190 if line.startswith('//'):
191 buf.append(line)
192 continue
194 if buf:
195 result.append(buf + [line])
196 buf = []
197 continue
199 result.append([line])
200 return result
203 def sort_file(path):
204 """Sort the given pref file alphabetically, preserving preceding comments
205 that start with '//'.
207 :param path: Path to the preference file to sort.
209 result = read_with_comments(path)
210 result = sorted(result, key=lambda x: x[-1])
211 result = chain(*result)
213 with open(path, 'w') as fh:
214 fh.write('\n'.join(result) + '\n')
217 def sort(profile):
218 """Sort all prefs in the given profile alphabetically. This will preserve
219 comments on preceding lines.
221 :param profile: The name of the profile to sort.
223 pref_files = Profile.preference_file_names
225 for name in pref_files:
226 path = os.path.join(here, profile, name)
227 if os.path.isfile(path):
228 sort_file(path)
231 def show(suite):
232 """Display all prefs set in profiles used by the given suite.
234 :param suite: The name of the suite to show preferences for. This must
235 be a key in suites.json.
237 for k, v in sorted(read(suite).items()):
238 print("{}: {}".format(k, repr(v)))
241 def rm(profile, pref_file):
242 if pref_file == '-':
243 lines = sys.stdin.readlines()
244 else:
245 with open(pref_file, 'r') as fh:
246 lines = fh.readlines()
248 lines = [l.strip() for l in lines if l.strip()]
249 if not lines:
250 return
252 def filter_line(content):
253 return not any(line in content[-1] for line in lines)
255 path = os.path.join(here, profile, 'user.js')
256 contents = read_with_comments(path)
257 contents = filter(filter_line, contents)
258 contents = chain(*contents)
259 with open(path, 'w') as fh:
260 fh.write('\n'.join(contents))
263 def cli(args=sys.argv[1:]):
264 parser = ArgumentParser()
265 subparsers = parser.add_subparsers(dest='func')
266 subparsers.required = True
268 diff_parser = subparsers.add_parser('diff')
269 diff_parser.add_argument('a', metavar='A',
270 help="Path to the first profile or suite name to diff.")
271 diff_parser.add_argument('b', metavar='B',
272 help="Path to the second profile or suite name to diff.")
273 diff_parser.add_argument('-f', '--format', dest='fmt', default='pretty',
274 choices=['pretty', 'json', 'names'],
275 help="Format to dump diff in (default: pretty)")
276 diff_parser.add_argument('-k', '--limit-key', default=None,
277 choices=['change', 'delete', 'insert', 'same'],
278 help="Restrict diff to the specified key.")
279 diff_parser.set_defaults(func=diff)
281 sort_parser = subparsers.add_parser('sort')
282 sort_parser.add_argument('profile', help="Path to profile to sort preferences.")
283 sort_parser.set_defaults(func=sort)
285 show_parser = subparsers.add_parser('show')
286 show_parser.add_argument('suite', help="Name of suite to show arguments for.")
287 show_parser.set_defaults(func=show)
289 rm_parser = subparsers.add_parser('rm')
290 rm_parser.add_argument('profile', help="Name of the profile to remove prefs from.")
291 rm_parser.add_argument('--pref-file', default='-', help="File containing a list of pref "
292 "substrings to delete (default: stdin)")
293 rm_parser.set_defaults(func=rm)
295 args = vars(parser.parse_args(args))
296 func = args.pop('func')
297 func(**args)
300 if __name__ == '__main__':
301 sys.exit(cli())