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