RunUnitTests.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2013-2018 Stefano Maggiolo <s.maggiolo@gmail.com>
  4. # Copyright © 2016 Luca Wehrstedt <luca.wehrstedt@gmail.com>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as
  8. # published by the Free Software Foundation, either version 3 of the
  9. # License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. import argparse
  19. import datetime
  20. import logging
  21. import os
  22. import re
  23. import subprocess
  24. import sys
  25. from cms import utf8_decoder
  26. from cmstestsuite import CONFIG, TestException, sh
  27. from cmstestsuite.coverage import clear_coverage, combine_coverage, \
  28. coverage_cmdline, send_coverage_to_codecov
  29. from cmstestsuite.profiling import \
  30. PROFILER_KERNPROF, PROFILER_NONE, PROFILER_YAPPI, profiling_cmdline
  31. logger = logging.getLogger(__name__)
  32. FAILED_UNITTEST_FILENAME = '.unittestfailures'
  33. def run_unittests(test_list):
  34. """Run all needed unit tests.
  35. test_list ([(string, string)]): a list of test to run in the
  36. format (path, filename.py).
  37. return (int):
  38. """
  39. logger.info("Running unit tests...")
  40. failures = []
  41. num_tests_to_execute = len(test_list)
  42. # For all tests...
  43. for i, (path, filename) in enumerate(test_list):
  44. logger.info("Running test %d/%d: %s.%s",
  45. i + 1, num_tests_to_execute, path, filename)
  46. cmdline = [os.path.join(path, filename)]
  47. cmdline = coverage_cmdline(cmdline)
  48. cmdline = profiling_cmdline(
  49. cmdline, os.path.join(path, filename).replace("/", "_"))
  50. try:
  51. sh(cmdline)
  52. except TestException:
  53. logger.info(" (FAILED: %s)", filename)
  54. # Add this case to our list of failures, if we haven't already.
  55. failures.append((path, filename))
  56. results = "\n\n"
  57. if not failures:
  58. results += "================== ALL TESTS PASSED! ==================\n"
  59. else:
  60. results += "------ TESTS FAILED: ------\n"
  61. results += " Executed: %d\n" % num_tests_to_execute
  62. results += " Failed: %d\n" % len(failures)
  63. results += "\n"
  64. for path, filename in failures:
  65. results += " %s.%s\n" % (path, filename)
  66. if failures:
  67. with open(FAILED_UNITTEST_FILENAME,
  68. "wt", encoding="utf-8") as failed_filename:
  69. for path, filename in failures:
  70. failed_filename.write("%s %s\n" % (path, filename))
  71. results += "\n"
  72. results += "Failed tests stored in %s.\n" % FAILED_UNITTEST_FILENAME
  73. results += "Run again with --retry-failed (or -r) to retry.\n"
  74. return len(failures) == 0, results
  75. def load_test_list_from_file(filename):
  76. """Load path and names of unittest files from a filename.
  77. filename (string): the file to load, containing strings in the
  78. format <path> <test_filename>.
  79. return ([(string, string)]): the content of the file.
  80. """
  81. if not os.path.exists(filename):
  82. return []
  83. try:
  84. with open(filename, "rt", encoding="utf-8") as f:
  85. return [line.strip().split(" ") for line in f.readlines()]
  86. except OSError as error:
  87. print("Failed to read test list. %s." % error)
  88. return None
  89. def get_all_tests():
  90. tests = []
  91. files = sorted(os.walk(os.path.join("cmstestsuite", "unit_tests")))
  92. for path, _, names in files:
  93. for name in sorted(names):
  94. full_path = os.path.join(path, name)
  95. if name.endswith(".py") and os.access(full_path, os.X_OK):
  96. tests.append((path, name))
  97. return tests
  98. def load_failed_tests():
  99. failed_tests = load_test_list_from_file(FAILED_UNITTEST_FILENAME)
  100. if failed_tests is None:
  101. sys.exit(1)
  102. return failed_tests
  103. def main():
  104. parser = argparse.ArgumentParser(
  105. description="Runs the CMS unittest suite.")
  106. parser.add_argument(
  107. "regex", action="store", type=utf8_decoder, nargs='*',
  108. help="a regex to match to run a subset of tests")
  109. parser.add_argument(
  110. "-n", "--dry-run", action="store_true",
  111. help="show what tests would be run, but do not run them")
  112. parser.add_argument(
  113. "-v", "--verbose", action="count", default=0,
  114. help="print debug information (use multiple times for more)")
  115. parser.add_argument(
  116. "-r", "--retry-failed", action="store_true",
  117. help="only run failed tests from the previous run (stored in %s)" %
  118. FAILED_UNITTEST_FILENAME)
  119. parser.add_argument(
  120. "--codecov", action="store_true",
  121. help="send coverage results to Codecov (requires --coverage)")
  122. g = parser.add_mutually_exclusive_group()
  123. g.add_argument(
  124. "--coverage", action="store_true",
  125. help="compute line coverage information")
  126. g.add_argument(
  127. "--profiler", choices=[PROFILER_YAPPI, PROFILER_KERNPROF],
  128. default=PROFILER_NONE, help="set profiler")
  129. # Unused parameters.
  130. parser.add_argument(
  131. "-l", "--languages", action="store", type=utf8_decoder, default="",
  132. help="unused")
  133. parser.add_argument(
  134. "-c", "--contest", action="store", type=utf8_decoder,
  135. help="unused")
  136. args = parser.parse_args()
  137. if args.codecov and not args.coverage:
  138. parser.error("--codecov requires --coverage")
  139. CONFIG["VERBOSITY"] = args.verbose
  140. CONFIG["COVERAGE"] = args.coverage
  141. CONFIG["PROFILER"] = args.profiler
  142. start_time = datetime.datetime.now()
  143. try:
  144. git_root = subprocess.check_output(
  145. "git rev-parse --show-toplevel", shell=True,
  146. stderr=subprocess.DEVNULL).decode('utf8').strip()
  147. except subprocess.CalledProcessError:
  148. print("Please run the unit tests from the git repository.")
  149. return 1
  150. if args.retry_failed:
  151. test_list = load_failed_tests()
  152. else:
  153. test_list = get_all_tests()
  154. if args.regex:
  155. # Require at least one regex to match to include it in the list.
  156. filter_regexps = [re.compile(regex) for regex in args.regex]
  157. def test_match(t):
  158. return any(r.search(t) is not None for r in filter_regexps)
  159. test_list = [t for t in test_list if test_match(' '.join(t))]
  160. if args.dry_run:
  161. for t in test_list:
  162. print(t[0], t[1])
  163. return 0
  164. if args.retry_failed:
  165. logger.info("Re-running %d failed tests from last run.",
  166. len(test_list))
  167. # Load config from cms.conf.
  168. CONFIG["TEST_DIR"] = git_root
  169. CONFIG["CONFIG_PATH"] = "%s/config/cms.conf" % CONFIG["TEST_DIR"]
  170. if CONFIG["TEST_DIR"] is None:
  171. CONFIG["CONFIG_PATH"] = "/usr/local/etc/cms.conf"
  172. if CONFIG["TEST_DIR"] is not None:
  173. # Set up our expected environment.
  174. os.chdir("%(TEST_DIR)s" % CONFIG)
  175. os.environ["PYTHONPATH"] = "%(TEST_DIR)s" % CONFIG
  176. clear_coverage()
  177. # Run all of our test cases.
  178. passed, test_results = run_unittests(test_list)
  179. combine_coverage()
  180. print(test_results)
  181. end_time = datetime.datetime.now()
  182. print("Time elapsed: %s" % (end_time - start_time))
  183. if args.codecov:
  184. send_coverage_to_codecov("unittests")
  185. if passed:
  186. return 0
  187. else:
  188. return 1
  189. if __name__ == "__main__":
  190. sys.exit(main())