multireport.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. # -*- coding: utf-8 -*-
  2. """Testing framework module for multi report
  3. Copyright (C) 2014 by the GRASS Development Team
  4. This program is free software under the GNU General Public
  5. License (>=v2). Read the file COPYING that comes with GRASS GIS
  6. for details.
  7. :authors: Vaclav Petras
  8. """
  9. import sys
  10. import os
  11. import argparse
  12. import itertools
  13. import datetime
  14. import operator
  15. from collections import defaultdict
  16. # TODO: we should be able to work without matplotlib
  17. import matplotlib
  18. matplotlib.use('Agg')
  19. import matplotlib.pyplot as plt
  20. from matplotlib.dates import date2num
  21. from grass.gunittest.checkers import text_to_keyvalue
  22. from grass.gunittest.utils import ensure_dir
  23. from grass.gunittest.reporters import success_to_html_percent
  24. class TestResultSummary(object):
  25. def __init__(self):
  26. self.timestamp = None
  27. self.svn_revision = None
  28. self.location = None
  29. self.location_type = None
  30. self.total = None
  31. self.successes = None
  32. self.failures = None
  33. self.errors = None
  34. self.skipped = []
  35. self.expected_failures = []
  36. self.unexpected_successes = []
  37. self.files_total = None
  38. self.files_successes = None
  39. self.files_failures = None
  40. self.tested_modules = []
  41. self.tested_dirs = []
  42. self.test_files_authors = []
  43. self.time = []
  44. self.names = []
  45. self.report = None
  46. def tests_plot(x, xlabels, results, filename):
  47. total = [result.total for result in results]
  48. successes = [result.successes for result in results]
  49. # TODO: document: counting errors and failures together
  50. failures = [result.failures + result.errors for result in results]
  51. fig = plt.figure()
  52. graph = fig.add_subplot(111)
  53. # Plot the data as a red line with round markers
  54. graph.plot(x, total, 'b-o')
  55. graph.plot(x, successes, 'g-o')
  56. graph.plot(x, failures, 'r-o')
  57. fig.autofmt_xdate()
  58. # Set the xtick locations to correspond to just the dates you entered.
  59. graph.set_xticks(x)
  60. # Set the xtick labels to correspond to just the dates you entered.
  61. graph.set_xticklabels(xlabels)
  62. fig.savefig(filename)
  63. def files_plot(x, xlabels, results, filename):
  64. total = [result.files_total for result in results]
  65. successes = [result.files_successes for result in results]
  66. failures = [result.files_failures for result in results]
  67. fig = plt.figure()
  68. graph = fig.add_subplot(111)
  69. # Plot the data as a red line with round markers
  70. graph.plot(x, total, 'b-o')
  71. graph.plot(x, successes, 'g-o')
  72. graph.plot(x, failures, 'r-o')
  73. #fig.autofmt_xdate(bottom=0.2, rotation=30, ha='left')
  74. fig.autofmt_xdate()
  75. # Set the xtick locations to correspond to just the dates you entered.
  76. graph.set_xticks(x)
  77. # Set the xtick labels to correspond to just the dates you entered.
  78. graph.set_xticklabels(xlabels)
  79. fig.savefig(filename)
  80. def info_plot(x, xlabels, results, filename):
  81. modules = [len(result.tested_modules) for result in results]
  82. names = [len(result.names) for result in results]
  83. authors = [len(result.test_files_authors) for result in results]
  84. fig = plt.figure()
  85. graph = fig.add_subplot(111)
  86. # Plot the data as a red line with round markers
  87. graph.plot(x, names, 'b-o', label="Test files")
  88. graph.plot(x, modules, 'g-o', label="Tested modules")
  89. graph.plot(x, authors, 'r-o', label="Test authors")
  90. fig.autofmt_xdate()
  91. # Now add the legend with some customizations.
  92. graph.legend(loc='upper center', shadow=True)
  93. # Set the xtick locations to correspond to just the dates you entered.
  94. graph.set_xticks(x)
  95. # Set the xtick labels to correspond to just the dates you entered.
  96. graph.set_xticklabels(xlabels)
  97. fig.savefig(filename)
  98. # TODO: solve the directory inconsitencies, implemement None
  99. def main_page(results, filename, images, captions, title='Test reports',
  100. directory=None):
  101. filename = os.path.join(directory, filename)
  102. with open(filename, 'w') as page:
  103. page.write(
  104. '<html><body>'
  105. '<h1>{title}</h1>'
  106. '<table>'
  107. '<thead><tr>'
  108. '<th>Date (timestamp)</th><th>SVN revision</th><th>Name</th>'
  109. '<th>Successful files</th><th>Successful tests</th>'
  110. '</tr></thead>'
  111. '<tbody>'
  112. .format(title=title)
  113. )
  114. for result in results:
  115. # TODO: include name to summary file
  116. # now using location or test report directory as name
  117. if result.location != 'unknown':
  118. name = result.location
  119. else:
  120. name = os.path.basename(result.report)
  121. if not name:
  122. # Python basename returns '' for 'abc/'
  123. for d in reversed(os.path.split(result.report)):
  124. if d:
  125. name = d
  126. break
  127. per_test = success_to_html_percent(
  128. total=result.total, successes=result.successes)
  129. per_file = success_to_html_percent(
  130. total=result.files_total, successes=result.files_successes)
  131. report_path = os.path.relpath(path=result.report, start=directory)
  132. page.write(
  133. '<tr>'
  134. '<td><a href={report_path}/index.html>{result.timestamp}</a></td>'
  135. '<td>{result.svn_revision}</td>'
  136. '<td><a href={report_path}/index.html>{name}</a></td>'
  137. '<td>{pfiles}</td><td>{ptests}</td>'
  138. '</tr>'
  139. .format(result=result, name=name, report_path=report_path,
  140. pfiles=per_file, ptests=per_test))
  141. page.write('</tbody></table>')
  142. for image, caption in itertools.izip(images, captions):
  143. page.write(
  144. '<h3>{caption}<h3>'
  145. '<img src="{image}" alt="{caption}" title="{caption}">'
  146. .format(image=image, caption=caption))
  147. page.write('</body></html>')
  148. def main():
  149. parser = argparse.ArgumentParser(
  150. description='Create overall report from several individual test reports')
  151. parser.add_argument('reports', metavar='report_directory',
  152. type=str, nargs='+',
  153. help='Directories with reports')
  154. parser.add_argument('--output', dest='output', action='store',
  155. default='testreports_summary',
  156. help='Output directory')
  157. parser.add_argument('--timestamps', dest='timestamps', action='store_true',
  158. help='Use file timestamp instead of date in test summary')
  159. args = parser.parse_args()
  160. output = args.output
  161. reports = args.reports
  162. use_timestamps = args.timestamps
  163. ensure_dir(output)
  164. all_results = []
  165. results_in_locations = defaultdict(list)
  166. for report in reports:
  167. try:
  168. summary_file = os.path.join(report, 'test_keyvalue_result.txt')
  169. if not os.path.exists(summary_file):
  170. sys.stderr.write('WARNING: Key-value summary not available in'
  171. ' report <%s>, skipping.\n' % summary_file)
  172. # skipping incomplete reports
  173. # use only results list for further processing
  174. continue
  175. summary = text_to_keyvalue(open(summary_file).read(), sep='=')
  176. if use_timestamps:
  177. test_timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(summary_file))
  178. else:
  179. test_timestamp = datetime.datetime.strptime(summary['timestamp'], "%Y-%m-%d %H:%M:%S")
  180. result = TestResultSummary()
  181. result.timestamp = test_timestamp
  182. result.total = summary['total']
  183. result.successes = summary['successes']
  184. result.failures = summary['failures']
  185. result.errors = summary['errors']
  186. result.files_total = summary['files_total']
  187. result.files_successes = summary['files_successes']
  188. result.files_failures = summary['files_failures']
  189. result.svn_revision = str(summary['svn_revision'])
  190. result.tested_modules = summary['tested_modules']
  191. result.names = summary['names']
  192. result.test_files_authors = summary['test_files_authors']
  193. result.report = report
  194. # let's consider no location as valid state and use 'unknown'
  195. result.location = summary.get('location', 'unknown')
  196. result.location_type = summary.get('location_type', 'unknown')
  197. # grouping according to location types
  198. # this can cause that two actual locations tested at the same time
  199. # will end up together, this is not ideal but testing with
  200. # one location type and different actual locations is not standard
  201. # and although it will not break anything it will not give a nice
  202. # report
  203. results_in_locations[result.location_type].append(result)
  204. all_results.append(result)
  205. del result
  206. except KeyError as e:
  207. print 'File %s does not have right values (%s)' % (report, e.message)
  208. locations_main_page = open(os.path.join(output, 'index.html'), 'w')
  209. locations_main_page.write(
  210. '<html><body>'
  211. '<h1>Test reports grouped by location type</h1>'
  212. '<table>'
  213. '<thead><tr>'
  214. '<th>Location</th>'
  215. '<th>Successful files</th><th>Successful tests</th>'
  216. '</tr></thead>'
  217. '<tbody>'
  218. )
  219. for location_type, results in results_in_locations.iteritems():
  220. results = sorted(results, key=operator.attrgetter('timestamp'))
  221. # TODO: document: location type must be a valid dir name
  222. directory = os.path.join(output, location_type)
  223. ensure_dir(directory)
  224. if location_type == 'unknown':
  225. title = 'Test reports'
  226. else:
  227. title = ('Test reports for &lt;{type}&gt; location type'
  228. .format(type=location_type))
  229. x = [date2num(result.timestamp) for result in results]
  230. xlabels = [result.timestamp.strftime("%Y-%m-%d") + ' (r' + result.svn_revision + ')' for result in results]
  231. tests_plot(x=x, xlabels=xlabels, results=results,
  232. filename=os.path.join(directory, 'tests_plot.png'))
  233. files_plot(x=x, xlabels=xlabels, results=results,
  234. filename=os.path.join(directory, 'files_plot.png'))
  235. info_plot(x=x, xlabels=xlabels, results=results,
  236. filename=os.path.join(directory, 'info_plot.png'))
  237. main_page(results=results, filename='index.html',
  238. images=['tests_plot.png', 'files_plot.png', 'info_plot.png'],
  239. captions=['Success of individual tests', 'Success of test files',
  240. 'Additional information'],
  241. directory=directory,
  242. title=title)
  243. files_successes = sum(result.files_successes for result in results)
  244. files_total = sum(result.files_total for result in results)
  245. successes = sum(result.successes for result in results)
  246. total = sum(result.total for result in results)
  247. per_test = success_to_html_percent(
  248. total=total, successes=successes)
  249. per_file = success_to_html_percent(
  250. total=files_total, successes=files_successes)
  251. locations_main_page.write(
  252. '<tr>'
  253. '<td><a href={location}/index.html>{location}</a></td>'
  254. '<td>{pfiles}</td><td>{ptests}</td>'
  255. '</tr>'
  256. .format(location=location_type,
  257. pfiles=per_file, ptests=per_test))
  258. locations_main_page.write('</tbody></table>')
  259. locations_main_page.write('</body></html>')
  260. locations_main_page.close()
  261. return 0
  262. if __name__ == '__main__':
  263. sys.exit(main())