blob: fb930af0ba46f689f5e3d114a6970ca41181f7ef [file] [log] [blame]
Fathi Boudra422bf772019-12-02 11:10:16 +02001#!/usr/bin/env python3
2#
3# Copyright (c) 2019, Arm Limited. All rights reserved.
4#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8# This script examines the checked out copy of a Git repository, inspects the
9# touched files in a commit, and then determines what test configs are suited to
10# be executed when testing the repository.
11#
12# The test nominations are based on the paths touched in a commit: for example,
13# when foo/bar is touched, run test blah:baz. All nominations are grouped under
14# NOMINATED directory.
15#
16# The script must be invoked from within a Git clone.
17
18import argparse
19import functools
20import os
21import re
22import subprocess
23import sys
24
25
26class Commit:
27 # REs to identify differ header
28 diff_re = re.compile(r"[+-]")
29 hunk_re = re.compile(r"(\+{3}|-{3}) [ab]/")
30
31 # A diff line looks like a diff, of course, but is not a hunk header
32 is_diff = lambda l: Commit.diff_re.match(l) and not Commit.hunk_re.match(l)
33
34 def __init__(self, refspec):
35 self.refspec = refspec
36
37 @functools.lru_cache()
38 def touched_files(self, parent):
39 git_cmd = ("git diff-tree --no-commit-id --name-only -r " +
40 self.refspec).split()
41 if parent:
42 git_cmd.append(parent)
43
44 return subprocess.check_output(git_cmd).decode(encoding='UTF-8').split(
45 "\n")
46
47 @functools.lru_cache()
48 def diff_lines(self, parent):
49 against = parent if parent else (self.refspec + "^")
50 git_cmd = "git diff {} {}".format(against, self.refspec).split()
51
52 # Filter valid diff lines from the git diff output
53 return list(filter(Commit.is_diff, subprocess.check_output(
54 git_cmd).decode(encoding="UTF-8").split("\n")))
55
56 def matches(self, rule, parent):
57 if type(rule) is str:
58 scheme, colon, rest = rule.partition(":")
59 if colon != ":":
60 raise Exception("rule {} doesn't have a scheme".format(rule))
61
62 if scheme == "path":
63 # Rule is path in plain string
64 return any(f.startswith(rest) for f in self.touched_files(parent))
65 elif scheme == "pathre":
66 # Rule is a regular expression matched against path
67 regex = re.compile(rest)
68 return any(regex.search(f) for f in self.touched_files(parent))
69 elif scheme == "has":
70 # Rule is a regular expression matched against the commit diff
71 has_upper = any(c.isupper() for c in rule)
72 pat_re = re.compile(rest, re.IGNORECASE if not has_upper else 0)
73
74 return any(pat_re.search(l) for l in self.diff_lines(parent))
75 elif scheme == "op":
76 pass
77 else:
78 raise Exception("unsupported scheme: " + scheme)
79 elif type(rule) is tuple:
80 # If op:match-all is found in the tuple, the tuple must match all
81 # rules (AND).
82 test = all if "op:match-all" in rule else any
83
84 # If the rule is a tuple, we match them individually
85 return test(self.matches(r, parent) for r in rule)
86 else:
87 raise Exception("unsupported rule type: {}".format(type(rule)))
88
89
90ci_root = os.path.abspath(os.path.join(__file__, os.pardir, os.pardir))
91group_dir = os.path.join(ci_root, "group")
92
93parser = argparse.ArgumentParser()
94
95# Argument setup
96parser.add_argument("--parent", help="Parent commit to compare against")
97parser.add_argument("--refspec", default="@", help="refspec")
98parser.add_argument("rules_file", help="Rules file")
99
100opts = parser.parse_args()
101
102# Import project-specific nomination_rules dictionary
103script_dir = os.path.dirname(os.path.abspath(__file__))
104with open(os.path.join(opts.rules_file)) as fd:
105 exec(fd.read())
106
107commit = Commit(opts.refspec)
108nominations = set()
109for rule, test_list in nomination_rules.items():
110 # Rule must be either string or tuple. Test list must be list
111 assert type(rule) is str or type(rule) is tuple
112 assert type(test_list) is list
113
114 if commit.matches(rule, opts.parent):
115 nominations |= set(test_list)
116
117for nom in nominations:
118 # Each test nomination must exist in the repository
119 if not os.path.isfile(os.path.join(group_dir, nom)):
120 raise Exception("nomination {} doesn't exist".format(nom))
121
122 print(nom)