1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
|
#!/usr/bin/env python3
import argparse
import os
import subprocess
from pathlib import Path
from common import (
get_testcases,
is_failure,
is_passing_skipped_test,
is_unexpected_success,
key,
open_test_results,
)
from download_reports import download_reports
"""
Usage: update_failures.py /path/to/dynamo_test_failures.py /path/to/test commit_sha
Best-effort updates the xfail and skip files under test directory
by parsing test reports.
You'll need to provide the commit_sha for the latest commit on a PR
from which we will pull CI test results.
Instructions:
- On your PR, add the "keep-going" label to ensure that all the tests are
failing (as opposed to CI stopping on the first failure). You may need to
restart your test jobs by force-pushing to your branch for CI to pick
up the "keep-going" label.
- Wait for all the tests to finish running.
- Find the full SHA of your commit and run this command.
This script requires the `gh` cli. You'll need to install it and then
authenticate with it via `gh auth login` before using this script.
https://docs.github.com/en/github-cli/github-cli/quickstart
"""
def patch_file(
filename, test_dir, unexpected_successes, new_xfails, new_skips, unexpected_skips
):
failures_directory = os.path.join(test_dir, "dynamo_expected_failures")
skips_directory = os.path.join(test_dir, "dynamo_skips")
dynamo_expected_failures = set(os.listdir(failures_directory))
dynamo_skips = set(os.listdir(skips_directory))
# These are hand written skips
extra_dynamo_skips = set()
with open(filename) as f:
start = False
for text in f.readlines():
text = text.strip()
if start:
if text == "}":
break
extra_dynamo_skips.add(text.strip(',"'))
else:
if text == "extra_dynamo_skips = {":
start = True
def format(testcase):
classname = testcase.attrib["classname"]
name = testcase.attrib["name"]
return f"{classname}.{name}"
formatted_unexpected_successes = {
f"{format(test)}" for test in unexpected_successes.values()
}
formatted_unexpected_skips = {
f"{format(test)}" for test in unexpected_skips.values()
}
formatted_new_xfails = [f"{format(test)}" for test in new_xfails.values()]
formatted_new_skips = [f"{format(test)}" for test in new_skips.values()]
def remove_file(path, name):
file = os.path.join(path, name)
cmd = ["git", "rm", file]
subprocess.run(cmd)
def add_file(path, name):
file = os.path.join(path, name)
with open(file, "w") as fp:
pass
cmd = ["git", "add", file]
subprocess.run(cmd)
covered_unexpected_successes = set()
# dynamo_expected_failures
for test in dynamo_expected_failures:
if test in formatted_unexpected_successes:
covered_unexpected_successes.add(test)
remove_file(failures_directory, test)
for test in formatted_new_xfails:
add_file(failures_directory, test)
leftover_unexpected_successes = (
formatted_unexpected_successes - covered_unexpected_successes
)
if len(leftover_unexpected_successes) > 0:
print(
"WARNING: we were unable to remove these "
f"{len(leftover_unexpected_successes)} expectedFailures:"
)
for stuff in leftover_unexpected_successes:
print(stuff)
# dynamo_skips
for test in dynamo_skips:
if test in formatted_unexpected_skips:
remove_file(skips_directory, test)
for test in extra_dynamo_skips:
if test in formatted_unexpected_skips:
print(
f"WARNING: {test} in dynamo_test_failures.py needs to be removed manually"
)
for test in formatted_new_skips:
add_file(skips_directory, test)
def get_intersection_and_outside(a_dict, b_dict):
a = set(a_dict.keys())
b = set(b_dict.keys())
intersection = a.intersection(b)
outside = (a.union(b)) - intersection
def build_dict(keys):
result = {}
for k in keys:
if k in a_dict:
result[k] = a_dict[k]
else:
result[k] = b_dict[k]
return result
return build_dict(intersection), build_dict(outside)
def update(filename, test_dir, py38_dir, py311_dir, also_remove_skips):
def read_test_results(directory):
xmls = open_test_results(directory)
testcases = get_testcases(xmls)
unexpected_successes = {
key(test): test for test in testcases if is_unexpected_success(test)
}
failures = {key(test): test for test in testcases if is_failure(test)}
passing_skipped_tests = {
key(test): test for test in testcases if is_passing_skipped_test(test)
}
return unexpected_successes, failures, passing_skipped_tests
(
py38_unexpected_successes,
py38_failures,
py38_passing_skipped_tests,
) = read_test_results(py38_dir)
(
py311_unexpected_successes,
py311_failures,
py311_passing_skipped_tests,
) = read_test_results(py311_dir)
unexpected_successes = {**py38_unexpected_successes, **py311_unexpected_successes}
_, skips = get_intersection_and_outside(
py38_unexpected_successes, py311_unexpected_successes
)
xfails, more_skips = get_intersection_and_outside(py38_failures, py311_failures)
if also_remove_skips:
unexpected_skips, _ = get_intersection_and_outside(
py38_passing_skipped_tests, py311_passing_skipped_tests
)
else:
unexpected_skips = {}
all_skips = {**skips, **more_skips}
print(
f"Discovered {len(unexpected_successes)} new unexpected successes, "
f"{len(xfails)} new xfails, {len(all_skips)} new skips, {len(unexpected_skips)} new unexpected skips"
)
return patch_file(
filename, test_dir, unexpected_successes, xfails, all_skips, unexpected_skips
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog="update_dynamo_test_failures",
description="Read from logs and update the dynamo_test_failures file",
)
# dynamo_test_failures path
parser.add_argument(
"filename",
nargs="?",
default=str(
Path(__file__).absolute().parent.parent.parent
/ "torch/testing/_internal/dynamo_test_failures.py"
),
help="Optional path to dynamo_test_failures.py",
)
# test path
parser.add_argument(
"test_dir",
nargs="?",
default=str(Path(__file__).absolute().parent.parent.parent / "test"),
help="Optional path to test folder",
)
parser.add_argument(
"commit",
help=(
"The commit sha for the latest commit on a PR from which we will "
"pull CI test results, e.g. 7e5f597aeeba30c390c05f7d316829b3798064a5"
),
)
parser.add_argument(
"--also-remove-skips",
help="Also attempt to remove skips. WARNING: does not guard against test flakiness",
action="store_true",
)
args = parser.parse_args()
assert Path(args.filename).exists(), args.filename
assert Path(args.test_dir).exists(), args.test_dir
dynamo39, dynamo311 = download_reports(args.commit, ("dynamo39", "dynamo311"))
update(args.filename, args.test_dir, dynamo39, dynamo311, args.also_remove_skips)
|