run-ci.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. command = 'toolset/run-tests.py '
  76. if mode == 'prereq':
  77. command = command + "--install server --test ''"
  78. elif mode == 'install':
  79. # Just a note that having an install-only mode would integrate nicely with
  80. # Travis-CI's line-folding
  81. log.warning('Currently there is no install-only mode available')
  82. return 1
  83. elif mode == 'test':
  84. command = command + "--install server --mode verify --test %s" % self.name
  85. else:
  86. log.critical('Unknown mode passed')
  87. return 1
  88. # Run the command
  89. log.info("Running %s", command)
  90. try:
  91. p = subprocess.Popen(command, shell=True)
  92. p.wait()
  93. return p.returncode
  94. except subprocess.CalledProcessError:
  95. log.critical("Subprocess Error")
  96. print traceback.format_exc()
  97. return 1
  98. except Exception as err:
  99. log.critical("Subprocess Error")
  100. log.error(err.child_traceback)
  101. return 1
  102. def gather_tests(self):
  103. ''' Returns all available tests as FrameworkTest list '''
  104. # Fake benchmarker fields that are used
  105. class bench_shim():
  106. def __init__(self):
  107. self.type = 'all'
  108. self.fwroot = os.getcwd()
  109. self.install_strategy='pertest'
  110. # Gather all tests
  111. tests = []
  112. for config_file_name in glob.glob('*/benchmark_config'):
  113. with open(config_file_name, 'r') as config_file:
  114. config = json.load(config_file)
  115. test = framework_test.parse_config(config, os.path.dirname(config_file_name), bench_shim())
  116. tests = tests + test
  117. tests.sort(key=lambda x: x.name)
  118. return tests
  119. def cancel_unneeded_jobs(self):
  120. log.info("I am jobcleaner")
  121. # Look for changes to core TFB framework code
  122. find_tool_changes = "git diff --name-only %s | grep toolset | wc -l" % self.commit_range
  123. changes = subprocess.check_output(find_tool_changes, shell=True)
  124. if int(changes) != 0:
  125. log.info("Found changes to core framework code. Running all tests")
  126. self.travis.cancel(self.travis.jobid) # Cancel ourselves
  127. return 0
  128. build = self.travis.build_details()
  129. log.info("Build details:\n%s", build)
  130. def parse_job_id(directory):
  131. for line in build.split('\n'):
  132. if "TESTDIR=%s" % directory in line:
  133. job = re.findall("\d+.\d+", line)[0]
  134. return job
  135. # Build a list of modified directories
  136. changes = subprocess.check_output("git diff --name-only %s" % self.commit_range, shell=True)
  137. dirchanges = []
  138. for line in changes.split('\n'):
  139. dirchanges.append(line[0:line.find('/')])
  140. # For each test, launch a Thread to cancel it's job if
  141. # it's directory has not been modified
  142. cancelled_testdirs = []
  143. threads = []
  144. for test in self.gather_tests():
  145. if test.directory not in dirchanges:
  146. job = parse_job_id(test.directory)
  147. log.info("No changes found for %s (job=%s) (dir=%s)", test.name, job, test.directory)
  148. if job and test.directory not in cancelled_testdirs:
  149. cancelled_testdirs.append(test.directory)
  150. t = threading.Thread(target=self.travis.cancel, args=(job,),
  151. name="%s (%s)" % (job, test.name))
  152. t.start()
  153. threads.append(t)
  154. # Wait for all threads
  155. for t in threads:
  156. t.join()
  157. # Cancel ourselves
  158. self.travis.cancel(self.travis.jobid)
  159. class Travis():
  160. '''Integrates the travis-ci build environment and the travis command line'''
  161. def __init__(self):
  162. self.jobid = os.environ['TRAVIS_JOB_NUMBER']
  163. self.buildid = os.environ['TRAVIS_BUILD_NUMBER']
  164. self.is_pull_req = "false" != os.environ['TRAVIS_PULL_REQUEST']
  165. # If this is a PR, we cannot access the secure variable
  166. # GH_TOKEN, and instead must return success for all jobs
  167. if not self.is_pull_req:
  168. self.token = os.environ['GH_TOKEN']
  169. self._login()
  170. else:
  171. log.info("Pull Request Detected. Non-necessary jobs will return pass instead of being canceled")
  172. def _login(self):
  173. subprocess.check_call("travis login --skip-version-check --no-interactive --github-token %s" % self.token, shell=True)
  174. log.info("Logged into travis") # NEVER PRINT OUTPUT, GH_TOKEN MIGHT BE REVEALED
  175. def cancel(self, job):
  176. # If this is a pull request, we cannot interact with the CLI
  177. if self.is_pull_req:
  178. log.info("Thread %s: Return pass for job %s", threading.current_thread().name, job)
  179. return
  180. # Ignore errors in case job is already cancelled
  181. try:
  182. subprocess.check_call("travis cancel %s --skip-version-check --no-interactive" % job, shell=True)
  183. log.info("Thread %s: Canceled job %s", threading.current_thread().name, job)
  184. except subprocess.CalledProcessError:
  185. log.exception("Error halting job %s. Report:", job)
  186. subprocess.call("travis report --skip-version-check --no-interactive --org", shell=True)
  187. log.error("Trying to halt %s one more time", job)
  188. subprocess.call("travis cancel %s --skip-version-check --no-interactive" % job, shell=True)
  189. def build_details(self):
  190. # If this is a pull request, we cannot interact with the CLI
  191. if self.is_pull_req:
  192. return "No details available"
  193. build = subprocess.check_output("travis show %s --skip-version-check" % self.buildid, shell=True)
  194. return build
  195. if __name__ == "__main__":
  196. args = sys.argv[1:]
  197. if len(args) != 2 or not (args[0] == "prereq" or args[0] == "install" or args[0] == "test"):
  198. print "Usage: toolset/run-ci.py [prereq|install|test] test-name"
  199. sys.exit(1)
  200. mode = args[0]
  201. testdir = args[1]
  202. runner = CIRunnner(mode, testdir)
  203. # Watch for the special test name indicating we are
  204. # the test in charge of cancelling unnecessary jobs
  205. if testdir == "jobcleaner":
  206. try:
  207. log.info("Sleeping to ensure Travis-CI has queued all jobs")
  208. time.sleep(20)
  209. runner.cancel_unneeded_jobs()
  210. except KeyError as ke:
  211. log.warning("Environment key missing, are you running inside Travis-CI?")
  212. except:
  213. log.critical("Unknown error")
  214. print traceback.format_exc()
  215. finally:
  216. sys.exit(0)
  217. retcode = 0
  218. try:
  219. retcode = runner.run()
  220. except KeyError as ke:
  221. log.warning("Environment key missing, are you running inside Travis-CI?")
  222. except:
  223. log.critical("Unknown error")
  224. print traceback.format_exc()
  225. finally:
  226. log.error("Running inside travis, so I will print err and out to console")
  227. log.error("Here is ERR:")
  228. try:
  229. with open("results/ec2/latest/logs/%s/err.txt" % runner.test.name, 'r') as err:
  230. for line in err:
  231. log.info(line)
  232. except IOError:
  233. if mode == "test":
  234. log.error("No ERR file found")
  235. log.error("Here is OUT:")
  236. try:
  237. with open("results/ec2/latest/logs/%s/out.txt" % runner.test.name, 'r') as out:
  238. for line in out:
  239. log.info(line)
  240. except IOError:
  241. if mode == "test":
  242. log.error("No OUT file found")
  243. sys.exit(retcode)