Bug 1460912 - [testing/profiles] Add --format options to ./profile diff draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Wed, 16 May 2018 17:10:20 -0400
changeset 796543 661b9ffd8aabd7df4c6bce3dcd3c3622a5198e68
parent 796542 aa0ad798070e98a97cdfac8790876547bce02087
child 796544 9913d027f866da6087173e87542113c7e556b52a
push id110278
push userahalberstadt@mozilla.com
push dateThu, 17 May 2018 19:42:43 +0000
bugs1460912
milestone62.0a1
Bug 1460912 - [testing/profiles] Add --format options to ./profile diff The main purpose of this change is to add some structured formats to the diff. In future commits, the output of |./profile diff ...| will be used as inputs to other ./profile commands. The intent of all this work is to make it easier to programmatically manipulate the pref files when adding in new suites. For example, I want to say "Automatically remove all prefs from the reftest profile that are shared with the common profile". MozReview-Commit-ID: nf8xOjmd1u
testing/profiles/profile
--- a/testing/profiles/profile
+++ b/testing/profiles/profile
@@ -42,16 +42,28 @@ 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 = {}
@@ -96,17 +108,42 @@ def read(key):
                 a suite as defined in suites.json.
     """
     prefs = {}
     for profile in get_profiles(key):
         prefs.update(read_prefs(profile))
     return prefs
 
 
-def diff(a, b):
+def format_diff(diff, fmt):
+    """Format a diff."""
+    if fmt == 'json':
+        print(json.dumps(diff, sort_keys=True, indent=2))
+        return 0
+
+    lines = []
+    for key, prefs in sorted(diff.items()):
+        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(text))
+        lines.append('')
+    print('\n'.join(lines).strip())
+
+
+def diff(a, b, fmt):
     """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')
@@ -114,41 +151,30 @@ def diff(a, b):
         return 0
 
     if isinstance(res, list) and len(res) == 2:
         res = {
             jsondiff.Symbol('delete'): res[0],
             jsondiff.Symbol('insert'): res[1],
         }
 
-    modified = [(k, v) for k, v in res.items() if not isinstance(k, jsondiff.Symbol)]
-    if modified:
-        print("modified ({} => {}):".format(a, b))
-        for k, v in sorted(modified):
-            del prefs_a[k]
-            print("  {}: {} => {}".format(k, repr(v[0]), repr(v[1])))
-
-    label_map = {
-        'insert': 'missing in {}'.format(a),
-        'delete': 'missing in {}'.format(b),
-    }
+    # 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)]
-    for sym, value in symbols:
-        prefs = []
-        for k, v in value.items():
-            if k in prefs_a:
-                del prefs_a[k]
-            prefs.append("  {}: {}".format(k, repr(v)))
-        print("\n{}:\n{}".format(label_map.get(sym.label, sym.label), "\n".join(sorted(prefs))))
+    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'}
 
-    if prefs_a:
-        print("\nidentical:")
-        for k, v in sorted(prefs_a.items()):
-            print("  {}: {}".format(k, repr(v)))
+    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)
 
 
 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.
     """
@@ -208,16 +234,19 @@ 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.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.")