16aa7eec5SAndrew Geissler#!/usr/bin/env python3
26aa7eec5SAndrew Geissler
36aa7eec5SAndrew Geissler# Yocto Project test results management tool
46aa7eec5SAndrew Geissler# This script is an thin layer over resulttool to manage tes results and regression reports.
56aa7eec5SAndrew Geissler# Its main feature is to translate tags or branch names to revisions SHA1, and then to run resulttool
66aa7eec5SAndrew Geissler# with those computed revisions
76aa7eec5SAndrew Geissler#
86aa7eec5SAndrew Geissler# Copyright (C) 2023 OpenEmbedded Contributors
96aa7eec5SAndrew Geissler#
106aa7eec5SAndrew Geissler# SPDX-License-Identifier: MIT
116aa7eec5SAndrew Geissler#
126aa7eec5SAndrew Geissler
136aa7eec5SAndrew Geisslerimport sys
146aa7eec5SAndrew Geisslerimport os
156aa7eec5SAndrew Geisslerimport argparse
166aa7eec5SAndrew Geisslerimport subprocess
176aa7eec5SAndrew Geisslerimport tempfile
186aa7eec5SAndrew Geisslerimport lib.scriptutils as scriptutils
196aa7eec5SAndrew Geissler
206aa7eec5SAndrew Geisslerscript_path = os.path.dirname(os.path.realpath(__file__))
216aa7eec5SAndrew Geisslerpoky_path = os.path.abspath(os.path.join(script_path, ".."))
226aa7eec5SAndrew Geisslerresulttool = os.path.abspath(os.path.join(script_path, "resulttool"))
236aa7eec5SAndrew Geisslerlogger = scriptutils.logger_create(sys.argv[0])
246aa7eec5SAndrew Geisslertestresults_default_url="git://git.yoctoproject.org/yocto-testresults"
256aa7eec5SAndrew Geissler
266aa7eec5SAndrew Geisslerdef create_workdir():
276aa7eec5SAndrew Geissler    workdir = tempfile.mkdtemp(prefix='yocto-testresults-query.')
286aa7eec5SAndrew Geissler    logger.info(f"Shallow-cloning testresults in {workdir}")
296aa7eec5SAndrew Geissler    subprocess.check_call(["git", "clone", testresults_default_url, workdir, "--depth", "1"])
306aa7eec5SAndrew Geissler    return workdir
316aa7eec5SAndrew Geissler
326aa7eec5SAndrew Geisslerdef get_sha1(pokydir, revision):
336aa7eec5SAndrew Geissler    try:
346aa7eec5SAndrew Geissler        rev = subprocess.check_output(["git", "rev-list", "-n", "1", revision], cwd=pokydir).decode('utf-8').strip()
356aa7eec5SAndrew Geissler        logger.info(f"SHA-1 revision for {revision} in {pokydir} is {rev}")
366aa7eec5SAndrew Geissler        return rev
376aa7eec5SAndrew Geissler    except subprocess.CalledProcessError:
386aa7eec5SAndrew Geissler        logger.error(f"Can not find SHA-1 for {revision} in {pokydir}")
396aa7eec5SAndrew Geissler        return None
406aa7eec5SAndrew Geissler
41fc113eadSAndrew Geisslerdef get_branch(tag):
42fc113eadSAndrew Geissler    # The tags in test results repository, as returned by git rev-list, have the following form:
43fc113eadSAndrew Geissler    # refs/tags/<branch>/<count>-g<sha1>/<num>
44fc113eadSAndrew Geissler    return '/'.join(tag.split("/")[2:-2])
45fc113eadSAndrew Geissler
466aa7eec5SAndrew Geisslerdef fetch_testresults(workdir, sha1):
476aa7eec5SAndrew Geissler    logger.info(f"Fetching test results for {sha1} in {workdir}")
486aa7eec5SAndrew Geissler    rawtags = subprocess.check_output(["git", "ls-remote", "--refs", "--tags", "origin", f"*{sha1}*"], cwd=workdir).decode('utf-8').strip()
496aa7eec5SAndrew Geissler    if not rawtags:
506aa7eec5SAndrew Geissler        raise Exception(f"No reference found for commit {sha1} in {workdir}")
51fc113eadSAndrew Geissler    branch = ""
526aa7eec5SAndrew Geissler    for rev in [rawtag.split()[1] for rawtag in rawtags.splitlines()]:
53fc113eadSAndrew Geissler        if not branch:
54fc113eadSAndrew Geissler            branch = get_branch(rev)
55fc113eadSAndrew Geissler        logger.info(f"Fetching matching revision: {rev}")
566aa7eec5SAndrew Geissler        subprocess.check_call(["git", "fetch", "--depth", "1", "origin", f"{rev}:{rev}"], cwd=workdir)
57fc113eadSAndrew Geissler    return branch
586aa7eec5SAndrew Geissler
59*ac13d5f3SPatrick Williamsdef compute_regression_report(workdir, basebranch, baserevision, targetbranch, targetrevision, args):
606aa7eec5SAndrew Geissler    logger.info(f"Running resulttool regression between SHA1 {baserevision} and {targetrevision}")
61*ac13d5f3SPatrick Williams    command = [resulttool, "regression-git", "--branch", basebranch, "--commit", baserevision, "--branch2", targetbranch, "--commit2", targetrevision, workdir]
62*ac13d5f3SPatrick Williams    if args.limit:
63*ac13d5f3SPatrick Williams        command.extend(["-l", args.limit])
64*ac13d5f3SPatrick Williams    report = subprocess.check_output(command).decode("utf-8")
656aa7eec5SAndrew Geissler    return report
666aa7eec5SAndrew Geissler
676aa7eec5SAndrew Geisslerdef print_report_with_header(report, baseversion, baserevision, targetversion, targetrevision):
686aa7eec5SAndrew Geissler    print("========================== Regression report ==============================")
696aa7eec5SAndrew Geissler    print(f'{"=> Target:": <16}{targetversion: <16}({targetrevision})')
706aa7eec5SAndrew Geissler    print(f'{"=> Base:": <16}{baseversion: <16}({baserevision})')
716aa7eec5SAndrew Geissler    print("===========================================================================\n")
726aa7eec5SAndrew Geissler    print(report, end='')
736aa7eec5SAndrew Geissler
746aa7eec5SAndrew Geisslerdef regression(args):
756aa7eec5SAndrew Geissler    logger.info(f"Compute regression report between {args.base} and {args.target}")
766aa7eec5SAndrew Geissler    if args.testresultsdir:
776aa7eec5SAndrew Geissler        workdir = args.testresultsdir
786aa7eec5SAndrew Geissler    else:
796aa7eec5SAndrew Geissler        workdir = create_workdir()
806aa7eec5SAndrew Geissler
816aa7eec5SAndrew Geissler    try:
826aa7eec5SAndrew Geissler        baserevision = get_sha1(poky_path, args.base)
836aa7eec5SAndrew Geissler        targetrevision = get_sha1(poky_path, args.target)
846aa7eec5SAndrew Geissler        if not baserevision or not targetrevision:
856aa7eec5SAndrew Geissler            logger.error("One or more revision(s) missing. You might be targeting nonexistant tags/branches, or are in wrong repository (you must use Poky and not oe-core)")
866aa7eec5SAndrew Geissler            if not args.testresultsdir:
876aa7eec5SAndrew Geissler                subprocess.check_call(["rm", "-rf",  workdir])
886aa7eec5SAndrew Geissler            sys.exit(1)
89fc113eadSAndrew Geissler        basebranch = fetch_testresults(workdir, baserevision)
90fc113eadSAndrew Geissler        targetbranch = fetch_testresults(workdir, targetrevision)
91*ac13d5f3SPatrick Williams        report = compute_regression_report(workdir, basebranch, baserevision, targetbranch, targetrevision, args)
926aa7eec5SAndrew Geissler        print_report_with_header(report, args.base, baserevision, args.target, targetrevision)
936aa7eec5SAndrew Geissler    finally:
946aa7eec5SAndrew Geissler        if not args.testresultsdir:
956aa7eec5SAndrew Geissler            subprocess.check_call(["rm", "-rf",  workdir])
966aa7eec5SAndrew Geissler
976aa7eec5SAndrew Geisslerdef main():
986aa7eec5SAndrew Geissler    parser = argparse.ArgumentParser(description="Yocto Project test results helper")
996aa7eec5SAndrew Geissler    subparsers = parser.add_subparsers(
1006aa7eec5SAndrew Geissler        help="Supported commands for test results helper",
1016aa7eec5SAndrew Geissler        required=True)
1026aa7eec5SAndrew Geissler    parser_regression_report = subparsers.add_parser(
1036aa7eec5SAndrew Geissler        "regression-report",
1046aa7eec5SAndrew Geissler        help="Generate regression report between two fixed revisions. Revisions can be branch name or tag")
1056aa7eec5SAndrew Geissler    parser_regression_report.add_argument(
1066aa7eec5SAndrew Geissler        'base',
1076aa7eec5SAndrew Geissler        help="Revision or tag against which to compare results (i.e: the older)")
1086aa7eec5SAndrew Geissler    parser_regression_report.add_argument(
1096aa7eec5SAndrew Geissler        'target',
1106aa7eec5SAndrew Geissler        help="Revision or tag to compare against the base (i.e: the newer)")
1116aa7eec5SAndrew Geissler    parser_regression_report.add_argument(
1126aa7eec5SAndrew Geissler        '-t',
1136aa7eec5SAndrew Geissler        '--testresultsdir',
1146aa7eec5SAndrew Geissler        help=f"An existing test results directory. {sys.argv[0]} will automatically clone it and use default branch if not provided")
115*ac13d5f3SPatrick Williams    parser_regression_report.add_argument(
116*ac13d5f3SPatrick Williams        '-l',
117*ac13d5f3SPatrick Williams        '--limit',
118*ac13d5f3SPatrick Williams        help=f"Maximum number of changes to display per test. Can be set to 0 to print all changes")
1196aa7eec5SAndrew Geissler    parser_regression_report.set_defaults(func=regression)
1206aa7eec5SAndrew Geissler
1216aa7eec5SAndrew Geissler    args = parser.parse_args()
1226aa7eec5SAndrew Geissler    args.func(args)
1236aa7eec5SAndrew Geissler
1246aa7eec5SAndrew Geisslerif __name__ == '__main__':
1256aa7eec5SAndrew Geissler    try:
1266aa7eec5SAndrew Geissler        ret =  main()
1276aa7eec5SAndrew Geissler    except Exception:
1286aa7eec5SAndrew Geissler        ret = 1
1296aa7eec5SAndrew Geissler        import traceback
1306aa7eec5SAndrew Geissler        traceback.print_exc()
1316aa7eec5SAndrew Geissler    sys.exit(ret)
132