1f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren#!/usr/bin/env python 2f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren# Copyright 2017 The Chromium Authors. All rights reserved. 3f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren# Use of this source code is governed by a BSD-style license that can be 4f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren# found in the LICENSE file. 5f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 6f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borenimport argparse 7f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borenimport json 8f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borenimport logging 9f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borenimport os 10f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borenimport subprocess 11f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borenimport sys 12f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 13f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 14f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borendef collect_task( 15f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren collect_cmd, merge_script, build_properties, merge_arguments, 16f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren task_output_dir, output_json): 17f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren """Collect and merge the results of a task. 18f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 19f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren This is a relatively thin wrapper script around a `swarming.py collect` 20f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren command and a subsequent results merge to ensure that the recipe system 21f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren treats them as a single step. The results merge can either be the default 22f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren one provided by results_merger or a python script provided as merge_script. 23f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 24f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren Args: 25f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren collect_cmd: The `swarming.py collect` command to run. Should not contain 26f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren a --task-output-dir argument. 27f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_script: A merge/postprocessing script that should be run to 28f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge the results. This script will be invoked as 29f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 30f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren <merge_script> \ 31f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren [--build-properties <string JSON>] \ 32f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren [merge arguments...] \ 33f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren --summary-json <summary json> \ 34f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren -o <merged json path> \ 35f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren <shard json>... 36f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 37f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren where the merge arguments are the contents of merge_arguments_json. 38f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren build_properties: A string containing build information to 39f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren pass to the merge script in JSON form. 40f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_arguments: A string containing additional arguments to pass to 41f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren the merge script in JSON form. 42f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren task_output_dir: A path to a directory in which swarming will write the 43f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren output of the task, including a summary JSON and all of the individual 44f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren shard results. 45f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren output_json: A path to a JSON file to which the merged results should be 46f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren written. The merged results should be in the JSON Results File Format 47f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren (https://www.chromium.org/developers/the-json-test-results-format) 48f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren and may optionally contain a top level "links" field that may contain a 49f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren dict mapping link text to URLs, for a set of links that will be included 50f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren in the buildbot output. 51f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren Returns: 52f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren The exit code of collect_cmd or merge_cmd. 53f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren """ 54f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.debug('Using task_output_dir: %r', task_output_dir) 55f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if os.path.exists(task_output_dir): 56f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn('task_output_dir %r already exists!', task_output_dir) 57f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren existing_contents = [] 58f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren try: 59f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren for p in os.listdir(task_output_dir): 60f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren existing_contents.append(os.path.join(task_output_dir, p)) 61f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren except (OSError, IOError) as e: 62f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.error('Error while examining existing task_output_dir: %s', e) 63f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 64f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn('task_output_dir existing content: %r', existing_contents) 65f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 66f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren collect_cmd.extend(['--task-output-dir', task_output_dir]) 67f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 68f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.info('collect_cmd: %s', ' '.join(collect_cmd)) 69f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren collect_result = subprocess.call(collect_cmd) 70f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if collect_result != 0: 71f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn('collect_cmd had non-zero return code: %s', collect_result) 72f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 73f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren task_output_dir_contents = [] 74f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren try: 75f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren task_output_dir_contents.extend( 76f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren os.path.join(task_output_dir, p) 77f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren for p in os.listdir(task_output_dir)) 78f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren except (OSError, IOError) as e: 79f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.error('Error while processing task_output_dir: %s', e) 80f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 81f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.debug('Contents of task_output_dir: %r', task_output_dir_contents) 82f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if not task_output_dir_contents: 83f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn( 84f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 'No files found in task_output_dir: %r', 85f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren task_output_dir) 86f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 87f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren task_output_subdirs = ( 88f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren p for p in task_output_dir_contents 89f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if os.path.isdir(p)) 90f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren shard_json_files = [ 91f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren os.path.join(subdir, 'output.json') 92f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren for subdir in task_output_subdirs] 93f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren extant_shard_json_files = [ 94f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren f for f in shard_json_files if os.path.exists(f)] 95f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 96f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if shard_json_files != extant_shard_json_files: 97f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn( 98f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 'Expected output.json file missing: %r\nFound: %r\nExpected: %r\n', 99f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren set(shard_json_files) - set(extant_shard_json_files), 100f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren extant_shard_json_files, 101f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren shard_json_files) 102f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 103f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if not extant_shard_json_files: 104f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn( 105f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 'No shard json files found in task_output_dir: %r\nFound %r', 106f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren task_output_dir, task_output_dir_contents) 107f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 108f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.debug('Found shard_json_files: %r', shard_json_files) 109f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 110f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren summary_json_file = os.path.join(task_output_dir, 'summary.json') 111f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 112f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_result = 0 113f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 114f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_cmd = [sys.executable, merge_script] 115f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if build_properties: 116f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_cmd.extend(('--build-properties', build_properties)) 117f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if os.path.exists(summary_json_file): 118f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_cmd.extend(('--summary-json', summary_json_file)) 119f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren else: 120f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn('Summary json file missing: %r', summary_json_file) 121f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if merge_arguments: 122f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_cmd.extend(json.loads(merge_arguments)) 123f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_cmd.extend(('-o', output_json)) 124f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_cmd.extend(extant_shard_json_files) 125f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 126f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.info('merge_cmd: %s', ' '.join(merge_cmd)) 127f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren merge_result = subprocess.call(merge_cmd) 128f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if merge_result != 0: 129f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn('merge_cmd had non-zero return code: %s', merge_result) 130f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 131f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if not os.path.exists(output_json): 132f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.warn( 133f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 'merge_cmd did not create output_json file: %r', output_json) 134f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 135f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren return collect_result or merge_result 136f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 137f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 138f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borendef main(): 139f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren parser = argparse.ArgumentParser() 140f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren parser.add_argument('--build-properties') 141f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren parser.add_argument('--merge-additional-args') 142f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren parser.add_argument('--merge-script', required=True) 143f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren parser.add_argument('--task-output-dir', required=True) 144f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren parser.add_argument('-o', '--output-json', required=True) 145f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren parser.add_argument('--verbose', action='store_true') 146f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren parser.add_argument('collect_cmd', nargs='+') 147f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 148f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren args = parser.parse_args() 149f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren if args.verbose: 150f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 151f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 152f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren return collect_task( 153f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren args.collect_cmd, 154f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren args.merge_script, args.build_properties, args.merge_additional_args, 155f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren args.task_output_dir, args.output_json) 156f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 157f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren 158f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Borenif __name__ == '__main__': 159f94514b0ff8eccb2eaef8c77bee8c5f462b83b90Eric Boren sys.exit(main()) 160