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