update_checks.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. #!/usr/bin/env python3
  2. """Updates the CHECK: lines in lit tests based on the AUTOUPDATE line."""
  3. __copyright__ = """
  4. Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  5. Exceptions. See /LICENSE for license information.
  6. SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  7. """
  8. from concurrent import futures
  9. import os
  10. import subprocess
  11. import sys
  12. from typing import Set
  13. _BIN = "./bazel-bin/executable_semantics/executable_semantics"
  14. _TESTDATA = "executable_semantics/testdata"
  15. # A prefix followed by a command to run for autoupdating checked output.
  16. _AUTOUPDATE_MARKER = "// AUTOUPDATE: "
  17. # Indicates no autoupdate is requested.
  18. _NOAUTOUPDATE_MARKER = "// NOAUTOUPDATE"
  19. def _get_tests() -> Set[str]:
  20. """Get the list of tests from the filesystem."""
  21. tests = set()
  22. for root, _, files in os.walk(_TESTDATA):
  23. for f in files:
  24. if f == "lit.cfg.py":
  25. # Ignore the lit config.
  26. continue
  27. if os.path.splitext(f)[1] == ".carbon":
  28. tests.add(os.path.join(root, f))
  29. else:
  30. sys.exit("Unrecognized file type in testdata: %s" % f)
  31. return tests
  32. def _update_check_once(test: str) -> bool:
  33. """Updates the CHECK: lines for `test` by running executable_semantics.
  34. Returns True if the number of lines changes.
  35. """
  36. with open(test) as f:
  37. orig_lines = f.readlines()
  38. # Remove old OUT.
  39. lines_without_check = [
  40. x for x in orig_lines if not x.startswith("// CHECK")
  41. ]
  42. num_orig_check_lines = len(orig_lines) - len(lines_without_check)
  43. autoupdate_index = None
  44. noautoupdate_index = None
  45. for line_index, line in enumerate(lines_without_check):
  46. if line.startswith(_AUTOUPDATE_MARKER):
  47. autoupdate_index = line_index
  48. autoupdate_cmd = line[len(_AUTOUPDATE_MARKER) :]
  49. if line.startswith(_NOAUTOUPDATE_MARKER):
  50. noautoupdate_index = line_index
  51. if autoupdate_index is None:
  52. if noautoupdate_index is None:
  53. raise ValueError(
  54. "%s must have either '%s' or '%s'"
  55. % (test, _AUTOUPDATE_MARKER, _NOAUTOUPDATE_MARKER)
  56. )
  57. else:
  58. return False
  59. elif noautoupdate_index is not None:
  60. raise ValueError(
  61. "%s has both '%s' and '%s', must have only one"
  62. % (test, _AUTOUPDATE_MARKER, _NOAUTOUPDATE_MARKER)
  63. )
  64. # Mirror lit.cfg.py substitutions; bazel runs don't need --prelude.
  65. autoupdate_cmd = autoupdate_cmd.replace("%{executable_semantics}", _BIN)
  66. # Run the autoupdate command to generate output.
  67. # (`bazel run` would serialize)
  68. p = subprocess.run(
  69. autoupdate_cmd % test,
  70. shell=True,
  71. stdout=subprocess.PIPE,
  72. stderr=subprocess.STDOUT,
  73. )
  74. out = p.stdout.decode("utf-8")
  75. # `lit` uses full paths to the test file, so use a regex to ignore paths
  76. # when used.
  77. # TODO: Maybe revisit and see if lit can be convinced to give a
  78. # root-relative path.
  79. out = out.replace(test, "{{.*}}/%s" % test)
  80. out_lines = out.splitlines()
  81. # Interleave the new CHECK: lines with the tested content.
  82. with open(test, "w") as f:
  83. f.writelines(lines_without_check[: autoupdate_index + 1])
  84. for line in out_lines:
  85. line = line.rstrip()
  86. if line:
  87. f.write("// CHECK: %s\n" % line)
  88. else:
  89. f.write("// CHECK-EMPTY:\n")
  90. f.writelines(lines_without_check[autoupdate_index + 1 :])
  91. # Compares the number of CHECK: lines originally with the number added.
  92. return num_orig_check_lines != len(out_lines)
  93. def _update_check(test: str) -> None:
  94. """Wraps CHECK: updates for test files."""
  95. if _update_check_once(test):
  96. # If the number of output lines changes, run again because output can be
  97. # line-specific. However, output should stabilize quickly.
  98. if _update_check_once(test):
  99. raise ValueError("The output of %s kept changing" % test)
  100. print(".", end="", flush=True)
  101. def _update_checks() -> None:
  102. """Runs bazel to update CHECK: lines in lit tests."""
  103. # TODO: It may be helpful if a list of tests can be passed in args; would
  104. # want to use argparse for this.
  105. tests = _get_tests()
  106. # Build all tests at once in order to allow parallel updates.
  107. print("Building executable_semantics...")
  108. subprocess.check_call(["bazel", "build", "//executable_semantics"])
  109. print("Updating %d lit tests..." % len(tests))
  110. with futures.ThreadPoolExecutor() as exec:
  111. # list() iterates to propagate exceptions.
  112. list(exec.map(_update_check, tests))
  113. # Each update call indicates progress with a dot without a newline, so put a
  114. # newline to wrap.
  115. print("\nUpdated lit tests.")
  116. def main() -> None:
  117. # Go to the repository root so that paths will match bazel's view.
  118. os.chdir(os.path.join(os.path.dirname(__file__), ".."))
  119. _update_checks()
  120. if __name__ == "__main__":
  121. main()