#!/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 ''' 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 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() 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())