ci/tools: Update changeset analyzer tool

- Add feature to read labels from github PR
- Match Python coding guidelines
- Update rule list to skip certain tests if Github labels say so
This commit is contained in:
Martin Braun 2024-10-04 17:06:14 +02:00 committed by mkoop-ni
parent 79ff09bb8d
commit aeff51b599
3 changed files with 123 additions and 64 deletions

View File

@ -29,7 +29,7 @@ jobs:
fi
python3 -m venv $AGENT_TEMPDIRECTORY/ac_venv
$AGENT_TEMPDIRECTORY/ac_venv/$VENV_BIN_DIR/pip install --upgrade pip
$AGENT_TEMPDIRECTORY/ac_venv/$VENV_BIN_DIR/pip install ruamel.yaml clang-format==14.0.6 ni-python-styleguide
$AGENT_TEMPDIRECTORY/ac_venv/$VENV_BIN_DIR/pip install ruamel.yaml clang-format==14.0.6 ni-python-styleguide requests
git branch
set -o xtrace
$AGENT_TEMPDIRECTORY/ac_venv/$VENV_BIN_DIR/python3 tools/changeset_testlist.py \

View File

@ -4,53 +4,57 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
Changeset analyzer: Reads changes made on a particular branch compared to
"""Changeset analyzer: Create list of required tests based on changes in a branch.
This script reads changes made on a particular branch compared to
another and computes a list of tests that need to be run to validate this branch.
"""
import argparse
import os
import pathlib
import re
import sys
import subprocess
import shutil
import argparse
import subprocess
import sys
import requests
from ruamel import yaml
def parse_args():
"""
Parse and return args.
"""
"""Parse and return args."""
parser = argparse.ArgumentParser(
description=__doc__,
)
parser.add_argument(
'--source-branch',
help="Source branch off diff. Defaults to the current branch.")
"--source-branch", help="Source branch off diff. Defaults to the current branch."
)
parser.add_argument(
'--target-branch', default='master',
help="Target branch off diff. Defaults to 'master'.")
"--target-branch", default="master", help="Target branch off diff. Defaults to 'master'."
)
parser.add_argument(
'--repo-path', default='.',
help="Path to the UHD git repository. Defaults to the current directory.")
"--repo-path",
default=".",
help="Path to the UHD git repository. Defaults to the current directory.",
)
parser.add_argument("--rule-file", help="Path to rules file.")
parser.add_argument("--set-azdo-var", help="Generate output to set an AzDO variable")
parser.add_argument("--list-tests", action="store_true", help="Show the generated test-list.")
parser.add_argument(
'--rule-file',
help="Path to rules file.")
"--include-target",
action="store_true",
help="Include changes that originate from the target branch.",
)
parser.add_argument(
'--set-azdo-var',
help="Generate output to set an AzDO variable")
parser.add_argument(
'--list-tests', action='store_true',
help="Show the generated test-list.")
parser.add_argument(
'--include-target', action='store_true',
help="Include changes that originate from the target branch.")
parser.add_argument(
'-v', '--verbose', action='store_true',
help="Verbose output")
"--github-label-api-endpoint",
help="Endpoint to use for GitHub API requests read back labels.",
)
parser.add_argument("--github-token", help="GitHub token to use for API requests.")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
return parser.parse_args()
def get_changed_files(repo_path, target_branch, source_branch, include_target):
"""
Returns a list of paths in the UHD repository that have are different between
@ -61,86 +65,125 @@ def get_changed_files(repo_path, target_branch, source_branch, include_target):
# not included. If we want to change this, then couple this with
# git diff --name-only (no further arguments)
if not include_target:
target_branch += '...'
git_cmd = shutil.which('git')
get_diff_args = [
git_cmd,
'diff',
'--name-only',
target_branch]
target_branch += "..."
git_cmd = shutil.which("git")
get_diff_args = [git_cmd, "diff", "--name-only", target_branch]
if source_branch:
get_diff_args.append(source_branch)
files = subprocess.check_output(get_diff_args, cwd=repo_path, encoding='utf-8')
files = subprocess.check_output(get_diff_args, cwd=repo_path, encoding="utf-8")
return files.strip().split("\n")
def load_rules(rule_file):
"""
Return the rules as a Python list.
"""
with open(rule_file, 'r', encoding='utf-8') as rfd:
y = yaml.YAML(typ='safe', pure=True)
"""Return the rules as a Python list."""
with open(rule_file, "r", encoding="utf-8") as rfd:
y = yaml.YAML(typ="safe", pure=True)
return y.load(rfd)
class RuleApplier:
"""
Helper class to update an internal test list based on a set of rules.
"""
def __init__(self, rules):
"""Helper class to update an internal test list based on a set of rules."""
def __init__(self, rules, labels):
"""Initialize.
Arguments:
rules: List of rules to apply.
"""
self.rules = rules
self.labels = labels
self.test_list = set()
def apply(self, filename, verbose=False):
"""
Apply rules against a file.
"""
"""Apply rules against a file."""
for rule in self.rules:
if self._apply_rule(rule, filename, verbose):
break
def apply_labels(self):
"""Apply rules based only on labels."""
for rule in self.rules:
if (
"label" in rule
and rule["label"] in self.labels
and ("re" not in rule and "name" not in rule)
):
self.test_list.update(rule.get("add", []))
self.test_list.difference_update(rule.get("remove", []))
for remove_re in rule.get("remove_re", []):
self.test_list.difference_update(
set([test for test in self.test_list if re.search(remove_re, test)])
)
def _apply_rule(self, rule, filename, verbose=False):
"""
Helper: Apply a single rule.
"""Apply a single rule.
Returns True if you can stop applying rules against this filename.
"""
# First: Check if this rule even applies to this file
if 're' in rule and not re.search(rule['re'], filename) \
or 'name' in rule and rule['name'] != filename:
if (
("re" in rule and not re.search(rule["re"], filename))
or ("name" in rule and rule["name"] != filename)
or ("name" not in rule and "re" not in rule)
):
return False
if verbose:
sys.stderr.write(f"Filename {filename} matches rule: {rule}\n")
if 'add' in rule:
self.test_list.update(rule['add'])
if "label" in rule and not rule["label"] in self.labels:
return False
if "label" in rule and verbose:
sys.stderr.write(f"Label {rule['label']} found\n")
if "add" in rule:
self.test_list.update(rule["add"])
if "remove" in rule:
self.test_list.difference_update(rule["remove"])
# If stop is specified as False, then we can still apply more rules.
if 'stop' in rule and not rule['stop']:
if "stop" in rule and not rule["stop"]:
return False
return True
def get_labels(api_endpoint, token):
"""Get a list of labels from the GitHub API."""
if not api_endpoint:
return []
if token:
headers = {"Authorization": f"Bearer {token}"}
else:
headers = {}
data = requests.get(api_endpoint, headers=headers).json()
labels = [label["name"] for label in data]
print("Labels:", ", ".join(labels))
return labels
def main():
"""
Gogogo!
"""
"""Gogogo! Run script."""
args = parse_args()
rule_file = args.rule_file
if not rule_file:
rule_file = os.path.join(pathlib.Path(__file__).parent.resolve(), 'changeset_testlist.yaml')
rule_file = os.path.join(pathlib.Path(__file__).parent.resolve(), "changeset_testlist.yaml")
file_list = get_changed_files(
args.repo_path,
args.target_branch,
args.source_branch,
args.include_target,
)
rule_applier = RuleApplier(load_rules(rule_file))
labels = get_labels(args.github_label_api_endpoint, args.github_token)
rule_applier = RuleApplier(load_rules(rule_file), labels)
for filename in file_list:
rule_applier.apply(filename, args.verbose)
rule_applier.apply_labels()
if args.set_azdo_var:
print(
f"##vso[task.setvariable variable={args.set_azdo_var};isoutput=true]" + \
';'.join(rule_applier.test_list))
f"##vso[task.setvariable variable={args.set_azdo_var};isoutput=true]"
+ ";".join(rule_applier.test_list)
)
if args.list_tests:
print("Required tests:")
print("---------------", end='')
print('\n* '.join(sorted([''] + list(rule_applier.test_list))))
print("---------------", end="")
print("\n* ".join(sorted([""] + list(rule_applier.test_list))))
if __name__ == "__main__":
sys.exit(main())

View File

@ -200,3 +200,19 @@
add:
- uhd.build.linux
- hw.rf.x4xx
###############################################################################
# LABEL-BASED RULES
###############################################################################
- label: ci:skip_hw_tests
remove_re:
- "^hw"
- "^devtest"
- label: ci:skip_devtests
remove_re:
- "^devtest"
- label: ci:skip_rf_tests
remove_re:
- "^hw.rf"