reporters.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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 sys
  12. import datetime
  13. import xml.sax.saxutils as saxutils
  14. import xml.etree.ElementTree as et
  15. import subprocess
  16. from .utils import ensure_dir
  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 not possible to get
  53. """
  54. # TODO: here should be starting directory
  55. # but now we are using current as starting
  56. p = subprocess.Popen(['svnversion', '.'],
  57. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  58. stdout, stderr = p.communicate()
  59. rc = p.poll()
  60. if not rc:
  61. stdout = stdout.strip()
  62. if stdout.endswith('M'):
  63. stdout = stdout[:-1]
  64. if ':' in stdout:
  65. # the first one is the one of source code
  66. stdout = stdout.split(':')[0]
  67. return stdout
  68. else:
  69. return None
  70. def get_svn_info():
  71. """Get important information from ``svn info``
  72. :returns: SVN info as dictionary or None
  73. if it is not possible to obtain it
  74. """
  75. try:
  76. # TODO: introduce directory, not only current
  77. p = subprocess.Popen(['svn', 'info', '.', '--xml'],
  78. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  79. stdout, stderr = p.communicate()
  80. rc = p.poll()
  81. info = {}
  82. if not rc:
  83. root = et.fromstring(stdout)
  84. # TODO: get also date if this make sense
  85. # expecting only one <entry>
  86. entry = root.find('entry')
  87. info['revision'] = entry.get('revision')
  88. info['url'] = entry.find('url').text
  89. relurl = entry.find('relative-url').text
  90. # relative path has ^ at the beginning
  91. if relurl.startswith('^'):
  92. relurl = relurl[1:]
  93. info['relative-url'] = relurl
  94. return info
  95. # TODO: add this to svnversion function
  96. except OSError as e:
  97. import errno
  98. # ignore No such file or directory
  99. if e.errno != errno.ENOENT:
  100. raise
  101. return None
  102. class GrassTestFilesReporter(object):
  103. def __init__(self, results_dir):
  104. # TODO: no directory cleaning (self.clean_before)? now cleaned by caller
  105. ensure_dir(os.path.abspath(results_dir))
  106. # having all variables public although not really part of API
  107. self.main_index = open(os.path.join(results_dir, 'index.html'), 'w')
  108. # this might be moved to some report start method
  109. self.main_start_time = datetime.datetime.now()
  110. svn_info = get_svn_info()
  111. if not svn_info:
  112. svn_text = ('<span style="font-size: 60%">'
  113. 'SVN revision cannot be be obtained'
  114. '</span>')
  115. else:
  116. url = get_source_url(path=svn_info['relative-url'],
  117. revision=svn_info['revision'])
  118. svn_text = ('SVN revision'
  119. ' <a href="{url}">'
  120. '{rev}</a>'
  121. ).format(url=url, rev=svn_info['revision'])
  122. self.main_index.write('<html><body>'
  123. '<h1>Test results</h1>'
  124. '{time:%Y-%m-%d %H:%M:%S}'
  125. ' ({svn})'
  126. '<table>'
  127. '<thead><tr>'
  128. '<th>Tested directory</th>'
  129. '<th>Test file</th>'
  130. '<th>Status</th>'
  131. '</tr></thead><tbody>'.format(
  132. time=self.main_start_time,
  133. svn=svn_text))
  134. self.file_start_time = None
  135. self._start_file_test_called = False
  136. def finish(self):
  137. self.main_index.write('<tbody></table>'
  138. '</body></html>')
  139. self.main_index.close()
  140. def start_file_test(self, module):
  141. self.file_start_time = datetime.datetime.now()
  142. self._start_file_test_called = True
  143. self.main_index.flush() # to get previous ones to the report
  144. def wrap_stdstream_to_html(self, infile, outfile, module, stream):
  145. before = '<html><body><h1>%s</h1><pre>' % (module.name + ' ' + stream)
  146. after = '</pre></body></html>'
  147. html = open(outfile, 'w')
  148. html.write(before)
  149. with open(infile) as text:
  150. for line in text:
  151. html.write(color_error_line(html_escape(line)))
  152. html.write(after)
  153. html.close()
  154. def returncode_to_html_text(self, returncode):
  155. if returncode:
  156. return '<span style="color: red">FAILED</span>'
  157. else:
  158. return '<span style="color: green">succeeded</span>' # SUCCEEDED
  159. def returncode_to_html_sentence(self, returncode):
  160. if returncode:
  161. return '<span style="color: red">&#x274c;</span> Test failed (return code %d)' % (returncode)
  162. else:
  163. return '<span style="color: green">&#x2713;</span> Test succeeded (return code %d)' % (returncode)
  164. def end_file_test(self, module, cwd, returncode, stdout, stderr):
  165. assert self._start_file_test_called
  166. file_time = datetime.datetime.now() - self.file_start_time
  167. self.main_index.write(
  168. '<tr><td>{d}</td>'
  169. '<td><a href="{d}/{m}/index.html">{m}</a></td><td>{sf}</td>'
  170. '<tr>'.format(
  171. d=module.tested_dir, m=module.name,
  172. sf=self.returncode_to_html_text(returncode)))
  173. self.wrap_stdstream_to_html(infile=stdout,
  174. outfile=os.path.join(cwd, 'stdout.html'),
  175. module=module, stream='stdout')
  176. self.wrap_stdstream_to_html(infile=stderr,
  177. outfile=os.path.join(cwd, 'stderr.html'),
  178. module=module, stream='stderr')
  179. file_index_path = os.path.join(cwd, 'index.html')
  180. file_index = open(file_index_path, 'w')
  181. file_index.write('<html><body>'
  182. '<h1>{m.name}</h1>'
  183. '<h2>{m.tested_dir} &ndash; {m.name}</h2>'
  184. '<p>{status}'
  185. '<p>Test duration: {dur}'
  186. '<ul>'
  187. '<li><a href="stdout.html">standard output (stdout)</a>'
  188. '<li><a href="stderr.html">standard error output (stderr)</a>'
  189. '<li><a href="testcodecoverage/index.html">code coverage</a>'
  190. '</ul>'
  191. '</body></html>'.format(
  192. dur=file_time, m=module,
  193. status=self.returncode_to_html_sentence(returncode)))
  194. file_index.close()
  195. if returncode:
  196. sys.stderr.write('{d}/{m} failed (see {f})\n'.format(d=module.tested_dir,
  197. m=module.name,
  198. f=file_index_path))
  199. self._start_file_test_called = False