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