Source code

Revision control

Other Tools

1
#!/bin/sh
2
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
3
# vim: set filetype=python:
4
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/.
8
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
'''
14
15
from __future__ import absolute_import, unicode_literals, print_function
16
17
"""This script can be used to:
18
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
22
23
To use, either make sure that `mach` is on your $PATH, or run:
24
$ ./mach python testing/profiles/profile <args>
25
26
For more details run:
27
$ ./profile -- --help
28
"""
29
30
import json
31
import os
32
import sys
33
from argparse import ArgumentParser
34
from itertools import chain
35
36
from mozprofile import Profile
37
from mozprofile.prefs import Preferences
38
39
here = os.path.abspath(os.path.dirname(__file__))
40
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
48
49
50
FORMAT_STRINGS = {
51
'names': (
52
'{pref}',
53
'{pref}',
54
),
55
'pretty': (
56
'{pref}: {value}',
57
'{pref}: {value_a} => {value_b}'
58
),
59
}
60
61
62
def read_prefs(profile, pref_files=None):
63
"""Read and return all preferences set in the given profile.
64
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
74
75
try:
76
prefs.update(Preferences.read_json(path))
77
except ValueError:
78
prefs.update(Preferences.read_prefs(path))
79
return prefs
80
81
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)
86
87
if '+' in key:
88
keys = key.split('+')
89
else:
90
keys = [key]
91
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)
98
99
if not names:
100
raise ValueError('{} is not a recognized suite or profile'.format(key))
101
return names
102
103
104
def read(key):
105
"""Read preferences relevant to either a profile or suite.
106
107
:param key: Can either be the name of a profile, or the name of
108
a suite as defined in suites.json.
109
"""
110
prefs = {}
111
for profile in get_profiles(key):
112
prefs.update(read_prefs(profile))
113
return prefs
114
115
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 = ''
122
123
if fmt == 'json':
124
print(json.dumps(diff, sort_keys=True, indent=2))
125
return 0
126
127
lines = []
128
for key, prefs in sorted(diff.items()):
129
if not limit_key:
130
lines.append("{}:".format(key))
131
132
for pref, value in sorted(prefs.items()):
133
context = {'pref': pref, 'value': repr(value)}
134
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)
141
142
lines.append('{}{}'.format(indent, text))
143
lines.append('')
144
print('\n'.join(lines).strip())
145
146
147
def diff(a, b, fmt, limit_key):
148
"""Diff two profiles or suites.
149
150
:param a: The first profile or suite name.
151
:param b: The second profile or suite name.
152
"""
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
158
159
if isinstance(res, list) and len(res) == 2:
160
res = {
161
jsondiff.Symbol('delete'): res[0],
162
jsondiff.Symbol('insert'): res[1],
163
}
164
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)}
169
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'}
175
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)
179
180
181
def read_with_comments(path):
182
with open(path, 'r') as fh:
183
lines = fh.readlines()
184
185
result = []
186
buf = []
187
for line in lines:
188
line = line.strip()
189
if not line:
190
continue
191
192
if line.startswith('//'):
193
buf.append(line)
194
continue
195
196
if buf:
197
result.append(buf + [line])
198
buf = []
199
continue
200
201
result.append([line])
202
return result
203
204
205
def sort_file(path):
206
"""Sort the given pref file alphabetically, preserving preceding comments
207
that start with '//'.
208
209
:param path: Path to the preference file to sort.
210
"""
211
result = read_with_comments(path)
212
result = sorted(result, key=lambda x: x[-1])
213
result = chain(*result)
214
215
with open(path, 'w') as fh:
216
fh.write('\n'.join(result) + '\n')
217
218
219
def sort(profile):
220
"""Sort all prefs in the given profile alphabetically. This will preserve
221
comments on preceding lines.
222
223
:param profile: The name of the profile to sort.
224
"""
225
pref_files = Profile.preference_file_names
226
227
for name in pref_files:
228
path = os.path.join(here, profile, name)
229
if os.path.isfile(path):
230
sort_file(path)
231
232
233
def show(suite):
234
"""Display all prefs set in profiles used by the given suite.
235
236
:param suite: The name of the suite to show preferences for. This must
237
be a key in suites.json.
238
"""
239
for k, v in sorted(read(suite).items()):
240
print("{}: {}".format(k, repr(v)))
241
242
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()
249
250
lines = [l.strip() for l in lines if l.strip()]
251
if not lines:
252
return
253
254
def filter_line(content):
255
return not any(line in content[-1] for line in lines)
256
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))
263
264
265
def cli(args=sys.argv[1:]):
266
parser = ArgumentParser()
267
subparsers = parser.add_subparsers()
268
269
diff_parser = subparsers.add_parser('diff')
270
diff_parser.add_argument('a', metavar='A',
271
help="Path to the first profile or suite name to diff.")
272
diff_parser.add_argument('b', metavar='B',
273
help="Path to the second profile or suite name to diff.")
274
diff_parser.add_argument('-f', '--format', dest='fmt', default='pretty',
275
choices=['pretty', 'json', 'names'],
276
help="Format to dump diff in (default: pretty)")
277
diff_parser.add_argument('-k', '--limit-key', default=None,
278
choices=['change', 'delete', 'insert', 'same'],
279
help="Restrict diff to the specified key.")
280
diff_parser.set_defaults(func=diff)
281
282
sort_parser = subparsers.add_parser('sort')
283
sort_parser.add_argument('profile', help="Path to profile to sort preferences.")
284
sort_parser.set_defaults(func=sort)
285
286
show_parser = subparsers.add_parser('show')
287
show_parser.add_argument('suite', help="Name of suite to show arguments for.")
288
show_parser.set_defaults(func=show)
289
290
rm_parser = subparsers.add_parser('rm')
291
rm_parser.add_argument('profile', help="Name of the profile to remove prefs from.")
292
rm_parser.add_argument('--pref-file', default='-', help="File containing a list of pref "
293
"substrings to delete (default: stdin)")
294
rm_parser.set_defaults(func=rm)
295
296
args = vars(parser.parse_args(args))
297
func = args.pop('func')
298
func(**args)
299
300
301
if __name__ == '__main__':
302
sys.exit(cli())