reporters.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. # -*- coding: utf-8 -*-
  2. """!@package grass.gunittest.reporters
  3. @brief GRASS Python testing framework module for report generation
  4. Copyright (C) 2014 by the GRASS Development Team
  5. This program is free software under the GNU General Public
  6. License (>=v2). Read the file COPYING that comes with GRASS GIS
  7. for details.
  8. @author Vaclav Petras
  9. """
  10. import os
  11. import datetime
  12. import xml.sax.saxutils as saxutils
  13. import xml.etree.ElementTree as et
  14. import subprocess
  15. from .utils import ensure_dir
  16. from .checkers import text_to_keyvalue
  17. def get_source_url(path, revision, line=None):
  18. """
  19. :param path: directory or file path relative to remote repository root
  20. :param revision: SVN revision (should be a number)
  21. :param line: line in the file (should be None for directories)
  22. """
  23. tracurl = 'http://trac.osgeo.org/grass/browser/'
  24. if line:
  25. return '{tracurl}{path}?rev={revision}#L{line}'.format(**locals())
  26. else:
  27. return '{tracurl}{path}?rev={revision}'.format(**locals())
  28. def html_escape(text):
  29. """Escape ``'&'``, ``'<'``, and ``'>'`` in a string of data."""
  30. return saxutils.escape(text)
  31. def html_unescape(text):
  32. """Unescape ``'&amp;'``, ``'&lt;'``, and ``'&gt;'`` in a string of data."""
  33. return saxutils.unescape(text)
  34. def color_error_line(line):
  35. if line.startswith('ERROR: '):
  36. # TODO: use CSS class
  37. # ignoring the issue with \n at the end, HTML don't mind
  38. line = '<span style="color: red">' + line + "</span>"
  39. if line.startswith('FAIL: '):
  40. # TODO: use CSS class
  41. # ignoring the issue with \n at the end, HTML don't mind
  42. line = '<span style="color: red">' + line + "</span>"
  43. if line.startswith('WARNING: '):
  44. # TODO: use CSS class
  45. # ignoring the issue with \n at the end, HTML don't mind
  46. line = '<span style="color: blue">' + line + "</span>"
  47. #if line.startswith('Traceback ('):
  48. # line = '<span style="color: red">' + line + "</span>"
  49. return line
  50. def get_svn_revision():
  51. """Get SVN revision number
  52. :returns: SVN revision number as string or None if it is
  53. not possible to get
  54. """
  55. # TODO: here should be starting directory
  56. # but now we are using current as starting
  57. p = subprocess.Popen(['svnversion', '.'],
  58. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  59. stdout, stderr = p.communicate()
  60. rc = p.poll()
  61. if not rc:
  62. stdout = stdout.strip()
  63. if stdout.endswith('M'):
  64. stdout = stdout[:-1]
  65. if ':' in stdout:
  66. # the first one is the one of source code
  67. stdout = stdout.split(':')[0]
  68. return stdout
  69. else:
  70. return None
  71. def get_svn_info():
  72. """Get important information from ``svn info``
  73. :returns: SVN info as dictionary or None
  74. if it is not possible to obtain it
  75. """
  76. try:
  77. # TODO: introduce directory, not only current
  78. p = subprocess.Popen(['svn', 'info', '.', '--xml'],
  79. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  80. stdout, stderr = p.communicate()
  81. rc = p.poll()
  82. info = {}
  83. if not rc:
  84. root = et.fromstring(stdout)
  85. # TODO: get also date if this make sense
  86. # expecting only one <entry> element
  87. entry = root.find('entry')
  88. info['revision'] = entry.get('revision')
  89. info['url'] = entry.find('url').text
  90. relurl = entry.find('relative-url')
  91. # element which is not found is None
  92. # empty element would be bool(el) == False
  93. if relurl is not None:
  94. relurl = relurl.text
  95. # relative path has ^ at the beginning in SVN version 1.8.8
  96. if relurl.startswith('^'):
  97. relurl = relurl[1:]
  98. else:
  99. # SVN version 1.8.8 supports relative-url but older do not
  100. # so, get relative part from absolute URL
  101. const_url_part = 'https://svn.osgeo.org/grass/'
  102. relurl = info['url'][len(const_url_part):]
  103. info['relative-url'] = relurl
  104. return info
  105. # TODO: add this to svnversion function
  106. except OSError as e:
  107. import errno
  108. # ignore No such file or directory
  109. if e.errno != errno.ENOENT:
  110. raise
  111. return None
  112. def years_ago(date, years):
  113. # dateutil relative delte would be better but this is more portable
  114. return date - datetime.timedelta(weeks=years * 52)
  115. # TODO: these functions should be called only if we know that svn is installed
  116. # this will simplify the functions, caller must handle it anyway
  117. def get_svn_path_authors(path, from_date=None):
  118. """
  119. :returns: a set of authors
  120. """
  121. if from_date is None:
  122. # this is the SVN default for local copies
  123. revision_range = 'BASE:1'
  124. else:
  125. revision_range = 'BASE:{%s}' % from_date
  126. try:
  127. # TODO: allow also usage of --limit
  128. p = subprocess.Popen(['svn', 'log', '--xml',
  129. '--revision', revision_range, path],
  130. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  131. stdout, stderr = p.communicate()
  132. rc = p.poll()
  133. if not rc:
  134. root = et.fromstring(stdout)
  135. # TODO: get also date if this make sense
  136. # expecting only one <entry> element
  137. author_nodes = root.iterfind('*/author')
  138. authors = [n.text for n in author_nodes]
  139. return set(authors)
  140. except OSError as e:
  141. import errno
  142. # ignore No such file or directory
  143. if e.errno != errno.ENOENT:
  144. raise
  145. return None
  146. def get_html_test_authors_table(directory, tests_authors):
  147. # SVN gives us authors of code together with authors of tests
  148. # so test code authors list also contains authors of tests only
  149. # TODO: don't do this for the top level directories?
  150. tests_authors = set(tests_authors)
  151. from_date = years_ago(datetime.date.today(), years=1)
  152. tested_dir_authors = get_svn_path_authors(directory, from_date)
  153. not_testing_authors = tested_dir_authors - tests_authors
  154. if not not_testing_authors:
  155. not_testing_authors = ['all recent authors contributed tests']
  156. test_authors = (
  157. '<h3>Code and test authors</h3>'
  158. '<p style="font-size: 60%"><em>'
  159. 'Note that determination of authors is approximate and only'
  160. ' recent code authors are considered.'
  161. '</em></p>'
  162. '<table><tbody>'
  163. '<tr><td>Test authors:</td><td>{file_authors}</td></tr>'
  164. '<tr><td>Authors of tested code:</td><td>{code_authors}</td></tr>'
  165. '<tr><td>Authors owing tests:</td><td>{not_testing}</td></tr>'
  166. '</tbody></table>'
  167. .format(
  168. file_authors=', '.join(sorted(tests_authors)),
  169. code_authors=', '.join(sorted(tested_dir_authors)),
  170. not_testing=', '.join(sorted(not_testing_authors))
  171. ))
  172. return test_authors
  173. class GrassTestFilesMultiReporter(object):
  174. def __init__(self, reporters, forgiving=False):
  175. self.reporters = reporters
  176. self.forgiving = forgiving
  177. def start(self, results_dir):
  178. # TODO: no directory cleaning (self.clean_before)? now cleaned by caller
  179. # TODO: perhaps only those whoe need it should do it (even multiple times)
  180. # and there is also the delet problem
  181. ensure_dir(os.path.abspath(results_dir))
  182. for reporter in self.reporters:
  183. try:
  184. reporter.start(results_dir)
  185. except AttributeError:
  186. if self.forgiving:
  187. pass
  188. else:
  189. raise
  190. def finish(self):
  191. for reporter in self.reporters:
  192. try:
  193. reporter.finish()
  194. except AttributeError:
  195. if self.forgiving:
  196. pass
  197. else:
  198. raise
  199. def start_file_test(self, module):
  200. for reporter in self.reporters:
  201. try:
  202. reporter.start_file_test(module)
  203. except AttributeError:
  204. if self.forgiving:
  205. pass
  206. else:
  207. raise
  208. def end_file_test(self, **kwargs):
  209. for reporter in self.reporters:
  210. try:
  211. reporter.end_file_test(**kwargs)
  212. except AttributeError:
  213. if self.forgiving:
  214. pass
  215. else:
  216. raise
  217. class GrassTestFilesCountingReporter(object):
  218. def __init__(self):
  219. self.test_files = None
  220. self.files_fail = None
  221. self.files_pass = None
  222. self.file_pass_per = None
  223. self.file_fail_per = None
  224. self.main_start_time = None
  225. self.main_end_time = None
  226. self.main_time = None
  227. self.file_start_time = None
  228. self.file_end_time = None
  229. self.file_time = None
  230. self._start_file_test_called = False
  231. def start(self, results_dir):
  232. self.test_files = 0
  233. self.files_fail = 0
  234. self.files_pass = 0
  235. # this might be moved to some report start method
  236. self.main_start_time = datetime.datetime.now()
  237. def finish(self):
  238. self.main_end_time = datetime.datetime.now()
  239. self.main_time = self.main_end_time - self.main_start_time
  240. assert self.test_files == self.files_fail + self.files_pass
  241. self.file_pass_per = 100 * float(self.files_pass) / self.test_files
  242. self.file_fail_per = 100 * float(self.files_fail) / self.test_files
  243. def start_file_test(self, module):
  244. self.file_start_time = datetime.datetime.now()
  245. self._start_file_test_called = True
  246. self.test_files += 1
  247. def end_file_test(self, returncode, **kwargs):
  248. assert self._start_file_test_called
  249. self.file_end_time = datetime.datetime.now()
  250. self.file_time = self.file_end_time - self.file_start_time
  251. if returncode:
  252. self.files_fail += 1
  253. else:
  254. self.files_pass += 1
  255. self._start_file_test_called = False
  256. def percent_to_html(percent):
  257. if percent > 100 or percent < 0:
  258. return "? {:.2f}% ?".format(percent)
  259. elif percent < 40:
  260. color = 'red'
  261. elif percent < 70:
  262. color = 'orange'
  263. else:
  264. color = 'green'
  265. return '<span style="color: {color}">{percent:.0f}%</span>'.format(
  266. percent=percent, color=color)
  267. def wrap_stdstream_to_html(infile, outfile, module, stream):
  268. before = '<html><body><h1>%s</h1><pre>' % (module.name + ' ' + stream)
  269. after = '</pre></body></html>'
  270. html = open(outfile, 'w')
  271. html.write(before)
  272. with open(infile) as text:
  273. for line in text:
  274. html.write(color_error_line(html_escape(line)))
  275. html.write(after)
  276. html.close()
  277. def returncode_to_html_text(returncode):
  278. if returncode:
  279. return '<span style="color: red">FAILED</span>'
  280. else:
  281. # alternatives: SUCCEEDED, passed, OK
  282. return '<span style="color: green">succeeded</span>'
  283. def returncode_to_html_sentence(returncode):
  284. if returncode:
  285. return ('<span style="color: red">&#x274c;</span>'
  286. ' Test failed (return code %d)' % (returncode))
  287. else:
  288. return ('<span style="color: green">&#x2713;</span>'
  289. ' Test succeeded (return code %d)' % (returncode))
  290. def success_to_html_text(total, successes):
  291. if successes < total:
  292. return '<span style="color: red">FAILED</span>'
  293. elif successes == total:
  294. # alternatives: SUCCEEDED, passed, OK
  295. return '<span style="color: green">succeeded</span>'
  296. else:
  297. return ('<span style="color: red; font-size: 60%">'
  298. '? more successes than total ?</span>')
  299. UNKNOWN_NUMBER_HTML = '<span style="font-size: 60%">unknown</span>'
  300. def success_to_html_percent(total, successes):
  301. if total:
  302. pass_per = 100 * (float(successes) / total)
  303. pass_per = percent_to_html(pass_per)
  304. else:
  305. pass_per = UNKNOWN_NUMBER_HTML
  306. return pass_per
  307. class GrassTestFilesHtmlReporter(GrassTestFilesCountingReporter):
  308. unknown_number = UNKNOWN_NUMBER_HTML
  309. def __init__(self):
  310. super(GrassTestFilesHtmlReporter, self).__init__()
  311. self.main_index = None
  312. def start(self, results_dir):
  313. super(GrassTestFilesHtmlReporter, self).start(results_dir)
  314. # having all variables public although not really part of API
  315. self.main_index = open(os.path.join(results_dir, 'index.html'), 'w')
  316. # TODO: this can be moved to the counter class
  317. self.failures = 0
  318. self.errors = 0
  319. self.skipped = 0
  320. self.successes = 0
  321. self.expected_failures = 0
  322. self.unexpected_success = 0
  323. self.total = 0
  324. svn_info = get_svn_info()
  325. if not svn_info:
  326. svn_text = ('<span style="font-size: 60%">'
  327. 'SVN revision cannot be be obtained'
  328. '</span>')
  329. else:
  330. url = get_source_url(path=svn_info['relative-url'],
  331. revision=svn_info['revision'])
  332. svn_text = ('SVN revision'
  333. ' <a href="{url}">'
  334. '{rev}</a>'
  335. ).format(url=url, rev=svn_info['revision'])
  336. self.main_index.write('<html><body>'
  337. '<h1>Test results</h1>'
  338. '{time:%Y-%m-%d %H:%M:%S}'
  339. ' ({svn})'
  340. '<table>'
  341. '<thead><tr>'
  342. '<th>Tested directory</th>'
  343. '<th>Test file</th>'
  344. '<th>Status</th>'
  345. '<th>Tests</th><th>Successful</td>'
  346. '<th>Failed</th><th>Percent successful</th>'
  347. '</tr></thead><tbody>'.format(
  348. time=self.main_start_time,
  349. svn=svn_text))
  350. def finish(self):
  351. super(GrassTestFilesHtmlReporter, self).finish()
  352. if self.total:
  353. pass_per = 100 * (float(self.successes) / self.total)
  354. pass_per = percent_to_html(pass_per)
  355. else:
  356. pass_per = self.unknown_number
  357. tfoot = ('<tfoot>'
  358. '<tr>'
  359. '<td>Summary</td>'
  360. '<td>{nfiles} test files</td>'
  361. '<td>{nsper}</td>'
  362. '<td>{total}</td><td>{st}</td><td>{ft}</td><td>{pt}</td>'
  363. '</tr>'
  364. '</tfoot>'.format(
  365. nfiles=self.test_files,
  366. nsper=percent_to_html(self.file_pass_per),
  367. st=self.successes, ft=self.failures + self.errors,
  368. total=self.total, pt=pass_per
  369. ))
  370. summary_sentence = ('Executed {nfiles} test files in {time:}.'
  371. ' From them'
  372. ' {nsfiles} files ({nsper:.0f}%) were successful'
  373. ' and {nffiles} files ({nfper:.0f}%) failed.'
  374. .format(
  375. nfiles=self.test_files,
  376. time=self.main_time,
  377. nsfiles=self.files_pass,
  378. nffiles=self.files_fail,
  379. nsper=self.file_pass_per,
  380. nfper=self.file_fail_per))
  381. self.main_index.write('<tbody>{tfoot}</table>'
  382. '<p>{summary}</p>'
  383. '</body></html>'
  384. .format(
  385. tfoot=tfoot,
  386. summary=summary_sentence))
  387. self.main_index.close()
  388. def start_file_test(self, module):
  389. super(GrassTestFilesHtmlReporter, self).start_file_test(module)
  390. self.main_index.flush() # to get previous lines to the report
  391. def end_file_test(self, module, cwd, returncode, stdout, stderr,
  392. test_summary):
  393. super(GrassTestFilesHtmlReporter, self).end_file_test(
  394. module=module, cwd=cwd, returncode=returncode,
  395. stdout=stdout, stderr=stderr)
  396. # TODO: considering others accoring to total, OK?
  397. total = test_summary.get('total', None)
  398. failures = test_summary.get('failures', 0)
  399. errors = test_summary.get('errors', 0)
  400. # Python unittest TestResult class is reporting success for no
  401. # errors or failures, so skipped, expected failures and unexpected
  402. # success are ignored
  403. # but successful tests are only total - the others
  404. # TODO: add success counter to GrassTestResult base class
  405. skipped = test_summary.get('skipped', 0)
  406. expected_failures = test_summary.get('expected_failures', 0)
  407. unexpected_successes = test_summary.get('unexpected_successes', 0)
  408. successes = test_summary.get('successes', 0)
  409. self.failures += failures
  410. self.errors += errors
  411. self.skipped += skipped
  412. self.expected_failures += expected_failures
  413. self.unexpected_success += unexpected_successes
  414. # TODO: should we test for zero?
  415. if total is not None:
  416. # success are only the clear ones
  417. # percentage is influenced by all but putting only failures to table
  418. self.successes += successes
  419. self.total += total
  420. pass_per = 100 * (float(successes) / total)
  421. pass_per = percent_to_html(pass_per)
  422. else:
  423. total = successes = pass_per = self.unknown_number
  424. bad_ones = failures + errors
  425. self.main_index.write(
  426. '<tr><td>{d}</td>'
  427. '<td><a href="{d}/{m}/index.html">{m}</a></td><td>{sf}</td>'
  428. '<td>{total}</td><td>{st}</td><td>{ft}</td><td>{pt}</td>'
  429. '<tr>'.format(
  430. d=module.tested_dir, m=module.name,
  431. sf=returncode_to_html_text(returncode),
  432. st=successes, ft=bad_ones, total=total, pt=pass_per))
  433. wrap_stdstream_to_html(infile=stdout,
  434. outfile=os.path.join(cwd, 'stdout.html'),
  435. module=module, stream='stdout')
  436. wrap_stdstream_to_html(infile=stderr,
  437. outfile=os.path.join(cwd, 'stderr.html'),
  438. module=module, stream='stderr')
  439. file_index_path = os.path.join(cwd, 'index.html')
  440. file_index = open(file_index_path, 'w')
  441. file_index.write(
  442. '<html><body>'
  443. '<h1>{m.name}</h1>'
  444. '<h2>{m.tested_dir} &ndash; {m.name}</h2>'
  445. '<p>{status}'
  446. '<p>Test duration: {dur}'
  447. '<ul>'
  448. '<li><a href="stdout.html">standard output (stdout)</a>'
  449. '<li><a href="stderr.html">standard error output (stderr)</a>'
  450. '<li><a href="testcodecoverage/index.html">code coverage</a>'
  451. '</ul>'
  452. '</body></html>'
  453. .format(
  454. dur=self.file_time, m=module,
  455. status=returncode_to_html_sentence(returncode),
  456. ))
  457. file_index.close()
  458. if returncode:
  459. pass
  460. # TODO: here we don't have oportunity to write error file
  461. # to stream (stdout/stderr)
  462. # a stream can be added and if not none, we could write
  463. class GrassTestFilesTextReporter(GrassTestFilesCountingReporter):
  464. def __init__(self, stream):
  465. super(GrassTestFilesTextReporter, self).__init__()
  466. self._stream = stream
  467. def start(self, results_dir):
  468. super(GrassTestFilesTextReporter, self).start(results_dir)
  469. def finish(self):
  470. super(GrassTestFilesTextReporter, self).finish()
  471. summary_sentence = ('\nExecuted {nfiles} test files in {time:}.'
  472. '\nFrom them'
  473. ' {nsfiles} files ({nsper:.0f}%) were successful'
  474. ' and {nffiles} files ({nfper:.0f}%) failed.\n'
  475. .format(
  476. nfiles=self.test_files,
  477. time=self.main_time,
  478. nsfiles=self.files_pass,
  479. nffiles=self.files_fail,
  480. nsper=self.file_pass_per,
  481. nfper=self.file_fail_per))
  482. self._stream.write(summary_sentence)
  483. def start_file_test(self, module):
  484. super(GrassTestFilesTextReporter, self).start_file_test(module)
  485. self._stream.flush() # to get previous lines to the report
  486. def end_file_test(self, module, cwd, returncode, stdout, stderr,
  487. test_summary):
  488. super(GrassTestFilesTextReporter, self).end_file_test(
  489. module=module, cwd=cwd, returncode=returncode,
  490. stdout=stdout, stderr=stderr)
  491. if returncode:
  492. self._stream.write(
  493. '{m} from {d} failed'
  494. .format(
  495. d=module.tested_dir,
  496. m=module.name))
  497. num_failed = test_summary.get('failures', 0)
  498. num_failed += test_summary.get('errors', 0)
  499. if num_failed:
  500. if num_failed > 1:
  501. text = ' ({f} tests failed)'
  502. else:
  503. text = ' ({f} test failed)'
  504. self._stream.write(text.format(f=num_failed))
  505. self._stream.write('\n')
  506. # TODO: here we lost the possibility to include also file name
  507. # of the appropriate report
  508. # TODO: there is a quite a lot duplication between this class and html reporter
  509. # TODO: document: do not use it for two reports, it accumulates the results
  510. # TODO: add also keyvalue summary generation?
  511. # wouldn't this conflict with collecting data from report afterwards?
  512. class TestsuiteDirReporter(object):
  513. def __init__(self, main_page_name, testsuite_page_name='index.html'):
  514. self.main_page_name = main_page_name
  515. self.testsuite_page_name = testsuite_page_name
  516. # TODO: this might be even a object which could add and validate
  517. self.failures = 0
  518. self.errors = 0
  519. self.skipped = 0
  520. self.successes = 0
  521. self.expected_failures = 0
  522. self.unexpected_successes = 0
  523. self.total = 0
  524. self.testsuites = 0
  525. self.testsuites_successes = 0
  526. self.files = 0
  527. self.files_successes = 0
  528. def report_for_dir(self, root, directory, test_files):
  529. # TODO: create object from this, so that it can be passed from
  530. # one function to another
  531. # TODO: put the inside of for loop to another fucntion
  532. dir_failures = 0
  533. dir_errors = 0
  534. dir_skipped = 0
  535. dir_successes = 0
  536. dir_expected_failures = 0
  537. dir_unexpected_success = 0
  538. dir_total = 0
  539. test_files_authors = []
  540. file_total = 0
  541. file_successes = 0
  542. page_name = os.path.join(root, directory, self.testsuite_page_name)
  543. page = open(page_name, 'w')
  544. head = (
  545. '<html><body>'
  546. '<h1>{name} testsuite results</h1>'
  547. .format(name=directory))
  548. tests_table_head = (
  549. '<h3>Test files results</h3>'
  550. '<table>'
  551. '<thead><tr>'
  552. '<th>Test file</th><th>Status</th>'
  553. '<th>Tests</th><th>Successful</td>'
  554. '<th>Failed</th><th>Percent successful</th>'
  555. '</tr></thead><tbody>'
  556. )
  557. page.write(head)
  558. page.write(tests_table_head)
  559. for test_file_name in test_files:
  560. # TODO: put keyvalue fine name to constant
  561. summary_filename = os.path.join(root, directory, test_file_name,
  562. 'test_keyvalue_result.txt')
  563. #if os.path.exists(summary_filename):
  564. with open(summary_filename, 'r') as keyval_file:
  565. summary = text_to_keyvalue(keyval_file.read(), sep='=')
  566. #else:
  567. # TODO: write else here
  568. # summary = None
  569. if 'total' not in summary:
  570. bad_ones = successes = UNKNOWN_NUMBER_HTML
  571. total = None
  572. else:
  573. bad_ones = summary['failures'] + summary['errors']
  574. successes = summary['successes']
  575. total = summary['total']
  576. self.failures += summary['failures']
  577. self.errors += summary['errors']
  578. self.skipped += summary['skipped']
  579. self.successes += summary['successes']
  580. self.expected_failures += summary['expected_failures']
  581. self.unexpected_successes += summary['unexpected_successes']
  582. self.total += summary['total']
  583. dir_failures += summary['failures']
  584. dir_errors += summary['failures']
  585. dir_skipped += summary['skipped']
  586. dir_successes += summary['successes']
  587. dir_expected_failures += summary['expected_failures']
  588. dir_unexpected_success += summary['unexpected_successes']
  589. dir_total += summary['total']
  590. # TODO: keyvalue method should have types for keys function
  591. # perhaps just the current post processing function is enough
  592. test_file_authors = summary['test_file_authors']
  593. if type(test_file_authors) is not list:
  594. test_file_authors = [test_file_authors]
  595. test_files_authors += test_file_authors
  596. file_total += 1
  597. file_successes += 0 if summary['returncode'] else 1
  598. pass_per = success_to_html_percent(total=total,
  599. successes=successes)
  600. row = (
  601. '<tr>'
  602. '<td><a href="{f}/index.html">{f}</a></td>'
  603. '<td>{status}</td>'
  604. '<td>{ntests}</td><td>{stests}</td>'
  605. '<td>{ftests}</td><td>{ptests}</td>'
  606. '<tr>'
  607. .format(
  608. f=test_file_name,
  609. status=returncode_to_html_text(summary['returncode']),
  610. stests=successes, ftests=bad_ones, ntests=total,
  611. ptests=pass_per))
  612. page.write(row)
  613. self.testsuites += 1
  614. self.testsuites_successes += 1 if file_successes == file_total else 0
  615. self.files += file_total
  616. self.files_successes += file_successes
  617. dir_pass_per = success_to_html_percent(total=dir_total,
  618. successes=dir_successes)
  619. file_pass_per = success_to_html_percent(total=file_total,
  620. successes=file_successes)
  621. tests_table_foot = (
  622. '</tbody><tfoot><tr>'
  623. '<td>Summary</td>'
  624. '<td>{status}</td>'
  625. '<td>{ntests}</td><td>{stests}</td>'
  626. '<td>{ftests}</td><td>{ptests}</td>'
  627. '</tr></tfoot></table>'
  628. .format(
  629. status=file_pass_per,
  630. stests=dir_successes, ftests=dir_failures + dir_errors,
  631. ntests=dir_total, ptests=dir_pass_per))
  632. page.write(tests_table_foot)
  633. test_authors = get_html_test_authors_table(
  634. directory=directory, tests_authors=test_files_authors)
  635. page.write(test_authors)
  636. page.write('</body></html>')
  637. status = success_to_html_text(total=file_total, successes=file_successes)
  638. row = (
  639. '<tr>'
  640. '<td><a href="{d}/{page}">{d}</a></td><td>{status}</td>'
  641. '<td>{nfiles}</td><td>{sfiles}</td><td>{pfiles}</td>'
  642. '<td>{ntests}</td><td>{stests}</td>'
  643. '<td>{ftests}</td><td>{ptests}</td>'
  644. '<tr>'
  645. .format(
  646. d=directory, page=self.testsuite_page_name, status=status,
  647. nfiles=file_total, sfiles=file_successes, pfiles=file_pass_per,
  648. stests=dir_successes, ftests=dir_failures + dir_errors,
  649. ntests=dir_total, ptests=dir_pass_per))
  650. return row
  651. def report_for_dirs(self, root, directories):
  652. # TODO: this will need chanages accoring to potential chnages in absolute/relative paths
  653. page_name = os.path.join(root, self.main_page_name)
  654. page = open(page_name, 'w')
  655. head = (
  656. '<html><body>'
  657. '<h1>Testsuites results</h1>'
  658. )
  659. tests_table_head = (
  660. '<table>'
  661. '<thead><tr>'
  662. '<th>Testsuite</th>'
  663. '<th>Status</th>'
  664. '<th>Test files</th><th>Successful</td>'
  665. '<th>Percent successful</th>'
  666. '<th>Tests</th><th>Successful</td>'
  667. '<th>Failed</th><th>Percent successful</th>'
  668. '</tr></thead><tbody>'
  669. )
  670. page.write(head)
  671. page.write(tests_table_head)
  672. for directory, test_files in directories.iteritems():
  673. row = self.report_for_dir(root=root, directory=directory,
  674. test_files=test_files)
  675. page.write(row)
  676. pass_per = success_to_html_percent(total=self.total,
  677. successes=self.successes)
  678. file_pass_per = success_to_html_percent(total=self.files,
  679. successes=self.files_successes)
  680. testsuites_pass_per = success_to_html_percent(
  681. total=self.testsuites, successes=self.testsuites_successes)
  682. tests_table_foot = (
  683. '<tfoot>'
  684. '<tr>'
  685. '<td>Summary</td><td>{status}</td>'
  686. '<td>{nfiles}</td><td>{sfiles}</td><td>{pfiles}</td>'
  687. '<td>{ntests}</td><td>{stests}</td>'
  688. '<td>{ftests}</td><td>{ptests}</td>'
  689. '</tr>'
  690. '</tfoot>'
  691. .format(
  692. status=testsuites_pass_per, nfiles=self.files,
  693. sfiles=self.files_successes, pfiles=file_pass_per,
  694. stests=self.successes, ftests=self.failures + self.errors,
  695. ntests=self.total, ptests=pass_per))
  696. page.write(tests_table_foot)
  697. page.write('</body></html>')