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