• 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 test_helpers import CreateTemporaryJsonFile
20
21
22class TryjobStatus(enum.Enum):
23  """Values for the 'status' field of a tryjob."""
24
25  GOOD = 'good'
26  BAD = 'bad'
27  PENDING = 'pending'
28  SKIP = 'skip'
29
30  # Executes the script passed into the command line (this script's exit code
31  # determines the 'status' value of the tryjob).
32  CUSTOM_SCRIPT = 'custom_script'
33
34
35class CustomScriptStatus(enum.Enum):
36  """Exit code values of a custom script."""
37
38  # NOTE: Not using 1 for 'bad' because the custom script can raise an
39  # exception which would cause the exit code of the script to be 1, so the
40  # tryjob's 'status' would be updated when there is an exception.
41  #
42  # Exit codes are as follows:
43  #   0: 'good'
44  #   124: 'bad'
45  #   125: 'skip'
46  GOOD = 0
47  BAD = 124
48  SKIP = 125
49
50
51custom_script_exit_value_mapping = {
52    CustomScriptStatus.GOOD.value: TryjobStatus.GOOD.value,
53    CustomScriptStatus.BAD.value: TryjobStatus.BAD.value,
54    CustomScriptStatus.SKIP.value: TryjobStatus.SKIP.value
55}
56
57
58def GetCommandLineArgs():
59  """Parses the command line for the command line arguments."""
60
61  # Default absoute path to the chroot if not specified.
62  cros_root = os.path.expanduser('~')
63  cros_root = os.path.join(cros_root, 'chromiumos')
64
65  # Create parser and add optional command-line arguments.
66  parser = argparse.ArgumentParser(
67      description='Updates the status of a tryjob.')
68
69  # Add argument for the JSON file to use for the update of a tryjob.
70  parser.add_argument(
71      '--status_file',
72      required=True,
73      help='The absolute path to the JSON file that contains the tryjobs used '
74      'for bisecting LLVM.')
75
76  # Add argument that sets the 'status' field to that value.
77  parser.add_argument(
78      '--set_status',
79      required=True,
80      choices=[tryjob_status.value for tryjob_status in TryjobStatus],
81      help='Sets the "status" field of the tryjob.')
82
83  # Add argument that determines which revision to search for in the list of
84  # tryjobs.
85  parser.add_argument(
86      '--revision',
87      required=True,
88      type=int,
89      help='The revision to set its status.')
90
91  # Add argument for the custom script to execute for the 'custom_script'
92  # option in '--set_status'.
93  parser.add_argument(
94      '--custom_script',
95      help='The absolute path to the custom script to execute (its exit code '
96      'should be %d for "good", %d for "bad", or %d for "skip")' %
97      (CustomScriptStatus.GOOD.value, CustomScriptStatus.BAD.value,
98       CustomScriptStatus.SKIP.value))
99
100  args_output = parser.parse_args()
101
102  if not (os.path.isfile(
103      args_output.status_file and
104      not args_output.status_file.endswith('.json'))):
105    raise ValueError('File does not exist or does not ending in ".json" '
106                     ': %s' % args_output.status_file)
107
108  if (args_output.set_status == TryjobStatus.CUSTOM_SCRIPT.value and
109      not args_output.custom_script):
110    raise ValueError('Please provide the absolute path to the script to '
111                     'execute.')
112
113  return args_output
114
115
116def FindTryjobIndex(revision, tryjobs_list):
117  """Searches the list of tryjob dictionaries to find 'revision'.
118
119  Uses the key 'rev' for each dictionary and compares the value against
120  'revision.'
121
122  Args:
123    revision: The revision to search for in the tryjobs.
124    tryjobs_list: A list of tryjob dictionaries of the format:
125      {
126        'rev' : [REVISION],
127        'url' : [URL_OF_CL],
128        'cl' : [CL_NUMBER],
129        'link' : [TRYJOB_LINK],
130        'status' : [TRYJOB_STATUS],
131        'buildbucket_id': [BUILDBUCKET_ID]
132      }
133
134  Returns:
135    The index within the list or None to indicate it was not found.
136  """
137
138  for cur_index, cur_tryjob_dict in enumerate(tryjobs_list):
139    if cur_tryjob_dict['rev'] == revision:
140      return cur_index
141
142  return None
143
144
145def GetCustomScriptResult(custom_script, status_file, tryjob_contents):
146  """Returns the conversion of the exit code of the custom script.
147
148  Args:
149    custom_script: Absolute path to the script to be executed.
150    status_file: Absolute path to the file that contains information about the
151    bisection of LLVM.
152    tryjob_contents: A dictionary of the contents of the tryjob (e.g. 'status',
153    'url', 'link', 'buildbucket_id', etc.).
154
155  Returns:
156    The exit code conversion to either return 'good', 'bad', or 'skip'.
157
158  Raises:
159    ValueError: The custom script failed to provide the correct exit code.
160  """
161
162  # Create a temporary file to write the contents of the tryjob at index
163  # 'tryjob_index' (the temporary file path will be passed into the custom
164  # script as a command line argument).
165  with CreateTemporaryJsonFile() as temp_json_file:
166    with open(temp_json_file, 'w') as tryjob_file:
167      json.dump(tryjob_contents, tryjob_file, indent=4, separators=(',', ': '))
168
169    exec_script_cmd = [custom_script, temp_json_file]
170
171    # Execute the custom script to get the exit code.
172    exec_script_cmd_obj = subprocess.Popen(
173        exec_script_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
174    _, stderr = exec_script_cmd_obj.communicate()
175
176    # Invalid exit code by the custom script.
177    if exec_script_cmd_obj.returncode not in custom_script_exit_value_mapping:
178      # Save the .JSON file to the directory of 'status_file'.
179      name_of_json_file = os.path.join(
180          os.path.dirname(status_file), os.path.basename(temp_json_file))
181
182      os.rename(temp_json_file, name_of_json_file)
183
184      raise ValueError(
185          'Custom script %s exit code %d did not match '
186          'any of the expected exit codes: %d for "good", %d '
187          'for "bad", or %d for "skip".\nPlease check %s for information '
188          'about the tryjob: %s' %
189          (custom_script, exec_script_cmd_obj.returncode,
190           CustomScriptStatus.GOOD.value, CustomScriptStatus.BAD.value,
191           CustomScriptStatus.SKIP.value, name_of_json_file, stderr))
192
193  return custom_script_exit_value_mapping[exec_script_cmd_obj.returncode]
194
195
196def UpdateTryjobStatus(revision, set_status, status_file, custom_script):
197  """Updates a tryjob's 'status' field based off of 'set_status'.
198
199  Args:
200    revision: The revision associated with the tryjob.
201    set_status: What to update the 'status' field to.
202      Ex: TryjobStatus.Good, TryjobStatus.BAD, TryjobStatus.PENDING, or
203      TryjobStatus.
204    status_file: The .JSON file that contains the tryjobs.
205    custom_script: The absolute path to a script that will be executed which
206    will determine the 'status' value of the tryjob.
207  """
208
209  # Format of 'bisect_contents':
210  # {
211  #   'start': [START_REVISION_OF_BISECTION]
212  #   'end': [END_REVISION_OF_BISECTION]
213  #   'jobs' : [
214  #       {[TRYJOB_INFORMATION]},
215  #       {[TRYJOB_INFORMATION]},
216  #       ...,
217  #       {[TRYJOB_INFORMATION]}
218  #   ]
219  # }
220  with open(status_file) as tryjobs:
221    bisect_contents = json.load(tryjobs)
222
223  if not bisect_contents['jobs']:
224    sys.exit('No tryjobs in %s' % status_file)
225
226  tryjob_index = FindTryjobIndex(revision, bisect_contents['jobs'])
227
228  # 'FindTryjobIndex()' returns None if the revision was not found.
229  if tryjob_index is None:
230    raise ValueError('Unable to find tryjob for %d in %s' %
231                     (revision, status_file))
232
233  # Set 'status' depending on 'set_status' for the tryjob.
234  if set_status == TryjobStatus.GOOD:
235    bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.GOOD.value
236  elif set_status == TryjobStatus.BAD:
237    bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.BAD.value
238  elif set_status == TryjobStatus.PENDING:
239    bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.PENDING.value
240  elif set_status == TryjobStatus.SKIP:
241    bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.SKIP.value
242  elif set_status == TryjobStatus.CUSTOM_SCRIPT:
243    bisect_contents['jobs'][tryjob_index]['status'] = GetCustomScriptResult(
244        custom_script, status_file, bisect_contents['jobs'][tryjob_index])
245  else:
246    raise ValueError('Invalid "set_status" option provided: %s' % set_status)
247
248  with open(status_file, 'w') as update_tryjobs:
249    json.dump(bisect_contents, update_tryjobs, indent=4, separators=(',', ': '))
250
251
252def main():
253  """Updates the status of a tryjob."""
254
255  chroot.VerifyOutsideChroot()
256
257  args_output = GetCommandLineArgs()
258
259  UpdateTryjobStatus(args_output.revision, TryjobStatus(args_output.set_status),
260                     args_output.status_file, args_output.custom_script)
261
262
263if __name__ == '__main__':
264  main()
265