mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 22:02:23 +01:00
150 lines
4.9 KiB
Python
Executable File
150 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""Updates the CHECK: lines in lit tests based on the AUTOUPDATE line."""
|
|
|
|
__copyright__ = """
|
|
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
Exceptions. See /LICENSE for license information.
|
|
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
"""
|
|
|
|
from concurrent import futures
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from typing import Set
|
|
|
|
_BIN = "./bazel-bin/executable_semantics/executable_semantics"
|
|
_TESTDATA = "executable_semantics/testdata"
|
|
|
|
# A prefix followed by a command to run for autoupdating checked output.
|
|
_AUTOUPDATE_MARKER = "// AUTOUPDATE: "
|
|
|
|
# Indicates no autoupdate is requested.
|
|
_NOAUTOUPDATE_MARKER = "// NOAUTOUPDATE"
|
|
|
|
|
|
def _get_tests() -> Set[str]:
|
|
"""Get the list of tests from the filesystem."""
|
|
tests = set()
|
|
for root, _, files in os.walk(_TESTDATA):
|
|
for f in files:
|
|
if f == "lit.cfg.py":
|
|
# Ignore the lit config.
|
|
continue
|
|
if os.path.splitext(f)[1] == ".carbon":
|
|
tests.add(os.path.join(root, f))
|
|
else:
|
|
sys.exit("Unrecognized file type in testdata: %s" % f)
|
|
return tests
|
|
|
|
|
|
def _update_check_once(test: str) -> bool:
|
|
"""Updates the CHECK: lines for `test` by running executable_semantics.
|
|
|
|
Returns True if the number of lines changes.
|
|
"""
|
|
with open(test) as f:
|
|
orig_lines = f.readlines()
|
|
|
|
# Remove old OUT.
|
|
lines_without_check = [
|
|
x for x in orig_lines if not x.startswith("// CHECK")
|
|
]
|
|
num_orig_check_lines = len(orig_lines) - len(lines_without_check)
|
|
autoupdate_index = None
|
|
noautoupdate_index = None
|
|
for line_index, line in enumerate(lines_without_check):
|
|
if line.startswith(_AUTOUPDATE_MARKER):
|
|
autoupdate_index = line_index
|
|
autoupdate_cmd = line[len(_AUTOUPDATE_MARKER) :]
|
|
if line.startswith(_NOAUTOUPDATE_MARKER):
|
|
noautoupdate_index = line_index
|
|
if autoupdate_index is None:
|
|
if noautoupdate_index is None:
|
|
raise ValueError(
|
|
"%s must have either '%s' or '%s'"
|
|
% (test, _AUTOUPDATE_MARKER, _NOAUTOUPDATE_MARKER)
|
|
)
|
|
else:
|
|
return False
|
|
elif noautoupdate_index is not None:
|
|
raise ValueError(
|
|
"%s has both '%s' and '%s', must have only one"
|
|
% (test, _AUTOUPDATE_MARKER, _NOAUTOUPDATE_MARKER)
|
|
)
|
|
|
|
# Mirror lit.cfg.py substitutions; bazel runs don't need --prelude.
|
|
autoupdate_cmd = autoupdate_cmd.replace("%{executable_semantics}", _BIN)
|
|
|
|
# Run the autoupdate command to generate output.
|
|
# (`bazel run` would serialize)
|
|
p = subprocess.run(
|
|
autoupdate_cmd % test,
|
|
shell=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
out = p.stdout.decode("utf-8")
|
|
|
|
# `lit` uses full paths to the test file, so use a regex to ignore paths
|
|
# when used.
|
|
# TODO: Maybe revisit and see if lit can be convinced to give a
|
|
# root-relative path.
|
|
out = out.replace(test, "{{.*}}/%s" % test)
|
|
out_lines = out.splitlines()
|
|
|
|
# Interleave the new CHECK: lines with the tested content.
|
|
with open(test, "w") as f:
|
|
f.writelines(lines_without_check[: autoupdate_index + 1])
|
|
for line in out_lines:
|
|
line = line.rstrip()
|
|
if line:
|
|
f.write("// CHECK: %s\n" % line)
|
|
else:
|
|
f.write("// CHECK-EMPTY:\n")
|
|
f.writelines(lines_without_check[autoupdate_index + 1 :])
|
|
|
|
# Compares the number of CHECK: lines originally with the number added.
|
|
return num_orig_check_lines != len(out_lines)
|
|
|
|
|
|
def _update_check(test: str) -> None:
|
|
"""Wraps CHECK: updates for test files."""
|
|
if _update_check_once(test):
|
|
# If the number of output lines changes, run again because output can be
|
|
# line-specific. However, output should stabilize quickly.
|
|
if _update_check_once(test):
|
|
raise ValueError("The output of %s kept changing" % test)
|
|
print(".", end="", flush=True)
|
|
|
|
|
|
def _update_checks() -> None:
|
|
"""Runs bazel to update CHECK: lines in lit tests."""
|
|
# TODO: It may be helpful if a list of tests can be passed in args; would
|
|
# want to use argparse for this.
|
|
tests = _get_tests()
|
|
|
|
# Build all tests at once in order to allow parallel updates.
|
|
print("Building executable_semantics...")
|
|
subprocess.check_call(["bazel", "build", "//executable_semantics"])
|
|
|
|
print("Updating %d lit tests..." % len(tests))
|
|
with futures.ThreadPoolExecutor() as exec:
|
|
# list() iterates to propagate exceptions.
|
|
list(exec.map(_update_check, tests))
|
|
# Each update call indicates progress with a dot without a newline, so put a
|
|
# newline to wrap.
|
|
print("\nUpdated lit tests.")
|
|
|
|
|
|
def main() -> None:
|
|
# Go to the repository root so that paths will match bazel's view.
|
|
os.chdir(os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
_update_checks()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|