Skip to content

Commit 68128bc

Browse files
authored
Merge pull request #11400 from hnrklssn/cherry-pick-update-tests-stable
[Cherry-pick][stable/20240723] Add --update-tests to llvm-lit
2 parents 82c1463 + 1de129d commit 68128bc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+545
-3
lines changed

clang/test/lit.cfg.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,19 @@ def calculate_arch_features(arch_string):
373373
# default configs for the test runs.
374374
config.environment["CLANG_NO_DEFAULT_CONFIG"] = "1"
375375

376+
if lit_config.update_tests:
377+
import sys
378+
import os
379+
380+
utilspath = os.path.join(config.llvm_src_root, "utils")
381+
sys.path.append(utilspath)
382+
from update_any_test_checks import utc_lit_plugin
383+
384+
lit_config.test_updaters.append(utc_lit_plugin)
385+
376386
# Restrict the size of the on-disk CAS for tests. This allows testing in
377387
# constrained environments (e.g. small TMPDIR). It also prevents leaving
378388
# behind large files on file systems that do not support sparse files if a test
379389
# crashes before resizing the file.
380390
config.environment["LLVM_CAS_MAX_MAPPING_SIZE"] = "%d" % (100 * 1024 * 1024)
391+

llvm/docs/CommandGuide/lit.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ ADDITIONAL OPTIONS
313313

314314
List all of the discovered tests and exit.
315315

316+
.. option:: --update-tests
317+
318+
Pass failing tests to functions in the ``lit_config.test_updaters`` list to
319+
check whether any of them know how to update the test to make it pass.
320+
316321
EXIT STATUS
317322
-----------
318323

llvm/test/lit.cfg.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,3 +632,13 @@ def have_ld64_plugin_support():
632632

633633
if config.has_logf128:
634634
config.available_features.add("has_logf128")
635+
636+
if lit_config.update_tests:
637+
import sys
638+
import os
639+
640+
utilspath = os.path.join(config.llvm_src_root, "utils")
641+
sys.path.append(utilspath)
642+
from update_any_test_checks import utc_lit_plugin
643+
644+
lit_config.test_updaters.append(utc_lit_plugin)

