testing/profiles/profile
author Boris Zbarsky <bzbarsky@mit.edu>
Wed, 03 Jul 2019 07:52:35 +0000
changeset 543971 59c08b215af55370b0a8eef16528e99654ffa558
parent 475475 631008a3bf2973db48cf70502751795ac1065b37
child 602473 0e4661c9061d74f1d975edf9c351f7bdf217447e
permissions -rwxr-xr-x
Bug 1562680. Implement the new syntax for Web IDL dictionary defaulting. r=peterv `= {}` can now be used to indicate that an optional dictionary should have the default value of 'default-initialized dictionary' Differential Revision: https://phabricator.services.mozilla.com/D36504

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

    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())