blob: 4cc988f5bf72c3b17982b6ae14ff21500d93d51b [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.
import argparse
import textwrap
import junitparser
from pathlib import Path
from typing import List, Optional
import os
import urllib.parse
import logging
from cmd_utils import init_log
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
def lstrip(s: str, prefix: str) -> str:
if s.startswith(prefix):
s = s[len(prefix) :]
return s
def classname_to_file(classname: str) -> str:
classname = lstrip(classname, "cython.")
classname = lstrip(classname, "ctypes.")
return classname.replace(".", "/") + ".py"
def failed_test_ids() -> List[str]:
FAILURE_TYPES = (junitparser.Failure, junitparser.Error)
junit_dir = REPO_ROOT / "build" / "pytest-results"
failed_node_ids = []
for junit in junit_dir.glob("*.xml"):
xml = junitparser.JUnitXml.fromfile(str(junit))
for suite in xml:
# handle suites
for case in suite:
if case.result is None:
logging.warn(f"Incorrectly formatted JUnit found, result was None on {case}")
continue
if len(case.result) > 0 and isinstance(case.result[0], FAILURE_TYPES):
node_id = classname_to_file(case.classname) + "::" + case.name
failed_node_ids.append(node_id)
return list(set(failed_node_ids))
def repro_command(build_type: str, failed_node_ids: List[str]) -> Optional[str]:
"""
Parse available JUnit XML files and output a command that users can run to
reproduce CI failures locally
"""
test_args = [f"--tests {node_id}" for node_id in failed_node_ids]
test_args_str = " ".join(test_args)
return f"python3 tests/scripts/ci.py {build_type} {test_args_str}"
def make_issue_url(failed_node_ids: List[str]) -> str:
names = [f"`{node_id}`" for node_id in failed_node_ids]
run_url = os.getenv("RUN_DISPLAY_URL", "<insert run URL>")
test_bullets = [f" - `{node_id}`" for node_id in failed_node_ids]
params = {
"labels": "test: flaky",
"title": "[Flaky Test] " + ", ".join(names),
"body": textwrap.dedent(
f"""
These tests were found to be flaky (intermittently failing on `main` or failed in a PR with unrelated changes). See [the docs](https://github.com/apache/tvm/blob/main/docs/contribute/ci.rst#handling-flaky-failures) for details.
### Tests(s)\n
"""
)
+ "\n".join(test_bullets)
+ f"\n\n### Jenkins Links\n\n - {run_url}",
}
return "https://github.com/apache/tvm/issues/new?" + urllib.parse.urlencode(params)
def show_failure_help(failed_suites: List[str]) -> None:
failed_node_ids = failed_test_ids()
if len(failed_node_ids) == 0:
return
build_type = os.getenv("PLATFORM")
if build_type is None:
raise RuntimeError("build type was None, cannot show command")
repro = repro_command(build_type=build_type, failed_node_ids=failed_node_ids)
if repro is None:
print("No test failures detected")
return
print(f"Report flaky test shortcut: {make_issue_url(failed_node_ids)}")
print("=============================== PYTEST FAILURES ================================")
print(
"These pytest suites failed to execute. The results can be found in the "
"Jenkins 'Tests' tab or by scrolling up through the raw logs here. "
"If there is no test listed below, the failure likely came from a segmentation "
"fault which you can find in the logs above.\n"
)
if failed_suites is not None and len(failed_suites) > 0:
print("\n".join([f" - {suite}" for suite in failed_suites]))
print("")
print("You can reproduce these specific failures locally with this command:\n")
print(textwrap.indent(repro, prefix=" "))
print("")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Print information about a failed pytest run")
args, other = parser.parse_known_args()
init_log()
try:
show_failure_help(failed_suites=other)
except Exception as e:
# This script shouldn't ever introduce failures since it's just there to
# add extra information, so ignore any errors
logging.exception(e)