run-ci.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. #!/usr/bin/env python
  2. import subprocess
  3. import os
  4. import sys
  5. from benchmark import framework_test
  6. from benchmark.utils import gather_tests
  7. import glob
  8. import json
  9. import traceback
  10. import re
  11. import logging
  12. log = logging.getLogger('run-ci')
  13. import time
  14. import threading
  15. # Needed for various imports
  16. sys.path.append('.')
  17. sys.path.append('toolset/setup/linux')
  18. sys.path.append('toolset/benchmark')
  19. class CIRunnner:
  20. '''
  21. Manages running TFB on the Travis Continuous Integration system.
  22. Makes a best effort to avoid wasting time and resources by running
  23. useless jobs.
  24. Only verifies the first test in each directory
  25. '''
  26. def __init__(self, mode, test_directory):
  27. '''
  28. mode = [prereq|install|verify] for what we want TFB to do
  29. dir = directory we are running
  30. '''
  31. logging.basicConfig(level=logging.INFO)
  32. if not test_directory == 'jobcleaner':
  33. tests = gather_tests()
  34. # Run the first linux-only test in this directory
  35. # At the moment, travis only supports mysql or none!
  36. dirtests = [t for t in tests if t.directory == test_directory]
  37. osvalidtests = [t for t in dirtests if t.os.lower() == "linux"
  38. and (t.database_os.lower() == "linux" or t.database_os.lower() == "none")]
  39. validtests = [t for t in osvalidtests if t.database.lower() == "mysql"
  40. or t.database.lower() == "postgres"
  41. or t.database.lower() == "none"]
  42. log.info("Found %s tests (%s for linux, %s for linux and mysql) in directory '%s'",
  43. len(dirtests), len(osvalidtests), len(validtests), test_directory)
  44. if len(validtests) == 0:
  45. log.critical("Found no test that is possible to run in Travis-CI! Aborting!")
  46. if len(osvalidtests) != 0:
  47. log.critical("Note: Found tests that could run in Travis-CI if more databases were supported")
  48. sys.exit(1)
  49. # Prefer database tests over 'none' if we have both
  50. preferred = [t for t in validtests if t.database.lower() != "none"]
  51. if len(preferred) > 0:
  52. self.test = preferred[0]
  53. else:
  54. self.test = validtests[0]
  55. self.name = self.test.name
  56. log.info("Choosing to run test %s in %s", self.name, test_directory)
  57. self.mode = mode
  58. self.travis = Travis()
  59. try:
  60. # See http://git.io/hs_qRQ
  61. # TRAVIS_COMMIT_RANGE is empty for pull requests
  62. if self.travis.is_pull_req:
  63. self.commit_range = "%s..FETCH_HEAD" % os.environ['TRAVIS_BRANCH'].rstrip('\n')
  64. else:
  65. self.commit_range = os.environ['TRAVIS_COMMIT_RANGE']
  66. except KeyError:
  67. log.warning("Run-ci.py should only be used for automated integration tests")
  68. last_commit = subprocess.check_output("git rev-parse HEAD^", shell=True).rstrip('\n')
  69. self.commit_range = "%s...master" % last_commit
  70. log.info("Using commit range %s", self.commit_range)
  71. log.info("Running `git diff --name-only %s`" % self.commit_range)
  72. changes = subprocess.check_output("git diff --name-only %s" % self.commit_range, shell=True)
  73. log.info(changes)
  74. def _should_run(self):
  75. '''
  76. Decides if the current framework test should be tested or if we can cancel it.
  77. Examines git commits included in the latest push to see if any files relevant to
  78. this framework were changed.
  79. This is a rather primitive strategy for things like pull requests, where
  80. we probably want to examine the entire branch of commits. Also, this cannot handle
  81. history re-writing very well, so avoid rebasing onto any published history
  82. '''
  83. # Look for changes to core TFB framework code
  84. find_tool_changes = "git diff --name-only %s | grep '^toolset/' | wc -l" % self.commit_range
  85. changes = subprocess.check_output(find_tool_changes, shell=True)
  86. if int(changes) != 0:
  87. log.info("Found changes to core framework code")
  88. return True
  89. # Look for changes relevant to this test
  90. find_test_changes = "git diff --name-only %s | grep '^%s/' | wc -l" % (self.commit_range, self.test.directory)
  91. changes = subprocess.check_output(find_test_changes, shell=True)
  92. if int(changes) == 0:
  93. log.info("No changes found for %s", self.name)
  94. return False
  95. log.info("Changes found for %s", self.name)
  96. return True
  97. def run(self):
  98. ''' Do the requested command using TFB '''
  99. if not self._should_run():
  100. log.info("Not running %s", self.name)
  101. # Cancel ourselves
  102. self.travis.cancel(self.travis.jobid)
  103. return 0
  104. log.info("Running %s for %s", self.mode, self.name)
  105. command = 'toolset/run-tests.py '
  106. if mode == 'prereq':
  107. command = command + "--install server --install-only --test ''"
  108. elif mode == 'install':
  109. command = command + "--install server --install-only --test %s" % self.name
  110. elif mode == 'verify':
  111. command = command + "--mode verify --test %s" % self.name
  112. else:
  113. log.critical('Unknown mode passed')
  114. return 1
  115. # Run the command
  116. log.info("Running %s", command)
  117. try:
  118. p = subprocess.Popen(command, shell=True)
  119. p.wait()
  120. return p.returncode
  121. except subprocess.CalledProcessError:
  122. log.critical("Subprocess Error")
  123. print traceback.format_exc()
  124. return 1
  125. except Exception as err:
  126. log.critical("Subprocess Error")
  127. log.error(err.child_traceback)
  128. return 1
  129. def cancel_unneeded_jobs(self):
  130. log.info("I am jobcleaner")
  131. # Look for changes to core TFB framework code
  132. find_tool_changes = "git diff --name-only %s | grep toolset | wc -l" % self.commit_range
  133. changes = subprocess.check_output(find_tool_changes, shell=True)
  134. if int(changes) != 0:
  135. log.info("Found changes to core framework code. Running all tests")
  136. self.travis.cancel(self.travis.jobid) # Cancel ourselves
  137. return 0
  138. build = self.travis.build_details()
  139. log.info("Build details:\n%s", build)
  140. def parse_job_id(directory):
  141. for line in build.split('\n'):
  142. if "TESTDIR=%s" % directory in line:
  143. job = re.findall("\d+.\d+", line)[0]
  144. return job
  145. # Build a list of modified directories
  146. changes = subprocess.check_output("git diff --name-only %s" % self.commit_range, shell=True)
  147. dirchanges = []
  148. for line in changes.split('\n'):
  149. dirchanges.append(line[0:line.find('/')])
  150. # For each test, launch a Thread to cancel it's job if
  151. # it's directory has not been modified
  152. cancelled_testdirs = []
  153. threads = []
  154. for test in self.gather_tests():
  155. if test.directory not in dirchanges:
  156. job = parse_job_id(test.directory)
  157. log.info("No changes found for %s (job=%s) (dir=%s)", test.name, job, test.directory)
  158. if job and test.directory not in cancelled_testdirs:
  159. cancelled_testdirs.append(test.directory)
  160. t = threading.Thread(target=self.travis.cancel, args=(job,),
  161. name="%s (%s)" % (job, test.name))
  162. t.start()
  163. threads.append(t)
  164. # Wait for all threads
  165. for t in threads:
  166. t.join()
  167. # Cancel ourselves
  168. self.travis.cancel(self.travis.jobid)
  169. class Travis():
  170. '''Integrates the travis-ci build environment and the travis command line'''
  171. def __init__(self):
  172. self.jobid = os.environ['TRAVIS_JOB_NUMBER']
  173. self.buildid = os.environ['TRAVIS_BUILD_NUMBER']
  174. self.is_pull_req = "false" != os.environ['TRAVIS_PULL_REQUEST']
  175. # If this is a PR, we cannot access the secure variable
  176. # GH_TOKEN, and instead must return success for all jobs
  177. if not self.is_pull_req:
  178. self.token = os.environ['GH_TOKEN']
  179. self._login()
  180. else:
  181. log.info("Pull Request Detected. Non-necessary jobs will return pass instead of being canceled")
  182. def _login(self):
  183. subprocess.check_call("travis login --skip-version-check --no-interactive --github-token %s" % self.token, shell=True)
  184. log.info("Logged into travis") # NEVER PRINT OUTPUT, GH_TOKEN MIGHT BE REVEALED
  185. def cancel(self, job):
  186. # If this is a pull request, we cannot interact with the CLI
  187. if self.is_pull_req:
  188. log.info("Thread %s: Return pass for job %s", threading.current_thread().name, job)
  189. return
  190. # Ignore errors in case job is already cancelled
  191. try:
  192. subprocess.check_call("travis cancel %s --skip-version-check --no-interactive" % job, shell=True)
  193. log.info("Thread %s: Canceled job %s", threading.current_thread().name, job)
  194. except subprocess.CalledProcessError:
  195. log.exception("Error halting job %s. Report:", job)
  196. subprocess.call("travis report --skip-version-check --no-interactive --org", shell=True)
  197. log.error("Trying to halt %s one more time", job)
  198. subprocess.call("travis cancel %s --skip-version-check --no-interactive" % job, shell=True)
  199. def build_details(self):
  200. # If this is a pull request, we cannot interact with the CLI
  201. if self.is_pull_req:
  202. return "No details available"
  203. build = subprocess.check_output("travis show %s --skip-version-check" % self.buildid, shell=True)
  204. return build
  205. if __name__ == "__main__":
  206. args = sys.argv[1:]
  207. if len(args) != 2 or not (args[0] == "prereq" or args[0] == "install" or args[0] == "verify"):
  208. print '''Usage: toolset/run-ci.py [prereq|install|verify] framework-directory
  209. run-ci.py selects one test from <framework-directory>/benchark_config, and
  210. automates a number of calls into run-tests.py specific to the selected test.
  211. It is guaranteed to always select the same test from the benchark_config, so
  212. multiple runs with the same <framework-directory> reference the same test.
  213. The name of the selected test will be printed to standard output.
  214. prereq - trigger standard prerequisite installation
  215. install - trigger server installation for the selected test_directory
  216. verify - run a verification on the selected test using `--mode verify`
  217. run-ci.py expects to be run inside the Travis-CI build environment, and
  218. will expect environment variables such as $TRAVIS_BUILD'''
  219. sys.exit(1)
  220. mode = args[0]
  221. testdir = args[1]
  222. runner = CIRunnner(mode, testdir)
  223. # Watch for the special test name indicating we are
  224. # the test in charge of cancelling unnecessary jobs
  225. if testdir == "jobcleaner":
  226. try:
  227. log.info("Sleeping to ensure Travis-CI has queued all jobs")
  228. time.sleep(20)
  229. runner.cancel_unneeded_jobs()
  230. except KeyError as ke:
  231. log.warning("Environment key missing, are you running inside Travis-CI?")
  232. except:
  233. log.critical("Unknown error")
  234. print traceback.format_exc()
  235. finally:
  236. sys.exit(0)
  237. retcode = 0
  238. try:
  239. retcode = runner.run()
  240. except KeyError as ke:
  241. log.warning("Environment key missing, are you running inside Travis-CI?")
  242. except:
  243. log.critical("Unknown error")
  244. print traceback.format_exc()
  245. finally:
  246. # Only print logs if we ran a verify
  247. if mode != "verify":
  248. sys.exit(retcode)
  249. log.error("Running inside travis, so I will print err and out to console")
  250. log.error("Here is ERR:")
  251. try:
  252. with open("results/ec2/latest/logs/%s/err.txt" % runner.test.name, 'r') as err:
  253. for line in err:
  254. log.info(line)
  255. except IOError:
  256. if mode == "test":
  257. log.error("No ERR file found")
  258. log.error("Here is OUT:")
  259. try:
  260. with open("results/ec2/latest/logs/%s/out.txt" % runner.test.name, 'r') as out:
  261. for line in out:
  262. log.info(line)
  263. except IOError:
  264. if mode == "test":
  265. log.error("No OUT file found")
  266. sys.exit(retcode)