blob: ee0352f786d7adaa0898ddaa683594f1a9202b8e [file] [log] [blame]
Stephen Martinis3c3b0e52019-03-06 18:39:511#!/usr/bin/env python
Avi Drissmandfd880852022-09-15 20:11:092# Copyright 2016 The Chromium Authors
Stephen Martinis3c3b0e52019-03-06 18:39:513# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Wenbin Zhangc56b1252021-10-05 02:49:066from __future__ import print_function
7
Stephen Martinis3c3b0e52019-03-06 18:39:518import copy
9import json
10import sys
11
12# These fields must appear in the test result output
13REQUIRED = {
14 'interrupted',
15 'num_failures_by_type',
16 'seconds_since_epoch',
17 'tests',
18 }
19
20# These fields are optional, but must have the same value on all shards
21OPTIONAL_MATCHING = (
22 'builder_name',
23 'build_number',
24 'chromium_revision',
25 'has_pretty_patch',
26 'has_wdiff',
27 'path_delimiter',
28 'pixel_tests_enabled',
Rakib M. Hasan69ea25ce2019-06-29 23:11:5129 'random_order_seed'
Stephen Martinis3c3b0e52019-03-06 18:39:5130 )
31
Rakib M. Hasan69ea25ce2019-06-29 23:11:5132# The last shard's value for these fields will show up in the merged results
Stephen Martinis3c3b0e52019-03-06 18:39:5133OPTIONAL_IGNORED = (
34 'layout_tests_dir',
Rakib M. Hasan69ea25ce2019-06-29 23:11:5135 'metadata'
Stephen Martinis3c3b0e52019-03-06 18:39:5136 )
37
38# These fields are optional and will be summed together
39OPTIONAL_COUNTS = (
40 'fixable',
41 'num_flaky',
42 'num_passes',
43 'num_regressions',
44 'skipped',
45 'skips',
46 )
47
48
49class MergeException(Exception):
50 pass
51
52
53def merge_test_results(shard_results_list):
54 """ Merge list of results.
55
56 Args:
57 shard_results_list: list of results to merge. All the results must have the
58 same format. Supported format are simplified JSON format & Chromium JSON
59 test results format version 3 (see
60 https://www.chromium.org/developers/the-json-test-results-format)
61
62 Returns:
63 a dictionary that represent the merged results. Its format follow the same
64 format of all results in |shard_results_list|.
65 """
66 shard_results_list = [x for x in shard_results_list if x]
67 if not shard_results_list:
68 return {}
69
70 if 'seconds_since_epoch' in shard_results_list[0]:
71 return _merge_json_test_result_format(shard_results_list)
Joshua Hoodbb3cd222022-03-03 00:40:4872
73 return _merge_simplified_json_format(shard_results_list)
Stephen Martinis3c3b0e52019-03-06 18:39:5174
75
76def _merge_simplified_json_format(shard_results_list):
77 # This code is specialized to the "simplified" JSON format that used to be
78 # the standard for recipes.
79
80 # These are the only keys we pay attention to in the output JSON.
81 merged_results = {
82 'successes': [],
83 'failures': [],
84 'valid': True,
85 }
86
87 for result_json in shard_results_list:
88 successes = result_json.get('successes', [])
89 failures = result_json.get('failures', [])
90 valid = result_json.get('valid', True)
91
92 if (not isinstance(successes, list) or not isinstance(failures, list) or
93 not isinstance(valid, bool)):
94 raise MergeException(
95 'Unexpected value type in %s' % result_json) # pragma: no cover
96
97 merged_results['successes'].extend(successes)
98 merged_results['failures'].extend(failures)
99 merged_results['valid'] = merged_results['valid'] and valid
100 return merged_results
101
102
103def _merge_json_test_result_format(shard_results_list):
104 # This code is specialized to the Chromium JSON test results format version 3:
105 # https://www.chromium.org/developers/the-json-test-results-format
106
107 # These are required fields for the JSON test result format version 3.
108 merged_results = {
109 'tests': {},
110 'interrupted': False,
111 'version': 3,
112 'seconds_since_epoch': float('inf'),
113 'num_failures_by_type': {
114 }
115 }
116
117 # To make sure that we don't mutate existing shard_results_list.
118 shard_results_list = copy.deepcopy(shard_results_list)
119 for result_json in shard_results_list:
Darwin Huang663c17d2020-02-06 01:10:01120 # TODO(tansell): check whether this deepcopy is actually necessary.
Stephen Martinis3c3b0e52019-03-06 18:39:51121 result_json = copy.deepcopy(result_json)
122
123 # Check the version first
124 version = result_json.pop('version', -1)
125 if version != 3:
126 raise MergeException( # pragma: no cover (covered by
127 # results_merger_unittest).
128 'Unsupported version %s. Only version 3 is supported' % version)
129
130 # Check the results for each shard have the required keys
131 missing = REQUIRED - set(result_json)
132 if missing:
133 raise MergeException( # pragma: no cover (covered by
134 # results_merger_unittest).
135 'Invalid json test results (missing %s)' % missing)
136
137 # Curry merge_values for this result_json.
138 merge = lambda key, merge_func: merge_value(
139 result_json, merged_results, key, merge_func)
140
141 # Traverse the result_json's test trie & merged_results's test tries in
142 # DFS order & add the n to merged['tests'].
143 merge('tests', merge_tries)
144
145 # If any were interrupted, we are interrupted.
146 merge('interrupted', lambda x,y: x|y)
147
148 # Use the earliest seconds_since_epoch value
149 merge('seconds_since_epoch', min)
150
151 # Sum the number of failure types
152 merge('num_failures_by_type', sum_dicts)
153
154 # Optional values must match
155 for optional_key in OPTIONAL_MATCHING:
156 if optional_key not in result_json:
157 continue
158
159 if optional_key not in merged_results:
160 # Set this value to None, then blindly copy over it.
161 merged_results[optional_key] = None
162 merge(optional_key, lambda src, dst: src)
163 else:
164 merge(optional_key, ensure_match)
165
166 # Optional values ignored
167 for optional_key in OPTIONAL_IGNORED:
168 if optional_key in result_json:
169 merged_results[optional_key] = result_json.pop(
170 # pragma: no cover (covered by
171 # results_merger_unittest).
172 optional_key)
173
174 # Sum optional value counts
175 for count_key in OPTIONAL_COUNTS:
176 if count_key in result_json: # pragma: no cover
177 # TODO(mcgreevy): add coverage.
178 merged_results.setdefault(count_key, 0)
179 merge(count_key, lambda a, b: a+b)
180
181 if result_json:
182 raise MergeException( # pragma: no cover (covered by
183 # results_merger_unittest).
Wenbin Zhangc56b1252021-10-05 02:49:06184 'Unmergable values %s' % list(result_json.keys()))
Stephen Martinis3c3b0e52019-03-06 18:39:51185
186 return merged_results
187
188
189def merge_tries(source, dest):
190 """ Merges test tries.
191
192 This is intended for use as a merge_func parameter to merge_value.
193
194 Args:
195 source: A result json test trie.
196 dest: A json test trie merge destination.
197 """
198 # merge_tries merges source into dest by performing a lock-step depth-first
199 # traversal of dest and source.
200 # pending_nodes contains a list of all sub-tries which have been reached but
201 # need further merging.
202 # Each element consists of a trie prefix, and a sub-trie from each of dest
203 # and source which is reached via that prefix.
204 pending_nodes = [('', dest, source)]
205 while pending_nodes:
206 prefix, dest_node, curr_node = pending_nodes.pop()
Wenbin Zhangc56b1252021-10-05 02:49:06207 for k, v in curr_node.items():
Stephen Martinis3c3b0e52019-03-06 18:39:51208 if k in dest_node:
209 if not isinstance(v, dict):
210 raise MergeException(
211 "%s:%s: %r not mergable, curr_node: %r\ndest_node: %r" % (
212 prefix, k, v, curr_node, dest_node))
213 pending_nodes.append(("%s:%s" % (prefix, k), dest_node[k], v))
214 else:
215 dest_node[k] = v
216 return dest
217
218
219def ensure_match(source, dest):
220 """ Returns source if it matches dest.
221
222 This is intended for use as a merge_func parameter to merge_value.
223
224 Raises:
225 MergeException if source != dest
226 """
227 if source != dest:
228 raise MergeException( # pragma: no cover (covered by
229 # results_merger_unittest).
230 "Values don't match: %s, %s" % (source, dest))
231 return source
232
233
234def sum_dicts(source, dest):
235 """ Adds values from source to corresponding values in dest.
236
237 This is intended for use as a merge_func parameter to merge_value.
238 """
Wenbin Zhangc56b1252021-10-05 02:49:06239 for k, v in source.items():
Stephen Martinis3c3b0e52019-03-06 18:39:51240 dest.setdefault(k, 0)
241 dest[k] += v
242
243 return dest
244
245
246def merge_value(source, dest, key, merge_func):
247 """ Merges a value from source to dest.
248
249 The value is deleted from source.
250
251 Args:
252 source: A dictionary from which to pull a value, identified by key.
253 dest: The dictionary into to which the value is to be merged.
254 key: The key which identifies the value to be merged.
255 merge_func(src, dst): A function which merges its src into dst,
256 and returns the result. May modify dst. May raise a MergeException.
257
258 Raises:
259 MergeException if the values can not be merged.
260 """
261 try:
262 dest[key] = merge_func(source[key], dest[key])
263 except MergeException as e:
Joshua Hoodbb3cd222022-03-03 00:40:48264 message = "MergeFailure for %s\n%s" % (key, e.args[0])
265 e.args = (message,) + e.args[1:]
Stephen Martinis3c3b0e52019-03-06 18:39:51266 raise
267 del source[key]
268
269
270def main(files):
271 if len(files) < 2:
272 sys.stderr.write("Not enough JSON files to merge.\n")
273 return 1
274 sys.stderr.write('Starting with %s\n' % files[0])
275 result = json.load(open(files[0]))
276 for f in files[1:]:
277 sys.stderr.write('Merging %s\n' % f)
278 result = merge_test_results([result, json.load(open(f))])
Wenbin Zhangc56b1252021-10-05 02:49:06279 print(json.dumps(result))
Stephen Martinis3c3b0e52019-03-06 18:39:51280 return 0
281
282
283if __name__ == "__main__":
284 sys.exit(main(sys.argv[1:]))