RunFunctionalTests.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2012 Bernard Blackham <bernard@largestprime.net>
  4. # Copyright © 2013-2018 Stefano Maggiolo <s.maggiolo@gmail.com>
  5. # Copyright © 2014 Luca Versari <veluca93@gmail.com>
  6. # Copyright © 2016 Luca Wehrstedt <luca.wehrstedt@gmail.com>
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Affero General Public License as
  10. # published by the Free Software Foundation, either version 3 of the
  11. # License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Affero General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Affero General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. import argparse
  21. import logging
  22. import os
  23. import re
  24. import sys
  25. from cms import utf8_decoder
  26. from cmstestsuite import CONFIG
  27. from cmstestsuite.Tests import ALL_TESTS
  28. from cmstestsuite.coverage import clear_coverage, combine_coverage, \
  29. send_coverage_to_codecov
  30. from cmstestsuite.profiling import \
  31. PROFILER_KERNPROF, PROFILER_NONE, PROFILER_YAPPI
  32. from cmstestsuite.testrunner import TestRunner
  33. logger = logging.getLogger(__name__)
  34. FAILED_TEST_FILENAME = '.testfailures'
  35. def load_test_list_from_file(filename):
  36. """Load a list of tests to execute from the given file. Each line of the
  37. file should be of the format:
  38. testname language1
  39. """
  40. if not os.path.exists(filename):
  41. return []
  42. try:
  43. with open(filename, "rt", encoding="utf-8") as f:
  44. lines = f.readlines()
  45. except OSError as e:
  46. print("Failed to read test list. %s." % (e))
  47. return None
  48. errors = False
  49. name_to_test_map = {}
  50. for test in ALL_TESTS:
  51. if test.name in name_to_test_map:
  52. print("ERROR: Multiple tests with the same name `%s'." % test.name)
  53. errors = True
  54. name_to_test_map[test.name] = test
  55. tests = []
  56. for i, line in enumerate(lines):
  57. bits = [x.strip() for x in line.split(" ", 1)]
  58. if len(bits) != 2:
  59. print("ERROR: %s:%d invalid line: %s" % (filename, i + 1, line))
  60. errors = True
  61. continue
  62. name, lang = bits
  63. if lang == "None":
  64. lang = None
  65. if name not in name_to_test_map:
  66. print("ERROR: %s:%d invalid test case: %s" %
  67. (filename, i + 1, name))
  68. errors = True
  69. continue
  70. test = name_to_test_map[name]
  71. if lang not in test.languages:
  72. print("ERROR: %s:%d test `%s' does not have language `%s'" %
  73. (filename, i + 1, name, lang))
  74. errors = True
  75. continue
  76. tests.append((test, lang))
  77. if errors:
  78. sys.exit(1)
  79. return tests
  80. def load_failed_tests():
  81. test_lang_list = load_test_list_from_file(FAILED_TEST_FILENAME)
  82. if test_lang_list is None:
  83. sys.exit(1)
  84. tests = set(test_lang[0] for test_lang in test_lang_list)
  85. test_list = []
  86. for new_test in tests:
  87. langs = new_test.languages
  88. new_test.languages = []
  89. for test, lang in test_lang_list:
  90. if new_test == test and lang in langs:
  91. new_test.languages.append(lang)
  92. test_list.append(new_test)
  93. return test_list
  94. def filter_tests(orig_test_list, regexes, languages):
  95. """Filter out skipped test cases from a list."""
  96. # Select only those (test, language) pairs that pass our checks.
  97. new_test_list = []
  98. for test in orig_test_list:
  99. # No regexes means no constraint on test names.
  100. ok = not regexes
  101. for regex in regexes:
  102. if regex.search(test.name):
  103. ok = True
  104. break
  105. if ok:
  106. remaining_languages = test.languages
  107. if languages:
  108. remaining_languages = tuple(lang
  109. for lang in test.languages
  110. if lang in languages)
  111. if remaining_languages != ():
  112. test.languages = remaining_languages
  113. new_test_list.append(test)
  114. return new_test_list
  115. def write_test_case_list(test_list, filename):
  116. with open(filename, 'wt', encoding="utf-8") as f:
  117. for test, lang in test_list:
  118. f.write('%s %s\n' % (test.name, lang))
  119. def main():
  120. parser = argparse.ArgumentParser(
  121. description="Runs the CMS functional test suite.")
  122. parser.add_argument(
  123. "regex", action="store", type=utf8_decoder, nargs='*',
  124. help="a regex to match to run a subset of tests")
  125. parser.add_argument(
  126. "-l", "--languages", action="store", type=utf8_decoder, default="",
  127. help="a comma-separated list of languages to test")
  128. parser.add_argument(
  129. "-c", "--contest", action="store", type=utf8_decoder,
  130. help="use an existing contest (and the tasks in it)")
  131. parser.add_argument(
  132. "-r", "--retry-failed", action="store_true",
  133. help="only run failed tests from the previous run (stored in %s)" %
  134. FAILED_TEST_FILENAME)
  135. parser.add_argument(
  136. "-n", "--dry-run", action="store_true",
  137. help="show what tests would be run, but do not run them")
  138. parser.add_argument(
  139. "-v", "--verbose", action="count", default=0,
  140. help="print debug information (use multiple times for more)")
  141. parser.add_argument(
  142. "--codecov", action="store_true",
  143. help="send coverage results to Codecov (requires --coverage)")
  144. g = parser.add_mutually_exclusive_group()
  145. g.add_argument(
  146. "--coverage", action="store_true",
  147. help="compute line coverage information")
  148. g.add_argument(
  149. "--profiler", choices=[PROFILER_YAPPI, PROFILER_KERNPROF],
  150. default=PROFILER_NONE, help="set profiler")
  151. args = parser.parse_args()
  152. if args.codecov and not args.coverage:
  153. parser.error("--codecov requires --coverage")
  154. CONFIG["VERBOSITY"] = args.verbose
  155. CONFIG["COVERAGE"] = args.coverage
  156. CONFIG["PROFILER"] = args.profiler
  157. # Pre-process our command-line arguments to figure out which tests to run.
  158. regexes = [re.compile(s) for s in args.regex]
  159. if args.languages:
  160. languages = frozenset(args.languages.split(','))
  161. else:
  162. languages = frozenset()
  163. if args.retry_failed:
  164. test_list = load_failed_tests()
  165. else:
  166. test_list = ALL_TESTS
  167. test_list = filter_tests(test_list, regexes, languages)
  168. if not test_list:
  169. logger.info(
  170. "There are no tests to run! (was your filter too restrictive?)")
  171. return 0
  172. tests = 0
  173. for test in test_list:
  174. for language in test.languages:
  175. if args.dry_run:
  176. logger.info("Test %s in %s.", test.name, language)
  177. tests += 1
  178. if test.user_tests:
  179. for language in test.languages:
  180. if args.dry_run:
  181. logger.info("Test %s in %s (for usertest).",
  182. test.name, language)
  183. tests += 1
  184. if args.dry_run:
  185. return 0
  186. if args.retry_failed:
  187. logger.info(
  188. "Re-running %s failed tests from last run.", len(test_list))
  189. clear_coverage()
  190. # Startup the test runner.
  191. runner = TestRunner(test_list, contest_id=args.contest, workers=4)
  192. # Submit and wait for all tests to complete.
  193. runner.submit_tests()
  194. failures = runner.wait_for_evaluation()
  195. write_test_case_list(
  196. [(test, lang) for test, lang, _ in failures],
  197. FAILED_TEST_FILENAME)
  198. # And good night!
  199. runner.shutdown()
  200. runner.log_elapsed_time()
  201. combine_coverage()
  202. if args.codecov:
  203. send_coverage_to_codecov("functionaltests")
  204. logger.info("Executed: %s", tests)
  205. logger.info("Failed: %s", len(failures))
  206. if not failures:
  207. logger.info("All tests passed!")
  208. return 0
  209. else:
  210. logger.error("Some test failed!")
  211. logger.info("Run again with --retry-failed (or -r) to retry.")
  212. logger.info("Failed tests:")
  213. for test, lang, msg in failures:
  214. logger.info("%s (%s): %s\n", test.name, lang, msg)
  215. return 1
  216. if __name__ == "__main__":
  217. sys.exit(main())