llvm/utils/lit/lit/DiffUpdater.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import shutil
2+
import os
3+
import shlex
4+
import pathlib
5+
6+
"""
7+
This file provides the `diff_test_updater` function, which is invoked on failed RUN lines when lit is executed with --update-tests.
8+
It checks whether the failed command is `diff` and, if so, uses heuristics to determine which file is the checked-in reference file and which file is output from the test case.
9+
The heuristics are currently as follows:
10+
- if exactly one file originates from the `split-file` command, that file is the reference file and the other is the output file
11+
- if exactly one file ends with ".expected" (common pattern in LLVM), that file is the reference file and the other is the output file
12+
- if exactly one file path contains ".tmp" (e.g. because it contains the expansion of "%t"), that file is the reference file and the other is the output file
13+
If the command matches one of these patterns the output file content is copied to the reference file to make the test pass.
14+
If the reference file originated in `split-file`, the output file content is instead copied to the corresponding slice of the test file.
15+
Otherwise the test is ignored.
16+
17+
Possible improvements:
18+
- Support stdin patterns like "my_binary %s | diff expected.txt"
19+
- Scan RUN lines to see if a file is the source of output from a previous command (other than `split-file`).
20+
If it is then it is not a reference file that can be copied to, regardless of name, since the test will overwrite it anyways.
21+
- Only update the parts that need updating (based on the diff output). Could help avoid noisy updates when e.g. whitespace changes are ignored.
22+
"""
23+
24+
25+
class NormalFileTarget:
26+
def __init__(self, target):
27+
self.target = target
28+
29+
def copyFrom(self, source):
30+
shutil.copy(source, self.target)
31+
32+
def __str__(self):
33+
return self.target
34+
35+
36+
class SplitFileTarget:
37+
def __init__(self, slice_start_idx, test_path, lines):
38+
self.slice_start_idx = slice_start_idx
39+
self.test_path = test_path
40+
self.lines = lines
41+
42+
def copyFrom(self, source):
43+
lines_before = self.lines[: self.slice_start_idx + 1]
44+
self.lines = self.lines[self.slice_start_idx + 1 :]
45+
slice_end_idx = None
46+
for i, l in enumerate(self.lines):
47+
if SplitFileTarget._get_split_line_path(l) != None:
48+
slice_end_idx = i
49+
break
50+
if slice_end_idx is not None:
51+
lines_after = self.lines[slice_end_idx:]
52+
else:
53+
lines_after = []
54+
with open(source, "r") as f:
55+
new_lines = lines_before + f.readlines() + lines_after
56+
with open(self.test_path, "w") as f:
57+
for l in new_lines:
58+
f.write(l)
59+
60+
def __str__(self):
61+
return f"slice in {self.test_path}"
62+
63+
@staticmethod
64+
def get_target_dir(commands, test_path):
65+
# posix=True breaks Windows paths because \ is treated as an escaping character
66+
for cmd in commands:
67+
split = shlex.split(cmd, posix=False)
68+
if "split-file" not in split:
69+
continue
70+
start_idx = split.index("split-file")
71+
split = split[start_idx:]
72+
if len(split) < 3:
73+
continue
74+
p = unquote(split[1].strip())
75+
if not test_path.samefile(p):
76+
continue
77+
return unquote(split[2].strip())
78+
return None
79+
80+
@staticmethod
81+
def create(path, commands, test_path, target_dir):
82+
path = pathlib.Path(path)
83+
with open(test_path, "r") as f:
84+
lines = f.readlines()
85+
for i, l in enumerate(lines):
86+
p = SplitFileTarget._get_split_line_path(l)
87+
if p and path.samefile(os.path.join(target_dir, p)):
88+
idx = i
89+
break
90+
else:
91+
return None
92+
return SplitFileTarget(idx, test_path, lines)
93+
94+
@staticmethod
95+
def _get_split_line_path(l):
96+
if len(l) < 6:
97+
return None
98+
if l.startswith("//"):
99+
l = l[2:]
100+
else:
101+
l = l[1:]
102+
if l.startswith("--- "):
103+
l = l[4:]
104+
else:
105+
return None
106+
return l.rstrip()
107+
108+
109+
def unquote(s):
110+
if len(s) > 1 and s[0] == s[-1] and (s[0] == '"' or s[0] == "'"):
111+
return s[1:-1]
112+
return s
113+
114+
115+
def get_source_and_target(a, b, test_path, commands):
116+
"""
117+
Try to figure out which file is the test output and which is the reference.
118+
"""
119+
split_target_dir = SplitFileTarget.get_target_dir(commands, test_path)
120+
if split_target_dir:
121+
a_target = SplitFileTarget.create(a, commands, test_path, split_target_dir)
122+
b_target = SplitFileTarget.create(b, commands, test_path, split_target_dir)
123+
if a_target and b_target:
124+
return None
125+
if a_target:
126+
return b, a_target
127+
if b_target:
128+
return a, b_target
129+
130+
expected_suffix = ".expected"
131+
if a.endswith(expected_suffix) and not b.endswith(expected_suffix):
132+
return b, NormalFileTarget(a)
133+
if b.endswith(expected_suffix) and not a.endswith(expected_suffix):
134+
return a, NormalFileTarget(b)
135+
136+
tmp_substr = ".tmp"
137+
if tmp_substr in a and not tmp_substr in b:
138+
return a, NormalFileTarget(b)
139+
if tmp_substr in b and not tmp_substr in a:
140+
return b, NormalFileTarget(a)
141+
142+
return None
143+
144+
145+
def filter_flags(args):
146+
return [arg for arg in args if not arg.startswith("-")]
147+
148+
149+
def diff_test_updater(result, test, commands):
150+
args = filter_flags(result.command.args)
151+
if len(args) != 3:
152+
return None
153+
[cmd, a, b] = args
154+
if cmd != "diff":
155+
return None
156+
res = get_source_and_target(a, b, pathlib.Path(test.getFilePath()), commands)
157+
if not res:
158+
return f"update-diff-test: could not deduce source and target from {a} and {b}"
159+
source, target = res
160+
target.copyFrom(source)
161+
return f"update-diff-test: copied {source} to {target}"

llvm/utils/lit/lit/LitConfig.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import lit.formats
99
import lit.TestingConfig
1010
import lit.util
11+
from lit.DiffUpdater import diff_test_updater
1112

