Initial commit for TF-A CI scripts

Signed-off-by: Fathi Boudra <fathi.boudra@linaro.org>
diff --git a/script/gen_nomination.py b/script/gen_nomination.py
new file mode 100755
index 0000000..fb930af
--- /dev/null
+++ b/script/gen_nomination.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+# This script examines the checked out copy of a Git repository, inspects the
+# touched files in a commit, and then determines what test configs are suited to
+# be executed when testing the repository.
+#
+# The test nominations are based on the paths touched in a commit: for example,
+# when foo/bar is touched, run test blah:baz. All nominations are grouped under
+# NOMINATED directory.
+#
+# The script must be invoked from within a Git clone.
+
+import argparse
+import functools
+import os
+import re
+import subprocess
+import sys
+
+
+class Commit:
+    # REs to identify differ header
+    diff_re = re.compile(r"[+-]")
+    hunk_re = re.compile(r"(\+{3}|-{3}) [ab]/")
+
+    # A diff line looks like a diff, of course, but is not a hunk header
+    is_diff = lambda l: Commit.diff_re.match(l) and not Commit.hunk_re.match(l)
+
+    def __init__(self, refspec):
+        self.refspec = refspec
+
+    @functools.lru_cache()
+    def touched_files(self, parent):
+        git_cmd = ("git diff-tree --no-commit-id --name-only -r " +
+                self.refspec).split()
+        if parent:
+            git_cmd.append(parent)
+
+        return subprocess.check_output(git_cmd).decode(encoding='UTF-8').split(
+                "\n")
+
+    @functools.lru_cache()
+    def diff_lines(self, parent):
+        against = parent if parent else (self.refspec + "^")
+        git_cmd = "git diff {} {}".format(against, self.refspec).split()
+
+        # Filter valid diff lines from the git diff output
+        return list(filter(Commit.is_diff, subprocess.check_output(
+                git_cmd).decode(encoding="UTF-8").split("\n")))
+
+    def matches(self, rule, parent):
+        if type(rule) is str:
+            scheme, colon, rest = rule.partition(":")
+            if colon != ":":
+                raise Exception("rule {} doesn't have a scheme".format(rule))
+
+            if scheme == "path":
+                # Rule is path in plain string
+                return any(f.startswith(rest) for f in self.touched_files(parent))
+            elif scheme == "pathre":
+                # Rule is a regular expression matched against path
+                regex = re.compile(rest)
+                return any(regex.search(f) for f in self.touched_files(parent))
+            elif scheme == "has":
+                # Rule is a regular expression matched against the commit diff
+                has_upper = any(c.isupper() for c in rule)
+                pat_re = re.compile(rest, re.IGNORECASE if not has_upper else 0)
+
+                return any(pat_re.search(l) for l in self.diff_lines(parent))
+            elif scheme == "op":
+                pass
+            else:
+                raise Exception("unsupported scheme: " + scheme)
+        elif type(rule) is tuple:
+            # If op:match-all is found in the tuple, the tuple must match all
+            # rules (AND).
+            test = all if "op:match-all" in rule else any
+
+            # If the rule is a tuple, we match them individually
+            return test(self.matches(r, parent) for r in rule)
+        else:
+            raise Exception("unsupported rule type: {}".format(type(rule)))
+
+
+ci_root = os.path.abspath(os.path.join(__file__, os.pardir, os.pardir))
+group_dir = os.path.join(ci_root, "group")
+
+parser = argparse.ArgumentParser()
+
+# Argument setup
+parser.add_argument("--parent", help="Parent commit to compare against")
+parser.add_argument("--refspec", default="@", help="refspec")
+parser.add_argument("rules_file", help="Rules file")
+
+opts = parser.parse_args()
+
+# Import project-specific nomination_rules dictionary
+script_dir = os.path.dirname(os.path.abspath(__file__))
+with open(os.path.join(opts.rules_file)) as fd:
+    exec(fd.read())
+
+commit = Commit(opts.refspec)
+nominations = set()
+for rule, test_list in nomination_rules.items():
+    # Rule must be either string or tuple. Test list must be list
+    assert type(rule) is str or type(rule) is tuple
+    assert type(test_list) is list
+
+    if commit.matches(rule, opts.parent):
+        nominations |= set(test_list)
+
+for nom in nominations:
+    # Each test nomination must exist in the repository
+    if not os.path.isfile(os.path.join(group_dir, nom)):
+        raise Exception("nomination {} doesn't exist".format(nom))
+
+    print(nom)