aboutsummaryrefslogtreecommitdiff
path: root/llvm_tools/update_tryjob_status.py
blob: 49c4865802e6500fdce4a1f4361ff87f99513bd7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Updates the status of a tryjob."""


import argparse
import enum
import json
import os
import subprocess
import sys

import chroot
from test_helpers import CreateTemporaryJsonFile


class TryjobStatus(enum.Enum):
    """Values for the 'status' field of a tryjob."""

    GOOD = "good"
    BAD = "bad"
    PENDING = "pending"
    SKIP = "skip"

    # Executes the script passed into the command line (this script's exit code
    # determines the 'status' value of the tryjob).
    CUSTOM_SCRIPT = "custom_script"


class CustomScriptStatus(enum.Enum):
    """Exit code values of a custom script."""

    # NOTE: Not using 1 for 'bad' because the custom script can raise an
    # exception which would cause the exit code of the script to be 1, so the
    # tryjob's 'status' would be updated when there is an exception.
    #
    # Exit codes are as follows:
    #   0: 'good'
    #   124: 'bad'
    #   125: 'skip'
    GOOD = 0
    BAD = 124
    SKIP = 125


custom_script_exit_value_mapping = {
    CustomScriptStatus.GOOD.value: TryjobStatus.GOOD.value,
    CustomScriptStatus.BAD.value: TryjobStatus.BAD.value,
    CustomScriptStatus.SKIP.value: TryjobStatus.SKIP.value,
}


def GetCommandLineArgs():
    """Parses the command line for the command line arguments."""

    # Default absoute path to the chroot if not specified.
    cros_root = os.path.expanduser("~")
    cros_root = os.path.join(cros_root, "chromiumos")

    # Create parser and add optional command-line arguments.
    parser = argparse.ArgumentParser(
        description="Updates the status of a tryjob."
    )

    # Add argument for the JSON file to use for the update of a tryjob.
    parser.add_argument(
        "--status_file",
        required=True,
        help="The absolute path to the JSON file that contains the tryjobs used "
        "for bisecting LLVM.",
    )

    # Add argument that sets the 'status' field to that value.
    parser.add_argument(
        "--set_status",
        required=True,
        choices=[tryjob_status.value for tryjob_status in TryjobStatus],
        help='Sets the "status" field of the tryjob.',
    )

    # Add argument that determines which revision to search for in the list of
    # tryjobs.
    parser.add_argument(
        "--revision",
        required=True,
        type=int,
        help="The revision to set its status.",
    )

    # Add argument for the custom script to execute for the 'custom_script'
    # option in '--set_status'.
    parser.add_argument(
        "--custom_script",
        help="The absolute path to the custom script to execute (its exit code "
        'should be %d for "good", %d for "bad", or %d for "skip")'
        % (
            CustomScriptStatus.GOOD.value,
            CustomScriptStatus.BAD.value,
            CustomScriptStatus.SKIP.value,
        ),
    )

    args_output = parser.parse_args()

    if not (
        os.path.isfile(
            args_output.status_file
            and not args_output.status_file.endswith(".json")
        )
    ):
        raise ValueError(
            'File does not exist or does not ending in ".json" '
            ": %s" % args_output.status_file
        )

    if (
        args_output.set_status == TryjobStatus.CUSTOM_SCRIPT.value
        and not args_output.custom_script
    ):
        raise ValueError(
            "Please provide the absolute path to the script to " "execute."
        )

    return args_output


def FindTryjobIndex(revision, tryjobs_list):
    """Searches the list of tryjob dictionaries to find 'revision'.

    Uses the key 'rev' for each dictionary and compares the value against
    'revision.'

    Args:
      revision: The revision to search for in the tryjobs.
      tryjobs_list: A list of tryjob dictionaries of the format:
        {
          'rev' : [REVISION],
          'url' : [URL_OF_CL],
          'cl' : [CL_NUMBER],
          'link' : [TRYJOB_LINK],
          'status' : [TRYJOB_STATUS],
          'buildbucket_id': [BUILDBUCKET_ID]
        }

    Returns:
      The index within the list or None to indicate it was not found.
    """

    for cur_index, cur_tryjob_dict in enumerate(tryjobs_list):
        if cur_tryjob_dict["rev"] == revision:
            return cur_index

    return None


def GetCustomScriptResult(custom_script, status_file, tryjob_contents):
    """Returns the conversion of the exit code of the custom script.

    Args:
      custom_script: Absolute path to the script to be executed.
      status_file: Absolute path to the file that contains information about the
      bisection of LLVM.
      tryjob_contents: A dictionary of the contents of the tryjob (e.g. 'status',
      'url', 'link', 'buildbucket_id', etc.).

    Returns:
      The exit code conversion to either return 'good', 'bad', or 'skip'.

    Raises:
      ValueError: The custom script failed to provide the correct exit code.
    """

    # Create a temporary file to write the contents of the tryjob at index
    # 'tryjob_index' (the temporary file path will be passed into the custom
    # script as a command line argument).
    with CreateTemporaryJsonFile() as temp_json_file:
        with open(temp_json_file, "w") as tryjob_file:
            json.dump(
                tryjob_contents, tryjob_file, indent=4, separators=(",", ": ")
            )

        exec_script_cmd = [custom_script, temp_json_file]

        # Execute the custom script to get the exit code.
        exec_script_cmd_obj = subprocess.Popen(
            exec_script_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        _, stderr = exec_script_cmd_obj.communicate()

        # Invalid exit code by the custom script.
        if (
            exec_script_cmd_obj.returncode
            not in custom_script_exit_value_mapping
        ):
            # Save the .JSON file to the directory of 'status_file'.
            name_of_json_file = os.path.join(
                os.path.dirname(status_file), os.path.basename(temp_json_file)
            )

            os.rename(temp_json_file, name_of_json_file)

            raise ValueError(
                "Custom script %s exit code %d did not match "
                'any of the expected exit codes: %d for "good", %d '
                'for "bad", or %d for "skip".\nPlease check %s for information '
                "about the tryjob: %s"
                % (
                    custom_script,
                    exec_script_cmd_obj.returncode,
                    CustomScriptStatus.GOOD.value,
                    CustomScriptStatus.BAD.value,
                    CustomScriptStatus.SKIP.value,
                    name_of_json_file,
                    stderr,
                )
            )

    return custom_script_exit_value_mapping[exec_script_cmd_obj.returncode]


def UpdateTryjobStatus(revision, set_status, status_file, custom_script):
    """Updates a tryjob's 'status' field based off of 'set_status'.

    Args:
      revision: The revision associated with the tryjob.
      set_status: What to update the 'status' field to.
        Ex: TryjobStatus.Good, TryjobStatus.BAD, TryjobStatus.PENDING, or
        TryjobStatus.
      status_file: The .JSON file that contains the tryjobs.
      custom_script: The absolute path to a script that will be executed which
      will determine the 'status' value of the tryjob.
    """

    # Format of 'bisect_contents':
    # {
    #   'start': [START_REVISION_OF_BISECTION]
    #   'end': [END_REVISION_OF_BISECTION]
    #   'jobs' : [
    #       {[TRYJOB_INFORMATION]},
    #       {[TRYJOB_INFORMATION]},
    #       ...,
    #       {[TRYJOB_INFORMATION]}
    #   ]
    # }
    with open(status_file) as tryjobs:
        bisect_contents = json.load(tryjobs)

    if not bisect_contents["jobs"]:
        sys.exit("No tryjobs in %s" % status_file)

    tryjob_index = FindTryjobIndex(revision, bisect_contents["jobs"])

    # 'FindTryjobIndex()' returns None if the revision was not found.
    if tryjob_index is None:
        raise ValueError(
            "Unable to find tryjob for %d in %s" % (revision, status_file)
        )

    # Set 'status' depending on 'set_status' for the tryjob.
    if set_status == TryjobStatus.GOOD:
        bisect_contents["jobs"][tryjob_index][
            "status"
        ] = TryjobStatus.GOOD.value
    elif set_status == TryjobStatus.BAD:
        bisect_contents["jobs"][tryjob_index]["status"] = TryjobStatus.BAD.value
    elif set_status == TryjobStatus.PENDING:
        bisect_contents["jobs"][tryjob_index][
            "status"
        ] = TryjobStatus.PENDING.value
    elif set_status == TryjobStatus.SKIP:
        bisect_contents["jobs"][tryjob_index][
            "status"
        ] = TryjobStatus.SKIP.value
    elif set_status == TryjobStatus.CUSTOM_SCRIPT:
        bisect_contents["jobs"][tryjob_index]["status"] = GetCustomScriptResult(
            custom_script, status_file, bisect_contents["jobs"][tryjob_index]
        )
    else:
        raise ValueError(
            'Invalid "set_status" option provided: %s' % set_status
        )

    with open(status_file, "w") as update_tryjobs:
        json.dump(
            bisect_contents, update_tryjobs, indent=4, separators=(",", ": ")
        )


def main():
    """Updates the status of a tryjob."""

    chroot.VerifyOutsideChroot()

    args_output = GetCommandLineArgs()

    UpdateTryjobStatus(
        args_output.revision,
        TryjobStatus(args_output.set_status),
        args_output.status_file,
        args_output.custom_script,
    )


if __name__ == "__main__":
    main()