Files
galaxy/scripts/tests_markdown.py
T
2024-04-29 00:09:38 +01:00

309 lines
10 KiB
Python

#!/usr/bin/env python
import argparse
import json
import os
import statistics
import sys
import jinja2
from mir import html_report
DESCRIPTION = "Script to generate (potentially merged) HTML summary of Galaxy Test Performance"
templateLoader = jinja2.FileSystemLoader(searchpath="./scripts")
template_env = jinja2.Environment(loader=templateLoader)
TEMPLATE_FILE = "tests_markdown.tpl"
TEMPLATE_COMPARE_FILE = "tests_markdown_compare.tpl"
LINKS = [{"href": "https://github.com/galaxyproject/galaxy", "title": "Galaxy"}]
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
args = _parser().parse_args(argv)
common_env = {
"include_raw_metrics": args.include_raw_metrics,
}
if len(args.input_path) == 1:
markdown_output = _summarize_one(args.input_path[0], common_env)
else:
markdown_output = _summarize_many(args.input_path, common_env)
html_output = html_report(markdown_output, args.title, links=LINKS)
with open(args.output_path, "w") as f:
f.write(html_output)
def _summarize_one(input_path, common_env):
environment = _prepare_raw_data(input_path)
environment.update(common_env)
template = template_env.get_template(TEMPLATE_FILE)
output = template.render(environment)
return output
def _summarize_many(input_paths, common_env):
raw_data_dicts = []
for arg in input_paths:
raw_data_dicts.append(_prepare_raw_data(arg))
environment = _merge_summarizes(raw_data_dicts)
environment.update(common_env)
template = template_env.get_template(TEMPLATE_COMPARE_FILE)
output = template.render(environment)
return output
def _empty_statistics():
return {"count": 0, "sum": "n/a", "median": "n/a", "stdev": "n/a", "mean": "n/a"}
def _merge_summarizes(raw_data_dicts):
tests = {}
api_endpoints = {}
internals = {}
all_tests = set()
all_api_endpoints = set()
all_internal_metrics = set()
all_labels = set()
for raw_data_dict in raw_data_dicts:
these_api_endpoints = raw_data_dict["raw_data"]["api_endpoint_metrics"]
for api_endpoint in these_api_endpoints.keys():
all_api_endpoints.add(api_endpoint)
these_internal_endpoints = raw_data_dict["raw_data"]["internals_metrics"]
for internal_endpoint in these_internal_endpoints.keys():
all_internal_metrics.add(internal_endpoint)
these_tests = raw_data_dict["raw_data"]["tests"]
for test in these_tests:
all_tests.add(test["nodeid"])
all_labels.add(raw_data_dict["label"])
for label in all_labels:
for api_endpoint in all_api_endpoints:
_ensure_has_dict_at_key(
api_endpoints,
api_endpoint,
total_time={},
sql_time={},
sql_queries={},
)
api_endpoints[api_endpoint]["total_time"][label] = _empty_statistics()
api_endpoints[api_endpoint]["sql_time"][label] = _empty_statistics()
for internal_endpoint in all_internal_metrics:
_ensure_has_dict_at_key(
internals,
internal_endpoint,
total_time={},
)
internals[internal_endpoint]["total_time"][label] = _empty_statistics()
for test in all_tests:
if test not in tests:
tests[test] = {}
tests[test][label] = {"outcome": "absent"}
for raw_data_dict in raw_data_dicts:
ab_label = raw_data_dict["label"]
these_api_endpoints = raw_data_dict["raw_data"]["api_endpoint_metrics"]
these_internals = raw_data_dict["raw_data"]["internals_metrics"]
these_tests = raw_data_dict["raw_data"]["tests"]
for api_endpoint, endpoint_metrics in these_api_endpoints.items():
api_endpoints[api_endpoint]["label"] = endpoint_metrics["label"]
api_endpoints[api_endpoint]["total_time"][ab_label].update(endpoint_metrics["total_time"])
api_endpoints[api_endpoint]["sql_time"][ab_label].update(endpoint_metrics["sql_time"])
for endpoint, endpoint_metrics in these_internals.items():
internals[endpoint]["label"] = endpoint_metrics["label"]
internals[endpoint]["total_time"][ab_label].update(endpoint_metrics["total_time"])
for test in these_tests:
tests[test["nodeid"]][ab_label]["outcome"] = test.get("outcome")
return {
"raw_data": {
"api_endpoint_metrics": api_endpoints,
"internals_metrics": internals,
"tests": tests,
}
}
def _prepare_raw_data(path):
with open(path) as f:
inp_dict = json.load(f)
environment = dict(raw_data=inp_dict, label=os.path.basename(path))
__inject_summary(environment)
__inject_api_timing_summary_environment(environment)
__inject_api_timing_summary_across_tests(environment)
__inject_raw_timings(environment)
return environment
def __inject_api_timing_summary_environment(environment):
for test in environment["raw_data"]["tests"]:
if "metadata" not in test:
continue
if "local_metrics" in test["metadata"]:
__inject_api_timing_summary_test(test)
def __inject_api_timing_summary_across_tests(environment):
api_endpoints = {}
internals = {}
for test in environment["raw_data"]["tests"]:
if "metadata" not in test:
continue
test_endpoints = test["api_endpoint_metrics"]
for api_endpoint, endpoint_metrics in test_endpoints.items():
_ensure_has_dict_at_key(
api_endpoints,
api_endpoint,
total_time={"raw": []},
sql_time={"raw": []},
sql_queries={"raw": []},
)
api_endpoints[api_endpoint]["label"] = endpoint_metrics["label"]
api_endpoints[api_endpoint]["total_time"]["raw"].extend(endpoint_metrics["total_time"]["raw"])
api_endpoints[api_endpoint]["sql_time"]["raw"].extend(endpoint_metrics["sql_time"]["raw"])
api_endpoints[api_endpoint]["sql_queries"]["raw"].extend(endpoint_metrics["sql_queries"]["raw"])
test_endpoints = test["internals_metrics"]
for api_endpoint, endpoint_metrics in test_endpoints.items():
_ensure_has_dict_at_key(
internals,
api_endpoint,
total_time={"raw": []},
)
internals[api_endpoint]["label"] = endpoint_metrics["label"]
internals[api_endpoint]["total_time"]["raw"].extend(endpoint_metrics["total_time"]["raw"])
for endpoint_metrics in api_endpoints.values():
__inject_statistics(endpoint_metrics["total_time"])
__inject_statistics(endpoint_metrics["sql_time"])
__inject_statistics(endpoint_metrics["sql_queries"])
for endpoint_metrics in internals.values():
__inject_statistics(endpoint_metrics["total_time"])
environment["raw_data"]["api_endpoint_metrics"] = api_endpoints
environment["raw_data"]["internals_metrics"] = internals
def __inject_raw_timings(environment):
all_timings = []
for test in environment["raw_data"]["tests"]:
if "metadata" not in test or "local_metrics" not in test["metadata"]:
continue
metrics = test["metadata"]["local_metrics"]
timing = metrics["timing"]
for endpoint, timings in timing.items():
recording = timings[0].copy()
recording["endpoint"] = endpoint
all_timings.append(recording)
environment["raw_data"]["all_timings"] = all_timings
def _ensure_has_dict_at_key(the_dict, key, **kwd):
if key not in the_dict:
the_dict[key] = dict(**kwd)
def __inject_api_timing_summary_test(test):
metrics = test["metadata"]["local_metrics"]
timing = metrics["timing"]
counter = metrics["counter"]
api_endpoints = {}
internal_timings = {}
def summarize_times(timings):
times = [t["time"] for t in timings]
return __inject_statistics(
{
"raw": times,
}
)
def summarize_counter(c):
counters = [t["n"] for t in c]
return __inject_statistics(
{
"raw": counters,
}
)
for endpoint, timings in timing.items():
if not endpoint.startswith("api"):
continue
endpoint_summary = {"total_time": summarize_times(timings), "label": endpoint[len("api.") :]}
sql_times = f"sql.{endpoint}"
if sql_times in timing:
endpoint_summary["sql_time"] = summarize_times(timing[sql_times])
sql_queries = f"sqlqueries.{endpoint}"
if sql_queries in counter:
endpoint_summary["sql_queries"] = summarize_counter(counter[sql_queries])
api_endpoints[endpoint] = endpoint_summary
for endpoint, timings in timing.items():
if not endpoint.startswith("internals"):
continue
internal_summary = {"total_time": summarize_times(timings), "label": endpoint[len("internals.") :]}
internal_timings[endpoint] = internal_summary
test["api_endpoint_metrics"] = api_endpoints
test["internals_metrics"] = internal_timings
def __inject_statistics(from_dict):
raw_values = from_dict["raw"]
from_dict["sum"] = sum(raw_values)
from_dict["median"] = f"{statistics.median(raw_values):.2f}"
if len(raw_values) > 1:
from_dict["stdev"] = f"{statistics.stdev(raw_values):.4f}"
else:
from_dict["stdev"] = "n/a"
from_dict["mean"] = f"{statistics.mean(raw_values):.2f}"
from_dict["count"] = len(raw_values)
return from_dict
def __inject_summary(environment):
total = 0
failures = 0
skips = 0
for test in environment["raw_data"]["tests"]:
total += 1
status = test.get("outcome")
if status == "failed":
failures += 1
elif status == "skipped":
skips += 1
environment["raw_data"]["results"] = {
"total": total,
"failures": failures,
"skips": skips,
}
def _parser():
parser = argparse.ArgumentParser(description=DESCRIPTION)
parser.add_argument("input_path", metavar="INPUT", type=str, nargs="+", help="structured input path (.json)")
parser.add_argument("--output_path", type=str, default="test.html", help="output path (.html)")
parser.add_argument("--title", type=str, default="Test Performance Summary", help="Performance Test Results")
parser.add_argument("--include_raw_metrics", action="store_true", default=False)
return parser
if __name__ == "__main__":
main()