| #!/usr/bin/env python3 |
| # |
| # Copyright 2018 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import os |
| import sys |
| import subprocess |
| import time |
| import yaml |
| |
| import bazelci |
| from config import DOWNSTREAM_PROJECTS, PLATFORMS |
| from utils import ( |
| eprint, |
| execute_command, |
| fetch_bazelcipy_command, |
| print_collapsed_group, |
| print_expanded_group, |
| python_binary, |
| ) |
| from steps import create_step |
| from update_last_green_commit import get_last_green_commit |
| from runner import clone_git_repository |
| |
| BAZEL_REPO_DIR = os.getcwd() |
| |
| |
| def bazel_culprit_finder_py_url(): |
| """ |
| URL to the latest version of this script. |
| """ |
| return "https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/buildkite/culprit_finder.py?{}".format( |
| int(time.time()) |
| ) |
| |
| |
| def fetch_culprit_finder_py_command(): |
| return "curl -s {0} -o culprit_finder.py".format(bazel_culprit_finder_py_url()) |
| |
| |
| def get_bazel_commits_between(first_commit, second_commit): |
| """ |
| Get bazel commits between first_commit and second_commit as a list. |
| first_commit is not included in the list. |
| second_commit is included in the list. |
| """ |
| try: |
| os.chdir(BAZEL_REPO_DIR) |
| output = subprocess.check_output( |
| ["git", "log", "--pretty=tformat:%H", "%s..%s" % (first_commit, second_commit)] |
| ) |
| return [i for i in reversed(output.decode("utf-8").split("\n")) if i] |
| except subprocess.CalledProcessError as e: |
| raise Exception( |
| "Failed to get bazel commits between %s..%s:\n%s" |
| % (first_commit, second_commit, str(e)) |
| ) |
| |
| |
| def test_with_bazel_at_commit( |
| project_name, platform_name, git_repo_location, bazel_commit, needs_clean |
| ): |
| http_config = DOWNSTREAM_PROJECTS[project_name]["http_config"] |
| try: |
| return_code = bazelci.main( |
| [ |
| "runner", |
| "--task=" + platform_name, |
| "--http_config=" + http_config, |
| "--git_repo_location=" + git_repo_location, |
| "--use_bazel_at_commit=" + bazel_commit, |
| ] |
| + (["--needs_clean"] if needs_clean else []) |
| ) |
| except subprocess.CalledProcessError as e: |
| eprint(str(e)) |
| return False |
| return return_code == 0 |
| |
| |
| def clone_git_repository_for_project(project_name, platform_name): |
| git_repository = DOWNSTREAM_PROJECTS[project_name]["git_repository"] |
| git_commit = get_last_green_commit( |
| git_repository, DOWNSTREAM_PROJECTS[project_name]["pipeline_slug"] |
| ) |
| return clone_git_repository(git_repository, platform_name, git_commit) |
| |
| |
| def start_bisecting(project_name, platform_name, git_repo_location, commits_list, needs_clean): |
| left = 0 |
| right = len(commits_list) |
| while left < right: |
| mid = (left + right) // 2 |
| mid_commit = commits_list[mid] |
| print_expanded_group(":bazel: Test with Bazel built at " + mid_commit) |
| eprint("Remaining suspected commits are:\n") |
| for i in range(left, right): |
| eprint(commits_list[i] + "\n") |
| if test_with_bazel_at_commit( |
| project_name, platform_name, git_repo_location, mid_commit, needs_clean |
| ): |
| print_collapsed_group(":bazel: Succeeded at " + mid_commit) |
| left = mid + 1 |
| else: |
| print_collapsed_group(":bazel: Failed at " + mid_commit) |
| right = mid |
| |
| print_expanded_group(":bazel: Bisect Result") |
| if right == len(commits_list): |
| eprint("first bad commit not found, every commit succeeded.") |
| else: |
| first_bad_commit = commits_list[right] |
| eprint("first bad commit is " + first_bad_commit) |
| os.chdir(BAZEL_REPO_DIR) |
| execute_command(["git", "--no-pager", "log", "-n", "1", first_bad_commit]) |
| |
| |
| def print_culprit_finder_pipeline( |
| project_name, platform_name, good_bazel_commit, bad_bazel_commit, needs_clean |
| ): |
| label = PLATFORMS[platform_name]["emoji-name"] + " Bisecting for {0}".format(project_name) |
| command = ( |
| '%s culprit_finder.py runner --project_name="%s" --platform_name=%s --good_bazel_commit=%s --bad_bazel_commit=%s %s' |
| % ( |
| python_binary(platform_name), |
| project_name, |
| platform_name, |
| good_bazel_commit, |
| bad_bazel_commit, |
| "--needs_clean" if needs_clean else "", |
| ) |
| ) |
| commands = [fetch_bazelcipy_command(), fetch_culprit_finder_py_command(), command] |
| pipeline_steps = [] |
| pipeline_steps.append(create_step(label, commands, platform_name)) |
| print(yaml.dump({"steps": pipeline_steps})) |
| |
| |
| def main(argv=None): |
| if argv is None: |
| argv = sys.argv[1:] |
| |
| parser = argparse.ArgumentParser(description="Bazel Culprit Finder Script") |
| |
| subparsers = parser.add_subparsers(dest="subparsers_name") |
| |
| subparsers.add_parser("culprit_finder") |
| |
| runner = subparsers.add_parser("runner") |
| runner.add_argument("--project_name", type=str) |
| runner.add_argument("--platform_name", type=str) |
| runner.add_argument("--good_bazel_commit", type=str) |
| runner.add_argument("--bad_bazel_commit", type=str) |
| runner.add_argument("--needs_clean", type=bool, nargs="?", const=True) |
| |
| args = parser.parse_args(argv) |
| |
| if args.subparsers_name == "culprit_finder": |
| try: |
| project_name = os.environ["PROJECT_NAME"] |
| platform_name = os.environ["PLATFORM_NAME"] |
| good_bazel_commit = os.environ["GOOD_BAZEL_COMMIT"] |
| bad_bazel_commit = os.environ["BAD_BAZEL_COMMIT"] |
| except KeyError as e: |
| raise Exception("Environment variable %s must be set" % str(e)) |
| |
| needs_clean = False |
| if "NEEDS_CLEAN" in os.environ: |
| needs_clean = True |
| |
| if project_name not in DOWNSTREAM_PROJECTS: |
| raise Exception( |
| "Project name '%s' not recognized, available projects are %s" |
| % (project_name, str((DOWNSTREAM_PROJECTS.keys()))) |
| ) |
| |
| if platform_name not in PLATFORMS: |
| raise Exception( |
| "Platform name '%s' not recognized, available platforms are %s" |
| % (platform_name, str((PLATFORMS.keys()))) |
| ) |
| print_culprit_finder_pipeline( |
| project_name=project_name, |
| platform_name=platform_name, |
| good_bazel_commit=good_bazel_commit, |
| bad_bazel_commit=bad_bazel_commit, |
| needs_clean=needs_clean, |
| ) |
| elif args.subparsers_name == "runner": |
| git_repo_location = clone_git_repository_for_project(args.project_name, args.platform_name) |
| print_collapsed_group("Check good bazel commit " + args.good_bazel_commit) |
| if not test_with_bazel_at_commit( |
| project_name=args.project_name, |
| platform_name=args.platform_name, |
| git_repo_location=git_repo_location, |
| bazel_commit=args.good_bazel_commit, |
| needs_clean=args.needs_clean, |
| ): |
| raise Exception( |
| "Given good commit (%s) is not actually good, abort bisecting." |
| % args.good_bazel_commit |
| ) |
| start_bisecting( |
| project_name=args.project_name, |
| platform_name=args.platform_name, |
| git_repo_location=git_repo_location, |
| commits_list=get_bazel_commits_between(args.good_bazel_commit, args.bad_bazel_commit), |
| needs_clean=args.needs_clean, |
| ) |
| else: |
| parser.print_help() |
| return 2 |
| |
| return 0 |