tests.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #!/usr/bin/env python3
  2. """Helps manage tests."""
  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. import argparse
  9. from concurrent import futures
  10. import os
  11. import re
  12. import subprocess
  13. import sys
  14. _BINDIR = "./bazel-bin/executable_semantics"
  15. _TESTDATA = "executable_semantics/testdata"
  16. _TEST_LIST_BZL = "executable_semantics/test_list.bzl"
  17. _TEST_LIST_HEADER = """
  18. # Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  19. # Exceptions. See /LICENSE for license information.
  20. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  21. \"""Auto-generated list of tests. Run `./tests.py --update_list` to update.\"""
  22. TEST_LIST = [
  23. """
  24. _TEST_LIST_FOOTER = """
  25. ]
  26. """
  27. def _parse_args(args=None):
  28. """Parses command-line arguments and flags."""
  29. parser = argparse.ArgumentParser(description=__doc__)
  30. group = parser.add_mutually_exclusive_group(required=True)
  31. group.add_argument(
  32. "--update_all",
  33. action="store_true",
  34. help="Runs all updates.",
  35. )
  36. group.add_argument(
  37. "--update_goldens",
  38. action="store_true",
  39. help="Updates golden files by running executable_semantics.",
  40. )
  41. group.add_argument(
  42. "--update_list", action="store_true", help="Updates test_list.bzl."
  43. )
  44. parser.add_argument(
  45. "--use_git_ls_files",
  46. action="store_true",
  47. help="Uses `git ls-files` when gathering files for --update_list.",
  48. )
  49. parsed_args = parser.parse_args(args=args)
  50. if parsed_args.use_git_ls_files and not (
  51. parsed_args.update_list or parsed_args.update_all
  52. ):
  53. parser.error("--use_git_ls_files requires --update_list")
  54. return parsed_args
  55. def _update_list(use_git_state):
  56. """Updates test_list.bzl."""
  57. # Get the list of tests and goldens from the filesystem.
  58. tests = set()
  59. goldens = set()
  60. if use_git_state:
  61. ls_files = subprocess.check_output(["git", "ls-files", _TESTDATA])
  62. files = ls_files.decode("utf-8").splitlines()
  63. else:
  64. files = list(os.listdir(_TESTDATA))
  65. for path in files:
  66. f = os.path.basename(path)
  67. basename, ext = os.path.splitext(f)
  68. if ext == ".carbon":
  69. tests.add(basename)
  70. elif ext == ".golden":
  71. goldens.add(basename)
  72. else:
  73. sys.exit("Unrecognized file type in testdata: %s" % f)
  74. # Update test_list.bzl if needed, creating any missing golden files too.
  75. test_list = _TEST_LIST_HEADER.lstrip("\n")
  76. for test in sorted(tests):
  77. test_list += ' "%s",\n' % test
  78. if test not in goldens:
  79. print("Creating empty golden '%s.golden' for test." % test)
  80. open(os.path.join(_TESTDATA, "%s.golden" % test), "w").close()
  81. test_list += _TEST_LIST_FOOTER.lstrip("\n")
  82. bzl_content = open(_TEST_LIST_BZL).read()
  83. if bzl_content != test_list:
  84. print("Updating test_list.bzl")
  85. with open(_TEST_LIST_BZL, "w") as bzl:
  86. bzl.write(test_list)
  87. else:
  88. print("test_list.bzl is up-to-date")
  89. # Garbage collect unnecessary golden files.
  90. for golden in sorted(goldens):
  91. if golden not in tests:
  92. print(
  93. "Removing golden '%s.golden' because it has no test." % golden
  94. )
  95. os.unlink(os.path.join(_TESTDATA, golden))
  96. def _update_golden(test):
  97. """Updates the golden file for `test` by running executable_semantics."""
  98. # Invoke the test update directly in order to allow parallel execution
  99. # (`bazel run` will serialize).
  100. p = subprocess.run(
  101. [
  102. "%s/%s_test" % (_BINDIR, test),
  103. "%s/%s.golden" % (_TESTDATA, test),
  104. "%s/executable_semantics %s/%s.carbon" % (_BINDIR, _TESTDATA, test),
  105. "--update",
  106. ],
  107. stdout=subprocess.PIPE,
  108. stderr=subprocess.STDOUT,
  109. )
  110. if p.returncode != 0:
  111. out = p.stdout.decode("utf-8")
  112. print(out, file=sys.stderr, end="")
  113. sys.exit("ERROR: Updating test '%s' failed" % test)
  114. print(".", end="", flush=True)
  115. def _update_goldens():
  116. """Runs bazel to update golden files."""
  117. # This should typically be called through pyenv due to the shebang. However,
  118. # pyenv then modifies the PATH, which affects build caching. In order to
  119. # mimic the calling environment for bazel, this strips out PATH entries
  120. # which pyenv likely added.
  121. # TODO: remove this when/if we're able to add
  122. # `--incompatible_strict_action_env=true` to the project .bazelrc, because
  123. # that will cause Bazel to ignore PATH.
  124. env = os.environ.copy()
  125. stripped_path = []
  126. for x in env["PATH"].split(":"):
  127. if not ("/Cellar/pyenv/" in x or "/.pyenv/versions/" in x):
  128. stripped_path.append(x)
  129. env["PATH"] = ":".join(stripped_path)
  130. # Load tests from the bzl file. This isn't done through os.listdir because
  131. # building new tests requires --update_list.
  132. bzl_content = open(_TEST_LIST_BZL).read()
  133. tests = re.findall(r'"(\w+)",', bzl_content)
  134. # Build all tests at once in order to allow parallel updates.
  135. print("Building tests...")
  136. subprocess.check_call(
  137. [
  138. "bazel",
  139. "build",
  140. "//executable_semantics:golden_tests",
  141. ],
  142. env=env,
  143. )
  144. print("Updating %d goldens..." % len(tests))
  145. with futures.ThreadPoolExecutor() as exec:
  146. # list() iterates to propagate exceptions.
  147. list([exec.map(_update_golden, tests)])
  148. # Each golden indicates progress with a dot without a newline, so put a
  149. # newline to wrap.
  150. print("\nUpdated goldens.")
  151. def main():
  152. # Go to the repository root so that paths will match bazel's view.
  153. os.chdir(os.path.join(os.path.dirname(__file__), ".."))
  154. parsed_args = _parse_args()
  155. if parsed_args.update_all or parsed_args.update_list:
  156. _update_list(parsed_args.use_git_ls_files)
  157. if parsed_args.update_all or parsed_args.update_goldens:
  158. _update_goldens()
  159. if __name__ == "__main__":
  160. main()