123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- """
- Testing framework module for multi report
- Copyright (C) 2014 by the GRASS Development Team
- This program is free software under the GNU General Public
- License (>=v2). Read the file COPYING that comes with GRASS GIS
- for details.
- :authors: Vaclav Petras
- """
- import sys
- import os
- import argparse
- import itertools
- import datetime
- import operator
- from collections import defaultdict, namedtuple
- from grass.gunittest.checkers import text_to_keyvalue
- from grass.gunittest.utils import ensure_dir
- from grass.gunittest.reporters import success_to_html_percent
- # TODO: we should be able to work without matplotlib
- import matplotlib
- matplotlib.use("Agg")
- # This counts as code already, so silence "import not at top of file".
- # Perhaps in the future, switch_backend() could be used.
- import matplotlib.pyplot as plt # noqa: E402
- from matplotlib.dates import date2num # noqa: E402
- class TestResultSummary(object):
- def __init__(self):
- self.timestamp = None
- self.svn_revision = None
- self.location = None
- self.location_type = None
- self.total = None
- self.successes = None
- self.failures = None
- self.errors = None
- self.skipped = []
- self.expected_failures = []
- self.unexpected_successes = []
- self.files_total = None
- self.files_successes = None
- self.files_failures = None
- self.tested_modules = []
- self.tested_dirs = []
- self.test_files_authors = []
- self.tested_dirs = []
- self.time = []
- self.names = []
- self.report = None
- def plot_percents(x, xticks, xlabels, successes, failures, filename, style):
- fig = plt.figure()
- graph = fig.add_subplot(111)
- # Plot the data as a red line with round markers
- graph.plot(
- x,
- successes,
- color=style.success_color,
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- graph.plot(
- x,
- failures,
- color=style.fail_color,
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- fig.autofmt_xdate()
- graph.set_xticks(xticks)
- graph.set_xticklabels(xlabels)
- percents = range(0, 110, 10)
- graph.set_yticks(percents)
- graph.set_yticklabels(["%d%%" % p for p in percents])
- fig.savefig(filename)
- def plot_percent_successful(x, xticks, xlabels, successes, filename, style):
- fig = plt.figure()
- graph = fig.add_subplot(111)
- def median(values):
- n = len(values)
- if n == 1:
- return values[0]
- sorted_values = sorted(values)
- if n % 2 == 0:
- return (sorted_values[n / 2 - 1] + sorted_values[n / 2]) / 2
- else:
- return sorted_values[n / 2]
- # this is useful for debugging or some other stat
- # cmeans = []
- # cmedians = []
- # csum = 0
- # count = 0
- # for i, s in enumerate(successes):
- # csum += s
- # count += 1
- # cmeans.append(csum/count)
- # cmedians.append(median(successes[:i + 1]))
- smedian = median(successes)
- smax = max(successes)
- if successes[-1] < smedian:
- color = "r"
- else:
- color = "g"
- # another possibility is to color according to the gradient, ideally
- # on the whole curve but that's much more complicated
- graph.plot(
- x, successes, color=color, linestyle=style.linestyle, linewidth=style.linewidth
- )
- # rotates the xlabels
- fig.autofmt_xdate()
- graph.set_xticks(xticks)
- graph.set_xticklabels(xlabels)
- step = 5
- ymin = int(min(successes) / step) * step
- ymax = int(smax / step) * step
- percents = range(ymin, ymax + step + 1, step)
- graph.set_yticks(percents)
- graph.set_yticklabels(["%d%%" % p for p in percents])
- fig.savefig(filename)
- def tests_successful_plot(x, xticks, xlabels, results, filename, style):
- successes = []
- for result in results:
- if result.total:
- successes.append(float(result.successes) / result.total * 100)
- else:
- # this is not expected to happen
- # but we don't want any exceptions if it happens
- successes.append(0)
- plot_percent_successful(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- successes=successes,
- filename=filename,
- style=style,
- )
- def tests_plot(x, xticks, xlabels, results, filename, style):
- total = [result.total for result in results]
- successes = [result.successes for result in results]
- # TODO: document: counting errors and failures together
- failures = [result.failures + result.errors for result in results]
- fig = plt.figure()
- graph = fig.add_subplot(111)
- graph.plot(
- x,
- total,
- color=style.total_color,
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- graph.plot(
- x,
- successes,
- color=style.success_color,
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- graph.plot(
- x,
- failures,
- color=style.fail_color,
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- fig.autofmt_xdate()
- graph.set_xticks(xticks)
- graph.set_xticklabels(xlabels)
- fig.savefig(filename)
- def tests_percent_plot(x, xticks, xlabels, results, filename, style):
- successes = []
- failures = []
- for result in results:
- if result.total:
- successes.append(float(result.successes) / result.total * 100)
- # TODO: again undocumented, counting errors and failures together
- failures.append(float(result.failures + result.errors) / result.total * 100)
- else:
- # this is not expected to happen
- # but we don't want any exceptions if it happens
- successes.append(0)
- failures.append(0)
- plot_percents(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- successes=successes,
- failures=failures,
- filename=filename,
- style=style,
- )
- def files_successful_plot(x, xticks, xlabels, results, filename, style):
- successes = []
- for result in results:
- if result.total:
- successes.append(float(result.files_successes) / result.files_total * 100)
- else:
- # this is not expected to happen
- # but we don't want any exceptions if it happens
- successes.append(0)
- plot_percent_successful(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- successes=successes,
- filename=filename,
- style=style,
- )
- def files_plot(x, xticks, xlabels, results, filename, style):
- total = [result.files_total for result in results]
- successes = [result.files_successes for result in results]
- failures = [result.files_failures for result in results]
- fig = plt.figure()
- graph = fig.add_subplot(111)
- graph.plot(
- x,
- total,
- color=style.total_color,
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- graph.plot(
- x,
- successes,
- color=style.success_color,
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- graph.plot(
- x,
- failures,
- color=style.fail_color,
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- fig.autofmt_xdate()
- graph.set_xticks(xticks)
- graph.set_xticklabels(xlabels)
- fig.savefig(filename)
- def files_percent_plot(x, xticks, xlabels, results, filename, style):
- successes = []
- failures = []
- for result in results:
- if result.files_total:
- successes.append(float(result.files_successes) / result.files_total * 100)
- failures.append(float(result.files_failures) / result.files_total * 100)
- else:
- # this is not expected to happen
- # but we don't want any exceptions if it happens
- successes.append(0)
- failures.append(0)
- plot_percents(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- successes=successes,
- failures=failures,
- filename=filename,
- style=style,
- )
- def info_plot(x, xticks, xlabels, results, filename, style):
- modules = [len(result.tested_modules) for result in results]
- names = [len(result.names) for result in results]
- authors = [len(result.test_files_authors) for result in results]
- # we want just unique directories
- dirs = [len(set(result.tested_dirs)) for result in results]
- fig = plt.figure()
- graph = fig.add_subplot(111)
- graph.plot(
- x,
- names,
- color="b",
- label="Test files",
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- graph.plot(
- x,
- modules,
- color="g",
- label="Tested modules",
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- # dirs == testsuites
- graph.plot(
- x,
- dirs,
- color="orange",
- label="Tested directories",
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- graph.plot(
- x,
- authors,
- color="r",
- label="Test authors",
- linestyle=style.linestyle,
- linewidth=style.linewidth,
- )
- graph.legend(loc="best", shadow=False)
- fig.autofmt_xdate()
- graph.set_xticks(xticks)
- graph.set_xticklabels(xlabels)
- fig.savefig(filename)
- # TODO: solve the directory inconsitencies, implement None
- def main_page(
- results, filename, images, captions, title="Test reports", directory=None
- ):
- filename = os.path.join(directory, filename)
- with open(filename, "w") as page:
- page.write(
- "<html><body>"
- "<h1>{title}</h1>"
- "<table>"
- "<thead><tr>"
- "<th>Date (timestamp)</th><th>SVN revision</th><th>Name</th>"
- "<th>Successful files</th><th>Successful tests</th>"
- "</tr></thead>"
- "<tbody>".format(title=title)
- )
- for result in reversed(results):
- # TODO: include name to summary file
- # now using location or test report directory as name
- if result.location != "unknown":
- name = result.location
- else:
- name = os.path.basename(result.report)
- if not name:
- # Python basename returns '' for 'abc/'
- for d in reversed(os.path.split(result.report)):
- if d:
- name = d
- break
- per_test = success_to_html_percent(
- total=result.total, successes=result.successes
- )
- per_file = success_to_html_percent(
- total=result.files_total, successes=result.files_successes
- )
- report_path = os.path.relpath(path=result.report, start=directory)
- page.write(
- "<tr>"
- "<td><a href={report_path}/index.html>{result.timestamp}</a></td>"
- "<td>{result.svn_revision}</td>"
- "<td><a href={report_path}/index.html>{name}</a></td>"
- "<td>{pfiles}</td><td>{ptests}</td>"
- "</tr>".format(
- result=result,
- name=name,
- report_path=report_path,
- pfiles=per_file,
- ptests=per_test,
- )
- )
- page.write("</tbody></table>")
- for image, caption in itertools.izip(images, captions):
- page.write(
- "<h3>{caption}<h3>"
- '<img src="{image}" alt="{caption}" title="{caption}">'.format(
- image=image, caption=caption
- )
- )
- page.write("</body></html>")
- def main():
- parser = argparse.ArgumentParser(
- description="Create overall report from several individual test reports"
- )
- parser.add_argument(
- "reports",
- metavar="report_directory",
- type=str,
- nargs="+",
- help="Directories with reports",
- )
- parser.add_argument(
- "--output",
- dest="output",
- action="store",
- default="testreports_summary",
- help="Output directory",
- )
- parser.add_argument(
- "--timestamps",
- dest="timestamps",
- action="store_true",
- help="Use file timestamp instead of date in test summary",
- )
- args = parser.parse_args()
- output = args.output
- reports = args.reports
- use_timestamps = args.timestamps
- ensure_dir(output)
- all_results = []
- results_in_locations = defaultdict(list)
- for report in reports:
- try:
- summary_file = os.path.join(report, "test_keyvalue_result.txt")
- if not os.path.exists(summary_file):
- sys.stderr.write(
- "WARNING: Key-value summary not available in"
- " report <%s>, skipping.\n" % summary_file
- )
- # skipping incomplete reports
- # use only results list for further processing
- continue
- summary = text_to_keyvalue(open(summary_file).read(), sep="=")
- if use_timestamps:
- test_timestamp = datetime.datetime.fromtimestamp(
- os.path.getmtime(summary_file)
- )
- else:
- test_timestamp = datetime.datetime.strptime(
- summary["timestamp"], "%Y-%m-%d %H:%M:%S"
- )
- result = TestResultSummary()
- result.timestamp = test_timestamp
- result.total = summary["total"]
- result.successes = summary["successes"]
- result.failures = summary["failures"]
- result.errors = summary["errors"]
- result.files_total = summary["files_total"]
- result.files_successes = summary["files_successes"]
- result.files_failures = summary["files_failures"]
- result.svn_revision = str(summary["svn_revision"])
- result.tested_modules = summary["tested_modules"]
- result.names = summary["names"]
- result.test_files_authors = summary["test_files_authors"]
- result.tested_dirs = summary["tested_dirs"]
- result.report = report
- # let's consider no location as valid state and use 'unknown'
- result.location = summary.get("location", "unknown")
- result.location_type = summary.get("location_type", "unknown")
- # grouping according to location types
- # this can cause that two actual locations tested at the same time
- # will end up together, this is not ideal but testing with
- # one location type and different actual locations is not standard
- # and although it will not break anything it will not give a nice
- # report
- results_in_locations[result.location_type].append(result)
- all_results.append(result)
- del result
- except KeyError as e:
- print("File %s does not have right values (%s)" % (report, e.message))
- locations_main_page = open(os.path.join(output, "index.html"), "w")
- locations_main_page.write(
- "<html><body>"
- "<h1>Test reports grouped by location type</h1>"
- "<table>"
- "<thead><tr>"
- "<th>Location</th>"
- "<th>Successful files</th><th>Successful tests</th>"
- "</tr></thead>"
- "<tbody>"
- )
- PlotStyle = namedtuple(
- "PlotStyle",
- ["linestyle", "linewidth", "success_color", "fail_color", "total_color"],
- )
- plot_style = PlotStyle(
- linestyle="-", linewidth=4.0, success_color="g", fail_color="r", total_color="b"
- )
- for location_type, results in results_in_locations.items():
- results = sorted(results, key=operator.attrgetter("timestamp"))
- # TODO: document: location type must be a valid dir name
- directory = os.path.join(output, location_type)
- ensure_dir(directory)
- if location_type == "unknown":
- title = "Test reports"
- else:
- title = "Test reports for <{type}> location type".format(
- type=location_type
- )
- x = [date2num(result.timestamp) for result in results]
- # the following would be an alternative but it does not work with
- # labels and automatic axis limits even after removing another date fun
- # x = [result.svn_revision for result in results]
- xlabels = [
- result.timestamp.strftime("%Y-%m-%d") + " (r" + result.svn_revision + ")"
- for result in results
- ]
- step = len(x) / 10
- xticks = x[step::step]
- xlabels = xlabels[step::step]
- tests_successful_plot(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- results=results,
- filename=os.path.join(directory, "tests_successful_plot.png"),
- style=plot_style,
- )
- files_successful_plot(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- results=results,
- filename=os.path.join(directory, "files_successful_plot.png"),
- style=plot_style,
- )
- tests_plot(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- results=results,
- filename=os.path.join(directory, "tests_plot.png"),
- style=plot_style,
- )
- tests_percent_plot(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- results=results,
- filename=os.path.join(directory, "tests_percent_plot.png"),
- style=plot_style,
- )
- files_plot(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- results=results,
- filename=os.path.join(directory, "files_plot.png"),
- style=plot_style,
- )
- files_percent_plot(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- results=results,
- filename=os.path.join(directory, "files_percent_plot.png"),
- style=plot_style,
- )
- info_plot(
- x=x,
- xticks=xticks,
- xlabels=xlabels,
- results=results,
- filename=os.path.join(directory, "info_plot.png"),
- style=plot_style,
- )
- main_page(
- results=results,
- filename="index.html",
- images=[
- "tests_successful_plot.png",
- "files_successful_plot.png",
- "tests_plot.png",
- "files_plot.png",
- "tests_percent_plot.png",
- "files_percent_plot.png",
- "info_plot.png",
- ],
- captions=[
- "Success of individual tests in percents",
- "Success of test files in percents",
- "Successes, failures and number of individual tests",
- "Successes, failures and number of test files",
- "Successes and failures of individual tests in percent",
- "Successes and failures of test files in percents",
- "Additional information",
- ],
- directory=directory,
- title=title,
- )
- files_successes = sum(result.files_successes for result in results)
- files_total = sum(result.files_total for result in results)
- successes = sum(result.successes for result in results)
- total = sum(result.total for result in results)
- per_test = success_to_html_percent(total=total, successes=successes)
- per_file = success_to_html_percent(total=files_total, successes=files_successes)
- locations_main_page.write(
- "<tr>"
- "<td><a href={location}/index.html>{location}</a></td>"
- "<td>{pfiles}</td><td>{ptests}</td>"
- "</tr>".format(location=location_type, pfiles=per_file, ptests=per_test)
- )
- locations_main_page.write("</tbody></table>")
- locations_main_page.write("</body></html>")
- locations_main_page.close()
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|