PageRenderTime 54ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/testing/tps/tps/testrunner.py

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