Loading...
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2021 Intel Corporation

# A script to generate twister options based on modified files.

import re, os
import sh
import argparse
import glob
import yaml

if "ZEPHYR_BASE" not in os.environ:
    exit("$ZEPHYR_BASE environment variable undefined.")

repository_path = os.environ['ZEPHYR_BASE']
sh_special_args = {
    '_tty_out': False,
    '_cwd': repository_path
}

def parse_args():
    parser = argparse.ArgumentParser(
                description="Generate twister argument files based on modified file")
    parser.add_argument('-c', '--commits', default=None,
            help="Commit range in the form: a..b")
    return parser.parse_args()

def find_archs(files):
    # we match both arch/<arch>/* and include/arch/<arch> and skip common.
    # Some architectures like riscv require special handling, i.e. riscv
    # directory covers 2 architectures known to twister: riscv32 and riscv64.
    archs = set()

    for f in files:
        p = re.match(r"^arch\/([^/]+)\/", f)
        if not p:
            p = re.match(r"^include\/arch\/([^/]+)\/", f)
        if p:
            if p.group(1) != 'common':
                if p.group(1) == 'riscv':
                    archs.add('riscv32')
                    archs.add('riscv64')
                else:
                    archs.add(p.group(1))

    if archs:
        with open("modified_archs.args", "w") as fp:
            fp.write("-a\n%s" %("\n-a\n".join(archs)))

def find_boards(files):
    boards = set()
    all_boards = set()

    for f in files:
        if f.endswith(".rst") or f.endswith(".png") or f.endswith(".jpg"):
            continue
        p = re.match(r"^boards\/[^/]+\/([^/]+)\/", f)
        if p and p.groups():
            boards.add(p.group(1))

    for b in boards:
        suboards = glob.glob("boards/*/%s/*.yaml" %(b))
        for subboard in suboards:
            name = os.path.splitext(os.path.basename(subboard))[0]
            if name:
                all_boards.add(name)

    if all_boards:
        with open("modified_boards.args", "w") as fp:
            fp.write("-p\n%s" %("\n-p\n".join(all_boards)))

def find_tests(files):
    tests = set()
    for f in files:
        if f.endswith(".rst"):
            continue
        d = os.path.dirname(f)
        while d:
            if os.path.exists(os.path.join(d, "testcase.yaml")) or \
                os.path.exists(os.path.join(d, "sample.yaml")):
                tests.add(d)
                break
            else:
                d = os.path.dirname(d)

    if tests:
        with open("modified_tests.args", "w") as fp:
            fp.write("-T\n%s\n--all" %("\n-T\n".join(tests)))

def _get_match_fn(globs, regexes):
    # Constructs a single regex that tests for matches against the globs in
    # 'globs' and the regexes in 'regexes'. Parts are joined with '|' (OR).
    # Returns the search() method of the compiled regex.
    #
    # Returns None if there are neither globs nor regexes, which should be
    # interpreted as no match.

    if not (globs or regexes):
        return None

    regex = ""

    if globs:
        glob_regexes = []
        for glob in globs:
            # Construct a regex equivalent to the glob
            glob_regex = glob.replace(".", "\\.").replace("*", "[^/]*") \
                             .replace("?", "[^/]")

            if not glob.endswith("/"):
                # Require a full match for globs that don't end in /
                glob_regex += "$"

            glob_regexes.append(glob_regex)

        # The glob regexes must anchor to the beginning of the path, since we
        # return search(). (?:) is a non-capturing group.
        regex += "^(?:{})".format("|".join(glob_regexes))

    if regexes:
        if regex:
            regex += "|"
        regex += "|".join(regexes)

    return re.compile(regex).search

class Tag:
    """
    Represents an entry for a tag in tags.yaml.

    These attributes are available:

    name:
        List of GitHub labels for the area. Empty if the area has no 'labels'
        key.

    description:
        Text from 'description' key, or None if the area has no 'description'
        key
    """
    def _contains(self, path):
        # Returns True if the area contains 'path', and False otherwise

        return self._match_fn and self._match_fn(path) and not \
            (self._exclude_match_fn and self._exclude_match_fn(path))

    def __repr__(self):
        return "<Tag {}>".format(self.name)

def find_tags(files):

    tag_cfg_file = os.path.join(repository_path, 'scripts', 'ci', 'tags.yaml')
    with open(tag_cfg_file, 'r') as ymlfile:
        tags_config = yaml.safe_load(ymlfile)

    tags = {}
    for t,x in tags_config.items():
        tag = Tag()
        tag.exclude = True
        tag.name = t

        # tag._match_fn(path) tests if the path matches files and/or
        # files-regex
        tag._match_fn = _get_match_fn(x.get("files"), x.get("files-regex"))

        # Like tag._match_fn(path), but for files-exclude and
        # files-regex-exclude
        tag._exclude_match_fn = \
            _get_match_fn(x.get("files-exclude"), x.get("files-regex-exclude"))

        tags[tag.name] = tag

    for f in files:
        for t in tags.values():
            if t._contains(f):
                t.exclude = False

    exclude_tags = set()
    for t in tags.values():
        if t.exclude:
            exclude_tags.add(t.name)

    if exclude_tags:
        with open("modified_tags.args", "w") as fp:
            fp.write("-e\n%s" %("\n-e\n".join(exclude_tags)))


if __name__ == "__main__":

    args = parse_args()
    if not args.commits:
        exit(1)

    # pylint does not like the 'sh' library
    # pylint: disable=too-many-function-args,unexpected-keyword-arg
    commit = sh.git("diff", "--name-only", args.commits, **sh_special_args)
    files = commit.split("\n")

    find_boards(files)
    find_archs(files)
    find_tests(files)
    find_tags(files)