PageRenderTime 53ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/testing/tps/tps/testrunner.py

https://bitbucket.org/bgirard/tiling
Python | 536 lines | 442 code | 42 blank | 52 comment | 50 complexity | 766abd80336eed0f1e514136d6f8d6de MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause, BSD-2-Clause, LGPL-3.0, AGPL-1.0, MPL-2.0-no-copyleft-exception, GPL-2.0, JSON, Apache-2.0, 0BSD, MIT
  1. # ***** BEGIN LICENSE BLOCK *****
  2. # Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3. #
  4. # The contents of this file are subject to the Mozilla Public License Version
  5. # 1.1 (the "License"); you may not use this file except in compliance with
  6. # the License. You may obtain a copy of the License at
  7. # http://www.mozilla.org/MPL/
  8. #
  9. # Software distributed under the License is distributed on an "AS IS" basis,
  10. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11. # for the specific language governing rights and limitations under the
  12. # License.
  13. #
  14. # The Original Code is TPS.
  15. #
  16. # The Initial Developer of the Original Code is
  17. # Mozilla Foundation.
  18. # Portions created by the Initial Developer are Copyright (C) 2011
  19. # the Initial Developer. All Rights Reserved.
  20. #
  21. # Contributor(s):
  22. # Jonathan Griffin <jgriffin@mozilla.com>
  23. #
  24. # Alternatively, the contents of this file may be used under the terms of
  25. # either the GNU General Public License Version 2 or later (the "GPL"), or
  26. # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27. # in which case the provisions of the GPL or the LGPL are applicable instead
  28. # of those above. If you wish to allow use of your version of this file only
  29. # under the terms of either the GPL or the LGPL, and not to allow others to
  30. # use your version of this file under the terms of the MPL, indicate your
  31. # decision by deleting the provisions above and replace them with the notice
  32. # and other provisions required by the GPL or the LGPL. If you do not delete
  33. # the provisions above, a recipient may use your version of this file under
  34. # the terms of any one of the MPL, the GPL or the LGPL.
  35. #
  36. # ***** END LICENSE BLOCK *****
  37. import httplib
  38. import json
  39. import os
  40. import platform
  41. import random
  42. import re
  43. import socket
  44. import tempfile
  45. import time
  46. import traceback
  47. import urllib
  48. from threading import RLock
  49. from mozprofile import Profile
  50. from tps.firefoxrunner import TPSFirefoxRunner
  51. from tps.phase import TPSTestPhase
  52. from tps.mozhttpd import MozHttpd
  53. class TempFile(object):
  54. """Class for temporary files that delete themselves when garbage-collected.
  55. """
  56. def __init__(self, prefix=None):
  57. self.fd, self.filename = self.tmpfile = tempfile.mkstemp(prefix=prefix)
  58. def write(self, data):
  59. if self.fd:
  60. os.write(self.fd, data)
  61. def close(self):
  62. if self.fd:
  63. os.close(self.fd)
  64. self.fd = None
  65. def cleanup(self):
  66. if self.fd:
  67. self.close()
  68. if os.access(self.filename, os.F_OK):
  69. os.remove(self.filename)
  70. __del__ = cleanup
  71. class TPSTestRunner(object):
  72. default_env = { 'MOZ_CRASHREPORTER_DISABLE': '1',
  73. 'GNOME_DISABLE_CRASH_DIALOG': '1',
  74. 'XRE_NO_WINDOWS_CRASH_DIALOG': '1',
  75. 'MOZ_NO_REMOTE': '1',
  76. 'XPCOM_DEBUG_BREAK': 'warn',
  77. }
  78. default_preferences = { 'app.update.enabled' : False,
  79. 'extensions.getAddons.get.url': 'http://127.0.0.1:4567/en-US/firefox/api/%API_VERSION%/search/guid:%IDS%',
  80. 'extensions.update.enabled' : False,
  81. 'extensions.update.notifyUser' : False,
  82. 'browser.shell.checkDefaultBrowser' : False,
  83. 'browser.tabs.warnOnClose' : False,
  84. 'browser.warnOnQuit': False,
  85. 'browser.sessionstore.resume_from_crash': False,
  86. 'services.sync.addons.ignoreRepositoryChecking': True,
  87. 'services.sync.firstSync': 'notReady',
  88. 'services.sync.lastversion': '1.0',
  89. 'services.sync.log.rootLogger': 'Trace',
  90. 'services.sync.log.logger.engine.addons': 'Trace',
  91. 'services.sync.log.logger.service.main': 'Trace',
  92. 'services.sync.log.logger.engine.bookmarks': 'Trace',
  93. 'services.sync.log.appender.console': 'Trace',
  94. 'services.sync.log.appender.debugLog.enabled': True,
  95. 'toolkit.startup.max_resumed_crashes': -1,
  96. 'browser.dom.window.dump.enabled': True,
  97. # Allow installing extensions dropped into the profile folder
  98. 'extensions.autoDisableScopes': 10,
  99. # Don't open a dialog to show available add-on updates
  100. 'extensions.update.notifyUser' : False,
  101. }
  102. syncVerRe = re.compile(
  103. r"Sync version: (?P<syncversion>.*)\n")
  104. ffVerRe = re.compile(
  105. r"Firefox version: (?P<ffver>.*)\n")
  106. ffDateRe = re.compile(
  107. r"Firefox builddate: (?P<ffdate>.*)\n")
  108. def __init__(self, extensionDir, emailresults=False, testfile="sync.test",
  109. binary=None, config=None, rlock=None, mobile=False,
  110. autolog=False, logfile="tps.log",
  111. ignore_unused_engines=False):
  112. self.extensions = []
  113. self.emailresults = emailresults
  114. self.testfile = testfile
  115. self.logfile = os.path.abspath(logfile)
  116. self.binary = binary
  117. self.ignore_unused_engines = ignore_unused_engines
  118. self.config = config if config else {}
  119. self.repo = None
  120. self.changeset = None
  121. self.branch = None
  122. self.numfailed = 0
  123. self.numpassed = 0
  124. self.nightly = False
  125. self.rlock = rlock
  126. self.mobile = mobile
  127. self.autolog = autolog
  128. self.tpsxpi = None
  129. self.firefoxRunner = None
  130. self.extensionDir = extensionDir
  131. self.productversion = None
  132. self.addonversion = None
  133. self.postdata = {}
  134. self.errorlogs = {}
  135. @property
  136. def mobile(self):
  137. return self._mobile
  138. @mobile.setter
  139. def mobile(self, value):
  140. self._mobile = value
  141. self.synctype = 'desktop' if not self._mobile else 'mobile'
  142. def log(self, msg, printToConsole=False):
  143. """Appends a string to the logfile"""
  144. f = open(self.logfile, 'a')
  145. f.write(msg)
  146. f.close()
  147. if printToConsole:
  148. print msg
  149. def _zip_add_file(self, zip, file, rootDir):
  150. zip.write(os.path.join(rootDir, file), file)
  151. def _zip_add_dir(self, zip, dir, rootDir):
  152. try:
  153. zip.write(os.path.join(rootDir, dir), dir)
  154. except:
  155. # on some OS's, adding directory entries doesn't seem to work
  156. pass
  157. for root, dirs, files in os.walk(os.path.join(rootDir, dir)):
  158. for f in files:
  159. zip.write(os.path.join(root, f), os.path.join(dir, f))
  160. def run_single_test(self, testdir, testname):
  161. testpath = os.path.join(testdir, testname)
  162. self.log("Running test %s\n" % testname)
  163. # Create a random account suffix that is used when creating test
  164. # accounts on a staging server.
  165. account_suffix = {"account-suffix": ''.join([str(random.randint(0,9))
  166. for i in range(1,6)])}
  167. self.config['account'].update(account_suffix)
  168. # Read and parse the test file, merge it with the contents of the config
  169. # file, and write the combined output to a temporary file.
  170. f = open(testpath, 'r')
  171. testcontent = f.read()
  172. f.close()
  173. try:
  174. test = json.loads(testcontent)
  175. except:
  176. test = json.loads(testcontent[testcontent.find("{"):testcontent.find("}") + 1])
  177. testcontent += 'var config = %s;\n' % json.dumps(self.config, indent=2)
  178. testcontent += 'var seconds_since_epoch = %d;\n' % int(time.time())
  179. tmpfile = TempFile(prefix='tps_test_')
  180. tmpfile.write(testcontent)
  181. tmpfile.close()
  182. # generate the profiles defined in the test, and a list of test phases
  183. profiles = {}
  184. phaselist = []
  185. for phase in test:
  186. profilename = test[phase]
  187. # create the profile if necessary
  188. if not profilename in profiles:
  189. profiles[profilename] = Profile(preferences = self.preferences,
  190. addons = self.extensions)
  191. # create the test phase
  192. phaselist.append(TPSTestPhase(
  193. phase,
  194. profiles[profilename],
  195. testname,
  196. tmpfile.filename,
  197. self.logfile,
  198. self.env,
  199. self.firefoxRunner,
  200. self.log,
  201. ignore_unused_engines=self.ignore_unused_engines))
  202. # sort the phase list by name
  203. phaselist = sorted(phaselist, key=lambda phase: phase.phase)
  204. # run each phase in sequence, aborting at the first failure
  205. for phase in phaselist:
  206. phase.run()
  207. # if a failure occurred, dump the entire sync log into the test log
  208. if phase.status != "PASS":
  209. for profile in profiles:
  210. self.log("\nDumping sync log for profile %s\n" % profiles[profile].profile)
  211. for root, dirs, files in os.walk(os.path.join(profiles[profile].profile, 'weave', 'logs')):
  212. for f in files:
  213. weavelog = os.path.join(profiles[profile].profile, 'weave', 'logs', f)
  214. if os.access(weavelog, os.F_OK):
  215. with open(weavelog, 'r') as fh:
  216. for line in fh:
  217. possible_time = line[0:13]
  218. if len(possible_time) == 13 and possible_time.isdigit():
  219. time_ms = int(possible_time)
  220. formatted = time.strftime('%Y-%m-%d %H:%M:%S',
  221. time.localtime(time_ms / 1000))
  222. self.log('%s.%03d %s' % (
  223. formatted, time_ms % 1000, line[14:] ))
  224. else:
  225. self.log(line)
  226. break;
  227. # grep the log for FF and sync versions
  228. f = open(self.logfile)
  229. logdata = f.read()
  230. match = self.syncVerRe.search(logdata)
  231. sync_version = match.group("syncversion") if match else 'unknown'
  232. match = self.ffVerRe.search(logdata)
  233. firefox_version = match.group("ffver") if match else 'unknown'
  234. match = self.ffDateRe.search(logdata)
  235. firefox_builddate = match.group("ffdate") if match else 'unknown'
  236. f.close()
  237. if phase.status == 'PASS':
  238. logdata = ''
  239. else:
  240. # we only care about the log data for this specific test
  241. logdata = logdata[logdata.find('Running test %s' % (str(testname))):]
  242. result = {
  243. 'PASS': lambda x: ('TEST-PASS', ''),
  244. 'FAIL': lambda x: ('TEST-UNEXPECTED-FAIL', x.rstrip()),
  245. 'unknown': lambda x: ('TEST-UNEXPECTED-FAIL', 'test did not complete')
  246. } [phase.status](phase.errline)
  247. logstr = "\n%s | %s%s\n" % (result[0], testname, (' | %s' % result[1] if result[1] else ''))
  248. try:
  249. repoinfo = self.firefoxRunner.runner.get_repositoryInfo()
  250. except:
  251. repoinfo = {}
  252. apprepo = repoinfo.get('application_repository', '')
  253. appchangeset = repoinfo.get('application_changeset', '')
  254. # save logdata to a temporary file for posting to the db
  255. tmplogfile = None
  256. if logdata:
  257. tmplogfile = TempFile(prefix='tps_log_')
  258. tmplogfile.write(logdata)
  259. tmplogfile.close()
  260. self.errorlogs[testname] = tmplogfile
  261. resultdata = ({ "productversion": { "version": firefox_version,
  262. "buildid": firefox_builddate,
  263. "builddate": firefox_builddate[0:8],
  264. "product": "Firefox",
  265. "repository": apprepo,
  266. "changeset": appchangeset,
  267. },
  268. "addonversion": { "version": sync_version,
  269. "product": "Firefox Sync" },
  270. "name": testname,
  271. "message": result[1],
  272. "state": result[0],
  273. "logdata": logdata
  274. })
  275. self.log(logstr, True)
  276. for phase in phaselist:
  277. print "\t%s: %s" % (phase.phase, phase.status)
  278. if phase.status == 'FAIL':
  279. break
  280. return resultdata
  281. def run_tests(self):
  282. # delete the logfile if it already exists
  283. if os.access(self.logfile, os.F_OK):
  284. os.remove(self.logfile)
  285. # Make a copy of the default env variables and preferences, and update
  286. # them for mobile settings if needed.
  287. self.env = self.default_env.copy()
  288. self.preferences = self.default_preferences.copy()
  289. if self.mobile:
  290. self.preferences.update({'services.sync.client.type' : 'mobile'})
  291. # Acquire a lock to make sure no other threads are running tests
  292. # at the same time.
  293. if self.rlock:
  294. self.rlock.acquire()
  295. try:
  296. # Create the Firefox runner, which will download and install the
  297. # build, as needed.
  298. if not self.firefoxRunner:
  299. self.firefoxRunner = TPSFirefoxRunner(self.binary)
  300. # now, run the test group
  301. self.run_test_group()
  302. except:
  303. traceback.print_exc()
  304. self.numpassed = 0
  305. self.numfailed = 1
  306. if self.emailresults:
  307. try:
  308. self.sendEmail('<pre>%s</pre>' % traceback.format_exc(),
  309. sendTo='crossweave@mozilla.com')
  310. except:
  311. traceback.print_exc()
  312. else:
  313. raise
  314. else:
  315. try:
  316. if self.autolog:
  317. self.postToAutolog()
  318. if self.emailresults:
  319. self.sendEmail()
  320. except:
  321. traceback.print_exc()
  322. try:
  323. self.sendEmail('<pre>%s</pre>' % traceback.format_exc(),
  324. sendTo='crossweave@mozilla.com')
  325. except:
  326. traceback.print_exc()
  327. # release our lock
  328. if self.rlock:
  329. self.rlock.release()
  330. # dump out a summary of test results
  331. print 'Test Summary\n'
  332. for test in self.postdata.get('tests', {}):
  333. print '%s | %s | %s' % (test['state'], test['name'], test['message'])
  334. def run_test_group(self):
  335. self.results = []
  336. self.extensions = []
  337. # set the OS we're running on
  338. os_string = platform.uname()[2] + " " + platform.uname()[3]
  339. if os_string.find("Darwin") > -1:
  340. os_string = "Mac OS X " + platform.mac_ver()[0]
  341. if platform.uname()[0].find("Linux") > -1:
  342. os_string = "Linux " + platform.uname()[5]
  343. if platform.uname()[0].find("Win") > -1:
  344. os_string = "Windows " + platform.uname()[3]
  345. # reset number of passed/failed tests
  346. self.numpassed = 0
  347. self.numfailed = 0
  348. # build our tps.xpi extension
  349. self.extensions.append(os.path.join(self.extensionDir, 'tps'))
  350. self.extensions.append(os.path.join(self.extensionDir, "mozmill"))
  351. # build the test list
  352. try:
  353. f = open(self.testfile)
  354. jsondata = f.read()
  355. f.close()
  356. testfiles = json.loads(jsondata)
  357. testlist = testfiles['tests']
  358. except ValueError:
  359. testlist = [os.path.basename(self.testfile)]
  360. testdir = os.path.dirname(self.testfile)
  361. self.mozhttpd = MozHttpd(port=4567, docroot=testdir)
  362. self.mozhttpd.start()
  363. # run each test, and save the results
  364. for test in testlist:
  365. result = self.run_single_test(testdir, test)
  366. if not self.productversion:
  367. self.productversion = result['productversion']
  368. if not self.addonversion:
  369. self.addonversion = result['addonversion']
  370. self.results.append({'state': result['state'],
  371. 'name': result['name'],
  372. 'message': result['message'],
  373. 'logdata': result['logdata']})
  374. if result['state'] == 'TEST-PASS':
  375. self.numpassed += 1
  376. else:
  377. self.numfailed += 1
  378. self.mozhttpd.stop()
  379. # generate the postdata we'll use to post the results to the db
  380. self.postdata = { 'tests': self.results,
  381. 'os':os_string,
  382. 'testtype': 'crossweave',
  383. 'productversion': self.productversion,
  384. 'addonversion': self.addonversion,
  385. 'synctype': self.synctype,
  386. }
  387. def sendEmail(self, body=None, sendTo=None):
  388. # send the result e-mail
  389. if self.config.get('email') and self.config['email'].get('username') \
  390. and self.config['email'].get('password'):
  391. from tps.sendemail import SendEmail
  392. from tps.emailtemplate import GenerateEmailBody
  393. if body is None:
  394. buildUrl = None
  395. if self.firefoxRunner and self.firefoxRunner.url:
  396. buildUrl = self.firefoxRunner.url
  397. body = GenerateEmailBody(self.postdata,
  398. self.numpassed,
  399. self.numfailed,
  400. self.config['account']['serverURL'],
  401. buildUrl)
  402. subj = "TPS Report: "
  403. if self.numfailed == 0 and self.numpassed > 0:
  404. subj += "YEEEAAAHHH"
  405. else:
  406. subj += "PC LOAD LETTER"
  407. changeset = self.postdata['productversion']['changeset'] if \
  408. self.postdata and self.postdata.get('productversion') and \
  409. self.postdata['productversion'].get('changeset') \
  410. else 'unknown'
  411. subj +=", changeset " + changeset + "; " + str(self.numfailed) + \
  412. " failed, " + str(self.numpassed) + " passed"
  413. To = [sendTo] if sendTo else None
  414. if not To:
  415. if self.numfailed > 0 or self.numpassed == 0:
  416. To = self.config['email'].get('notificationlist')
  417. else:
  418. To = self.config['email'].get('passednotificationlist')
  419. if To:
  420. SendEmail(From=self.config['email']['username'],
  421. To=To,
  422. Subject=subj,
  423. HtmlData=body,
  424. Username=self.config['email']['username'],
  425. Password=self.config['email']['password'])
  426. def postToAutolog(self):
  427. from mozautolog import RESTfulAutologTestGroup as AutologTestGroup
  428. group = AutologTestGroup(
  429. harness='crossweave',
  430. testgroup='crossweave-%s' % self.synctype,
  431. server=self.config.get('es'),
  432. restserver=self.config.get('restserver'),
  433. machine=socket.gethostname(),
  434. platform=self.config.get('platform', None),
  435. os=self.config.get('os', None),
  436. )
  437. tree = self.postdata['productversion']['repository']
  438. group.set_primary_product(
  439. tree=tree[tree.rfind("/")+1:],
  440. version=self.postdata['productversion']['version'],
  441. buildid=self.postdata['productversion']['buildid'],
  442. buildtype='opt',
  443. revision=self.postdata['productversion']['changeset'],
  444. )
  445. group.add_test_suite(
  446. passed=self.numpassed,
  447. failed=self.numfailed,
  448. todo=0,
  449. )
  450. for test in self.results:
  451. if test['state'] != "TEST-PASS":
  452. errorlog = self.errorlogs.get(test['name'])
  453. errorlog_filename = errorlog.filename if errorlog else None
  454. group.add_test_failure(
  455. test = test['name'],
  456. status = test['state'],
  457. text = test['message'],
  458. logfile = errorlog_filename
  459. )
  460. group.submit()
  461. # Iterate through all testfailure objects, and update the postdata
  462. # dict with the testfailure logurl's, if any.
  463. for tf in group.testsuites[-1].testfailures:
  464. result = [x for x in self.results if x.get('name') == tf.test]
  465. if not result:
  466. continue
  467. result[0]['logurl'] = tf.logurl