tests.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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. _TESTDATA = "./testdata"
  15. _TEST_LIST_BZL = "./test_list.bzl"
  16. _TEST_LIST_HEADER = """
  17. # Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  18. # Exceptions. See /LICENSE for license information.
  19. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  20. \"""Auto-generated list of tests. Run `./tests.py --update_list` to update.\"""
  21. TEST_LIST = [
  22. """
  23. _TEST_LIST_FOOTER = """
  24. ]
  25. """
  26. def _parse_args(args=None):
  27. """Parses command-line arguments and flags."""
  28. parser = argparse.ArgumentParser(description=__doc__)
  29. group = parser.add_mutually_exclusive_group(required=True)
  30. group.add_argument(
  31. "--update_all",
  32. action="store_true",
  33. help="Runs all updates.",
  34. )
  35. group.add_argument(
  36. "--update_goldens",
  37. action="store_true",
  38. help="Updates golden files by running executable_semantics.",
  39. )
  40. group.add_argument(
  41. "--update_list", action="store_true", help="Updates test_list.bzl."
  42. )
  43. parsed_args = parser.parse_args(args=args)
  44. return parsed_args
  45. def _update_list():
  46. """Updates test_list.bzl."""
  47. # Get the list of tests and goldens from the filesystem.
  48. tests = set()
  49. goldens = set()
  50. for f in os.listdir(_TESTDATA):
  51. basename, ext = os.path.splitext(f)
  52. if ext == ".carbon":
  53. tests.add(basename)
  54. elif ext == ".golden":
  55. goldens.add(basename)
  56. else:
  57. sys.exit("Unrecognized file type in testdata: %s" % f)
  58. # Update test_list.bzl if needed, creating any missing golden files too.
  59. test_list = _TEST_LIST_HEADER.lstrip("\n")
  60. for test in sorted(tests):
  61. test_list += ' "%s",\n' % test
  62. if test not in goldens:
  63. print("Creating empty golden '%s.golden' for test." % test)
  64. open(os.path.join(_TESTDATA, "%s.golden" % test), "w").close()
  65. test_list += _TEST_LIST_FOOTER.lstrip("\n")
  66. bzl_content = open(_TEST_LIST_BZL).read()
  67. if bzl_content != test_list:
  68. print("Updating test_list.bzl")
  69. with open(_TEST_LIST_BZL, "w") as bzl:
  70. bzl.write(test_list)
  71. else:
  72. print("test_list.bzl is up-to-date")
  73. # Garbage collect unnecessary golden files.
  74. for golden in sorted(goldens):
  75. if golden not in tests:
  76. print(
  77. "Removing golden '%s.golden' because it has no test." % golden
  78. )
  79. os.unlink(os.path.join(_TESTDATA, golden))
  80. def _update_golden(test):
  81. """Updates the golden file for `test` by running executable_semantics."""
  82. # TODO(#580): Remove this when leaks are fixed.
  83. env = os.environ.copy()
  84. env["ASAN_OPTIONS"] = "detect_leaks=0"
  85. # Invoke the test update directly in order to allow parallel execution
  86. # (`bazel run` will serialize).
  87. p = subprocess.run(
  88. [
  89. "../bazel-bin/executable_semantics/%s_test" % test,
  90. "./testdata/%s.golden" % test,
  91. "../bazel-bin/executable_semantics/executable_semantics "
  92. + "./testdata/%s.carbon" % test,
  93. "--update",
  94. ],
  95. env=env,
  96. stdout=subprocess.PIPE,
  97. stderr=subprocess.STDOUT,
  98. )
  99. if p.returncode != 0:
  100. out = p.stdout.decode("utf-8")
  101. print(out, file=sys.stderr, end="")
  102. sys.exit("ERROR: Updating test '%s' failed" % test)
  103. print(".", end="", flush=True)
  104. def _update_goldens():
  105. """Runs bazel to update golden files."""
  106. # Load tests from the bzl file. This isn't done through os.listdir because
  107. # building new tests requires --update_list.
  108. bzl_content = open(_TEST_LIST_BZL).read()
  109. tests = re.findall(r'"(\w+)",', bzl_content)
  110. # Build all tests at once in order to allow parallel updates.
  111. print("Building tests...")
  112. subprocess.check_call(
  113. ["bazel", "build", "//executable_semantics:golden_tests"]
  114. )
  115. print("Updating %d goldens..." % len(tests))
  116. with futures.ThreadPoolExecutor() as exec:
  117. results = [exec.submit(lambda: _update_golden(test)) for test in tests]
  118. # Propagate exceptions.
  119. for result in results:
  120. result.result()
  121. # Each golden indicates progress with a dot without a newline, so put a
  122. # newline to wrap.
  123. print("\nUpdated goldens.")
  124. def main():
  125. parsed_args = _parse_args()
  126. if parsed_args.update_all or parsed_args.update_list:
  127. _update_list()
  128. if parsed_args.update_all or parsed_args.update_goldens:
  129. _update_goldens()
  130. if __name__ == "__main__":
  131. main()