2018-05-05 14:30:38 +03:00
|
|
|
#!/bin/sh
|
|
|
|
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
|
|
|
# vim: set filetype=python:
|
|
|
|
|
|
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
|
|
# The beginning of this script is both valid shell and valid python,
|
|
|
|
# such that the script starts with the shell and is reexecuted python
|
|
|
|
'''which' mach > /dev/null 2>&1 && exec mach python "$0" "$@" ||
|
2018-05-17 00:10:13 +03:00
|
|
|
echo "mach not found, either add it to your \$PATH or run this script via ./mach python testing/profiles/profile"; exit # noqa
|
2018-05-05 14:30:38 +03:00
|
|
|
'''
|
|
|
|
|
|
|
|
from __future__ import absolute_import, unicode_literals, print_function
|
|
|
|
|
|
|
|
"""This script can be used to:
|
|
|
|
|
|
|
|
1) Show all preferences for a given suite
|
|
|
|
2) Diff preferences between two suites or profiles
|
|
|
|
3) Sort preference files alphabetically for a given profile
|
|
|
|
|
|
|
|
To use, either make sure that `mach` is on your $PATH, or run:
|
|
|
|
$ ./mach python testing/profiles/profile <args>
|
|
|
|
|
|
|
|
For more details run:
|
|
|
|
$ ./profile -- --help
|
|
|
|
"""
|
|
|
|
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
from argparse import ArgumentParser
|
|
|
|
from itertools import chain
|
|
|
|
|
|
|
|
from mozprofile import Profile
|
|
|
|
from mozprofile.prefs import Preferences
|
|
|
|
|
|
|
|
here = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
|
|
|
|
try:
|
|
|
|
import jsondiff
|
|
|
|
except ImportError:
|
|
|
|
from mozbuild.base import MozbuildObject
|
|
|
|
build = MozbuildObject.from_environment(cwd=here)
|
|
|
|
build.virtualenv_manager.install_pip_package("jsondiff")
|
|
|
|
import jsondiff
|
|
|
|
|
|
|
|
|
2018-05-17 00:10:20 +03:00
|
|
|
FORMAT_STRINGS = {
|
|
|
|
'names': (
|
|
|
|
'{pref}',
|
|
|
|
'{pref}',
|
|
|
|
),
|
|
|
|
'pretty': (
|
|
|
|
'{pref}: {value}',
|
|
|
|
'{pref}: {value_a} => {value_b}'
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-05-05 14:30:38 +03:00
|
|
|
def read_prefs(profile, pref_files=None):
|
|
|
|
"""Read and return all preferences set in the given profile.
|
|
|
|
|
|
|
|
:param profile: Profile name relative to this `here`.
|
|
|
|
:returns: A dictionary of preferences set in the profile.
|
|
|
|
"""
|
|
|
|
pref_files = pref_files or Profile.preference_file_names
|
|
|
|
prefs = {}
|
|
|
|
for name in pref_files:
|
|
|
|
path = os.path.join(here, profile, name)
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
prefs.update(Preferences.read_json(path))
|
|
|
|
except ValueError:
|
|
|
|
prefs.update(Preferences.read_prefs(path))
|
|
|
|
return prefs
|
|
|
|
|
|
|
|
|
2018-05-17 00:10:13 +03:00
|
|
|
def get_profiles(key):
|
|
|
|
"""Return a list of profile names for key."""
|
2018-05-05 14:30:38 +03:00
|
|
|
with open(os.path.join(here, 'profiles.json'), 'r') as fh:
|
|
|
|
profiles = json.load(fh)
|
|
|
|
|
2018-05-17 00:10:13 +03:00
|
|
|
if '+' in key:
|
|
|
|
keys = key.split('+')
|
2018-05-05 14:30:38 +03:00
|
|
|
else:
|
2018-05-17 00:10:13 +03:00
|
|
|
keys = [key]
|
|
|
|
|
|
|
|
names = set()
|
|
|
|
for key in keys:
|
|
|
|
if key in profiles:
|
|
|
|
names.update(profiles[key])
|
|
|
|
elif os.path.isdir(os.path.join(here, key)):
|
|
|
|
names.add(key)
|
|
|
|
|
|
|
|
if not names:
|
2018-05-05 14:30:38 +03:00
|
|
|
raise ValueError('{} is not a recognized suite or profile'.format(key))
|
2018-05-17 00:10:13 +03:00
|
|
|
return names
|
2018-05-05 14:30:38 +03:00
|
|
|
|
2018-05-17 00:10:13 +03:00
|
|
|
|
|
|
|
def read(key):
|
|
|
|
"""Read preferences relevant to either a profile or suite.
|
|
|
|
|
|
|
|
:param key: Can either be the name of a profile, or the name of
|
|
|
|
a suite as defined in suites.json.
|
|
|
|
"""
|
2018-05-05 14:30:38 +03:00
|
|
|
prefs = {}
|
2018-05-17 00:10:13 +03:00
|
|
|
for profile in get_profiles(key):
|
2018-05-05 14:30:38 +03:00
|
|
|
prefs.update(read_prefs(profile))
|
|
|
|
return prefs
|
|
|
|
|
|
|
|
|
2018-05-14 20:17:09 +03:00
|
|
|
def format_diff(diff, fmt, limit_key):
|
2018-05-17 00:10:20 +03:00
|
|
|
"""Format a diff."""
|
2018-05-14 20:17:09 +03:00
|
|
|
indent = ' '
|
|
|
|
if limit_key:
|
|
|
|
diff = {limit_key: diff[limit_key]}
|
|
|
|
indent = ''
|
|
|
|
|
2018-05-17 00:10:20 +03:00
|
|
|
if fmt == 'json':
|
|
|
|
print(json.dumps(diff, sort_keys=True, indent=2))
|
|
|
|
return 0
|
|
|
|
|
|
|
|
lines = []
|
|
|
|
for key, prefs in sorted(diff.items()):
|
2018-05-14 20:17:09 +03:00
|
|
|
if not limit_key:
|
|
|
|
lines.append("{}:".format(key))
|
2018-05-17 00:10:20 +03:00
|
|
|
|
|
|
|
for pref, value in sorted(prefs.items()):
|
|
|
|
context = {'pref': pref, 'value': repr(value)}
|
|
|
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
context['value_a'] = repr(value[0])
|
|
|
|
context['value_b'] = repr(value[1])
|
|
|
|
text = FORMAT_STRINGS[fmt][1].format(**context)
|
|
|
|
else:
|
|
|
|
text = FORMAT_STRINGS[fmt][0].format(**context)
|
|
|
|
|
2018-05-14 20:17:09 +03:00
|
|
|
lines.append('{}{}'.format(indent, text))
|
2018-05-17 00:10:20 +03:00
|
|
|
lines.append('')
|
|
|
|
print('\n'.join(lines).strip())
|
|
|
|
|
|
|
|
|
2018-05-14 20:17:09 +03:00
|
|
|
def diff(a, b, fmt, limit_key):
|
2018-05-05 14:30:38 +03:00
|
|
|
"""Diff two profiles or suites.
|
|
|
|
|
|
|
|
:param a: The first profile or suite name.
|
|
|
|
:param b: The second profile or suite name.
|
|
|
|
"""
|
|
|
|
prefs_a = read(a)
|
|
|
|
prefs_b = read(b)
|
|
|
|
res = jsondiff.diff(prefs_a, prefs_b, syntax='symmetric')
|
|
|
|
if not res:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
if isinstance(res, list) and len(res) == 2:
|
|
|
|
res = {
|
|
|
|
jsondiff.Symbol('delete'): res[0],
|
|
|
|
jsondiff.Symbol('insert'): res[1],
|
|
|
|
}
|
|
|
|
|
2018-05-17 00:10:20 +03:00
|
|
|
# Post process results to make them JSON compatible and a
|
|
|
|
# bit more clear. Also calculate identical prefs.
|
|
|
|
results = {}
|
|
|
|
results['change'] = {k: v for k, v in res.items() if not isinstance(k, jsondiff.Symbol)}
|
2018-05-05 14:30:38 +03:00
|
|
|
|
|
|
|
symbols = [(k, v) for k, v in res.items() if isinstance(k, jsondiff.Symbol)]
|
2018-05-17 00:10:20 +03:00
|
|
|
results['insert'] = {k: v for sym, pref in symbols for k, v in pref.items()
|
|
|
|
if sym.label == 'insert'}
|
|
|
|
results['delete'] = {k: v for sym, pref in symbols for k, v in pref.items()
|
|
|
|
if sym.label == 'delete'}
|
2018-05-05 14:30:38 +03:00
|
|
|
|
2018-05-17 00:10:20 +03:00
|
|
|
same = set(prefs_a.keys()) - set(chain(*results.values()))
|
|
|
|
results['same'] = {k: v for k, v in prefs_a.items() if k in same}
|
2018-05-14 20:17:09 +03:00
|
|
|
return format_diff(results, fmt, limit_key)
|
2018-05-05 14:30:38 +03:00
|
|
|
|
|
|
|
|
2018-05-15 04:43:47 +03:00
|
|
|
def read_with_comments(path):
|
2018-05-05 14:30:38 +03:00
|
|
|
with open(path, 'r') as fh:
|
|
|
|
lines = fh.readlines()
|
|
|
|
|
|
|
|
result = []
|
|
|
|
buf = []
|
|
|
|
for line in lines:
|
|
|
|
line = line.strip()
|
|
|
|
if not line:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if line.startswith('//'):
|
|
|
|
buf.append(line)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if buf:
|
|
|
|
result.append(buf + [line])
|
|
|
|
buf = []
|
|
|
|
continue
|
|
|
|
|
|
|
|
result.append([line])
|
2018-05-15 04:43:47 +03:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def sort_file(path):
|
|
|
|
"""Sort the given pref file alphabetically, preserving preceding comments
|
|
|
|
that start with '//'.
|
2018-05-05 14:30:38 +03:00
|
|
|
|
2018-05-15 04:43:47 +03:00
|
|
|
:param path: Path to the preference file to sort.
|
|
|
|
"""
|
|
|
|
result = read_with_comments(path)
|
2018-05-05 14:30:38 +03:00
|
|
|
result = sorted(result, key=lambda x: x[-1])
|
|
|
|
result = chain(*result)
|
|
|
|
|
|
|
|
with open(path, 'w') as fh:
|
2018-05-15 04:43:47 +03:00
|
|
|
fh.write('\n'.join(result) + '\n')
|
2018-05-05 14:30:38 +03:00
|
|
|
|
|
|
|
|
|
|
|
def sort(profile):
|
|
|
|
"""Sort all prefs in the given profile alphabetically. This will preserve
|
|
|
|
comments on preceding lines.
|
|
|
|
|
|
|
|
:param profile: The name of the profile to sort.
|
|
|
|
"""
|
|
|
|
pref_files = Profile.preference_file_names
|
|
|
|
|
|
|
|
for name in pref_files:
|
|
|
|
path = os.path.join(here, profile, name)
|
|
|
|
if os.path.isfile(path):
|
|
|
|
sort_file(path)
|
|
|
|
|
|
|
|
|
|
|
|
def show(suite):
|
|
|
|
"""Display all prefs set in profiles used by the given suite.
|
|
|
|
|
|
|
|
:param suite: The name of the suite to show preferences for. This must
|
|
|
|
be a key in suites.json.
|
|
|
|
"""
|
|
|
|
for k, v in sorted(read(suite).items()):
|
|
|
|
print("{}: {}".format(k, repr(v)))
|
|
|
|
|
|
|
|
|
2018-05-15 04:43:47 +03:00
|
|
|
def rm(profile, pref_file):
|
|
|
|
if pref_file == '-':
|
|
|
|
lines = sys.stdin.readlines()
|
|
|
|
else:
|
|
|
|
with open(pref_file, 'r') as fh:
|
|
|
|
lines = fh.readlines()
|
|
|
|
|
|
|
|
lines = [l.strip() for l in lines if l.strip()]
|
|
|
|
if not lines:
|
|
|
|
return
|
|
|
|
|
|
|
|
def filter_line(content):
|
|
|
|
return not any(line in content[-1] for line in lines)
|
|
|
|
|
|
|
|
path = os.path.join(here, profile, 'user.js')
|
|
|
|
contents = read_with_comments(path)
|
|
|
|
contents = filter(filter_line, contents)
|
|
|
|
contents = chain(*contents)
|
|
|
|
with open(path, 'w') as fh:
|
|
|
|
fh.write('\n'.join(contents))
|
|
|
|
|
|
|
|
|
2018-05-05 14:30:38 +03:00
|
|
|
def cli(args=sys.argv[1:]):
|
|
|
|
parser = ArgumentParser()
|
|
|
|
subparsers = parser.add_subparsers()
|
|
|
|
|
|
|
|
diff_parser = subparsers.add_parser('diff')
|
|
|
|
diff_parser.add_argument('a', metavar='A',
|
|
|
|
help="Path to the first profile or suite name to diff.")
|
|
|
|
diff_parser.add_argument('b', metavar='B',
|
|
|
|
help="Path to the second profile or suite name to diff.")
|
2018-05-17 00:10:20 +03:00
|
|
|
diff_parser.add_argument('-f', '--format', dest='fmt', default='pretty',
|
|
|
|
choices=['pretty', 'json', 'names'],
|
|
|
|
help="Format to dump diff in (default: pretty)")
|
2018-05-14 20:17:09 +03:00
|
|
|
diff_parser.add_argument('-k', '--limit-key', default=None,
|
|
|
|
choices=['change', 'delete', 'insert', 'same'],
|
|
|
|
help="Restrict diff to the specified key.")
|
2018-05-05 14:30:38 +03:00
|
|
|
diff_parser.set_defaults(func=diff)
|
|
|
|
|
|
|
|
sort_parser = subparsers.add_parser('sort')
|
|
|
|
sort_parser.add_argument('profile', help="Path to profile to sort preferences.")
|
|
|
|
sort_parser.set_defaults(func=sort)
|
|
|
|
|
|
|
|
show_parser = subparsers.add_parser('show')
|
|
|
|
show_parser.add_argument('suite', help="Name of suite to show arguments for.")
|
|
|
|
show_parser.set_defaults(func=show)
|
|
|
|
|
2018-05-15 04:43:47 +03:00
|
|
|
rm_parser = subparsers.add_parser('rm')
|
|
|
|
rm_parser.add_argument('profile', help="Name of the profile to remove prefs from.")
|
|
|
|
rm_parser.add_argument('--pref-file', default='-', help="File containing a list of pref "
|
|
|
|
"substrings to delete (default: stdin)")
|
|
|
|
rm_parser.set_defaults(func=rm)
|
|
|
|
|
2018-05-05 14:30:38 +03:00
|
|
|
args = vars(parser.parse_args(args))
|
|
|
|
func = args.pop('func')
|
|
|
|
func(**args)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
sys.exit(cli())
|