• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Updates the status of a tryjob."""
8
9from __future__ import print_function
10
11import argparse
12import enum
13import json
14import os
15import subprocess
16import sys
17
18import chroot
19from subprocess_helpers import ChrootRunCommand
20from test_helpers import CreateTemporaryJsonFile
21
22
23class TryjobStatus(enum.Enum):
24  """Values for the 'status' field of a tryjob."""
25
26  GOOD = 'good'
27  BAD = 'bad'
28  PENDING = 'pending'
29  SKIP = 'skip'
30
31  # Executes the script passed into the command line (this script's exit code
32  # determines the 'status' value of the tryjob).
33  CUSTOM_SCRIPT = 'custom_script'
34
35  # Uses the result returned by 'cros buildresult'.
36  AUTO = 'auto'
37
38
39class BuilderStatus(enum.Enum):
40  """Actual values given via 'cros buildresult'."""
41
42  PASS = 'pass'
43  FAIL = 'fail'
44  RUNNING = 'running'
45
46
47class CustomScriptStatus(enum.Enum):
48  """Exit code values of a custom script."""
49
50  # NOTE: Not using 1 for 'bad' because the custom script can raise an
51  # exception which would cause the exit code of the script to be 1, so the
52  # tryjob's 'status' would be updated when there is an exception.
53  #
54  # Exit codes are as follows:
55  #   0: 'good'
56  #   124: 'bad'
57  #   125: 'skip'
58  GOOD = 0
59  BAD = 124
60  SKIP = 125
61
62
63custom_script_exit_value_mapping = {
64    CustomScriptStatus.GOOD.value: TryjobStatus.GOOD.value,
65    CustomScriptStatus.BAD.value: TryjobStatus.BAD.value,
66    CustomScriptStatus.SKIP.value: TryjobStatus.SKIP.value
67}
68
69builder_status_mapping = {
70    BuilderStatus.PASS.value: TryjobStatus.GOOD.value,
71    BuilderStatus.FAIL.value: TryjobStatus.BAD.value,
72    BuilderStatus.RUNNING.value: TryjobStatus.PENDING.value
73}
74
75
76def GetCommandLineArgs():
77  """Parses the command line for the command line arguments."""
78
79  # Default absoute path to the chroot if not specified.
80  cros_root = os.path.expanduser('~')
81  cros_root = os.path.join(cros_root, 'chromiumos')
82
83  # Create parser and add optional command-line arguments.
84  parser = argparse.ArgumentParser(
85      description='Updates the status of a tryjob.')
86
87  # Add argument for the JSON file to use for the update of a tryjob.
88  parser.add_argument(
89      '--status_file',
90      required=True,
91      help='The absolute path to the JSON file that contains the tryjobs used '
92      'for bisecting LLVM.')
93
94  # Add argument that sets the 'status' field to that value.
95  parser.add_argument(
96      '--set_status',
97      required=True,
98      choices=[tryjob_status.value for tryjob_status in TryjobStatus],
99      help='Sets the "status" field of the tryjob.')
100
101  # Add argument that determines which revision to search for in the list of
102  # tryjobs.
103  parser.add_argument(
104      '--revision',
105      required=True,
106      type=int,
107      help='The revision to set its status.')
108
109  # Add argument for a specific chroot path.
110  parser.add_argument(
111      '--chroot_path',
112      default=cros_root,
113      help='the path to the chroot (default: %(default)s)')
114
115  # Add argument for the custom script to execute for the 'custom_script'
116  # option in '--set_status'.
117  parser.add_argument(
118      '--custom_script',
119      help='The absolute path to the custom script to execute (its exit code '
120      'should be %d for "good", %d for "bad", or %d for "skip")' %
121      (CustomScriptStatus.GOOD.value, CustomScriptStatus.BAD.value,
122       CustomScriptStatus.SKIP.value))
123
124  args_output = parser.parse_args()
125
126  if not os.path.isfile(args_output.status_file) or \
127      not args_output.status_file.endswith('.json'):
128    raise ValueError('File does not exist or does not ending in ".json" '
129                     ': %s' % args_output.status_file)
130
131  if args_output.set_status == TryjobStatus.CUSTOM_SCRIPT.value and \
132      not args_output.custom_script:
133    raise ValueError('Please provide the absolute path to the script to '
134                     'execute.')
135
136  return args_output
137
138
139def FindTryjobIndex(revision, tryjobs_list):
140  """Searches the list of tryjob dictionaries to find 'revision'.
141
142  Uses the key 'rev' for each dictionary and compares the value against
143  'revision.'
144
145  Args:
146    revision: The revision to search for in the tryjobs.
147    tryjobs_list: A list of tryjob dictionaries of the format:
148      {
149        'rev' : [REVISION],
150        'url' : [URL_OF_CL],
151        'cl' : [CL_NUMBER],
152        'link' : [TRYJOB_LINK],
153        'status' : [TRYJOB_STATUS],
154        'buildbucket_id': [BUILDBUCKET_ID]
155      }
156
157  Returns:
158    The index within the list or None to indicate it was not found.
159  """
160
161  for cur_index, cur_tryjob_dict in enumerate(tryjobs_list):
162    if cur_tryjob_dict['rev'] == revision:
163      return cur_index
164
165  return None
166
167
168def GetStatusFromCrosBuildResult(chroot_path, buildbucket_id):
169  """Retrieves the 'status' using 'cros buildresult'."""
170
171  get_buildbucket_id_cmd = [
172      'cros', 'buildresult', '--buildbucket-id',
173      str(buildbucket_id), '--report', 'json'
174  ]
175
176  tryjob_json = ChrootRunCommand(chroot_path, get_buildbucket_id_cmd)
177
178  tryjob_contents = json.loads(tryjob_json)
179
180  return str(tryjob_contents['%d' % buildbucket_id]['status'])
181
182
183def GetAutoResult(chroot_path, buildbucket_id):
184  """Returns the conversion of the result of 'cros buildresult'."""
185
186  # Calls 'cros buildresult' to get the status of the tryjob.
187  build_result = GetStatusFromCrosBuildResult(chroot_path, buildbucket_id)
188
189  # The string returned by 'cros buildresult' might not be in the mapping.
190  if build_result not in builder_status_mapping:
191    raise ValueError(
192        '"cros buildresult" return value is invalid: %s' % build_result)
193
194  return builder_status_mapping[build_result]
195
196
197def GetCustomScriptResult(custom_script, status_file, tryjob_contents):
198  """Returns the conversion of the exit code of the custom script.
199
200  Args:
201    custom_script: Absolute path to the script to be executed.
202    status_file: Absolute path to the file that contains information about the
203    bisection of LLVM.
204    tryjob_contents: A dictionary of the contents of the tryjob (e.g. 'status',
205    'url', 'link', 'buildbucket_id', etc.).
206
207  Returns:
208    The exit code conversion to either return 'good', 'bad', or 'skip'.
209
210  Raises:
211    ValueError: The custom script failed to provide the correct exit code.
212  """
213
214  # Create a temporary file to write the contents of the tryjob at index
215  # 'tryjob_index' (the temporary file path will be passed into the custom
216  # script as a command line argument).
217  with CreateTemporaryJsonFile() as temp_json_file:
218    with open(temp_json_file, 'w') as tryjob_file:
219      json.dump(tryjob_contents, tryjob_file, indent=4, separators=(',', ': '))
220
221    exec_script_cmd = [custom_script, temp_json_file]
222
223    # Execute the custom script to get the exit code.
224    exec_script_cmd_obj = subprocess.Popen(
225        exec_script_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
226    _, stderr = exec_script_cmd_obj.communicate()
227
228    # Invalid exit code by the custom script.
229    if exec_script_cmd_obj.returncode not in custom_script_exit_value_mapping:
230      # Save the .JSON file to the directory of 'status_file'.
231      name_of_json_file = os.path.join(
232          os.path.dirname(status_file), os.path.basename(temp_json_file))
233
234      os.rename(temp_json_file, name_of_json_file)
235
236      raise ValueError(
237          'Custom script %s exit code %d did not match '
238          'any of the expected exit codes: %d for "good", %d '
239          'for "bad", or %d for "skip".\nPlease check %s for information '
240          'about the tryjob: %s' %
241          (custom_script, exec_script_cmd_obj.returncode,
242           CustomScriptStatus.GOOD.value, CustomScriptStatus.BAD.value,
243           CustomScriptStatus.SKIP.value, name_of_json_file, stderr))
244
245  return custom_script_exit_value_mapping[exec_script_cmd_obj.returncode]
246
247
248def UpdateTryjobStatus(revision, set_status, status_file, chroot_path,
249                       custom_script):
250  """Updates a tryjob's 'status' field based off of 'set_status'.
251
252  Args:
253    revision: The revision associated with the tryjob.
254    set_status: What to update the 'status' field to.
255      Ex: TryjobStatus.Good, TryjobStatus.BAD, TryjobStatus.PENDING, or
256      TryjobStatus.AUTO where TryjobStatus.AUTO uses the result of
257      'cros buildresult'.
258    status_file: The .JSON file that contains the tryjobs.
259    chroot_path: The absolute path to the chroot (used by 'cros buildresult').
260    custom_script: The absolute path to a script that will be executed which
261    will determine the 'status' value of the tryjob.
262  """
263
264  # Format of 'bisect_contents':
265  # {
266  #   'start': [START_REVISION_OF_BISECTION]
267  #   'end': [END_REVISION_OF_BISECTION]
268  #   'jobs' : [
269  #       {[TRYJOB_INFORMATION]},
270  #       {[TRYJOB_INFORMATION]},
271  #       ...,
272  #       {[TRYJOB_INFORMATION]}
273  #   ]
274  # }
275  with open(status_file) as tryjobs:
276    bisect_contents = json.load(tryjobs)
277
278  if not bisect_contents['jobs']:
279    sys.exit('No tryjobs in %s' % status_file)
280
281  tryjob_index = FindTryjobIndex(revision, bisect_contents['jobs'])
282
283  # 'FindTryjobIndex()' returns None if the revision was not found.
284  if tryjob_index is None:
285    raise ValueError(
286        'Unable to find tryjob for %d in %s' % (revision, status_file))
287
288  # Set 'status' depending on 'set_status' for the tryjob.
289  if set_status == TryjobStatus.GOOD:
290    bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.GOOD.value
291  elif set_status == TryjobStatus.BAD:
292    bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.BAD.value
293  elif set_status == TryjobStatus.PENDING:
294    bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.PENDING.value
295  elif set_status == TryjobStatus.AUTO:
296    bisect_contents['jobs'][tryjob_index]['status'] = GetAutoResult(
297        chroot_path, bisect_contents['jobs'][tryjob_index]['buildbucket_id'])
298  elif set_status == TryjobStatus.SKIP:
299    bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.SKIP.value
300  elif set_status == TryjobStatus.CUSTOM_SCRIPT:
301    bisect_contents['jobs'][tryjob_index]['status'] = GetCustomScriptResult(
302        custom_script, status_file, bisect_contents['jobs'][tryjob_index])
303  else:
304    raise ValueError('Invalid "set_status" option provided: %s' % set_status)
305
306  with open(status_file, 'w') as update_tryjobs:
307    json.dump(bisect_contents, update_tryjobs, indent=4, separators=(',', ': '))
308
309
310def main():
311  """Updates the status of a tryjob."""
312
313  chroot.VerifyOutsideChroot()
314
315  args_output = GetCommandLineArgs()
316
317  UpdateTryjobStatus(args_output.revision, TryjobStatus(args_output.set_status),
318                     args_output.status_file, args_output.chroot_path,
319                     args_output.custom_script)
320
321
322if __name__ == '__main__':
323  main()
324