run-ci.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. #!/usr/bin/env python
  2. import subprocess
  3. import os
  4. import sys
  5. from benchmark import framework_test
  6. import glob
  7. import json
  8. import traceback
  9. import re
  10. import logging
  11. log = logging.getLogger('run-ci')
  12. import time
  13. import threading
  14. # Needed for various imports
  15. sys.path.append('.')
  16. sys.path.append('toolset/setup/linux')
  17. sys.path.append('toolset/benchmark')
  18. class CIRunnner:
  19. '''
  20. Manages running TFB on the Travis Continuous Integration system.
  21. Makes a best effort to avoid wasting time and resources by running
  22. useless jobs.
  23. Only verifies the first test in each directory
  24. '''
  25. def __init__(self, mode, test_directory):
  26. '''
  27. mode = [prereq|install|test] for what we want TFB to do
  28. dir = directory we are running
  29. '''
  30. logging.basicConfig(level=logging.INFO)
  31. try:
  32. self.commit_range = os.environ['TRAVIS_COMMIT_RANGE']
  33. except KeyError:
  34. log.warning("Run-ci.py should only be used for automated integration tests")
  35. last_commit = subprocess.check_output("git rev-parse HEAD^", shell=True).rstrip('\n')
  36. self.commit_range = "master...%s" % last_commit
  37. if not test_directory == 'jobcleaner':
  38. tests = self.gather_tests()
  39. # Only run the first test in this directory
  40. self.test = [t for t in tests if t.directory == test_directory][0]
  41. self.name = self.test.name
  42. self.mode = mode
  43. self.travis = Travis()
  44. def _should_run(self):
  45. '''
  46. Decides if the current framework test should be tested or if we can cancel it.
  47. Examines git commits included in the latest push to see if any files relevant to
  48. this framework were changed.
  49. This is a rather primitive strategy for things like pull requests, where
  50. we probably want to examine the entire branch of commits. Also, this cannot handle
  51. history re-writing very well, so avoid rebasing onto any published history
  52. '''
  53. # Look for changes to core TFB framework code
  54. find_tool_changes = "git diff --name-only %s | grep toolset | wc -l" % self.commit_range
  55. changes = subprocess.check_output(find_tool_changes, shell=True)
  56. if int(changes) != 0:
  57. log.info("Found changes to core framework code")
  58. return True
  59. # Look for changes relevant to this test
  60. find_test_changes = "git diff --name-only %s | grep %s | wc -l" % (self.commit_range, self.test.directory)
  61. changes = subprocess.check_output(find_test_changes, shell=True)
  62. if int(changes) == 0:
  63. log.info("No changes found for %s", self.name)
  64. return False
  65. log.info("Changes found for %s", self.name)
  66. return True
  67. def run(self):
  68. ''' Do the requested command using TFB '''
  69. if not self._should_run():
  70. log.info("Not running %s", self.name)
  71. # Cancel ourselves
  72. self.travis.cancel(self.travis.jobid)
  73. return 0
  74. log.info("Running %s for %s", self.mode, self.name)
  75. # Use coverage so we can send code coverate to coveralls.io
  76. command = "coverage run --source toolset,%s --parallel-mode " % self.test.directory
  77. command = command + 'toolset/run-tests.py '
  78. if mode == 'prereq':
  79. command = command + "--install server --test ''"
  80. elif mode == 'install':
  81. # Just a note that having an install-only mode would integrate nicely with
  82. # Travis-CI's line-folding
  83. log.warning('Currently there is no install-only mode available')
  84. return 1
  85. elif mode == 'test':
  86. command = command + "--install server --mode verify --test %s" % self.name
  87. else:
  88. log.critical('Unknown mode passed')
  89. return 1
  90. # Run the command
  91. log.info("Running %s", command)
  92. try:
  93. p = subprocess.Popen(command, shell=True)
  94. p.wait()
  95. return p.returncode
  96. except subprocess.CalledProcessError:
  97. log.critical("Subprocess Error")
  98. print traceback.format_exc()
  99. return 1
  100. except Exception as err:
  101. log.critical("Subprocess Error")
  102. log.error(err.child_traceback)
  103. return 1
  104. def gather_tests(self):
  105. ''' Returns all available tests as FrameworkTest list '''
  106. # Fake benchmarker fields that are used
  107. class bench_shim():
  108. def __init__(self):
  109. self.type = 'all'
  110. self.fwroot = os.getcwd()
  111. self.install_strategy='pertest'
  112. # Gather all tests
  113. tests = []
  114. for config_file_name in glob.glob('*/benchmark_config'):
  115. with open(config_file_name, 'r') as config_file:
  116. config = json.load(config_file)
  117. test = framework_test.parse_config(config, os.path.dirname(config_file_name), bench_shim())
  118. tests = tests + test
  119. tests.sort(key=lambda x: x.name)
  120. return tests
  121. def cancel_unneeded_jobs(self):
  122. log.info("I am jobcleaner")
  123. # Look for changes to core TFB framework code
  124. find_tool_changes = "git diff --name-only %s | grep toolset | wc -l" % self.commit_range
  125. changes = subprocess.check_output(find_tool_changes, shell=True)
  126. if int(changes) != 0:
  127. log.info("Found changes to core framework code. Running all tests")
  128. self.travis.cancel(self.travis.jobid) # Cancel ourselves
  129. return 0
  130. build = self.travis.build_details()
  131. log.info("Build details:\n%s", build)
  132. def parse_job_id(directory):
  133. for line in build.split('\n'):
  134. if "TESTDIR=%s" % directory in line:
  135. job = re.findall("\d+.\d+", line)[0]
  136. return job
  137. # Build a list of modified directories
  138. changes = subprocess.check_output("git diff --name-only %s" % self.commit_range, shell=True)
  139. dirchanges = []
  140. for line in changes.split('\n'):
  141. dirchanges.append(line[0:line.find('/')])
  142. # For each test, launch a Thread to cancel it's job if
  143. # it's directory has not been modified
  144. cancelled_testdirs = []
  145. threads = []
  146. for test in self.gather_tests():
  147. if test.directory not in dirchanges:
  148. job = parse_job_id(test.directory)
  149. log.info("No changes found for %s (job=%s) (dir=%s)", test.name, job, test.directory)
  150. if job and test.directory not in cancelled_testdirs:
  151. cancelled_testdirs.append(test.directory)
  152. t = threading.Thread(target=self.travis.cancel, args=(job,),
  153. name="%s (%s)" % (job, test.name))
  154. t.start()
  155. threads.append(t)
  156. # Wait for all threads
  157. for t in threads:
  158. t.join()
  159. # Cancel ourselves
  160. self.travis.cancel(self.travis.jobid)
  161. class Travis():
  162. '''Integrates the travis-ci build environment and the travis command line'''
  163. def __init__(self):
  164. self.token = os.environ['GH_TOKEN']
  165. self.jobid = os.environ['TRAVIS_JOB_NUMBER']
  166. self.buildid = os.environ['TRAVIS_BUILD_NUMBER']
  167. self._login()
  168. def _login(self):
  169. subprocess.check_call("travis login --skip-version-check --no-interactive --github-token %s" % self.token, shell=True)
  170. log.info("Logged into travis") # NEVER PRINT OUTPUT, GH_TOKEN MIGHT BE REVEALED
  171. def cancel(self, job):
  172. # Ignore errors in case job is already cancelled
  173. try:
  174. subprocess.check_call("travis cancel %s --skip-version-check --no-interactive" % job, shell=True)
  175. log.info("Thread %s: Canceled job %s", threading.current_thread().name, job)
  176. except subprocess.CalledProcessError:
  177. log.exception("Error halting job %s. Report:", job)
  178. subprocess.call("travis report --skip-version-check --no-interactive --org", shell=True)
  179. log.error("Trying to halt %s one more time", job)
  180. subprocess.call("travis cancel %s --skip-version-check --no-interactive" % job, shell=True)
  181. def build_details(self):
  182. build = subprocess.check_output("travis show %s --skip-version-check" % self.buildid, shell=True)
  183. return build
  184. if __name__ == "__main__":
  185. args = sys.argv[1:]
  186. if len(args) != 2 or not (args[0] == "prereq" or args[0] == "install" or args[0] == "test"):
  187. print "Usage: toolset/run-ci.py [prereq|install|test] test-name"
  188. sys.exit(1)
  189. mode = args[0]
  190. testdir = args[1]
  191. runner = CIRunnner(mode, testdir)
  192. # Watch for the special test name indicating we are
  193. # the test in charge of cancelling unnecessary jobs
  194. if testdir == "jobcleaner":
  195. try:
  196. log.info("Sleeping to ensure Travis-CI has queued all jobs")
  197. time.sleep(20)
  198. runner.cancel_unneeded_jobs()
  199. except KeyError as ke:
  200. log.warning("Environment key missing, are you running inside Travis-CI?")
  201. except:
  202. log.critical("Unknown error")
  203. print traceback.format_exc()
  204. finally:
  205. sys.exit(0)
  206. retcode = 0
  207. try:
  208. retcode = runner.run()
  209. except KeyError as ke:
  210. log.warning("Environment key missing, are you running inside Travis-CI?")
  211. except:
  212. log.critical("Unknown error")
  213. print traceback.format_exc()
  214. finally:
  215. log.error("Running inside travis, so I will print err and out to console")
  216. log.error("Here is ERR:")
  217. try:
  218. with open("results/ec2/latest/logs/%s/err.txt" % runner.test.name, 'r') as err:
  219. for line in err:
  220. log.info(line)
  221. except IOError:
  222. if mode == "test":
  223. log.error("No ERR file found")
  224. log.error("Here is OUT:")
  225. try:
  226. with open("results/ec2/latest/logs/%s/out.txt" % runner.test.name, 'r') as out:
  227. for line in out:
  228. log.info(line)
  229. except IOError:
  230. if mode == "test":
  231. log.error("No OUT file found")
  232. sys.exit(retcode)