run-ci.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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, testdir=None):
  27. '''
  28. mode = [cisetup|prereq|install|verify] for what we want to do
  29. testdir = framework directory we are running
  30. '''
  31. logging.basicConfig(level=logging.INFO)
  32. self.directory = testdir
  33. self.mode = mode
  34. try:
  35. # NOTE: THIS IS VERY TRICKY TO GET RIGHT!
  36. #
  37. # If modifying, please consider:
  38. # - the commit range for a pull request is the first PR commit to
  39. # the github auto-merge commit
  40. # - the commits in the commit range may include merge commits
  41. # other than the auto-merge commit. An git log with -m
  42. # will know that *all* the files in the merge were changed,
  43. # but that is not the changeset that we care about
  44. # - git diff shows differences, but we care about git log, which
  45. # shows information on what was changed during commits
  46. # - master can (and will!) move during a build. This is one
  47. # of the biggest problems with using git diff - master will
  48. # be updated, and those updates will include changes to toolset,
  49. # and suddenly every job in the build will start to run instead
  50. # of fast-failing
  51. # - commit_range is not set if there was only one commit pushed,
  52. # so be sure to test for that on both master and PR
  53. # - commit_range and commit are set very differently for pushes
  54. # to an owned branch versus pushes to a pull request, test
  55. # - For merge commits, the TRAVIS_COMMIT and TRAVIS_COMMIT_RANGE
  56. # will become invalid if additional commits are pushed while a job is
  57. # building. See https://github.com/travis-ci/travis-ci/issues/2666
  58. # - If you're really insane, consider that the last commit in a
  59. # pull request could have been a merge commit. This means that
  60. # the github auto-merge commit could have more than two parents
  61. #
  62. # - TEST ALL THESE OPTIONS:
  63. # - On a branch you own (e.g. your fork's master)
  64. # - single commit
  65. # - multiple commits pushed at once
  66. # - commit+push, then commit+push again before the first
  67. # build has finished. Verify all jobs in the first build
  68. # used the correct commit range
  69. # - multiple commits, including a merge commit. Verify that
  70. # the unrelated merge commit changes are not counted as
  71. # changes the user made
  72. # - On a pull request
  73. # - repeat all above variations
  74. #
  75. #
  76. # ==== CURRENT SOLUTION FOR PRs ====
  77. #
  78. # For pull requests, we will examine Github's automerge commit to see
  79. # what files would be touched if we merged this into the current master.
  80. # You can't trust the travis variables here, as the automerge commit can
  81. # be different for jobs on the same build. See https://github.com/travis-ci/travis-ci/issues/2666
  82. # We instead use the FETCH_HEAD, which will always point to the SHA of
  83. # the lastest merge commit. However, if we only used FETCH_HEAD than any
  84. # new commits to a pull request would instantly start affecting currently
  85. # running jobs and the the list of changed files may become incorrect for
  86. # those affected jobs. The solution is to walk backward from the FETCH_HEAD
  87. # to the last commit in the pull request. Based on how github currently
  88. # does the automerge, this is the second parent of FETCH_HEAD, and
  89. # therefore we use FETCH_HEAD^2 below
  90. #
  91. # This may not work perfectly in situations where the user had advanced
  92. # merging happening in their PR. We correctly handle them merging in
  93. # from upstream, but if they do wild stuff then this will likely break
  94. # on that. However, it will also likely break by seeing a change in
  95. # toolset and triggering a full run when a partial run would be
  96. # acceptable
  97. #
  98. # ==== CURRENT SOLUTION FOR OWNED BRANCHES (e.g. master) ====
  99. #
  100. # This one is fairly simple. Find the commit or commit range, and
  101. # examine the log of files changes. If you encounter any merges,
  102. # then fully explode the two parent commits that made the merge
  103. # and look for the files changed there. This is an aggressive
  104. # strategy to ensure that commits to master are always tested
  105. # well
  106. log.info("TRAVIS_COMMIT_RANGE: %s", os.environ['TRAVIS_COMMIT_RANGE'])
  107. log.info("TRAVIS_COMMIT : %s", os.environ['TRAVIS_COMMIT'])
  108. is_PR = (os.environ['TRAVIS_PULL_REQUEST'] != "false")
  109. if is_PR:
  110. log.info('I am testing a pull request')
  111. first_commit = os.environ['TRAVIS_COMMIT_RANGE'].split('...')[0]
  112. last_commit = subprocess.check_output("git rev-list -n 1 FETCH_HEAD^2", shell=True).rstrip('\n')
  113. log.info("Guessing that first commit in PR is : %s", first_commit)
  114. log.info("Guessing that final commit in PR is : %s", last_commit)
  115. if first_commit == "":
  116. # Travis-CI is not yet passing a commit range for pull requests
  117. # so we must use the automerge's changed file list. This has the
  118. # negative effect that new pushes to the PR will immediately
  119. # start affecting any new jobs, regardless of the build they are on
  120. log.info("No first commit, using Github's automerge commit")
  121. self.commit_range = "--first-parent -1 -m FETCH_HEAD"
  122. elif first_commit == last_commit:
  123. # There is only one commit in the pull request so far,
  124. # or Travis-CI is not yet passing the commit range properly
  125. # for pull requests. We examine just the one commit using -1
  126. #
  127. # On the oddball chance that it's a merge commit, we pray
  128. # it's a merge from upstream and also pass --first-parent
  129. log.info("Only one commit in range, examining %s", last_commit)
  130. self.commit_range = "-m --first-parent -1 %s" % last_commit
  131. else:
  132. # In case they merged in upstream, we only care about the first
  133. # parent. For crazier merges, we hope
  134. self.commit_range = "--first-parent %s...%s" % (first_commit, last_commit)
  135. if not is_PR:
  136. log.info('I am not testing a pull request')
  137. # If more than one commit was pushed, examine everything including
  138. # all details on all merges
  139. self.commit_range = "-m %s" % os.environ['TRAVIS_COMMIT_RANGE']
  140. # If only one commit was pushed, examine that one. If it was a
  141. # merge be sure to show all details
  142. if self.commit_range == "":
  143. self.commit_range = "-m -1 %s" % os.environ['TRAVIS_COMMIT']
  144. except KeyError:
  145. log.warning("I should only be used for automated integration tests e.g. Travis-CI")
  146. log.warning("Were you looking for run-tests.py?")
  147. self.commit_range = "-m HEAD^...HEAD"
  148. #
  149. # Find the one test from benchmark_config that we are going to run
  150. #
  151. tests = gather_tests()
  152. dirtests = [t for t in tests if t.directory == testdir]
  153. # Travis-CI is linux only
  154. osvalidtests = [t for t in dirtests if t.os.lower() == "linux"
  155. and (t.database_os.lower() == "linux" or t.database_os.lower() == "none")]
  156. # Travis-CI only has some supported databases
  157. validtests = [t for t in osvalidtests if t.database.lower() == "mysql"
  158. or t.database.lower() == "postgres"
  159. or t.database.lower() == "mongodb"
  160. or t.database.lower() == "none"]
  161. log.info("Found %s tests (%s for linux, %s for linux and mysql) in directory '%s'",
  162. len(dirtests), len(osvalidtests), len(validtests), testdir)
  163. if len(validtests) == 0:
  164. log.critical("Found no test that is possible to run in Travis-CI! Aborting!")
  165. if len(osvalidtests) != 0:
  166. log.critical("Note: Found these tests that could run in Travis-CI if more databases were supported")
  167. log.critical("Note: %s", osvalidtests)
  168. databases_needed = [t.database for t in osvalidtests]
  169. databases_needed = list(set(databases_needed))
  170. log.critical("Note: Here are the needed databases:")
  171. log.critical("Note: %s", databases_needed)
  172. sys.exit(1)
  173. self.names = [t.name for t in validtests]
  174. log.info("Choosing to use test %s to verify directory %s", self.names, testdir)
  175. def _should_run(self):
  176. '''
  177. Decides if the current framework test should be tested.
  178. Examines git commits included in the latest push to see if any files relevant to
  179. this framework were changed.
  180. If you do rewrite history (e.g. rebase) then it's up to you to ensure that both
  181. old and new (e.g. old...new) are available in the public repository. For simple
  182. rebase onto the public master this is not a problem, only more complex rebases
  183. may have issues
  184. '''
  185. # Don't use git diff multiple times, it's mega slow sometimes\
  186. # Put flag on filesystem so that future calls to run-ci see it too
  187. if os.path.isfile('.run-ci.should_run'):
  188. return True
  189. if os.path.isfile('.run-ci.should_not_run'):
  190. return False
  191. def touch(fname):
  192. open(fname, 'a').close()
  193. log.info("Using commit range `%s`", self.commit_range)
  194. log.info("Running `git log --name-only --pretty=\"format:\" %s`" % self.commit_range)
  195. changes = subprocess.check_output("git log --name-only --pretty=\"format:\" %s" % self.commit_range, shell=True)
  196. changes = os.linesep.join([s for s in changes.splitlines() if s]) # drop empty lines
  197. log.info("Result:\n%s", changes)
  198. # Look for changes to core TFB framework code
  199. if re.search(r'^toolset/', changes, re.M) is not None:
  200. log.info("Found changes to core framework code")
  201. touch('.run-ci.should_run')
  202. return True
  203. # Look for changes relevant to this test
  204. if re.search("^%s/" % self.directory, changes, re.M) is None:
  205. log.info("No changes found for directory %s", self.directory)
  206. touch('.run-ci.should_not_run')
  207. return False
  208. log.info("Changes found for directory %s", self.directory)
  209. touch('.run-ci.should_run')
  210. return True
  211. def run(self):
  212. ''' Do the requested command using TFB '''
  213. if not self._should_run():
  214. log.info("Not running directory %s", self.directory)
  215. return 0
  216. if self.mode == 'cisetup':
  217. self.run_travis_setup()
  218. return 0
  219. names = ' '.join(self.names)
  220. command = 'toolset/run-tests.py '
  221. if self.mode == 'prereq':
  222. command = command + "--install server --install-only --test ''"
  223. elif self.mode == 'install':
  224. command = command + "--install server --install-only --test %s" % names
  225. elif self.mode == 'verify':
  226. command = command + "--mode verify --test %s" % names
  227. else:
  228. log.critical('Unknown mode passed')
  229. return 1
  230. # Run the command
  231. log.info("Running mode %s with commmand %s", self.mode, command)
  232. try:
  233. p = subprocess.Popen(command, shell=True)
  234. p.wait()
  235. return p.returncode
  236. except subprocess.CalledProcessError:
  237. log.critical("Subprocess Error")
  238. print traceback.format_exc()
  239. return 1
  240. except Exception as err:
  241. log.critical("Exception from running+wait on subprocess")
  242. log.error(err.child_traceback)
  243. return 1
  244. def run_travis_setup(self):
  245. log.info("Setting up Travis-CI")
  246. script = '''
  247. # Needed to download latest MongoDB (use two different approaches)
  248. sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 || gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 7F0CEB10
  249. echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list
  250. sudo apt-get update
  251. # MongoDB takes a good 30-45 seconds to turn on, so install it first
  252. sudo apt-get install mongodb-org
  253. sudo apt-get install openssh-server
  254. # Run as travis user (who already has passwordless sudo)
  255. ssh-keygen -f /home/travis/.ssh/id_rsa -N '' -t rsa
  256. cat /home/travis/.ssh/id_rsa.pub > /home/travis/.ssh/authorized_keys
  257. chmod 600 /home/travis/.ssh/authorized_keys
  258. # =============Setup Databases===========================
  259. # NOTE: Do not run `--install database` in travis-ci!
  260. # It changes DB configuration files and will break everything
  261. # =======================================================
  262. # Add data to mysql
  263. mysql -uroot < config/create.sql
  264. # Setup Postgres
  265. psql --version
  266. sudo useradd benchmarkdbuser -p benchmarkdbpass
  267. sudo -u postgres psql template1 < config/create-postgres-database.sql
  268. sudo -u benchmarkdbuser psql hello_world < config/create-postgres.sql
  269. # Setup MongoDB (see install above)
  270. mongod --version
  271. until nc -z localhost 27017 ; do echo Waiting for MongoDB; sleep 1; done
  272. mongo < config/create.js
  273. '''
  274. def sh(command):
  275. log.info("Running `%s`", command)
  276. subprocess.check_call(command, shell=True)
  277. for command in script.split('\n'):
  278. command = command.lstrip()
  279. if command != "" and command[0] != '#':
  280. sh(command.lstrip())
  281. if __name__ == "__main__":
  282. args = sys.argv[1:]
  283. usage = '''Usage: toolset/run-ci.py [cisetup|prereq|install|verify] <framework-directory>
  284. run-ci.py selects one test from <framework-directory>/benchark_config, and
  285. automates a number of calls into run-tests.py specific to the selected test.
  286. It is guaranteed to always select the same test from the benchark_config, so
  287. multiple runs with the same <framework-directory> reference the same test.
  288. The name of the selected test will be printed to standard output.
  289. cisetup - configure the Travis-CI environment for our test suite
  290. prereq - trigger standard prerequisite installation
  291. install - trigger server installation for the selected test_directory
  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 == "install"
  301. or mode == "verify"
  302. or mode == 'prereq'
  303. or mode == 'cisetup'):
  304. runner = CIRunnner(mode, testdir)
  305. else:
  306. print usage
  307. sys.exit(1)
  308. retcode = 0
  309. try:
  310. retcode = runner.run()
  311. except KeyError as ke:
  312. log.warning("Environment key missing, are you running inside Travis-CI?")
  313. print traceback.format_exc()
  314. except:
  315. log.critical("Unknown error")
  316. print traceback.format_exc()
  317. finally: # Ensure that logs are printed
  318. # Only print logs if we ran a verify
  319. if mode != 'verify':
  320. sys.exit(retcode)
  321. # Only print logs if we actually did something
  322. if os.path.isfile('.run-ci.should_not_run'):
  323. sys.exit(retcode)
  324. log.error("Running inside Travis-CI, so I will print err and out to console...")
  325. for name in runner.names:
  326. log.error("Test %s", name)
  327. try:
  328. log.error("Here is ERR:")
  329. with open("results/ec2/latest/logs/%s/err.txt" % name, 'r') as err:
  330. for line in err:
  331. log.info(line.rstrip('\n'))
  332. except IOError:
  333. log.error("No ERR file found")
  334. try:
  335. log.error("Here is OUT:")
  336. with open("results/ec2/latest/logs/%s/out.txt" % name, 'r') as out:
  337. for line in out:
  338. log.info(line.rstrip('\n'))
  339. except IOError:
  340. log.error("No OUT file found")
  341. sys.exit(retcode)
  342. # vim: set sw=2 ts=2 expandtab