blob: ef0630620b9721d080c81a14222183cb38d48cae [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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 json
import os
import logging
import argparse
import subprocess
import sys
from urllib import error
from xml.etree import ElementTree
import requests
from git_utils import git, GitHubRepo, parse_remote
from cmd_utils import init_log
SKIPPED_TESTS_COMMENT_MARKER = "<!---skipped-tests-comment-->\n\n"
GITHUB_ACTIONS_BOT_LOGIN = "github-actions[bot]"
PR_TEST_REPORT_DIR = "pr-reports"
MAIN_TEST_REPORT_DIR = "main-reports"
def run_subprocess(command):
logging.info(f"Running command {command}")
proc = subprocess.run(command, shell=True, stdout=subprocess.PIPE, encoding="utf-8")
if proc.returncode != 0:
raise RuntimeError(f"Command failed {command}:\nstdout:\n{proc.stdout}")
return proc
def retrieve_test_report(s3_url, target_dir):
command = f"aws s3 cp {s3_url} {target_dir} --recursive"
run_subprocess(command)
def get_common_commit_sha():
command = "git merge-base origin/main HEAD"
proc = run_subprocess(command)
return proc.stdout.strip()
def get_main_jenkins_build_number(github, common_commit):
json = github.get(f"commits/{common_commit}/status")
for status in reversed(json["statuses"]):
if status["context"] != "tvm-ci/branch":
continue
state = status["state"]
target_url = str(status["target_url"])
build_number = (
target_url[target_url.find("job/main") : len(target_url)]
.strip("job/main/")
.strip("/display/redirect")
)
assert build_number.isdigit()
return {"build_number": build_number, "state": state}
raise RuntimeError(f"Failed to find main build number for commit {common_commit}")
def retrieve_test_reports(common_main_build, pr_number, build_number, s3_prefix):
cur_build_s3_link = (
f"s3://{s3_prefix}/tvm/PR-{str(pr_number)}/{str(build_number)}/pytest-results"
)
retrieve_test_report(cur_build_s3_link, PR_TEST_REPORT_DIR)
common_build_s3_link = f"s3://{s3_prefix}/tvm/main/{common_main_build}/pytest-results"
retrieve_test_report(common_build_s3_link, MAIN_TEST_REPORT_DIR)
def get_pr_and_build_numbers(target_url):
target_url = target_url[target_url.find("PR-") : len(target_url)]
split = target_url.split("/")
pr_number = split[0].strip("PR-")
build_number = split[1]
return {"pr_number": pr_number, "build_number": build_number}
def build_test_set(directory):
subdir_to_skipped = {}
subdirs = [
item for item in os.listdir(directory) if os.path.isdir(os.path.join(directory, item))
]
for subdir in subdirs:
subdir_to_skipped[subdir] = set()
for root, _, files in os.walk(directory + "/" + subdir):
for file in files:
test_report = ElementTree.parse(root + "/" + file)
for testcase in test_report.iter("testcase"):
skipped = testcase.find("skipped")
if skipped is not None:
key = testcase.attrib["classname"] + "#" + testcase.attrib["name"]
subdir_to_skipped[subdir].add(key)
return subdir_to_skipped
def to_node_name(dir_name: str):
return dir_name.replace("_", ": ", 1)
def build_comment(
common_commit_sha,
common_main_build,
skipped_list,
pr_number,
build_number,
commit_sha,
jenkins_prefix,
):
if common_main_build["state"] != "success":
return f"{SKIPPED_TESTS_COMMENT_MARKER}Unable to run tests bot because main failed to pass CI at {common_commit_sha}."
if len(skipped_list) == 0:
return f"{SKIPPED_TESTS_COMMENT_MARKER}No additional skipped tests found in this branch for commit {commit_sha}."
text = (
f"{SKIPPED_TESTS_COMMENT_MARKER}The list below shows some tests that ran in main {common_commit_sha} but were "
f"skipped in the CI build of {commit_sha}:\n"
f"```\n"
)
for skip in skipped_list:
text += skip + "\n"
text += (
f"```\nA detailed report of ran tests is [here](https://{jenkins_prefix}/job/tvm/job/PR-{str(pr_number)}"
f"/{str(build_number)}/testReport/)."
)
return text
def get_pr_comments(github, url):
try:
return github.get(url)
except error.HTTPError as e:
logging.exception(f"Failed to retrieve PR comments: {url}: {e}")
return []
def search_for_docs_comment(comments):
for comment in comments:
if (
comment["user"]["login"] == GITHUB_ACTIONS_BOT_LOGIN
and SKIPPED_TESTS_COMMENT_MARKER in comment["body"]
):
return comment
return None
if __name__ == "__main__":
help = (
"Compares the skipped tests of this PR against the last successful build on main. Also comments on the PR "
"issue when tests are skipped in this PR and not on main."
)
parser = argparse.ArgumentParser(description=help)
parser.add_argument("--remote", default="origin", help="ssh remote to parse")
parser.add_argument("--s3-prefix", default="tvm-jenkins-artifacts-prod")
parser.add_argument("--jenkins-prefix", default="ci.tlcpack.ai")
parser.add_argument("--common-main-build")
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="run but don't send any request to GitHub",
)
args = parser.parse_args()
init_log()
remote = git(["config", "--get", f"remote.{args.remote}.url"])
user, repo = parse_remote(remote)
target_url = os.environ["TARGET_URL"]
pr_and_build = get_pr_and_build_numbers(target_url)
commit_sha = os.environ["COMMIT_SHA"]
if not args.dry_run:
github = GitHubRepo(token=os.environ["GITHUB_TOKEN"], user=user, repo=repo)
common_commit_sha = get_common_commit_sha()
common_main_build = get_main_jenkins_build_number(github, common_commit_sha)
retrieve_test_reports(
common_main_build=common_main_build["build_number"],
pr_number=pr_and_build["pr_number"],
build_number=pr_and_build["build_number"],
s3_prefix=args.s3_prefix,
)
else:
assert args.common_main_build is not None
common_main_build = json.loads(args.common_main_build)
common_commit_sha = os.environ["COMMIT_SHA"]
main_tests = build_test_set(MAIN_TEST_REPORT_DIR)
build_tests = build_test_set(PR_TEST_REPORT_DIR)
skipped_list = []
for subdir, skipped_set in build_tests.items():
skipped_main = main_tests[subdir]
if skipped_main is None:
logging.warning(f"Could not find directory {subdir} in main.")
continue
diff_set = skipped_set - skipped_main
if len(diff_set) != 0:
for test in diff_set:
skipped_list.append(f"{to_node_name(subdir)} -> {test}")
# Sort the list to maintain an order in the output. Helps when validating the output in tests.
skipped_list.sort()
if len(skipped_list) == 0:
logging.info("No skipped tests found.")
body = build_comment(
common_commit_sha,
common_main_build,
skipped_list,
pr_and_build["pr_number"],
pr_and_build["build_number"],
commit_sha,
args.jenkins_prefix,
)
url = f'issues/{pr_and_build["pr_number"]}/comments'
if not args.dry_run:
# For now, only comment for PRs open by driazati, gigiblender and areusch.
get_pr_url = f'pulls/{pr_and_build["pr_number"]}'
pull_request_body = github.get(get_pr_url)
author = pull_request_body["user"]["login"]
if author not in ["driazati", "gigiblender", "areusch"]:
logging.info(f"Skipping this action for user {author}")
sys.exit(0)
pr_comments = get_pr_comments(github, url)
comment = search_for_docs_comment(pr_comments)
if comment is not None:
comment_url = comment["url"]
comment_id = comment_url[comment_url.find("comments/") : len(comment_url)].strip(
"comments/"
)
github.patch(f"issues/comments/{comment_id}", {"body": body})
else:
github.post(url, {"body": body})
else:
logging.info(f"Dry run, would have posted {url} with data {body}.")