mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 22:02:23 +01:00
This switches to generating per-file targets such as `//executable_semantics/testdata:tuple/equality_false.carbon.test`, instead of one test covering all files. Essentially this relies on bazel instead of lit to handle test parallelization.
150 lines
5.0 KiB
Python
Executable File
150 lines
5.0 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 in {"lit.cfg.py", "BUILD"}:
|
|
# 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()
|