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