run-ci.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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. # Cross-platform colored text
  17. from colorama import Fore, Back, Style
  18. # Needed for various imports
  19. sys.path.append('.')
  20. sys.path.append('toolset/setup/linux')
  21. sys.path.append('toolset/benchmark')
  22. from setup.linux import setup_util
  23. class CIRunnner:
  24. '''
  25. Manages running TFB on the Travis Continuous Integration system.
  26. Makes a best effort to avoid wasting time and resources by running
  27. useless jobs.
  28. Only verifies the first test in each directory
  29. '''
  30. def __init__(self, mode, testdir=None):
  31. '''
  32. mode = [cisetup|prereq|install|verify] for what we want to do
  33. testdir = framework directory we are running
  34. '''
  35. self.directory = testdir
  36. self.mode = mode
  37. if mode == "cisetup":
  38. logging.basicConfig(level=logging.DEBUG)
  39. else:
  40. logging.basicConfig(level=logging.INFO)
  41. try:
  42. # NOTE: THIS IS VERY TRICKY TO GET RIGHT!
  43. #
  44. # Our goal: Look at the files changed and determine if we need to
  45. # run a verification for this folder. For a pull request, we want to
  46. # see the list of files changed by any commit in that PR. For a
  47. # push to master, we want to see a list of files changed by the pushed
  48. # commits. If this list of files contains the current directory, or
  49. # contains the toolset/ directory, then we need to run a verification
  50. #
  51. # If modifying, please consider:
  52. # - the commit range for a pull request is the first PR commit to
  53. # the github auto-merge commit
  54. # - the commits in the commit range may include merge commits
  55. # other than the auto-merge commit. An git log with -m
  56. # will know that *all* the files in the merge were changed,
  57. # but that is not the changeset that we care about
  58. # - git diff shows differences, but we care about git log, which
  59. # shows information on what was changed during commits
  60. # - master can (and will!) move during a build. This is one
  61. # of the biggest problems with using git diff - master will
  62. # be updated, and those updates will include changes to toolset,
  63. # and suddenly every job in the build will start to run instead
  64. # of fast-failing
  65. # - commit_range is not set if there was only one commit pushed,
  66. # so be sure to test for that on both master and PR
  67. # - commit_range and commit are set very differently for pushes
  68. # to an owned branch versus pushes to a pull request, test
  69. # - For merge commits, the TRAVIS_COMMIT and TRAVIS_COMMIT_RANGE
  70. # will become invalid if additional commits are pushed while a job is
  71. # building. See https://github.com/travis-ci/travis-ci/issues/2666
  72. # - If you're really insane, consider that the last commit in a
  73. # pull request could have been a merge commit. This means that
  74. # the github auto-merge commit could have more than two parents
  75. # - Travis cannot really support rebasing onto an owned branch, the
  76. # commit_range they provide will include commits that are non-existant
  77. # in the repo cloned on the workers. See https://github.com/travis-ci/travis-ci/issues/2668
  78. #
  79. # - TEST ALL THESE OPTIONS:
  80. # - On a branch you own (e.g. your fork's master)
  81. # - single commit
  82. # - multiple commits pushed at once
  83. # - commit+push, then commit+push again before the first
  84. # build has finished. Verify all jobs in the first build
  85. # used the correct commit range
  86. # - multiple commits, including a merge commit. Verify that
  87. # the unrelated merge commit changes are not counted as
  88. # changes the user made
  89. # - On a pull request
  90. # - repeat all above variations
  91. #
  92. #
  93. # ==== CURRENT SOLUTION FOR PRs ====
  94. #
  95. # For pull requests, we will examine Github's automerge commit to see
  96. # what files would be touched if we merged this into the current master.
  97. # You can't trust the travis variables here, as the automerge commit can
  98. # be different for jobs on the same build. See https://github.com/travis-ci/travis-ci/issues/2666
  99. # We instead use the FETCH_HEAD, which will always point to the SHA of
  100. # the lastest merge commit. However, if we only used FETCH_HEAD than any
  101. # new commits to a pull request would instantly start affecting currently
  102. # running jobs and the the list of changed files may become incorrect for
  103. # those affected jobs. The solution is to walk backward from the FETCH_HEAD
  104. # to the last commit in the pull request. Based on how github currently
  105. # does the automerge, this is the second parent of FETCH_HEAD, and
  106. # therefore we use FETCH_HEAD^2 below
  107. #
  108. # This may not work perfectly in situations where the user had advanced
  109. # merging happening in their PR. We correctly handle them merging in
  110. # from upstream, but if they do wild stuff then this will likely break
  111. # on that. However, it will also likely break by seeing a change in
  112. # toolset and triggering a full run when a partial run would be
  113. # acceptable
  114. #
  115. # ==== CURRENT SOLUTION FOR OWNED BRANCHES (e.g. master) ====
  116. #
  117. # This one is fairly simple. Find the commit or commit range, and
  118. # examine the log of files changes. If you encounter any merges,
  119. # then fully explode the two parent commits that made the merge
  120. # and look for the files changed there. This is an aggressive
  121. # strategy to ensure that commits to master are always tested
  122. # well
  123. log.debug("TRAVIS_COMMIT_RANGE: %s", os.environ['TRAVIS_COMMIT_RANGE'])
  124. log.debug("TRAVIS_COMMIT : %s", os.environ['TRAVIS_COMMIT'])
  125. is_PR = (os.environ['TRAVIS_PULL_REQUEST'] != "false")
  126. if is_PR:
  127. log.debug('I am testing a pull request')
  128. first_commit = os.environ['TRAVIS_COMMIT_RANGE'].split('...')[0]
  129. last_commit = subprocess.check_output("git rev-list -n 1 FETCH_HEAD^2", shell=True).rstrip('\n')
  130. log.debug("Guessing that first commit in PR is : %s", first_commit)
  131. log.debug("Guessing that final commit in PR is : %s", last_commit)
  132. if first_commit == "":
  133. # Travis-CI is not yet passing a commit range for pull requests
  134. # so we must use the automerge's changed file list. This has the
  135. # negative effect that new pushes to the PR will immediately
  136. # start affecting any new jobs, regardless of the build they are on
  137. log.debug("No first commit, using Github's automerge commit")
  138. self.commit_range = "--first-parent -1 -m FETCH_HEAD"
  139. elif first_commit == last_commit:
  140. # There is only one commit in the pull request so far,
  141. # or Travis-CI is not yet passing the commit range properly
  142. # for pull requests. We examine just the one commit using -1
  143. #
  144. # On the oddball chance that it's a merge commit, we pray
  145. # it's a merge from upstream and also pass --first-parent
  146. log.debug("Only one commit in range, examining %s", last_commit)
  147. self.commit_range = "-m --first-parent -1 %s" % last_commit
  148. else:
  149. # In case they merged in upstream, we only care about the first
  150. # parent. For crazier merges, we hope
  151. self.commit_range = "--first-parent %s...%s" % (first_commit, last_commit)
  152. if not is_PR:
  153. log.debug('I am not testing a pull request')
  154. # If more than one commit was pushed, examine everything including
  155. # all details on all merges
  156. self.commit_range = "-m %s" % os.environ['TRAVIS_COMMIT_RANGE']
  157. # If only one commit was pushed, examine that one. If it was a
  158. # merge be sure to show all details
  159. if self.commit_range == "":
  160. self.commit_range = "-m -1 %s" % os.environ['TRAVIS_COMMIT']
  161. except KeyError:
  162. log.warning("I should only be used for automated integration tests e.g. Travis-CI")
  163. log.warning("Were you looking for run-tests.py?")
  164. self.commit_range = "-m HEAD^...HEAD"
  165. #
  166. # Find the one test from benchmark_config that we are going to run
  167. #
  168. tests = gather_tests()
  169. self.fwroot = setup_util.get_fwroot()
  170. target_dir = self.fwroot + '/frameworks/' + testdir
  171. log.debug("Target directory is %s", target_dir)
  172. dirtests = [t for t in tests if t.directory == target_dir]
  173. # Travis-CI is linux only
  174. osvalidtests = [t for t in dirtests if t.os.lower() == "linux"
  175. and (t.database_os.lower() == "linux" or t.database_os.lower() == "none")]
  176. # Our Travis-CI only has some databases supported
  177. validtests = [t for t in osvalidtests if t.database.lower() == "mysql"
  178. or t.database.lower() == "postgres"
  179. or t.database.lower() == "mongodb"
  180. or t.database.lower() == "cassandra"
  181. or t.database.lower() == "none"]
  182. log.info("Found %s usable tests (%s valid for linux, %s valid for linux and {mysql,postgres,mongodb,cassandra,none}) in directory '%s'",
  183. len(dirtests), len(osvalidtests), len(validtests), '$FWROOT/frameworks/' + testdir)
  184. if len(validtests) == 0:
  185. log.critical("Found no test that is possible to run in Travis-CI! Aborting!")
  186. if len(osvalidtests) != 0:
  187. log.critical("Note: Found these tests that could run in Travis-CI if more databases were supported")
  188. log.critical("Note: %s", osvalidtests)
  189. databases_needed = [t.database for t in osvalidtests]
  190. databases_needed = list(set(databases_needed))
  191. log.critical("Note: Here are the needed databases:")
  192. log.critical("Note: %s", databases_needed)
  193. sys.exit(1)
  194. self.names = [t.name for t in validtests]
  195. log.info("Using tests %s to verify directory %s", self.names, '$FWROOT/frameworks/' + testdir)
  196. def _should_run(self):
  197. '''
  198. Decides if the current framework test should be tested.
  199. Examines git commits included in the latest push to see if any files relevant to
  200. this framework were changed.
  201. If you do rewrite history (e.g. rebase) then it's up to you to ensure that both
  202. old and new (e.g. old...new) are available in the public repository. For simple
  203. rebase onto the public master this is not a problem, only more complex rebases
  204. may have issues
  205. '''
  206. # Don't use git diff multiple times, it's mega slow sometimes\
  207. # Put flag on filesystem so that future calls to run-ci see it too
  208. if os.path.isfile('.run-ci.should_run'):
  209. return True
  210. if os.path.isfile('.run-ci.should_not_run'):
  211. return False
  212. def touch(fname):
  213. open(fname, 'a').close()
  214. log.debug("Using commit range `%s`", self.commit_range)
  215. log.debug("Running `git log --name-only --pretty=\"format:\" %s`" % self.commit_range)
  216. changes = ""
  217. try:
  218. changes = subprocess.check_output("git log --name-only --pretty=\"format:\" %s" % self.commit_range, shell=True)
  219. except subprocess.CalledProcessError, e:
  220. log.error("Got errors when using git to detect your changes, assuming that we must run this verification!")
  221. log.error("Error was: %s", e.output)
  222. log.error("Did you rebase a branch? If so, you can safely disregard this error, it's a Travis limitation")
  223. return True
  224. changes = os.linesep.join([s for s in changes.splitlines() if s]) # drop empty lines
  225. if len(changes.splitlines()) > 1000:
  226. log.debug("Change list is >1000 lines, uploading to sprunge.us instead of printing to console")
  227. url = subprocess.check_output("git log --name-only %s | curl -F 'sprunge=<-' http://sprunge.us" % self.commit_range, shell=True)
  228. log.debug("Uploaded to %s", url)
  229. else:
  230. log.debug("Result:\n%s", changes)
  231. # Look for changes to core TFB framework code
  232. if re.search(r'^toolset/', changes, re.M) is not None:
  233. log.info("Found changes to core framework code")
  234. touch('.run-ci.should_run')
  235. return True
  236. # Look for changes relevant to this test
  237. if re.search("^frameworks/%s/" % re.escape(self.directory), changes, re.M) is None:
  238. log.info("No changes found for directory %s", self.directory)
  239. touch('.run-ci.should_not_run')
  240. return False
  241. log.info("Changes found for directory %s", self.directory)
  242. touch('.run-ci.should_run')
  243. return True
  244. def run(self):
  245. ''' Do the requested command using TFB '''
  246. if not self._should_run():
  247. log.info("I found no changes to `%s` or `toolset/`, aborting verification", self.directory)
  248. return 0
  249. if self.mode == 'cisetup':
  250. self.run_travis_setup()
  251. return 0
  252. names = ' '.join(self.names)
  253. command = 'toolset/run-tests.py '
  254. if self.mode == 'prereq':
  255. command = command + "--install server --install-only --test ''"
  256. elif self.mode == 'install':
  257. command = command + "--install server --install-only --test %s" % names
  258. elif self.mode == 'verify':
  259. command = command + "--mode verify --test %s" % names
  260. else:
  261. log.critical('Unknown mode passed')
  262. return 1
  263. # Run the command
  264. log.info("Running mode %s with commmand %s", self.mode, command)
  265. try:
  266. p = subprocess.Popen(command, shell=True)
  267. p.wait()
  268. return p.returncode
  269. except subprocess.CalledProcessError:
  270. log.critical("Subprocess Error")
  271. print traceback.format_exc()
  272. return 1
  273. except Exception as err:
  274. log.critical("Exception from running+wait on subprocess")
  275. log.error(err.child_traceback)
  276. return 1
  277. def run_travis_setup(self):
  278. log.info("Setting up Travis-CI")
  279. script = '''
  280. # Needed to download latest MongoDB
  281. # Due to TechEmpower/FrameworkBenchmarks#989 and travis-ci/travis-ci#2655,
  282. # we put this into a loop
  283. until timeout 15s sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; do echo 'Waiting for apt-key' ; done
  284. echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list
  285. # Add Apache Cassandra repository
  286. sudo apt-key adv --keyserver pgp.mit.edu --recv 4BD736A82B5C1B00
  287. sudo apt-add-repository 'deb http://www.apache.org/dist/cassandra/debian 20x main'
  288. sudo apt-get -q update
  289. # MongoDB takes a good 30-45 seconds to turn on, so install it first
  290. sudo apt-get -q install mongodb-org
  291. sudo apt-get install -o Dpkg::Options::="--force-confnew" cassandra
  292. sudo apt-get -q install openssh-server
  293. # Run as travis user (who already has passwordless sudo)
  294. ssh-keygen -f /home/travis/.ssh/id_rsa -N '' -t rsa
  295. cat /home/travis/.ssh/id_rsa.pub > /home/travis/.ssh/authorized_keys
  296. chmod 600 /home/travis/.ssh/authorized_keys
  297. # =============Setup Databases===========================
  298. # NOTE: Do not run `--install database` in travis-ci!
  299. # It changes DB configuration files and will break everything
  300. # =======================================================
  301. # Add data to mysql
  302. mysql -uroot < config/create.sql
  303. # Setup Postgres
  304. psql --version
  305. sudo useradd benchmarkdbuser -p benchmarkdbpass
  306. sudo -u postgres psql template1 < config/create-postgres-database.sql
  307. sudo -u benchmarkdbuser psql hello_world < config/create-postgres.sql
  308. # Setup Apache Cassandra
  309. sudo dpkg -l cassandra
  310. sudo apt-cache search cassandra
  311. which cqlsh
  312. cqlsh --version
  313. cqlsh -f config/cassandra/create-keyspace.cql
  314. python config/cassandra/db-data-gen.py | cqlsh
  315. # Setup MongoDB (see install above)
  316. mongod --version
  317. until nc -z localhost 27017 ; do echo Waiting for MongoDB; sleep 1; done
  318. mongo < config/create.js
  319. '''
  320. def sh(command):
  321. log.info("Running `%s`", command)
  322. subprocess.check_call(command, shell=True)
  323. for command in script.split('\n'):
  324. command = command.lstrip()
  325. if command != "" and command[0] != '#':
  326. sh(command.lstrip())
  327. if __name__ == "__main__":
  328. args = sys.argv[1:]
  329. usage = '''Usage: toolset/run-ci.py [cisetup|prereq|install|verify] <framework-directory>
  330. run-ci.py selects one test from <framework-directory>/benchark_config, and
  331. automates a number of calls into run-tests.py specific to the selected test.
  332. It is guaranteed to always select the same test from the benchark_config, so
  333. multiple runs with the same <framework-directory> reference the same test.
  334. The name of the selected test will be printed to standard output.
  335. cisetup - configure the Travis-CI environment for our test suite
  336. prereq - trigger standard prerequisite installation
  337. install - trigger server installation for the selected test_directory
  338. verify - run a verification on the selected test using `--mode verify`
  339. run-ci.py expects to be run inside the Travis-CI build environment, and
  340. will expect environment variables such as $TRAVIS_BUILD'''
  341. if len(args) != 2:
  342. print usage
  343. sys.exit(1)
  344. mode = args[0]
  345. testdir = args[1]
  346. if len(args) == 2 and (mode == "install"
  347. or mode == "verify"
  348. or mode == 'prereq'
  349. or mode == 'cisetup'):
  350. runner = CIRunnner(mode, testdir)
  351. else:
  352. print usage
  353. sys.exit(1)
  354. retcode = 0
  355. try:
  356. retcode = runner.run()
  357. except KeyError as ke:
  358. log.warning("Environment key missing, are you running inside Travis-CI?")
  359. print traceback.format_exc()
  360. retcode = 1
  361. except:
  362. log.critical("Unknown error")
  363. print traceback.format_exc()
  364. retcode = 1
  365. finally: # Ensure that logs are printed
  366. # Only print logs if we ran a verify
  367. if mode != 'verify':
  368. sys.exit(retcode)
  369. # Only print logs if we actually did something
  370. if os.path.isfile('.run-ci.should_not_run'):
  371. sys.exit(retcode)
  372. log.error("Running inside Travis-CI, so I will print err and out to console...")
  373. for name in runner.names:
  374. log.error("Test %s", name)
  375. try:
  376. log.error("Here is ERR:")
  377. with open("results/ec2/latest/logs/%s/err.txt" % name, 'r') as err:
  378. for line in err:
  379. log.info(line.rstrip('\n'))
  380. except IOError:
  381. log.error("No ERR file found")
  382. try:
  383. log.error("Here is OUT:")
  384. with open("results/ec2/latest/logs/%s/out.txt" % name, 'r') as out:
  385. for line in out:
  386. log.info(line.rstrip('\n'))
  387. except IOError:
  388. log.error("No OUT file found")
  389. log.error("Running inside Travis-CI, so I will print a copy of the verification summary")
  390. results = None
  391. try:
  392. with open('results/ec2/latest/results.json', 'r') as f:
  393. results = json.load(f)
  394. except IOError:
  395. log.critical("No results.json found, unable to print verification summary")
  396. sys.exit(retcode)
  397. target_dir = setup_util.get_fwroot() + '/frameworks/' + testdir
  398. dirtests = [t for t in gather_tests() if t.directory == target_dir]
  399. # Normally you don't have to use Fore.* before each line, but
  400. # Travis-CI seems to reset color codes on newline (see travis-ci/travis-ci#2692)
  401. # or stream flush, so we have to ensure that the color code is printed repeatedly
  402. prefix = Fore.CYAN
  403. for line in header("Verification Summary", top='=', bottom='').split('\n'):
  404. print prefix + line
  405. for test in dirtests:
  406. print prefix + "| Test: %s" % test.name
  407. if test.name not in runner.names:
  408. print prefix + "| " + Fore.YELLOW + "Unable to verify in Travis-CI"
  409. elif test.name in results['verify'].keys():
  410. for test_type, result in results['verify'][test.name].iteritems():
  411. if result.upper() == "PASS":
  412. color = Fore.GREEN
  413. elif result.upper() == "WARN":
  414. color = Fore.YELLOW
  415. else:
  416. color = Fore.RED
  417. print prefix + "| " + test_type.ljust(11) + ' : ' + color + result.upper()
  418. else:
  419. print prefix + "| " + Fore.RED + "NO RESULTS (Did framework launch?)"
  420. print prefix + header('', top='', bottom='=') + Style.RESET_ALL
  421. sys.exit(retcode)
  422. # vim: set sw=2 ts=2 expandtab