diff options
Diffstat (limited to 'testing/profiles/profile')
-rwxr-xr-x | testing/profiles/profile | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/testing/profiles/profile b/testing/profiles/profile new file mode 100755 index 0000000000..7ca300a0bd --- /dev/null +++ b/testing/profiles/profile @@ -0,0 +1,301 @@ +#!/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" "$@" || +echo "mach not found, either add it to your \$PATH or run this script via ./mach python testing/profiles/profile"; exit # noqa +''' + +"""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 + + +FORMAT_STRINGS = { + 'names': ( + '{pref}', + '{pref}', + ), + 'pretty': ( + '{pref}: {value}', + '{pref}: {value_a} => {value_b}' + ), +} + + +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 + + +def get_profiles(key): + """Return a list of profile names for key.""" + with open(os.path.join(here, 'profiles.json'), 'r') as fh: + profiles = json.load(fh) + + if '+' in key: + keys = key.split('+') + else: + 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: + raise ValueError('{} is not a recognized suite or profile'.format(key)) + return names + + +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. + """ + prefs = {} + for profile in get_profiles(key): + prefs.update(read_prefs(profile)) + return prefs + + +def format_diff(diff, fmt, limit_key): + """Format a diff.""" + indent = ' ' + if limit_key: + diff = {limit_key: diff[limit_key]} + indent = '' + + if fmt == 'json': + print(json.dumps(diff, sort_keys=True, indent=2)) + return 0 + + lines = [] + for key, prefs in sorted(diff.items()): + if not limit_key: + lines.append("{}:".format(key)) + + 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) + + lines.append('{}{}'.format(indent, text)) + lines.append('') + print('\n'.join(lines).strip()) + + +def diff(a, b, fmt, limit_key): + """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], + } + + # 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)} + + symbols = [(k, v) for k, v in res.items() if isinstance(k, jsondiff.Symbol)] + 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'} + + same = set(prefs_a.keys()) - set(chain(*results.values())) + results['same'] = {k: v for k, v in prefs_a.items() if k in same} + return format_diff(results, fmt, limit_key) + + +def read_with_comments(path): + 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]) + return result + + +def sort_file(path): + """Sort the given pref file alphabetically, preserving preceding comments + that start with '//'. + + :param path: Path to the preference file to sort. + """ + result = read_with_comments(path) + result = sorted(result, key=lambda x: x[-1]) + result = chain(*result) + + with open(path, 'w') as fh: + fh.write('\n'.join(result) + '\n') + + +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))) + + +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)) + + +def cli(args=sys.argv[1:]): + parser = ArgumentParser() + subparsers = parser.add_subparsers(dest='func') + subparsers.required = True + + 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.") + diff_parser.add_argument('-f', '--format', dest='fmt', default='pretty', + choices=['pretty', 'json', 'names'], + help="Format to dump diff in (default: pretty)") + diff_parser.add_argument('-k', '--limit-key', default=None, + choices=['change', 'delete', 'insert', 'same'], + help="Restrict diff to the specified key.") + 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) + + 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) + + args = vars(parser.parse_args(args)) + func = args.pop('func') + func(**args) + + +if __name__ == '__main__': + sys.exit(cli()) |