multireport.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. """
  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, namedtuple
  16. from grass.gunittest.checkers import text_to_keyvalue
  17. from grass.gunittest.utils import ensure_dir
  18. from grass.gunittest.reporters import success_to_html_percent
  19. # TODO: we should be able to work without matplotlib
  20. import matplotlib
  21. matplotlib.use("Agg")
  22. # This counts as code already, so silence "import not at top of file".
  23. # Perhaps in the future, switch_backend() could be used.
  24. import matplotlib.pyplot as plt # noqa: E402
  25. from matplotlib.dates import date2num # noqa: E402
  26. class TestResultSummary(object):
  27. def __init__(self):
  28. self.timestamp = None
  29. self.svn_revision = None
  30. self.location = None
  31. self.location_type = None
  32. self.total = None
  33. self.successes = None
  34. self.failures = None
  35. self.errors = None
  36. self.skipped = []
  37. self.expected_failures = []
  38. self.unexpected_successes = []
  39. self.files_total = None
  40. self.files_successes = None
  41. self.files_failures = None
  42. self.tested_modules = []
  43. self.tested_dirs = []
  44. self.test_files_authors = []
  45. self.tested_dirs = []
  46. self.time = []
  47. self.names = []
  48. self.report = None
  49. def plot_percents(x, xticks, xlabels, successes, failures, filename, style):
  50. fig = plt.figure()
  51. graph = fig.add_subplot(111)
  52. # Plot the data as a red line with round markers
  53. graph.plot(
  54. x,
  55. successes,
  56. color=style.success_color,
  57. linestyle=style.linestyle,
  58. linewidth=style.linewidth,
  59. )
  60. graph.plot(
  61. x,
  62. failures,
  63. color=style.fail_color,
  64. linestyle=style.linestyle,
  65. linewidth=style.linewidth,
  66. )
  67. fig.autofmt_xdate()
  68. graph.set_xticks(xticks)
  69. graph.set_xticklabels(xlabels)
  70. percents = range(0, 110, 10)
  71. graph.set_yticks(percents)
  72. graph.set_yticklabels(["%d%%" % p for p in percents])
  73. fig.savefig(filename)
  74. def plot_percent_successful(x, xticks, xlabels, successes, filename, style):
  75. fig = plt.figure()
  76. graph = fig.add_subplot(111)
  77. def median(values):
  78. n = len(values)
  79. if n == 1:
  80. return values[0]
  81. sorted_values = sorted(values)
  82. if n % 2 == 0:
  83. return (sorted_values[n / 2 - 1] + sorted_values[n / 2]) / 2
  84. else:
  85. return sorted_values[n / 2]
  86. # this is useful for debugging or some other stat
  87. # cmeans = []
  88. # cmedians = []
  89. # csum = 0
  90. # count = 0
  91. # for i, s in enumerate(successes):
  92. # csum += s
  93. # count += 1
  94. # cmeans.append(csum/count)
  95. # cmedians.append(median(successes[:i + 1]))
  96. smedian = median(successes)
  97. smax = max(successes)
  98. if successes[-1] < smedian:
  99. color = "r"
  100. else:
  101. color = "g"
  102. # another possibility is to color according to the gradient, ideally
  103. # on the whole curve but that's much more complicated
  104. graph.plot(
  105. x, successes, color=color, linestyle=style.linestyle, linewidth=style.linewidth
  106. )
  107. # rotates the xlabels
  108. fig.autofmt_xdate()
  109. graph.set_xticks(xticks)
  110. graph.set_xticklabels(xlabels)
  111. step = 5
  112. ymin = int(min(successes) / step) * step
  113. ymax = int(smax / step) * step
  114. percents = range(ymin, ymax + step + 1, step)
  115. graph.set_yticks(percents)
  116. graph.set_yticklabels(["%d%%" % p for p in percents])
  117. fig.savefig(filename)
  118. def tests_successful_plot(x, xticks, xlabels, results, filename, style):
  119. successes = []
  120. for result in results:
  121. if result.total:
  122. successes.append(float(result.successes) / result.total * 100)
  123. else:
  124. # this is not expected to happen
  125. # but we don't want any exceptions if it happens
  126. successes.append(0)
  127. plot_percent_successful(
  128. x=x,
  129. xticks=xticks,
  130. xlabels=xlabels,
  131. successes=successes,
  132. filename=filename,
  133. style=style,
  134. )
  135. def tests_plot(x, xticks, xlabels, results, filename, style):
  136. total = [result.total for result in results]
  137. successes = [result.successes for result in results]
  138. # TODO: document: counting errors and failures together
  139. failures = [result.failures + result.errors for result in results]
  140. fig = plt.figure()
  141. graph = fig.add_subplot(111)
  142. graph.plot(
  143. x,
  144. total,
  145. color=style.total_color,
  146. linestyle=style.linestyle,
  147. linewidth=style.linewidth,
  148. )
  149. graph.plot(
  150. x,
  151. successes,
  152. color=style.success_color,
  153. linestyle=style.linestyle,
  154. linewidth=style.linewidth,
  155. )
  156. graph.plot(
  157. x,
  158. failures,
  159. color=style.fail_color,
  160. linestyle=style.linestyle,
  161. linewidth=style.linewidth,
  162. )
  163. fig.autofmt_xdate()
  164. graph.set_xticks(xticks)
  165. graph.set_xticklabels(xlabels)
  166. fig.savefig(filename)
  167. def tests_percent_plot(x, xticks, xlabels, results, filename, style):
  168. successes = []
  169. failures = []
  170. for result in results:
  171. if result.total:
  172. successes.append(float(result.successes) / result.total * 100)
  173. # TODO: again undocumented, counting errors and failures together
  174. failures.append(float(result.failures + result.errors) / result.total * 100)
  175. else:
  176. # this is not expected to happen
  177. # but we don't want any exceptions if it happens
  178. successes.append(0)
  179. failures.append(0)
  180. plot_percents(
  181. x=x,
  182. xticks=xticks,
  183. xlabels=xlabels,
  184. successes=successes,
  185. failures=failures,
  186. filename=filename,
  187. style=style,
  188. )
  189. def files_successful_plot(x, xticks, xlabels, results, filename, style):
  190. successes = []
  191. for result in results:
  192. if result.total:
  193. successes.append(float(result.files_successes) / result.files_total * 100)
  194. else:
  195. # this is not expected to happen
  196. # but we don't want any exceptions if it happens
  197. successes.append(0)
  198. plot_percent_successful(
  199. x=x,
  200. xticks=xticks,
  201. xlabels=xlabels,
  202. successes=successes,
  203. filename=filename,
  204. style=style,
  205. )
  206. def files_plot(x, xticks, xlabels, results, filename, style):
  207. total = [result.files_total for result in results]
  208. successes = [result.files_successes for result in results]
  209. failures = [result.files_failures for result in results]
  210. fig = plt.figure()
  211. graph = fig.add_subplot(111)
  212. graph.plot(
  213. x,
  214. total,
  215. color=style.total_color,
  216. linestyle=style.linestyle,
  217. linewidth=style.linewidth,
  218. )
  219. graph.plot(
  220. x,
  221. successes,
  222. color=style.success_color,
  223. linestyle=style.linestyle,
  224. linewidth=style.linewidth,
  225. )
  226. graph.plot(
  227. x,
  228. failures,
  229. color=style.fail_color,
  230. linestyle=style.linestyle,
  231. linewidth=style.linewidth,
  232. )
  233. fig.autofmt_xdate()
  234. graph.set_xticks(xticks)
  235. graph.set_xticklabels(xlabels)
  236. fig.savefig(filename)
  237. def files_percent_plot(x, xticks, xlabels, results, filename, style):
  238. successes = []
  239. failures = []
  240. for result in results:
  241. if result.files_total:
  242. successes.append(float(result.files_successes) / result.files_total * 100)
  243. failures.append(float(result.files_failures) / result.files_total * 100)
  244. else:
  245. # this is not expected to happen
  246. # but we don't want any exceptions if it happens
  247. successes.append(0)
  248. failures.append(0)
  249. plot_percents(
  250. x=x,
  251. xticks=xticks,
  252. xlabels=xlabels,
  253. successes=successes,
  254. failures=failures,
  255. filename=filename,
  256. style=style,
  257. )
  258. def info_plot(x, xticks, xlabels, results, filename, style):
  259. modules = [len(result.tested_modules) for result in results]
  260. names = [len(result.names) for result in results]
  261. authors = [len(result.test_files_authors) for result in results]
  262. # we want just unique directories
  263. dirs = [len(set(result.tested_dirs)) for result in results]
  264. fig = plt.figure()
  265. graph = fig.add_subplot(111)
  266. graph.plot(
  267. x,
  268. names,
  269. color="b",
  270. label="Test files",
  271. linestyle=style.linestyle,
  272. linewidth=style.linewidth,
  273. )
  274. graph.plot(
  275. x,
  276. modules,
  277. color="g",
  278. label="Tested modules",
  279. linestyle=style.linestyle,
  280. linewidth=style.linewidth,
  281. )
  282. # dirs == testsuites
  283. graph.plot(
  284. x,
  285. dirs,
  286. color="orange",
  287. label="Tested directories",
  288. linestyle=style.linestyle,
  289. linewidth=style.linewidth,
  290. )
  291. graph.plot(
  292. x,
  293. authors,
  294. color="r",
  295. label="Test authors",
  296. linestyle=style.linestyle,
  297. linewidth=style.linewidth,
  298. )
  299. graph.legend(loc="best", shadow=False)
  300. fig.autofmt_xdate()
  301. graph.set_xticks(xticks)
  302. graph.set_xticklabels(xlabels)
  303. fig.savefig(filename)
  304. # TODO: solve the directory inconsitencies, implement None
  305. def main_page(
  306. results, filename, images, captions, title="Test reports", directory=None
  307. ):
  308. filename = os.path.join(directory, filename)
  309. with open(filename, "w") as page:
  310. page.write(
  311. "<html><body>"
  312. "<h1>{title}</h1>"
  313. "<table>"
  314. "<thead><tr>"
  315. "<th>Date (timestamp)</th><th>SVN revision</th><th>Name</th>"
  316. "<th>Successful files</th><th>Successful tests</th>"
  317. "</tr></thead>"
  318. "<tbody>".format(title=title)
  319. )
  320. for result in reversed(results):
  321. # TODO: include name to summary file
  322. # now using location or test report directory as name
  323. if result.location != "unknown":
  324. name = result.location
  325. else:
  326. name = os.path.basename(result.report)
  327. if not name:
  328. # Python basename returns '' for 'abc/'
  329. for d in reversed(os.path.split(result.report)):
  330. if d:
  331. name = d
  332. break
  333. per_test = success_to_html_percent(
  334. total=result.total, successes=result.successes
  335. )
  336. per_file = success_to_html_percent(
  337. total=result.files_total, successes=result.files_successes
  338. )
  339. report_path = os.path.relpath(path=result.report, start=directory)
  340. page.write(
  341. "<tr>"
  342. "<td><a href={report_path}/index.html>{result.timestamp}</a></td>"
  343. "<td>{result.svn_revision}</td>"
  344. "<td><a href={report_path}/index.html>{name}</a></td>"
  345. "<td>{pfiles}</td><td>{ptests}</td>"
  346. "</tr>".format(
  347. result=result,
  348. name=name,
  349. report_path=report_path,
  350. pfiles=per_file,
  351. ptests=per_test,
  352. )
  353. )
  354. page.write("</tbody></table>")
  355. for image, caption in itertools.izip(images, captions):
  356. page.write(
  357. "<h3>{caption}<h3>"
  358. '<img src="{image}" alt="{caption}" title="{caption}">'.format(
  359. image=image, caption=caption
  360. )
  361. )
  362. page.write("</body></html>")
  363. def main():
  364. parser = argparse.ArgumentParser(
  365. description="Create overall report from several individual test reports"
  366. )
  367. parser.add_argument(
  368. "reports",
  369. metavar="report_directory",
  370. type=str,
  371. nargs="+",
  372. help="Directories with reports",
  373. )
  374. parser.add_argument(
  375. "--output",
  376. dest="output",
  377. action="store",
  378. default="testreports_summary",
  379. help="Output directory",
  380. )
  381. parser.add_argument(
  382. "--timestamps",
  383. dest="timestamps",
  384. action="store_true",
  385. help="Use file timestamp instead of date in test summary",
  386. )
  387. args = parser.parse_args()
  388. output = args.output
  389. reports = args.reports
  390. use_timestamps = args.timestamps
  391. ensure_dir(output)
  392. all_results = []
  393. results_in_locations = defaultdict(list)
  394. for report in reports:
  395. try:
  396. summary_file = os.path.join(report, "test_keyvalue_result.txt")
  397. if not os.path.exists(summary_file):
  398. sys.stderr.write(
  399. "WARNING: Key-value summary not available in"
  400. " report <%s>, skipping.\n" % summary_file
  401. )
  402. # skipping incomplete reports
  403. # use only results list for further processing
  404. continue
  405. summary = text_to_keyvalue(open(summary_file).read(), sep="=")
  406. if use_timestamps:
  407. test_timestamp = datetime.datetime.fromtimestamp(
  408. os.path.getmtime(summary_file)
  409. )
  410. else:
  411. test_timestamp = datetime.datetime.strptime(
  412. summary["timestamp"], "%Y-%m-%d %H:%M:%S"
  413. )
  414. result = TestResultSummary()
  415. result.timestamp = test_timestamp
  416. result.total = summary["total"]
  417. result.successes = summary["successes"]
  418. result.failures = summary["failures"]
  419. result.errors = summary["errors"]
  420. result.files_total = summary["files_total"]
  421. result.files_successes = summary["files_successes"]
  422. result.files_failures = summary["files_failures"]
  423. result.svn_revision = str(summary["svn_revision"])
  424. result.tested_modules = summary["tested_modules"]
  425. result.names = summary["names"]
  426. result.test_files_authors = summary["test_files_authors"]
  427. result.tested_dirs = summary["tested_dirs"]
  428. result.report = report
  429. # let's consider no location as valid state and use 'unknown'
  430. result.location = summary.get("location", "unknown")
  431. result.location_type = summary.get("location_type", "unknown")
  432. # grouping according to location types
  433. # this can cause that two actual locations tested at the same time
  434. # will end up together, this is not ideal but testing with
  435. # one location type and different actual locations is not standard
  436. # and although it will not break anything it will not give a nice
  437. # report
  438. results_in_locations[result.location_type].append(result)
  439. all_results.append(result)
  440. del result
  441. except KeyError as e:
  442. print("File %s does not have right values (%s)" % (report, e.message))
  443. locations_main_page = open(os.path.join(output, "index.html"), "w")
  444. locations_main_page.write(
  445. "<html><body>"
  446. "<h1>Test reports grouped by location type</h1>"
  447. "<table>"
  448. "<thead><tr>"
  449. "<th>Location</th>"
  450. "<th>Successful files</th><th>Successful tests</th>"
  451. "</tr></thead>"
  452. "<tbody>"
  453. )
  454. PlotStyle = namedtuple(
  455. "PlotStyle",
  456. ["linestyle", "linewidth", "success_color", "fail_color", "total_color"],
  457. )
  458. plot_style = PlotStyle(
  459. linestyle="-", linewidth=4.0, success_color="g", fail_color="r", total_color="b"
  460. )
  461. for location_type, results in results_in_locations.items():
  462. results = sorted(results, key=operator.attrgetter("timestamp"))
  463. # TODO: document: location type must be a valid dir name
  464. directory = os.path.join(output, location_type)
  465. ensure_dir(directory)
  466. if location_type == "unknown":
  467. title = "Test reports"
  468. else:
  469. title = "Test reports for &lt;{type}&gt; location type".format(
  470. type=location_type
  471. )
  472. x = [date2num(result.timestamp) for result in results]
  473. # the following would be an alternative but it does not work with
  474. # labels and automatic axis limits even after removing another date fun
  475. # x = [result.svn_revision for result in results]
  476. xlabels = [
  477. result.timestamp.strftime("%Y-%m-%d") + " (r" + result.svn_revision + ")"
  478. for result in results
  479. ]
  480. step = len(x) / 10
  481. xticks = x[step::step]
  482. xlabels = xlabels[step::step]
  483. tests_successful_plot(
  484. x=x,
  485. xticks=xticks,
  486. xlabels=xlabels,
  487. results=results,
  488. filename=os.path.join(directory, "tests_successful_plot.png"),
  489. style=plot_style,
  490. )
  491. files_successful_plot(
  492. x=x,
  493. xticks=xticks,
  494. xlabels=xlabels,
  495. results=results,
  496. filename=os.path.join(directory, "files_successful_plot.png"),
  497. style=plot_style,
  498. )
  499. tests_plot(
  500. x=x,
  501. xticks=xticks,
  502. xlabels=xlabels,
  503. results=results,
  504. filename=os.path.join(directory, "tests_plot.png"),
  505. style=plot_style,
  506. )
  507. tests_percent_plot(
  508. x=x,
  509. xticks=xticks,
  510. xlabels=xlabels,
  511. results=results,
  512. filename=os.path.join(directory, "tests_percent_plot.png"),
  513. style=plot_style,
  514. )
  515. files_plot(
  516. x=x,
  517. xticks=xticks,
  518. xlabels=xlabels,
  519. results=results,
  520. filename=os.path.join(directory, "files_plot.png"),
  521. style=plot_style,
  522. )
  523. files_percent_plot(
  524. x=x,
  525. xticks=xticks,
  526. xlabels=xlabels,
  527. results=results,
  528. filename=os.path.join(directory, "files_percent_plot.png"),
  529. style=plot_style,
  530. )
  531. info_plot(
  532. x=x,
  533. xticks=xticks,
  534. xlabels=xlabels,
  535. results=results,
  536. filename=os.path.join(directory, "info_plot.png"),
  537. style=plot_style,
  538. )
  539. main_page(
  540. results=results,
  541. filename="index.html",
  542. images=[
  543. "tests_successful_plot.png",
  544. "files_successful_plot.png",
  545. "tests_plot.png",
  546. "files_plot.png",
  547. "tests_percent_plot.png",
  548. "files_percent_plot.png",
  549. "info_plot.png",
  550. ],
  551. captions=[
  552. "Success of individual tests in percents",
  553. "Success of test files in percents",
  554. "Successes, failures and number of individual tests",
  555. "Successes, failures and number of test files",
  556. "Successes and failures of individual tests in percent",
  557. "Successes and failures of test files in percents",
  558. "Additional information",
  559. ],
  560. directory=directory,
  561. title=title,
  562. )
  563. files_successes = sum(result.files_successes for result in results)
  564. files_total = sum(result.files_total for result in results)
  565. successes = sum(result.successes for result in results)
  566. total = sum(result.total for result in results)
  567. per_test = success_to_html_percent(total=total, successes=successes)
  568. per_file = success_to_html_percent(total=files_total, successes=files_successes)
  569. locations_main_page.write(
  570. "<tr>"
  571. "<td><a href={location}/index.html>{location}</a></td>"
  572. "<td>{pfiles}</td><td>{ptests}</td>"
  573. "</tr>".format(location=location_type, pfiles=per_file, ptests=per_test)
  574. )
  575. locations_main_page.write("</tbody></table>")
  576. locations_main_page.write("</body></html>")
  577. locations_main_page.close()
  578. return 0
  579. if __name__ == "__main__":
  580. sys.exit(main())