| #!/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) |