run-ci.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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. if not test_directory == 'jobcleaner':
  32. tests = self.gather_tests()
  33. # Run the first linux-only test in this directory
  34. dirtests = [t for t in tests if t.directory == test_directory]
  35. validtests = [t for t in dirtests if t.os.lower() == "linux"
  36. and t.database_os.lower() == "linux"]
  37. log.info("Found %s tests (%s valid) in directory %s",
  38. len(dirtests), len(validtests), test_directory)
  39. if len(validtests) == 0:
  40. log.critical("Found No Valid Tests, Aborting!")
  41. sys.exit(1)
  42. self.test = validtests[0]
  43. self.name = self.test.name
  44. log.info("Choosing to run test %s in %s", self.name, test_directory)
  45. self.mode = mode
  46. self.travis = Travis()
  47. try:
  48. # See http://git.io/hs_qRQ
  49. # TRAVIS_COMMIT_RANGE is empty for pull requests
  50. if self.travis.is_pull_req:
  51. self.commit_range = "%s..FETCH_HEAD" % os.environ['TRAVIS_BRANCH'].rstrip('\n')
  52. else:
  53. self.commit_range = os.environ['TRAVIS_COMMIT_RANGE']
  54. except KeyError:
  55. log.warning("Run-ci.py should only be used for automated integration tests")
  56. last_commit = subprocess.check_output("git rev-parse HEAD^", shell=True).rstrip('\n')
  57. self.commit_range = "master...%s" % last_commit
  58. log.info("Using commit range %s", self.commit_range)
  59. log.info("Running `git diff --name-only %s`" % self.commit_range)
  60. changes = subprocess.check_output("git diff --name-only %s" % self.commit_range, shell=True)
  61. log.info(changes)
  62. def _should_run(self):
  63. '''
  64. Decides if the current framework test should be tested or if we can cancel it.
  65. Examines git commits included in the latest push to see if any files relevant to
  66. this framework were changed.
  67. This is a rather primitive strategy for things like pull requests, where
  68. we probably want to examine the entire branch of commits. Also, this cannot handle
  69. history re-writing very well, so avoid rebasing onto any published history
  70. '''
  71. # Look for changes to core TFB framework code
  72. find_tool_changes = "git diff --name-only %s | grep toolset | wc -l" % self.commit_range
  73. changes = subprocess.check_output(find_tool_changes, shell=True)
  74. if int(changes) != 0:
  75. log.info("Found changes to core framework code")
  76. return True
  77. # Look for changes relevant to this test
  78. find_test_changes = "git diff --name-only %s | grep %s | wc -l" % (self.commit_range, self.test.directory)
  79. changes = subprocess.check_output(find_test_changes, shell=True)
  80. if int(changes) == 0:
  81. log.info("No changes found for %s", self.name)
  82. return False
  83. log.info("Changes found for %s", self.name)
  84. return True
  85. def run(self):
  86. ''' Do the requested command using TFB '''
  87. if not self._should_run():
  88. log.info("Not running %s", self.name)
  89. # Cancel ourselves
  90. self.travis.cancel(self.travis.jobid)
  91. return 0
  92. log.info("Running %s for %s", self.mode, self.name)
  93. command = 'toolset/run-tests.py '
  94. if mode == 'prereq':
  95. command = command + "--install server --test ''"
  96. elif mode == 'install':
  97. # Just a note that having an install-only mode would integrate nicely with
  98. # Travis-CI's line-folding
  99. log.warning('Currently there is no install-only mode available')
  100. return 1
  101. elif mode == 'test':
  102. command = command + "--install server --mode verify --test %s" % self.name
  103. else:
  104. log.critical('Unknown mode passed')
  105. return 1
  106. # Run the command
  107. log.info("Running %s", command)
  108. try:
  109. p = subprocess.Popen(command, shell=True)
  110. p.wait()
  111. return p.returncode
  112. except subprocess.CalledProcessError:
  113. log.critical("Subprocess Error")
  114. print traceback.format_exc()
  115. return 1
  116. except Exception as err:
  117. log.critical("Subprocess Error")
  118. log.error(err.child_traceback)
  119. return 1
  120. def gather_tests(self):
  121. ''' Returns all available tests as FrameworkTest list '''
  122. # Fake benchmarker fields that are used
  123. class bench_shim():
  124. def __init__(self):
  125. self.type = 'all'
  126. self.fwroot = os.getcwd()
  127. self.install_strategy='pertest'
  128. # Gather all tests
  129. tests = []
  130. for config_file_name in glob.glob('*/benchmark_config'):
  131. with open(config_file_name, 'r') as config_file:
  132. config = json.load(config_file)
  133. test = framework_test.parse_config(config, os.path.dirname(config_file_name), bench_shim())
  134. tests = tests + test
  135. tests.sort(key=lambda x: x.name)
  136. return tests
  137. def cancel_unneeded_jobs(self):
  138. log.info("I am jobcleaner")
  139. # Look for changes to core TFB framework code
  140. find_tool_changes = "git diff --name-only %s | grep toolset | wc -l" % self.commit_range
  141. changes = subprocess.check_output(find_tool_changes, shell=True)
  142. if int(changes) != 0:
  143. log.info("Found changes to core framework code. Running all tests")
  144. self.travis.cancel(self.travis.jobid) # Cancel ourselves
  145. return 0
  146. build = self.travis.build_details()
  147. log.info("Build details:\n%s", build)
  148. def parse_job_id(directory):
  149. for line in build.split('\n'):
  150. if "TESTDIR=%s" % directory in line:
  151. job = re.findall("\d+.\d+", line)[0]
  152. return job
  153. # Build a list of modified directories
  154. changes = subprocess.check_output("git diff --name-only %s" % self.commit_range, shell=True)
  155. dirchanges = []
  156. for line in changes.split('\n'):
  157. dirchanges.append(line[0:line.find('/')])
  158. # For each test, launch a Thread to cancel it's job if
  159. # it's directory has not been modified
  160. cancelled_testdirs = []
  161. threads = []
  162. for test in self.gather_tests():
  163. if test.directory not in dirchanges:
  164. job = parse_job_id(test.directory)
  165. log.info("No changes found for %s (job=%s) (dir=%s)", test.name, job, test.directory)
  166. if job and test.directory not in cancelled_testdirs:
  167. cancelled_testdirs.append(test.directory)
  168. t = threading.Thread(target=self.travis.cancel, args=(job,),
  169. name="%s (%s)" % (job, test.name))
  170. t.start()
  171. threads.append(t)
  172. # Wait for all threads
  173. for t in threads:
  174. t.join()
  175. # Cancel ourselves
  176. self.travis.cancel(self.travis.jobid)
  177. class Travis():
  178. '''Integrates the travis-ci build environment and the travis command line'''
  179. def __init__(self):
  180. self.jobid = os.environ['TRAVIS_JOB_NUMBER']
  181. self.buildid = os.environ['TRAVIS_BUILD_NUMBER']
  182. self.is_pull_req = "false" != os.environ['TRAVIS_PULL_REQUEST']
  183. # If this is a PR, we cannot access the secure variable
  184. # GH_TOKEN, and instead must return success for all jobs
  185. if not self.is_pull_req:
  186. self.token = os.environ['GH_TOKEN']
  187. self._login()
  188. else:
  189. log.info("Pull Request Detected. Non-necessary jobs will return pass instead of being canceled")
  190. def _login(self):
  191. subprocess.check_call("travis login --skip-version-check --no-interactive --github-token %s" % self.token, shell=True)
  192. log.info("Logged into travis") # NEVER PRINT OUTPUT, GH_TOKEN MIGHT BE REVEALED
  193. def cancel(self, job):
  194. # If this is a pull request, we cannot interact with the CLI
  195. if self.is_pull_req:
  196. log.info("Thread %s: Return pass for job %s", threading.current_thread().name, job)
  197. return
  198. # Ignore errors in case job is already cancelled
  199. try:
  200. subprocess.check_call("travis cancel %s --skip-version-check --no-interactive" % job, shell=True)
  201. log.info("Thread %s: Canceled job %s", threading.current_thread().name, job)
  202. except subprocess.CalledProcessError:
  203. log.exception("Error halting job %s. Report:", job)
  204. subprocess.call("travis report --skip-version-check --no-interactive --org", shell=True)
  205. log.error("Trying to halt %s one more time", job)
  206. subprocess.call("travis cancel %s --skip-version-check --no-interactive" % job, shell=True)
  207. def build_details(self):
  208. # If this is a pull request, we cannot interact with the CLI
  209. if self.is_pull_req:
  210. return "No details available"
  211. build = subprocess.check_output("travis show %s --skip-version-check" % self.buildid, shell=True)
  212. return build
  213. if __name__ == "__main__":
  214. args = sys.argv[1:]
  215. if len(args) != 2 or not (args[0] == "prereq" or args[0] == "install" or args[0] == "test"):
  216. print "Usage: toolset/run-ci.py [prereq|install|test] test-name"
  217. sys.exit(1)
  218. mode = args[0]
  219. testdir = args[1]
  220. runner = CIRunnner(mode, testdir)
  221. # Watch for the special test name indicating we are
  222. # the test in charge of cancelling unnecessary jobs
  223. if testdir == "jobcleaner":
  224. try:
  225. log.info("Sleeping to ensure Travis-CI has queued all jobs")
  226. time.sleep(20)
  227. runner.cancel_unneeded_jobs()
  228. except KeyError as ke:
  229. log.warning("Environment key missing, are you running inside Travis-CI?")
  230. except:
  231. log.critical("Unknown error")
  232. print traceback.format_exc()
  233. finally:
  234. sys.exit(0)
  235. retcode = 0
  236. try:
  237. retcode = runner.run()
  238. except KeyError as ke:
  239. log.warning("Environment key missing, are you running inside Travis-CI?")
  240. except:
  241. log.critical("Unknown error")
  242. print traceback.format_exc()
  243. finally:
  244. log.error("Running inside travis, so I will print err and out to console")
  245. log.error("Here is ERR:")
  246. try:
  247. with open("results/ec2/latest/logs/%s/err.txt" % runner.test.name, 'r') as err:
  248. for line in err:
  249. log.info(line)
  250. except IOError:
  251. if mode == "test":
  252. log.error("No ERR file found")
  253. log.error("Here is OUT:")
  254. try:
  255. with open("results/ec2/latest/logs/%s/out.txt" % runner.test.name, 'r') as out:
  256. for line in out:
  257. log.info(line)
  258. except IOError:
  259. if mode == "test":
  260. log.error("No OUT file found")
  261. sys.exit(retcode)