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