run-ci.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. #!/usr/bin/env python
  2. import subprocess
  3. import os
  4. import sys
  5. import glob
  6. import json
  7. import traceback
  8. import re
  9. import logging
  10. log = logging.getLogger('run-ci')
  11. import time
  12. import threading
  13. from benchmark import framework_test
  14. from benchmark.utils import gather_tests
  15. from benchmark.utils import header
  16. # Needed for various imports
  17. sys.path.append('.')
  18. sys.path.append('toolset/setup/linux')
  19. sys.path.append('toolset/benchmark')
  20. from setup.linux import setup_util
  21. class CIRunnner:
  22. '''
  23. Manages running TFB on the Travis Continuous Integration system.
  24. Makes a best effort to avoid wasting time and resources by running
  25. useless jobs.
  26. Only verifies the first test in each directory
  27. '''
  28. SUPPORTED_DATABASES = "mysql postgres mongodb cassandra elasticsearch sqlite redis none".split()
  29. def __init__(self, mode, testdir=None):
  30. '''
  31. mode = [verify] for what we want to do
  32. testdir = framework directory we are running
  33. '''
  34. self.directory = testdir
  35. self.mode = mode
  36. logging.basicConfig(level=logging.INFO)
  37. try:
  38. # NOTE: THIS IS VERY TRICKY TO GET RIGHT!
  39. #
  40. # Our goal: Look at the files changed and determine if we need to
  41. # run a verification for this folder. For a pull request, we want to
  42. # see the list of files changed by any commit in that PR. For a
  43. # push to master, we want to see a list of files changed by the pushed
  44. # commits. If this list of files contains the current directory, or
  45. # contains the toolset/ directory, then we need to run a verification
  46. #
  47. # If modifying, please consider:
  48. # - the commit range for a pull request is the first PR commit to
  49. # the github auto-merge commit
  50. # - the commits in the commit range may include merge commits
  51. # other than the auto-merge commit. An git log with -m
  52. # will know that *all* the files in the merge were changed,
  53. # but that is not the changeset that we care about
  54. # - git diff shows differences, but we care about git log, which
  55. # shows information on what was changed during commits
  56. # - master can (and will!) move during a build. This is one
  57. # of the biggest problems with using git diff - master will
  58. # be updated, and those updates will include changes to toolset,
  59. # and suddenly every job in the build will start to run instead
  60. # of fast-failing
  61. # - commit_range is not set if there was only one commit pushed,
  62. # so be sure to test for that on both master and PR
  63. # - commit_range and commit are set very differently for pushes
  64. # to an owned branch versus pushes to a pull request, test
  65. # - For merge commits, the TRAVIS_COMMIT and TRAVIS_COMMIT_RANGE
  66. # will become invalid if additional commits are pushed while a job is
  67. # building. See https://github.com/travis-ci/travis-ci/issues/2666
  68. # - If you're really insane, consider that the last commit in a
  69. # pull request could have been a merge commit. This means that
  70. # the github auto-merge commit could have more than two parents
  71. # - Travis cannot really support rebasing onto an owned branch, the
  72. # commit_range they provide will include commits that are non-existant
  73. # in the repo cloned on the workers. See https://github.com/travis-ci/travis-ci/issues/2668
  74. #
  75. # - TEST ALL THESE OPTIONS:
  76. # - On a branch you own (e.g. your fork's master)
  77. # - single commit
  78. # - multiple commits pushed at once
  79. # - commit+push, then commit+push again before the first
  80. # build has finished. Verify all jobs in the first build
  81. # used the correct commit range
  82. # - multiple commits, including a merge commit. Verify that
  83. # the unrelated merge commit changes are not counted as
  84. # changes the user made
  85. # - On a pull request
  86. # - repeat all above variations
  87. #
  88. #
  89. # ==== CURRENT SOLUTION FOR PRs ====
  90. #
  91. # For pull requests, we will examine Github's automerge commit to see
  92. # what files would be touched if we merged this into the current master.
  93. # You can't trust the travis variables here, as the automerge commit can
  94. # be different for jobs on the same build. See https://github.com/travis-ci/travis-ci/issues/2666
  95. # We instead use the FETCH_HEAD, which will always point to the SHA of
  96. # the lastest merge commit. However, if we only used FETCH_HEAD than any
  97. # new commits to a pull request would instantly start affecting currently
  98. # running jobs and the the list of changed files may become incorrect for
  99. # those affected jobs. The solution is to walk backward from the FETCH_HEAD
  100. # to the last commit in the pull request. Based on how github currently
  101. # does the automerge, this is the second parent of FETCH_HEAD, and
  102. # therefore we use FETCH_HEAD^2 below
  103. #
  104. # This may not work perfectly in situations where the user had advanced
  105. # merging happening in their PR. We correctly handle them merging in
  106. # from upstream, but if they do wild stuff then this will likely break
  107. # on that. However, it will also likely break by seeing a change in
  108. # toolset and triggering a full run when a partial run would be
  109. # acceptable
  110. #
  111. # ==== CURRENT SOLUTION FOR OWNED BRANCHES (e.g. master) ====
  112. #
  113. # This one is fairly simple. Find the commit or commit range, and
  114. # examine the log of files changes. If you encounter any merges,
  115. # then fully explode the two parent commits that made the merge
  116. # and look for the files changed there. This is an aggressive
  117. # strategy to ensure that commits to master are always tested
  118. # well
  119. log.debug("TRAVIS_COMMIT_RANGE: %s", os.environ['TRAVIS_COMMIT_RANGE'])
  120. log.debug("TRAVIS_COMMIT : %s", os.environ['TRAVIS_COMMIT'])
  121. is_PR = (os.environ['TRAVIS_PULL_REQUEST'] != "false")
  122. if is_PR:
  123. log.debug('I am testing a pull request')
  124. first_commit = os.environ['TRAVIS_COMMIT_RANGE'].split('...')[0]
  125. last_commit = subprocess.check_output("git rev-list -n 1 FETCH_HEAD^2", shell=True).rstrip('\n')
  126. log.debug("Guessing that first commit in PR is : %s", first_commit)
  127. log.debug("Guessing that final commit in PR is : %s", last_commit)
  128. if first_commit == "":
  129. # Travis-CI is not yet passing a commit range for pull requests
  130. # so we must use the automerge's changed file list. This has the
  131. # negative effect that new pushes to the PR will immediately
  132. # start affecting any new jobs, regardless of the build they are on
  133. log.debug("No first commit, using Github's automerge commit")
  134. self.commit_range = "--first-parent -1 -m FETCH_HEAD"
  135. elif first_commit == last_commit:
  136. # There is only one commit in the pull request so far,
  137. # or Travis-CI is not yet passing the commit range properly
  138. # for pull requests. We examine just the one commit using -1
  139. #
  140. # On the oddball chance that it's a merge commit, we pray
  141. # it's a merge from upstream and also pass --first-parent
  142. log.debug("Only one commit in range, examining %s", last_commit)
  143. self.commit_range = "-m --first-parent -1 %s" % last_commit
  144. else:
  145. # In case they merged in upstream, we only care about the first
  146. # parent. For crazier merges, we hope
  147. self.commit_range = "--first-parent %s...%s" % (first_commit, last_commit)
  148. if not is_PR:
  149. log.debug('I am not testing a pull request')
  150. # Three main scenarios to consider
  151. # - 1 One non-merge commit pushed to master
  152. # - 2 One merge commit pushed to master (e.g. a PR was merged).
  153. # This is an example of merging a topic branch
  154. # - 3 Multiple commits pushed to master
  155. #
  156. # 1 and 2 are actually handled the same way, by showing the
  157. # changes being brought into to master when that one commit
  158. # was merged. Fairly simple, `git log -1 COMMIT`. To handle
  159. # the potential merge of a topic branch you also include
  160. # `--first-parent -m`.
  161. #
  162. # 3 needs to be handled by comparing all merge children for
  163. # the entire commit range. The best solution here would *not*
  164. # use --first-parent because there is no guarantee that it
  165. # reflects changes brought into master. Unfortunately we have
  166. # no good method inside Travis-CI to easily differentiate
  167. # scenario 1/2 from scenario 3, so I cannot handle them all
  168. # separately. 1/2 are the most common cases, 3 with a range
  169. # of non-merge commits is the next most common, and 3 with
  170. # a range including merge commits is the least common, so I
  171. # am choosing to make our Travis-CI setup potential not work
  172. # properly on the least common case by always using
  173. # --first-parent
  174. # Handle 3
  175. # Note: Also handles 2 because Travis-CI sets COMMIT_RANGE for
  176. # merged PR commits
  177. self.commit_range = "--first-parent -m %s" % os.environ['TRAVIS_COMMIT_RANGE']
  178. # Handle 1
  179. if self.commit_range == "":
  180. self.commit_range = "--first-parent -m -1 %s" % os.environ['TRAVIS_COMMIT']
  181. except KeyError:
  182. log.warning("I should only be used for automated integration tests e.g. Travis-CI")
  183. log.warning("Were you looking for run-tests.py?")
  184. self.commit_range = "-m HEAD^...HEAD"
  185. #
  186. # Find the one test from benchmark_config.json that we are going to run
  187. #
  188. tests = gather_tests()
  189. self.fwroot = setup_util.get_fwroot()
  190. target_dir = self.fwroot + '/frameworks/' + testdir
  191. log.debug("Target directory is %s", target_dir)
  192. dirtests = [t for t in tests if t.directory == target_dir]
  193. # Travis-CI is linux only
  194. osvalidtests = [t for t in dirtests if t.os.lower() == "linux"
  195. and (t.database_os.lower() == "linux" or t.database_os.lower() == "none")]
  196. # Our Travis-CI only has some databases supported
  197. validtests = [t for t in osvalidtests if t.database.lower() in self.SUPPORTED_DATABASES]
  198. supported_databases = ','.join(self.SUPPORTED_DATABASES)
  199. log.info("Found %s usable tests (%s valid for linux, %s valid for linux and {%s}) in directory '%s'",
  200. len(dirtests), len(osvalidtests), len(validtests), supported_databases, '$FWROOT/frameworks/' + testdir)
  201. if len(validtests) == 0:
  202. log.critical("Found no test that is possible to run in Travis-CI! Aborting!")
  203. if len(osvalidtests) != 0:
  204. log.critical("Note: Found these tests that could run in Travis-CI if more databases were supported")
  205. log.critical("Note: %s", osvalidtests)
  206. databases_needed = [t.database for t in osvalidtests]
  207. databases_needed = list(set(databases_needed))
  208. log.critical("Note: Here are the needed databases:")
  209. log.critical("Note: %s", databases_needed)
  210. sys.exit(1)
  211. self.names = [t.name for t in validtests]
  212. log.info("Using tests %s to verify directory %s", self.names, '$FWROOT/frameworks/' + testdir)
  213. def _should_run(self):
  214. '''
  215. Decides if the current framework test should be tested.
  216. Examines git commits included in the latest push to see if any files relevant to
  217. this framework were changed.
  218. If you do rewrite history (e.g. rebase) then it's up to you to ensure that both
  219. old and new (e.g. old...new) are available in the public repository. For simple
  220. rebase onto the public master this is not a problem, only more complex rebases
  221. may have issues
  222. '''
  223. # Don't use git diff multiple times, it's mega slow sometimes\
  224. # Put flag on filesystem so that future calls to run-ci see it too
  225. if os.path.isfile('.run-ci.should_run'):
  226. return True
  227. if os.path.isfile('.run-ci.should_not_run'):
  228. return False
  229. def touch(fname):
  230. open(fname, 'a').close()
  231. log.debug("Using commit range `%s`", self.commit_range)
  232. log.debug("Running `git log --name-only --pretty=\"format:\" %s`" % self.commit_range)
  233. changes = ""
  234. try:
  235. changes = subprocess.check_output("git log --name-only --pretty=\"format:\" %s" % self.commit_range, shell=True)
  236. except subprocess.CalledProcessError, e:
  237. log.error("Got errors when using git to detect your changes, assuming that we must run this verification!")
  238. log.error("Error was: %s", e.output)
  239. log.error("Did you rebase a branch? If so, you can safely disregard this error, it's a Travis limitation")
  240. return True
  241. changes = os.linesep.join([s for s in changes.splitlines() if s]) # drop empty lines
  242. if len(changes.splitlines()) > 1000:
  243. log.debug("Change list is >1000 lines, uploading to sprunge.us instead of printing to console")
  244. url = subprocess.check_output("git log --name-only %s | curl -F 'sprunge=<-' http://sprunge.us" % self.commit_range, shell=True)
  245. log.debug("Uploaded to %s", url)
  246. else:
  247. log.debug("Result:\n%s", changes)
  248. # Look for changes to core TFB framework code
  249. if re.search(r'^toolset/', changes, re.M) is not None:
  250. log.info("Found changes to core framework code")
  251. touch('.run-ci.should_run')
  252. return True
  253. # Look for changes relevant to this test
  254. if re.search("^frameworks/%s/" % re.escape(self.directory), changes, re.M) is None:
  255. log.info("No changes found for directory %s", self.directory)
  256. touch('.run-ci.should_not_run')
  257. return False
  258. log.info("Changes found for directory %s", self.directory)
  259. touch('.run-ci.should_run')
  260. return True
  261. def run(self):
  262. ''' Do the requested command using TFB '''
  263. if not self._should_run():
  264. log.info("I found no changes to `%s` or `toolset/`, aborting verification", self.directory)
  265. return 0
  266. names = ' '.join(self.names)
  267. # Assume mode is verify
  268. # os.environ["TRAVIS_TESTS"] = "%s" % names
  269. command = "toolset/run-tests.py --mode verify --test %s" % names
  270. # Run the command
  271. log.info("Running mode %s with commmand %s", self.mode, command)
  272. try:
  273. p = subprocess.Popen(command, shell=True)
  274. p.wait()
  275. return p.returncode
  276. except subprocess.CalledProcessError:
  277. log.critical("Subprocess Error")
  278. print traceback.format_exc()
  279. return 1
  280. except Exception as err:
  281. log.critical("Exception from running+wait on subprocess")
  282. log.error(err.child_traceback)
  283. return 1
  284. if __name__ == "__main__":
  285. args = sys.argv[1:]
  286. usage = '''Usage: toolset/run-ci.py [verify] <framework-directory>
  287. run-ci.py selects one test from <framework-directory>/benchark_config, and
  288. automates a number of calls into run-tests.py specific to the selected test.
  289. It is guaranteed to always select the same test from the benchark_config, so
  290. multiple runs with the same <framework-directory> reference the same test.
  291. The name of the selected test will be printed to standard output.
  292. verify - run a verification on the selected test using `--mode verify`
  293. run-ci.py expects to be run inside the Travis-CI build environment, and
  294. will expect environment variables such as $TRAVIS_BUILD'''
  295. if len(args) != 2:
  296. print usage
  297. sys.exit(1)
  298. mode = args[0]
  299. testdir = args[1]
  300. if len(args) == 2 and (mode == 'verify'):
  301. runner = CIRunnner(mode, testdir)
  302. else:
  303. print usage
  304. sys.exit(1)
  305. retcode = 0
  306. try:
  307. retcode = runner.run()
  308. except KeyError as ke:
  309. log.warning("Environment key missing, are you running inside Travis-CI?")
  310. print traceback.format_exc()
  311. retcode = 1
  312. except Exception:
  313. log.critical("Unknown error")
  314. print traceback.format_exc()
  315. retcode = 1
  316. finally:
  317. sys.exit(retcode)
  318. # vim: set sw=2 ts=2 expandtab