From 1d558285c27c9e578bb1ac533a06d6f77d9a53c2 Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Wed, 28 Jun 2023 22:08:47 +0200 Subject: [PATCH] frontend: add failed-to-succeeded-stats command Fix #2779 --- .../commands/failed_to_succeeded_stats.py | 147 ++++++++++++++++++ frontend/coprs_frontend/manage.py | 2 + 2 files changed, 149 insertions(+) create mode 100644 frontend/coprs_frontend/commands/failed_to_succeeded_stats.py diff --git a/frontend/coprs_frontend/commands/failed_to_succeeded_stats.py b/frontend/coprs_frontend/commands/failed_to_succeeded_stats.py new file mode 100644 index 000000000..8745a2478 --- /dev/null +++ b/frontend/coprs_frontend/commands/failed_to_succeeded_stats.py @@ -0,0 +1,147 @@ +""" +Generate failed to succeeded stats +""" + +import os +from datetime import datetime + +import click +import pygal +from coprs import models + + +@click.command() +@click.option("--dest", "-D", required=True, help="Result directory") +def failed_to_succeeded_stats(dest): + """ + Generate failed to succeeded stats + """ + print("Please wait, this will take at least 20 minutes.") + categories = { + "immediately": 0, + "seconds": 0, + "minutes": 0, + "hours": 0, + "days": 0, + "weeks": 0, + } + tuples = failed_to_succeeded_tuples() + for failed, succeeded in tuples: + delta = datetime.fromtimestamp(succeeded) \ + - datetime.fromtimestamp(failed) + categories[delta_to_category(delta)] += 1 + + os.makedirs(dest, exist_ok=True) + path = os.path.join(dest, "failed-to-succeeded-stats.svg") + generate_graph(categories, path) + print("Created: {0}".format(path)) + + +def get_builds(): + """ + Return list of all builds + """ + query = ( + models.Build.query + .join(models.Package) + .join(models.Copr) + .order_by(models.Build.id) + ) + + # Packit user + # query = query.filter(models.Copr.user_id==5576) + + # For faster development + # query = query.limit(1000) + + return query.all() + + +def builds_per_package(): + """ + Return a `dict` where keys are package IDs and values are lists + of all their builds. + """ + builds = get_builds() + result = {} + for build in builds: + result.setdefault(build.package_id, []) + result[build.package_id].append(build) + return result + + +def failed_to_succeeded_tuples(): + """ + Return a list of tuples. First value of each tuple is when the package + failed, and the second value is when it succeeded. + """ + tuples = [] + for builds in builds_per_package().values(): + if len(builds) <= 1: + # This package has only one build + # Not dealing with this now. + continue + + failed = None + succeeded = None + + for build in builds: + if not build.ended_on: + continue + + if build.state == "failed" and not failed: + failed = build + + elif build.state == "succeeded" and failed: + succeeded = build + + if failed and succeeded: + assert failed.id < succeeded.id + tuples.append((failed.ended_on, succeeded.ended_on)) + failed = None + succeeded = None + return tuples + + +def delta_to_category(delta): + """ + Convert timedelta into a custom time category + """ + seconds = delta.total_seconds() + if seconds < 0: + return "immediately" + if seconds < 60: + return "seconds" + if seconds < 60 * 60: + return "minutes" + if seconds < 60 * 60 * 24: + return "hours" + if seconds < 60 * 60 * 24 * 7: + return "days" + return "weeks" + + +def generate_graph(data, path): + """ + Generate graph from the data + """ + title = "How long before devs submit a successfull package after a failure?" + chart = pygal.Bar( + title=title, + print_values=True, + legend_at_bottom=True, + ) + for key, value in data.items(): + label = label_for_group(key) + chart.add(label, value) + chart.render_to_file(path) + return path + + +def label_for_group(key): + """ + User-friendly labels for the graph + """ + if key == "immediately": + return "Before it finished" + return key.capitalize() diff --git a/frontend/coprs_frontend/manage.py b/frontend/coprs_frontend/manage.py index f6f6df5e2..689cce9f6 100755 --- a/frontend/coprs_frontend/manage.py +++ b/frontend/coprs_frontend/manage.py @@ -42,6 +42,7 @@ import commands.chroots_template import commands.warning_banner import commands.usage_treemap +import commands.failed_to_succeeded_stats from coprs import app @@ -94,6 +95,7 @@ "delete_dirs", "warning_banner", "usage_treemap", + "failed_to_succeeded_stats", ]