blob: 74224195c5500523bdfae33bf3db143c5e3c6b01 [file] [log] [blame]
#!/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.
# ruff: noqa: E501
import json
import logging
import os
import subprocess
from pathlib import Path
from typing import Any, Dict, Optional
from xml.etree import ElementTree
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 --region us-west-2 s3 cp {s3_url} {target_dir} --recursive --no-sign-request"
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, pr_test_report_dir, main_test_report_dir
):
cur_build_s3_link = f"s3://{s3_prefix}/tvm/PR-{pr_number!s}/{build_number!s}/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):
directory = Path(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(Path(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_diff_comment_with_main(
common_commit_sha,
skipped_list,
commit_sha,
):
if len(skipped_list) == 0:
return f"No diff in skipped tests with main found in this branch for commit {commit_sha}.\n"
text = (
f"The list below shows 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 += "```\n"
return text
def build_comment(
common_commit_sha,
common_main_build,
skipped_list,
additional_skipped_list,
pr_number,
build_number,
commit_sha,
jenkins_prefix,
):
if common_main_build["state"] != "success":
return f"Unable to run tests bot because main failed to pass CI at {common_commit_sha}."
text = build_diff_comment_with_main(common_commit_sha, skipped_list, commit_sha)
if len(additional_skipped_list) != 0:
text += "\n"
text += (
"Additional tests that were skipped in the CI build and present in the [`required_tests_to_run`]"
"(https://github.com/apache/tvm/blob/main/ci/scripts/github/required_tests_to_run.json) file:"
"\n```\n"
)
for skip in additional_skipped_list:
text += skip + "\n"
text += "```\n"
text += (
f"A detailed report of ran tests is [here](https://{jenkins_prefix}/job/tvm/job/PR-{pr_number!s}"
f"/{build_number!s}/testReport/)."
)
return text
def find_target_url(pr_head: Dict[str, Any]):
for status in pr_head["statusCheckRollup"]["contexts"]["nodes"]:
if status.get("context", "") == "tvm-ci/pr-head":
return status["targetUrl"]
raise RuntimeError(f"Unable to find tvm-ci/pr-head status in {pr_head}")
def get_skipped_tests_comment(
pr: Dict[str, Any],
github,
s3_prefix: str = "tvm-jenkins-artifacts-prod",
jenkins_prefix: str = "ci.tlcpack.ai",
pr_test_report_dir: str = "pr-reports",
main_test_report_dir: str = "main-reports",
common_commit_sha: Optional[str] = None,
common_main_build: Optional[Dict[str, Any]] = None,
additional_tests_to_check_file: str = "required_tests_to_run.json",
) -> str:
pr_head = pr["commits"]["nodes"][0]["commit"]
target_url = find_target_url(pr_head)
pr_and_build = get_pr_and_build_numbers(target_url)
logging.info(f"Getting comment for {pr_head} with target {target_url}")
commit_sha = pr_head["oid"]
is_dry_run = common_commit_sha is not None
if not is_dry_run:
logging.info("Fetching common commit sha and build info")
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=s3_prefix,
main_test_report_dir=main_test_report_dir,
pr_test_report_dir=pr_test_report_dir,
)
else:
logging.info("Dry run, expecting PR and main reports on disk")
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.")
if not is_dry_run:
current_file = Path(__file__).resolve()
additional_tests_to_check_file = Path(current_file).parent / "required_tests_to_run.json"
logging.info(
f"Checking additional tests in file {additional_tests_to_check_file} are not skipped."
)
try:
with open(additional_tests_to_check_file) as f:
additional_tests_to_check = json.load(f)
except OSError:
logging.info(
f"Failed to read additional tests from file: {additional_tests_to_check_file}."
)
additional_tests_to_check = {}
# Assert that tests present in "required_tests_to_run.json" are not skipped.
additional_skipped_tests = []
for subdir, test_set in additional_tests_to_check.items():
if subdir not in build_tests.keys():
logging.warning(f"Could not find directory {subdir} in the build test set.")
continue
for test in test_set:
if test in build_tests[subdir]:
additional_skipped_tests.append(f"{to_node_name(subdir)} -> {test}")
if len(additional_skipped_tests) == 0:
logging.info("No skipped tests found in the additional list.")
body = build_comment(
common_commit_sha,
common_main_build,
skipped_list,
additional_skipped_tests,
pr_and_build["pr_number"],
pr_and_build["build_number"],
commit_sha,
jenkins_prefix,
)
return body