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