diff --git a/squad-track-duration b/squad-track-duration new file mode 100755 index 0000000..54e7a19 --- /dev/null +++ b/squad-track-duration @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim: set ts=4 +# +# Copyright 2024-present Linaro Limited +# +# SPDX-License-Identifier: MIT + + +import argparse +import json +import logging +import os +import sys +from datetime import date, timedelta +from pathlib import Path + +import pandas as pd +import plotly.express as px +from squad_client.core.api import SquadApi +from squad_client.core.models import ALL, Squad + +squad_host_url = "https://qa-reports.linaro.org/" +SquadApi.configure(cache=3600, url=os.getenv("SQUAD_HOST", squad_host_url)) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +ARTIFACTORIAL_FILENAME = "builds.json" + + +class MetaFigure: + def __init__(self, plotly_fig, title, description): + self.plotly_fig = plotly_fig + self.title = title + self.description = description + + def fig(self): + return self.fig + + def title(self): + return self.title + + def description(self): + return self.description + + +def parse_args(): + parser = argparse.ArgumentParser(description="Track duration") + + parser.add_argument( + "--group", + required=True, + help="squad group", + ) + + parser.add_argument( + "--project", + required=True, + help="squad project", + ) + + parser.add_argument( + "--start-datetime", + required=True, + help="Starting date time. Example: 2022-01-01 or 2022-01-01T00:00:00", + ) + + parser.add_argument( + "--end-datetime", + required=True, + help="Ending date time. Example: 2022-12-31 or 2022-12-31T00:00:00", + ) + + parser.add_argument( + "--build-name", + required=False, + default="gcc-13-lkftconfig", + help="Build name", + ) + + parser.add_argument( + "--debug", + action="store_true", + default=False, + help="Display debug messages", + ) + + return parser.parse_args() + + +def get_cache_from_artifactorial(): + exists = os.path.exists(ARTIFACTORIAL_FILENAME) + if not exists: + return {} + + with open(ARTIFACTORIAL_FILENAME, "r") as fp: + builds = json.load(fp) + return builds + + return {} + + +def save_build_cache_to_artifactorial(data, days_ago=None): + with open(ARTIFACTORIAL_FILENAME, "w") as fp: + json.dump(data, fp) + + +def get_data(args, build_cache): + start_datetime = args.start_datetime + if "T" not in start_datetime: + start_datetime = f"{start_datetime}T00:00:00" + + end_datetime = args.end_datetime + if "T" not in end_datetime: + end_datetime = f"{end_datetime}T23:59:59" + + + group = Squad().group(args.group) + project = group.project(args.project) + environments = project.environments(count=ALL).values() + + start_date = start_datetime.split('T')[0] + end_date = end_datetime.split('T')[0] + + start_year = int(start_date.split("-")[0]) + start_month = int(start_date.split("-")[1]) + start_day = int(start_date.split("-")[2]) + to_year = int(end_date.split("-")[0]) + to_month = int(end_date.split("-")[1]) + to_day = int(end_date.split("-")[2]) + + first_start_day = True + tmp_data = [] + + tmp_start_date = date(start_year, start_month, start_day) + end_date = date(to_year, to_month, to_day) + delta = timedelta(days=1) + + tmp_start_date -= delta + + while tmp_start_date < end_date: + tmp_end_date = tmp_start_date + delta + tmp_start_date += delta + + if first_start_day: + first_start_day = False + start_time = f"T{start_datetime.split('T')[1]}" + else: + start_time = "T00:00:00" + + if tmp_end_date == end_date: + to_time = f"T{end_datetime.split('T')[1]}" + else: + to_time = "T23:59:59" + + logger.info(f"Fetching builds from SQUAD, start_datetime: {tmp_start_date}{start_time}, end_datetime: {tmp_end_date}{to_time}") + + filters = { + "created_at__lt": f"{tmp_end_date}{to_time}", + "created_at__gt": f"{tmp_start_date}{start_time}", + "count": ALL, + } + + builds = project.builds(**filters) + device_dict = {} + + # Loop through the environments and create a lookup table for URL -> device name (slug) + for env in environments: + device_dict[env.url] = env.slug + + # Loop through the builds in the specified window and cache their data + # to a file if they are marked as finished. This will mean that we don't + # have to look them up again is SQUAD if we have already looked them up. + for build_id, build in builds.items(): + if str(build_id) in build_cache.keys(): + logger.debug(f"cached: {build_id}") + tmp_data = tmp_data + build_cache[str(build_id)] + else: + logger.debug(f"no-cache: {build_id}") + tmp_build_cache = [] + testruns = build.testruns(count=ALL, prefetch_metadata=True) + for testrun_key, testrun in testruns.items(): + device = device_dict[testrun.environment] + metadata = testrun.metadata + + durations = metadata.durations + # Ignore testruns without duration data + if durations is None: + continue + + build_name = metadata.build_name + # Ignore testruns without a build_name + if build_name is None: + continue + + # Read the boot time from the duration data + boottime = durations["tests"]["boot"] + tmp = { + "build_id": build_id, + "build_name": build_name, + "git_describe": build.version.strip(), + "device": device, + "boottime": float(boottime), + "finished": build.finished, + "created_at": build.created_at, + } + tmp_data.append(tmp) + tmp_build_cache.append(tmp) + + # Cache data for builds that are marked finished + if build.finished and len(tmp_build_cache) > 0: + build_cache[str(build_id)] = tmp_build_cache + logger.debug(f"finished: {build_id}, {build.finished}") + + return tmp_data, build_cache + + +def combine_plotly_figs_to_html( + figs, + html_fname, + main_title, + main_description, + include_plotlyjs="cdn", + separator=None, + auto_open=False, +): + with open(html_fname, "w") as f: + f.write(f"