Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Compare the artifacts from two builds.""" | 6 """Compare the artifacts from two builds.""" |
| 7 | 7 |
| 8 import difflib | 8 import difflib |
| 9 import json | 9 import json |
| 10 import optparse | 10 import optparse |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 27 return False | 27 return False |
| 28 ext = os.path.splitext(f)[1] | 28 ext = os.path.splitext(f)[1] |
| 29 if ext == '.isolated': | 29 if ext == '.isolated': |
| 30 return True | 30 return True |
| 31 return ext in allowed and os.access(f, os.X_OK) | 31 return ext in allowed and os.access(f, os.X_OK) |
| 32 | 32 |
| 33 return set(f for f in os.listdir(build_dir) if | 33 return set(f for f in os.listdir(build_dir) if |
| 34 check(os.path.join(build_dir, f))) | 34 check(os.path.join(build_dir, f))) |
| 35 | 35 |
| 36 | 36 |
| 37 def diff_dict(a, b): | |
| 38 """Returns a yaml-like textural diff of two dict. | |
| 39 | |
| 40 It is currently optimized for the .isolated format. | |
| 41 """ | |
| 42 out = '' | |
| 43 for key in set(a) | set(b): | |
| 44 va = a.get(key) | |
| 45 vb = b.get(key) | |
| 46 if va.__class__ != vb.__class__: | |
| 47 out += '- %s:\n %r != %r\n' % (key, va, vb) | |
| 48 elif isinstance(va, dict): | |
| 49 c = diff_dict(va, vb) | |
| 50 if c: | |
| 51 out += '- %s:\n%s\n' % ( | |
| 52 key, '\n'.join(' ' + l for l in c.splitlines())) | |
| 53 elif va != vb: | |
| 54 out += '- %s:\n %s != %s\n' % (key, va, vb) | |
| 55 return out.rstrip() | |
| 56 | |
| 57 | |
| 58 def diff_binary(first_filepath, second_filepath, file_len): | |
| 59 """Returns a compact binary diff if the diff is small enough.""" | |
| 60 CHUNK_SIZE = 32 | |
| 61 MAX_STREAMS = 10 | |
| 62 diffs = 0 | |
| 63 streams = [] | |
| 64 offset = 0 | |
| 65 with open(first_filepath, 'rb') as lhs: | |
| 66 with open(second_filepath, 'rb') as rhs: | |
| 67 while True: | |
| 68 lhs_data = lhs.read(CHUNK_SIZE) | |
| 69 rhs_data = rhs.read(CHUNK_SIZE) | |
| 70 if not lhs_data: | |
| 71 break | |
| 72 if lhs_data != rhs_data: | |
| 73 diffs += sum(l != r for l, r in zip(lhs_data, rhs_data)) | |
| 74 if streams is not None: | |
| 75 if len(streams) < MAX_STREAMS: | |
| 76 streams.append((offset, lhs_data, rhs_data)) | |
| 77 else: | |
| 78 streams = None | |
| 79 offset += len(lhs_data) | |
| 80 del lhs_data | |
| 81 del rhs_data | |
| 82 if not diffs: | |
| 83 return None | |
| 84 result = '%d out of %d bytes are different (%.2f%%)' % ( | |
| 85 diffs, file_len, 100.0 * diffs / file_len) | |
| 86 if streams: | |
| 87 result += ''.join( | |
|
Sébastien Marchand
2014/11/04 20:29:57
Should we also print the ascii representation of t
| |
| 88 '\n%9d: %s\n %s' % (a, b.encode('hex'), c.encode('hex')) | |
| 89 for a, b, c in streams) | |
| 90 return result | |
| 91 | |
| 92 | |
| 37 def compare_files(first_filepath, second_filepath): | 93 def compare_files(first_filepath, second_filepath): |
| 38 """Compares two binaries and return the number of differences between them. | 94 """Compares two binaries and return the number of differences between them. |
| 39 | 95 |
| 40 Returns None if the files are equal, a string otherwise. | 96 Returns None if the files are equal, a string otherwise. |
| 41 """ | 97 """ |
| 98 if first_filepath.endswith('.isolated'): | |
| 99 with open(first_filepath, 'rb') as f: | |
| 100 lhs = json.load(f) | |
| 101 with open(second_filepath, 'rb') as f: | |
| 102 rhs = json.load(f) | |
| 103 diff = diff_dict(lhs, rhs) | |
| 104 if diff: | |
| 105 return '\n' + diff | |
| 106 | |
| 42 file_len = os.stat(first_filepath).st_size | 107 file_len = os.stat(first_filepath).st_size |
| 43 if file_len != os.stat(second_filepath).st_size: | 108 if file_len != os.stat(second_filepath).st_size: |
| 44 return 'different size: %d != %d' % ( | 109 return 'different size: %d != %d' % ( |
| 45 file_len, os.stat(second_filepath).st_size) | 110 file_len, os.stat(second_filepath).st_size) |
| 46 | 111 |
| 47 chunk_size = 1024 * 1024 | 112 return diff_binary(first_filepath, second_filepath, file_len) |
| 48 diffs = 0 | |
| 49 with open(first_filepath, 'rb') as lhs: | |
| 50 with open(second_filepath, 'rb') as rhs: | |
| 51 while True: | |
| 52 lhs_data = lhs.read(chunk_size) | |
| 53 rhs_data = rhs.read(chunk_size) | |
| 54 if not lhs_data: | |
| 55 break | |
| 56 diffs += sum(l != r for l, r in zip(lhs_data, rhs_data)) | |
| 57 if not diffs: | |
| 58 return None | |
| 59 | |
| 60 result = '%d out of %d bytes are different (%.2f%%)' % ( | |
| 61 diffs, file_len, 100.0 * diffs / file_len) | |
| 62 | |
| 63 if diffs and first_filepath.endswith('.isolated'): | |
| 64 # Unpack the files. | |
| 65 with open(first_filepath, 'rb') as f: | |
| 66 lhs = json.dumps( | |
| 67 json.load(f), indent=2, sort_keys=True, | |
| 68 separators=(',', ': ')).splitlines() | |
| 69 with open(second_filepath, 'rb') as f: | |
| 70 rhs = json.dumps( | |
| 71 json.load(f), indent=2, sort_keys=True, | |
| 72 separators=(',', ': ')).splitlines() | |
| 73 | |
| 74 result += '\n' + '\n'.join( | |
| 75 line for line in difflib.unified_diff(lhs, rhs) | |
| 76 if not line.startswith(('---', '+++'))) | |
| 77 return result | |
| 78 | 113 |
| 79 | 114 |
| 80 def compare_build_artifacts(first_dir, second_dir): | 115 def compare_build_artifacts(first_dir, second_dir): |
| 81 """Compare the artifacts from two distinct builds.""" | 116 """Compare the artifacts from two distinct builds.""" |
| 82 if not os.path.isdir(first_dir): | 117 if not os.path.isdir(first_dir): |
| 83 print >> sys.stderr, '%s isn\'t a valid directory.' % first_dir | 118 print >> sys.stderr, '%s isn\'t a valid directory.' % first_dir |
| 84 return 1 | 119 return 1 |
| 85 if not os.path.isdir(second_dir): | 120 if not os.path.isdir(second_dir): |
| 86 print >> sys.stderr, '%s isn\'t a valid directory.' % second_dir | 121 print >> sys.stderr, '%s isn\'t a valid directory.' % second_dir |
| 87 return 1 | 122 return 1 |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 129 parser.error('--first-build-dir is required') | 164 parser.error('--first-build-dir is required') |
| 130 if not options.second_build_dir: | 165 if not options.second_build_dir: |
| 131 parser.error('--second-build-dir is required') | 166 parser.error('--second-build-dir is required') |
| 132 | 167 |
| 133 return compare_build_artifacts(options.first_build_dir, | 168 return compare_build_artifacts(options.first_build_dir, |
| 134 options.second_build_dir) | 169 options.second_build_dir) |
| 135 | 170 |
| 136 | 171 |
| 137 if __name__ == '__main__': | 172 if __name__ == '__main__': |
| 138 sys.exit(main()) | 173 sys.exit(main()) |
| OLD | NEW |