CmpDriver revision ad8958cb22f860a148e22d8f78511caf763482cf
177a1fe945b3d0f1efa22e9d36a8ea7e272dd93c2Eli Friedman#!/usr/bin/env python 2ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 3ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbarimport subprocess 4ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 5ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbardef splitArgs(s): 6ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar it = iter(s) 7ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar current = '' 8ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar inQuote = False 9ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar for c in it: 10ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if c == '"': 11ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if inQuote: 12ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar inQuote = False 13ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar yield current + '"' 14ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar else: 15ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar inQuote = True 16ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar current = '"' 17ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar elif inQuote: 18ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if c == '\\': 19ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar current += c 20ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar current += it.next() 21ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar else: 22ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar current += c 23ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar elif not c.isspace(): 24ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar yield c 25ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 26ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbardef insertMinimumPadding(a, b, dist): 27ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar """insertMinimumPadding(a,b) -> (a',b') 28ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 29ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar Return two lists of equal length, where some number of Nones have 30ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar been inserted into the shorter list such that sum(map(dist, a', 31ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar b')) is minimized. 32ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 33ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar Assumes dist(X, Y) -> int and non-negative. 34ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar """ 35ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 368f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar def cost(a, b): 378f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar return sum(map(dist, a + [None] * (len(b) - len(a)), b)) 388f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar 398f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar # Normalize so a is shortest. 408f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar if len(b) < len(a): 418f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar b, a = insertMinimumPadding(b, a, dist) 428f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar return a,b 438f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar 448f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar # For each None we have to insert... 458f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar for i in range(len(b) - len(a)): 468f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar # For each position we could insert it... 478f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar current = cost(a, b) 488f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar best = None 498f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar for j in range(len(a) + 1): 508f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar a_0 = a[:j] + [None] + a[j:] 518f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar candidate = cost(a_0, b) 528f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar if best is None or candidate < best[0]: 538f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar best = (candidate, a_0, j) 548f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar a = best[1] 558f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar return a,b 56ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 57ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbarclass ZipperDiff(object): 58ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar """ZipperDiff - Simple (slow) diff only accomodating inserts.""" 59ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 60ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar def __init__(self, a, b): 61ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.a = a 62ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.b = b 63ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 64ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar def dist(self, a, b): 65ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar return a != b 66ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 67ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar def getDiffs(self): 68ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar a,b = insertMinimumPadding(self.a, self.b, self.dist) 69ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar for aElt,bElt in zip(a,b): 70ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if self.dist(aElt, bElt): 71ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar yield aElt,bElt 72ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 73ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbarclass DriverZipperDiff(ZipperDiff): 74ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar def isTempFile(self, filename): 75ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if filename[0] != '"' or filename[-1] != '"': 76ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar return False 77ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar return (filename.startswith('/tmp/', 1) or 78ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar filename.startswith('/var/', 1)) 79ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 80ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar def dist(self, a, b): 81ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if a and b and self.isTempFile(a) and self.isTempFile(b): 82ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar return 0 83ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar return super(DriverZipperDiff, self).dist(a,b) 84ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 85ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbarclass CompileInfo: 86ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar def __init__(self, out, err, res): 87ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.commands = [] 88ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 89ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar # Standard out isn't used for much. 90ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.stdout = out 91ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.stderr = '' 92ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 93ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar # FIXME: Compare error messages as well. 94ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar for ln in err.split('\n'): 95ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if (ln == 'Using built-in specs.' or 96ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar ln.startswith('Target: ') or 97ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar ln.startswith('Configured with: ') or 98ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar ln.startswith('Thread model: ') or 9976715913cd6400feb23950a650cd1777ebe10967Daniel Dunbar ln.startswith('gcc version') or 100ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar ln.startswith('clang version')): 101ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar pass 102ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar elif ln.strip().startswith('"'): 103ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.commands.append(list(splitArgs(ln))) 104ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar else: 105ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.stderr += ln + '\n' 106ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 107ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.stderr = self.stderr.strip() 108ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar self.exitCode = res 109ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 110ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbardef captureDriverInfo(cmd, args): 111ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar p = subprocess.Popen([cmd,'-###'] + args, 112ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar stdin=None, 113ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar stdout=subprocess.PIPE, 114ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar stderr=subprocess.PIPE) 115ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar out,err = p.communicate() 116ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar res = p.wait() 117ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar return CompileInfo(out,err,res) 118ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 119ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbardef main(): 1202253dc4705e7816a6cafcf95498da7080041a3e2Daniel Dunbar import os, sys 121ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 122ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar args = sys.argv[1:] 1232253dc4705e7816a6cafcf95498da7080041a3e2Daniel Dunbar driverA = os.getenv('DRIVER_A') or 'gcc' 1248f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar driverB = os.getenv('DRIVER_B') or 'clang' 125ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 126ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar infoA = captureDriverInfo(driverA, args) 127ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar infoB = captureDriverInfo(driverB, args) 128ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 129c97c05f849e051afcd1f4a9d4dfa65c88a642b53Daniel Dunbar differ = False 130c97c05f849e051afcd1f4a9d4dfa65c88a642b53Daniel Dunbar 131ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar # Compare stdout. 132ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if infoA.stdout != infoB.stdout: 133ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print '-- STDOUT DIFFERS -' 134ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'A OUTPUT: ',infoA.stdout 135ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'B OUTPUT: ',infoB.stdout 136ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 137ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar 138ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar diff = ZipperDiff(infoA.stdout.split('\n'), 139ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar infoB.stdout.split('\n')) 140ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar for i,(aElt,bElt) in enumerate(diff.getDiffs()): 141ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar if aElt is None: 142ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'A missing: %s' % bElt 143ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar elif bElt is None: 144ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'B missing: %s' % aElt 145ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar else: 146ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'mismatch: A: %s' % aElt 147ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print ' B: %s' % bElt 148ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar 149c97c05f849e051afcd1f4a9d4dfa65c88a642b53Daniel Dunbar differ = True 150ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 151ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar # Compare stderr. 152ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if infoA.stderr != infoB.stderr: 153ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print '-- STDERR DIFFERS -' 154ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'A STDERR: ',infoA.stderr 155ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'B STDERR: ',infoB.stderr 156ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 157ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar 158ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar diff = ZipperDiff(infoA.stderr.split('\n'), 159ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar infoB.stderr.split('\n')) 160ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar for i,(aElt,bElt) in enumerate(diff.getDiffs()): 161ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar if aElt is None: 162ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'A missing: %s' % bElt 163ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar elif bElt is None: 164ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'B missing: %s' % aElt 165ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar else: 166ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print 'mismatch: A: %s' % aElt 167ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar print ' B: %s' % bElt 168ad8958cb22f860a148e22d8f78511caf763482cfDaniel Dunbar 169c97c05f849e051afcd1f4a9d4dfa65c88a642b53Daniel Dunbar differ = True 170ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 171ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar # Compare commands. 172163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar for i,(a,b) in enumerate(map(None, infoA.commands, infoB.commands)): 173163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar if a is None: 174163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar print 'A MISSING:',' '.join(b) 175163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar differ = True 176163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar continue 177163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar elif b is None: 178163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar print 'B MISSING:',' '.join(a) 179163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar differ = True 180163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar continue 181163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar 182ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar diff = DriverZipperDiff(a,b) 183ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar diffs = list(diff.getDiffs()) 184ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if diffs: 185ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print '-- COMMAND %d DIFFERS -' % i 186ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print 'A COMMAND:',' '.join(a) 187ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print 'B COMMAND:',' '.join(b) 188ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print 189ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar for i,(aElt,bElt) in enumerate(diffs): 190ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if aElt is None: 191ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print 'A missing: %s' % bElt 192ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar elif bElt is None: 193ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print 'B missing: %s' % aElt 194ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar else: 195ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print 'mismatch: A: %s' % aElt 196ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print ' B: %s' % bElt 19706172d680766ffb96a22c48c390ec3e0fd41625bDaniel Dunbar differ = True 198163f31a0194fcff8ccea6e1c2a495f58deb286aaDaniel Dunbar 199ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar # Compare result codes. 200ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar if infoA.exitCode != infoB.exitCode: 201ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print '-- EXIT CODES DIFFER -' 202ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print 'A: ',infoA.exitCode 203ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar print 'B: ',infoB.exitCode 204c97c05f849e051afcd1f4a9d4dfa65c88a642b53Daniel Dunbar differ = True 205c97c05f849e051afcd1f4a9d4dfa65c88a642b53Daniel Dunbar 206c97c05f849e051afcd1f4a9d4dfa65c88a642b53Daniel Dunbar if differ: 207c97c05f849e051afcd1f4a9d4dfa65c88a642b53Daniel Dunbar sys.exit(1) 208ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbar 209ab13061fe7bb8dcd8ef6bdecd08c5728eda9359eDaniel Dunbarif __name__ == '__main__': 2108f5fdbefcde5543cb92f541de2db1d463bc36838Daniel Dunbar main() 211