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 |