1# Copyright 2021 The ChromiumOS Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import itertools 6import multiprocessing 7import subprocess 8import sys 9import time 10 11# escape sequence to clear the current line and return to column 0 12CLEAR_LINE = "\033[2K\r" 13 14 15def clear_line(value): 16 """Return value with line clearing prefix added to it.""" 17 return CLEAR_LINE + value 18 19 20class Spinner: 21 """Simple class to print a message and update a little spinning icon.""" 22 23 def __init__(self, message): 24 self.message = message 25 self.spin = itertools.cycle("◐◓◑◒") 26 27 def tick(self): 28 sys.stderr.write(CLEAR_LINE + "[%c] %s" % (next(self.spin), self.message)) 29 30 def done(self, success=True): 31 if success: 32 sys.stderr.write(CLEAR_LINE + "[✔] %s\n" % self.message) 33 else: 34 sys.stderr.write(CLEAR_LINE + "[✘] %s\n" % self.message) 35 36 37def call_and_spin(message, stdin, *cmd): 38 """Execute a command and print a nice status while we wait. 39 40 Args: 41 message (str): message to print while we wait (along with spinner) 42 stdin (bytes): array of bytes to send as the stdin (or None) 43 cmd ([str]): command and any options and arguments 44 45 Return: 46 tuple of (data, status) containing process stdout and status 47 """ 48 49 with multiprocessing.pool.ThreadPool(processes=1) as pool: 50 result = pool.apply_async(subprocess.run, (cmd,), { 51 'input': stdin, 52 'capture_output': True, 53 'text': True, 54 }) 55 56 spinner = Spinner(message) 57 spinner.tick() 58 59 while not result.ready(): 60 spinner.tick() 61 time.sleep(0.05) 62 63 process = result.get() 64 spinner.done(process.returncode == 0) 65 66 return process.stdout, process.returncode 67 68 69def jqdiff(filea, fileb): 70 """Diff two json files using jq with ordered keys. 71 72 Args: 73 filea (str): first file to compare 74 fileb (str): second file to compare 75 76 Return: 77 Diff between jq output with -S (sorted keys) enabled 78 """ 79 80 # Enable recursive sort on arrays 81 filt = """ 82def safeget(f): 83 if type == "object" then 84 f 85 else 86 . 87 end; 88 89walk( 90 if type == "array" then 91 sort_by(safeget(.hwidLabel), safeget(.id), .) 92 else 93 . 94 end 95) 96""" 97 98 # if inputs aren't declared, use a file that will (almost surely) never 99 # exist and pass -N to diff so it treats it as an empty file and gives a 100 # full diff 101 input0 = "<(jq -S '{}' {})".format(filt, filea) if filea else "/dev/__empty" 102 input1 = "<(jq -S '{}' {})".format(filt, fileb) if fileb else "/dev/__empty" 103 104 process = subprocess.run( 105 "diff -uN {} {}".format(input0, input1), 106 check=False, # diff returns non-zero error status if there's a diff 107 shell=True, 108 text=True, 109 stdout=subprocess.PIPE, 110 stderr=subprocess.STDOUT, 111 ) 112 return process.stdout 113