| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- #!/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 re
- import subprocess
- import sys
- from abc import ABC, abstractmethod
- from typing import Any, Dict, List, Optional, Set
- _BIN = "./bazel-bin/explorer/explorer"
- _TESTDATA = "explorer/testdata"
- # A prefix followed by a command to run for autoupdating checked output.
- _AUTOUPDATE_MARKER = "// AUTOUPDATE: "
- # Indicates no autoupdate is requested.
- _NOAUTOUPDATE_MARKER = "// NOAUTOUPDATE"
- # A regexp matching lines that contain line number references.
- _LINE_NUMBER_RE = r"((?:COMPILATION|RUNTIME) ERROR: [^:]*:)([1-9][0-9]*)(:.*)"
- 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
- class Line(ABC):
- """A line that may appear in the resulting test file."""
- @abstractmethod
- def format(
- self, *, output_line_number: int, line_number_remap: Dict[int, int]
- ) -> str:
- raise NotImplementedError
- class OriginalLine(Line):
- """A line that was copied from the original test file."""
- def __init__(self, line_number: int, text: str) -> None:
- self.line_number = line_number
- self.text = text
- def format(self, **kwargs: Any) -> str:
- return self.text
- class CheckLine(Line):
- """A `// CHECK:` line generated from the test output."""
- def __init__(self) -> None:
- self.indent = ""
- @staticmethod
- def escape(s: str) -> str:
- """Escape any FileCheck special characters in `s`."""
- return s.replace("{{", "{{[{][{]}}").replace("[[", "{{[[][[]}}")
- def print_before_line(self, line: int) -> bool:
- """Determine if we'd prefer to print this CHECK before line `line`."""
- return True
- class SimpleCheckLine(CheckLine):
- """A `// CHECK:` line that checks for an exact string."""
- def __init__(self, expected: str) -> None:
- super().__init__()
- self.expected = expected
- def format(self, **kwargs: Any) -> str:
- if self.expected:
- return f"{self.indent}// CHECK: {self.expected}\n"
- else:
- return f"{self.indent}// CHECK-EMPTY:\n"
- class CheckLineWithLineNumber(CheckLine):
- """A `// CHECK:` line where the expected output includes a line number.
- Such result lines need to be fixed up after we've figured out which lines
- to include in the resulting test file and in what order, because their
- contents depend on where an original input line appears in the output.
- """
- def __init__(self, before: str, line_number: int, after: str) -> None:
- super().__init__()
- self.before = before
- self.line_number = line_number
- self.after = after
- def format(
- self, *, output_line_number: int, line_number_remap: Dict[int, int]
- ) -> str:
- delta = line_number_remap[self.line_number] - output_line_number
- # We use `:+d` here to produce `LINE-n` or `LINE+n` as appropriate.
- return (
- f"{self.indent}// CHECK: {self.before}[[@LINE{delta:+d}]]"
- + f"{self.after}\n"
- )
- def print_before_line(self, line: int) -> bool:
- return line >= self.line_number
- def _make_check_line(out_line: str) -> CheckLine:
- """Given a line of output, determine what CHECK line to produce."""
- out_line = out_line.rstrip()
- match = re.match(_LINE_NUMBER_RE, out_line)
- if match:
- # Convert from 1-based line numbers to 0-based indexes.
- diagnostic_line_number = int(match[2]) - 1
- return CheckLineWithLineNumber(
- match[1], diagnostic_line_number, match[3]
- )
- else:
- return SimpleCheckLine(out_line)
- def _should_produce_check_line(
- check_line: CheckLine,
- orig_line: Optional[OriginalLine],
- autoupdate_index: int,
- ) -> bool:
- """Determine whether it's time to produce a given CHECK line."""
- if not orig_line:
- # If there's no original line, we have no choice.
- return True
- if orig_line.line_number <= autoupdate_index:
- # Don't put any CHECK lines before the AUTOUPDATE line.
- return False
- return check_line.print_before_line(orig_line.line_number)
- def _update_check_once(test: str) -> bool:
- """Updates the CHECK: lines for `test` by running explorer.
- Returns True if the number of lines changes.
- """
- with open(test) as f:
- orig_lines = f.readlines()
- # Remove old OUT.
- autoupdate_index = None
- noautoupdate_index = None
- for line_index, line in enumerate(orig_lines):
- 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("%{explorer}", _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 = CheckLine.escape(out).replace(test, "{{.*}}/%s" % test)
- out_lines = out.splitlines()
- orig_line_iter = iter(
- OriginalLine(i, line) for i, line in enumerate(orig_lines)
- )
- check_line_iter = iter(_make_check_line(out_line) for out_line in out_lines)
- next_orig_line: Optional[OriginalLine] = next(orig_line_iter, None)
- next_check_line: Optional[CheckLine] = next(check_line_iter, None)
- # Interleave the original lines and the CHECK: lines into a list of
- # `result_lines`.
- result_lines: List[Line] = []
- # Mapping from `orig_lines` indexes to `result_lines` indexes.
- line_number_remap: Dict[int, int] = {}
- while next_orig_line or next_check_line:
- if next_check_line and _should_produce_check_line(
- next_check_line, next_orig_line, autoupdate_index
- ):
- # Indent the CHECK: line to match the next original line.
- if next_orig_line:
- match = re.match(" *", next_orig_line.text)
- if match:
- next_check_line.indent = match[0]
- result_lines.append(next_check_line)
- next_check_line = next(check_line_iter, None)
- else:
- assert next_orig_line, "no lines left"
- # Include this original line if it isn't a CHECK: line.
- if not re.match(" *// CHECK", next_orig_line.text):
- line_number_remap[next_orig_line.line_number] = len(
- result_lines
- )
- result_lines.append(next_orig_line)
- next_orig_line = next(orig_line_iter, None)
- # Generate contents for any lines that depend on line numbers.
- formatted_result_lines = [
- line.format(output_line_number=i, line_number_remap=line_number_remap)
- for i, line in enumerate(result_lines)
- ]
- # If nothing's changed, we're done.
- if formatted_result_lines == orig_lines:
- return False
- # Interleave the new CHECK: lines with the tested content.
- with open(test, "w") as f:
- f.writelines(formatted_result_lines)
- return True
- def _update_check(test: str) -> None:
- """Wraps CHECK: updates for test files."""
- # 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)
- and _update_check_once(test)
- and _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 explorer...")
- subprocess.check_call(["bazel", "build", "//explorer"])
- 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()
|