boxster: write script to run mass gen_config and generate diffs

For development so we can see what effects a change to the proto
schema has on existing projects.

BUG=None
TEST=manually run

Change-Id: Ic8ec5aa7b1bf845d7050e27d2029aef338c7285f
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/config/+/2831842
Commit-Queue: Sean McAllister <smcallis@google.com>
Reviewed-by: Andrew Lamb <andrewlamb@chromium.org>
diff --git a/scripts/common/utilities.py b/scripts/common/utilities.py
new file mode 100644
index 0000000..a220b4c
--- /dev/null
+++ b/scripts/common/utilities.py
@@ -0,0 +1,96 @@
+# Copyright 2021 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import itertools
+import multiprocessing
+import subprocess
+import sys
+import time
+
+# escape sequence to clear the current line and return to column 0
+CLEAR_LINE = "\033[2K\r"
+
+
+def clear_line(value):
+  """Return value with line clearing prefix added to it."""
+  return CLEAR_LINE + value
+
+
+class Spinner:
+  """Simple class to print a message and update a little spinning icon."""
+
+  def __init__(self, message):
+    self.message = message
+    self.spin = itertools.cycle("◐◓◑◒")
+
+  def tick(self):
+    sys.stderr.write(CLEAR_LINE + "[%c] %s" % (next(self.spin), self.message))
+
+  def done(self, success=True):
+    if success:
+      sys.stderr.write(CLEAR_LINE + "[✔] %s\n" % self.message)
+    else:
+      sys.stderr.write(CLEAR_LINE + "[✘] %s\n" % self.message)
+
+
+def call_and_spin(message, stdin, *cmd):
+  """Execute a command and print a nice status while we wait.
+
+    Args:
+      message (str): message to print while we wait (along with spinner)
+      stdin (bytes): array of bytes to send as the stdin (or None)
+      cmd   ([str]): command and any options and arguments
+
+    Return:
+      tuple of (data, status) containing process stdout and status
+  """
+
+  with multiprocessing.pool.ThreadPool(processes=1) as pool:
+    result = pool.apply_async(subprocess.run, (cmd,), {
+        'input': stdin,
+        'capture_output': True,
+        'text': True,
+    })
+
+    spinner = Spinner(message)
+    spinner.tick()
+
+    while not result.ready():
+      spinner.tick()
+      time.sleep(0.05)
+
+    process = result.get()
+    spinner.done(process.returncode == 0)
+
+    return process.stdout, process.returncode
+
+
+def jqdiff(filea, fileb, filt="."):
+  """Diff two json files using jq with ordered keys.
+
+    Args:
+      filea (str): first file to compare
+      fileb (str): second file to compare
+      filt (str): if supplied, jq filter to apply to inputs before comparing
+        The filter is quoted with '' for the user so take care when specifying.
+
+    Return:
+      Diff between jq output with -S (sorted keys) enabled
+    """
+
+  # if inputs aren't declared, use a file that will (almost surely) never
+  # exist and pass -N to diff so it treats it as an empty file and gives a
+  # full diff
+  input0 = "<(jq -S '{}' {})".format(filt, filea) if filea else "/dev/__empty"
+  input1 = "<(jq -S '{}' {})".format(filt, fileb) if fileb else "/dev/__empty"
+
+  process = subprocess.run(
+      "diff -uN {} {}".format(input0, input1),
+      check=False,  # diff returns non-zero error status if there's a diff
+      shell=True,
+      text=True,
+      stdout=subprocess.PIPE,
+      stderr=subprocess.STDOUT,
+  )
+  return process.stdout