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