blob: bf7e9566e3cb3f61a94e51a1d2fbb6822e8d818f [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
6import copy
Brian Sheedy822e03742024-08-09 18:48:147import functools
Stephen Martinis3c3b0e52019-03-06 18:39:518import json
9import sys
10
11# These fields must appear in the test result output
12REQUIRED = {
13 'interrupted',
14 'num_failures_by_type',
15 'seconds_since_epoch',
16 'tests',
Ben Pasteneb5c67262024-05-15 21:24:0117}
Stephen Martinis3c3b0e52019-03-06 18:39:5118
19# These fields are optional, but must have the same value on all shards
Ben Pasteneb5c67262024-05-15 21:24:0120OPTIONAL_MATCHING = ('builder_name', 'build_number', 'chromium_revision',
21 'has_pretty_patch', 'has_wdiff', 'path_delimiter',
22 'pixel_tests_enabled', 'random_order_seed')
Stephen Martinis3c3b0e52019-03-06 18:39:5123
Rakib M. Hasan69ea25ce2019-06-29 23:11:5124# The last shard's value for these fields will show up in the merged results
Ben Pasteneb5c67262024-05-15 21:24:0125OPTIONAL_IGNORED = ('layout_tests_dir', 'metadata')
Stephen Martinis3c3b0e52019-03-06 18:39:5126
27# These fields are optional and will be summed together
28OPTIONAL_COUNTS = (
29 'fixable',
30 'num_flaky',
31 'num_passes',
32 'num_regressions',
33 'skipped',
34 'skips',
Ben Pasteneb5c67262024-05-15 21:24:0135)
Stephen Martinis3c3b0e52019-03-06 18:39:5136
37
38class MergeException(Exception):
39 pass
40
41
42def merge_test_results(shard_results_list):
43 """ Merge list of results.
44
45 Args:
46 shard_results_list: list of results to merge. All the results must have the
47 same format. Supported format are simplified JSON format & Chromium JSON
48 test results format version 3 (see
49 https://www.chromium.org/developers/the-json-test-results-format)
50
51 Returns:
52 a dictionary that represent the merged results. Its format follow the same
53 format of all results in |shard_results_list|.
54 """
55 shard_results_list = [x for x in shard_results_list if x]
56 if not shard_results_list:
57 return {}
58
59 if 'seconds_since_epoch' in shard_results_list[0]:
60 return _merge_json_test_result_format(shard_results_list)
Joshua Hoodbb3cd222022-03-03 00:40:4861
62 return _merge_simplified_json_format(shard_results_list)
Stephen Martinis3c3b0e52019-03-06 18:39:5163
64
65def _merge_simplified_json_format(shard_results_list):
66 # This code is specialized to the "simplified" JSON format that used to be
67 # the standard for recipes.
68
69 # These are the only keys we pay attention to in the output JSON.
70 merged_results = {
Ben Pasteneb5c67262024-05-15 21:24:0171 'successes': [],
72 'failures': [],
73 'valid': True,
Stephen Martinis3c3b0e52019-03-06 18:39:5174 }
75
76 for result_json in shard_results_list:
77 successes = result_json.get('successes', [])
78 failures = result_json.get('failures', [])
79 valid = result_json.get('valid', True)
80
Ben Pasteneb5c67262024-05-15 21:24:0181 if (not isinstance(successes, list) or not isinstance(failures, list)
82 or not isinstance(valid, bool)):
83 raise MergeException('Unexpected value type in %s' %
84 result_json) # pragma: no cover
Stephen Martinis3c3b0e52019-03-06 18:39:5185
86 merged_results['successes'].extend(successes)
87 merged_results['failures'].extend(failures)
88 merged_results['valid'] = merged_results['valid'] and valid
89 return merged_results
90
91
92def _merge_json_test_result_format(shard_results_list):
93 # This code is specialized to the Chromium JSON test results format version 3:
94 # https://www.chromium.org/developers/the-json-test-results-format
95
96 # These are required fields for the JSON test result format version 3.
97 merged_results = {
Ben Pasteneb5c67262024-05-15 21:24:0198 'tests': {},
99 'interrupted': False,
100 'version': 3,
101 'seconds_since_epoch': float('inf'),
102 'num_failures_by_type': {}
Stephen Martinis3c3b0e52019-03-06 18:39:51103 }
104
105 # To make sure that we don't mutate existing shard_results_list.
106 shard_results_list = copy.deepcopy(shard_results_list)
107 for result_json in shard_results_list:
Darwin Huang663c17d2020-02-06 01:10:01108 # TODO(tansell): check whether this deepcopy is actually necessary.
Stephen Martinis3c3b0e52019-03-06 18:39:51109 result_json = copy.deepcopy(result_json)
110
111 # Check the version first
112 version = result_json.pop('version', -1)
113 if version != 3:
114 raise MergeException( # pragma: no cover (covered by
Ben Pasteneb5c67262024-05-15 21:24:01115 # results_merger_unittest).
Stephen Martinis3c3b0e52019-03-06 18:39:51116 'Unsupported version %s. Only version 3 is supported' % version)
117
118 # Check the results for each shard have the required keys
119 missing = REQUIRED - set(result_json)
120 if missing:
121 raise MergeException( # pragma: no cover (covered by
Ben Pasteneb5c67262024-05-15 21:24:01122 # results_merger_unittest).
Stephen Martinis3c3b0e52019-03-06 18:39:51123 'Invalid json test results (missing %s)' % missing)
124
125 # Curry merge_values for this result_json.
Brian Sheedy822e03742024-08-09 18:48:14126 merge = functools.partial(merge_value, result_json, merged_results)
Stephen Martinis3c3b0e52019-03-06 18:39:51127
128 # Traverse the result_json's test trie & merged_results's test tries in
129 # DFS order & add the n to merged['tests'].
130 merge('tests', merge_tries)
131
132 # If any were interrupted, we are interrupted.
Ben Pasteneb5c67262024-05-15 21:24:01133 merge('interrupted', lambda x, y: x | y)
Stephen Martinis3c3b0e52019-03-06 18:39:51134
135 # Use the earliest seconds_since_epoch value
136 merge('seconds_since_epoch', min)
137
138 # Sum the number of failure types
139 merge('num_failures_by_type', sum_dicts)
140
141 # Optional values must match
142 for optional_key in OPTIONAL_MATCHING:
143 if optional_key not in result_json:
144 continue
145
146 if optional_key not in merged_results:
147 # Set this value to None, then blindly copy over it.
148 merged_results[optional_key] = None
149 merge(optional_key, lambda src, dst: src)
150 else:
151 merge(optional_key, ensure_match)
152
153 # Optional values ignored
154 for optional_key in OPTIONAL_IGNORED:
155 if optional_key in result_json:
156 merged_results[optional_key] = result_json.pop(
157 # pragma: no cover (covered by
158 # results_merger_unittest).
159 optional_key)
160
161 # Sum optional value counts
162 for count_key in OPTIONAL_COUNTS:
163 if count_key in result_json: # pragma: no cover
164 # TODO(mcgreevy): add coverage.
165 merged_results.setdefault(count_key, 0)
Ben Pasteneb5c67262024-05-15 21:24:01166 merge(count_key, lambda a, b: a + b)
Stephen Martinis3c3b0e52019-03-06 18:39:51167
168 if result_json:
169 raise MergeException( # pragma: no cover (covered by
Ben Pasteneb5c67262024-05-15 21:24:01170 # results_merger_unittest).
Wenbin Zhangc56b1252021-10-05 02:49:06171 'Unmergable values %s' % list(result_json.keys()))
Stephen Martinis3c3b0e52019-03-06 18:39:51172
173 return merged_results
174
175
176def merge_tries(source, dest):
177 """ Merges test tries.
178
179 This is intended for use as a merge_func parameter to merge_value.
180
181 Args:
182 source: A result json test trie.
183 dest: A json test trie merge destination.
184 """
185 # merge_tries merges source into dest by performing a lock-step depth-first
186 # traversal of dest and source.
187 # pending_nodes contains a list of all sub-tries which have been reached but
188 # need further merging.
189 # Each element consists of a trie prefix, and a sub-trie from each of dest
190 # and source which is reached via that prefix.
191 pending_nodes = [('', dest, source)]
192 while pending_nodes:
193 prefix, dest_node, curr_node = pending_nodes.pop()
Wenbin Zhangc56b1252021-10-05 02:49:06194 for k, v in curr_node.items():
Stephen Martinis3c3b0e52019-03-06 18:39:51195 if k in dest_node:
196 if not isinstance(v, dict):
197 raise MergeException(
Brian Sheedy0d2300f32024-08-13 23:14:41198 '%s:%s: %r not mergable, curr_node: %r\ndest_node: %r' %
Ben Pasteneb5c67262024-05-15 21:24:01199 (prefix, k, v, curr_node, dest_node))
Brian Sheedy0d2300f32024-08-13 23:14:41200 pending_nodes.append(('%s:%s' % (prefix, k), dest_node[k], v))
Stephen Martinis3c3b0e52019-03-06 18:39:51201 else:
202 dest_node[k] = v
203 return dest
204
205
206def ensure_match(source, dest):
207 """ Returns source if it matches dest.
208
209 This is intended for use as a merge_func parameter to merge_value.
210
211 Raises:
212 MergeException if source != dest
213 """
214 if source != dest:
215 raise MergeException( # pragma: no cover (covered by
Ben Pasteneb5c67262024-05-15 21:24:01216 # results_merger_unittest).
Stephen Martinis3c3b0e52019-03-06 18:39:51217 "Values don't match: %s, %s" % (source, dest))
218 return source
219
220
221def sum_dicts(source, dest):
222 """ Adds values from source to corresponding values in dest.
223
224 This is intended for use as a merge_func parameter to merge_value.
225 """
Wenbin Zhangc56b1252021-10-05 02:49:06226 for k, v in source.items():
Stephen Martinis3c3b0e52019-03-06 18:39:51227 dest.setdefault(k, 0)
228 dest[k] += v
229
230 return dest
231
232
233def merge_value(source, dest, key, merge_func):
234 """ Merges a value from source to dest.
235
236 The value is deleted from source.
237
238 Args:
239 source: A dictionary from which to pull a value, identified by key.
240 dest: The dictionary into to which the value is to be merged.
241 key: The key which identifies the value to be merged.
242 merge_func(src, dst): A function which merges its src into dst,
243 and returns the result. May modify dst. May raise a MergeException.
244
245 Raises:
246 MergeException if the values can not be merged.
247 """
248 try:
249 dest[key] = merge_func(source[key], dest[key])
250 except MergeException as e:
Brian Sheedy0d2300f32024-08-13 23:14:41251 message = 'MergeFailure for %s\n%s' % (key, e.args[0])
Ben Pasteneb5c67262024-05-15 21:24:01252 e.args = (message, ) + e.args[1:]
Stephen Martinis3c3b0e52019-03-06 18:39:51253 raise
254 del source[key]
255
256
257def main(files):
258 if len(files) < 2:
Brian Sheedy0d2300f32024-08-13 23:14:41259 sys.stderr.write('Not enough JSON files to merge.\n')
Stephen Martinis3c3b0e52019-03-06 18:39:51260 return 1
261 sys.stderr.write('Starting with %s\n' % files[0])
262 result = json.load(open(files[0]))
263 for f in files[1:]:
264 sys.stderr.write('Merging %s\n' % f)
265 result = merge_test_results([result, json.load(open(f))])
Wenbin Zhangc56b1252021-10-05 02:49:06266 print(json.dumps(result))
Stephen Martinis3c3b0e52019-03-06 18:39:51267 return 0
268
269
Brian Sheedy0d2300f32024-08-13 23:14:41270if __name__ == '__main__':
Stephen Martinis3c3b0e52019-03-06 18:39:51271 sys.exit(main(sys.argv[1:]))