1213
# LitConfig must be a new style class for properties to work
1314
class LitConfig(object):
@@ -38,6 +39,7 @@ def __init__(
3839
parallelism_groups={},
3940
per_test_coverage=False,
4041
gtest_sharding=True,
42+
update_tests=False,
4143
):
4244
# The name of the test runner.
4345
self.progname = progname
@@ -89,6 +91,8 @@ def __init__(
8991
self.parallelism_groups = parallelism_groups
9092
self.per_test_coverage = per_test_coverage
9193
self.gtest_sharding = bool(gtest_sharding)
94+
self.update_tests = update_tests
95+
self.test_updaters = [diff_test_updater]
9296

9397
@property
9498
def maxIndividualTestTime(self):

llvm/utils/lit/lit/TestRunner.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import tempfile
1414
import threading
1515
import typing
16+
import traceback
1617
from typing import Optional, Tuple
1718

1819
import io
@@ -47,6 +48,16 @@ def __init__(self, message):
4748
super().__init__(message)
4849

4950

51+
class TestUpdaterException(Exception):
52+
"""
53+
There was an error not during test execution, but while invoking a function
54+
in test_updaters on a failing RUN line.
55+
"""
56+
57+
def __init__(self, message):
58+
super().__init__(message)
59+
60+
5061
kIsWindows = platform.system() == "Windows"
5162

5263
# Don't use close_fds on Windows.
@@ -1154,6 +1165,28 @@ def executeScriptInternal(
11541165
str(result.timeoutReached),
11551166
)
11561167

1168+
if (
1169+
litConfig.update_tests
1170+
and result.exitCode != 0
1171+
and not timeoutInfo
1172+
# In theory tests marked XFAIL can fail in the form of XPASS, but the
1173+
# test updaters are not expected to be able to fix that, so always skip for XFAIL
1174+
and not test.isExpectedToFail()
1175+
):
1176+
for test_updater in litConfig.test_updaters:
1177+
try:
1178+
update_output = test_updater(result, test, commands)
1179+
except Exception as e:
1180+
output = out
1181+
output += err
1182+
output += "Exception occurred in test updater:\n"
1183+
output += traceback.format_exc()
1184+
raise TestUpdaterException(output)
1185+
if update_output:
1186+
for line in update_output.splitlines():
1187+
out += f"# {line}\n"
1188+
break
1189+
11571190
return out, err, exitCode, timeoutInfo
11581191

11591192

llvm/utils/lit/lit/cl_arguments.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ def parse_args():
204204
action="store_true",
205205
help="Exit with status zero even if some tests fail",
206206
)
207+
execution_group.add_argument(
208+
"--update-tests",
209+
dest="update_tests",
210+
action="store_true",
211+
help="Try to update regression tests to reflect current behavior, if possible",
212+
)
207213
execution_test_time_group = execution_group.add_mutually_exclusive_group()
208214
execution_test_time_group.add_argument(
209215
"--skip-test-time-recording",

llvm/utils/lit/lit/llvm/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,17 @@ def __init__(self, lit_config, config):
6464
self.with_environment("_TAG_REDIR_ERR", "TXT")
6565
self.with_environment("_CEE_RUNOPTS", "FILETAG(AUTOCVT,AUTOTAG) POSIX(ON)")
6666

67+
if lit_config.update_tests:
68+
self.use_lit_shell = True
69+
6770
# Choose between lit's internal shell pipeline runner and a real shell.
6871
# If LIT_USE_INTERNAL_SHELL is in the environment, we use that as an
6972
# override.
7073
lit_shell_env = os.environ.get("LIT_USE_INTERNAL_SHELL")
7174
if lit_shell_env:
7275
self.use_lit_shell = lit.util.pythonize_bool(lit_shell_env)
76+
if not self.use_lit_shell and lit_config.update_tests:
77+
print("note: --update-tests is not supported when using external shell")
7378

7479
if not self.use_lit_shell:
7580
features.add("shell")

llvm/utils/lit/lit/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def main(builtin_params={}):
4242
config_prefix=opts.configPrefix,
4343
per_test_coverage=opts.per_test_coverage,
4444
gtest_sharding=opts.gtest_sharding,
45+
update_tests=opts.update_tests,
4546
)
4647

4748
discovered_tests = lit.discovery.find_tests_for_inputs(

llvm/utils/lit/lit/worker.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import lit.Test
1515
import lit.util
16+
from lit.TestRunner import TestUpdaterException
1617

1718

1819
_lit_config = None
@@ -75,6 +76,10 @@ def _execute_test_handle_errors(test, lit_config):
7576
try:
7677
result = test.config.test_format.execute(test, lit_config)
7778
return _adapt_result(result)
79+
except TestUpdaterException as e:
80+
if lit_config.debug:
81+
raise
82+
return lit.Test.Result(lit.Test.UNRESOLVED, str(e))
7883
except:
7984
if lit_config.debug:
8085
raise

0 commit comments

Comments
 (0)