blob: e9f781a3c00948f61e7e8359218ec230216c04cb [file] [log] [blame]
#!/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