CoolFace
Apppublic

chendl/compositional_test

sourceHugging Faceupdated 3y agoView on Hugging Face
1likes
tests_fetcher.py723 linesDownload Raw Back to utils
1# coding=utf-82# Copyright 2021 The HuggingFace Inc. team.3#4# Licensed under the Apache License, Version 2.0 (the "License");5# you may not use this file except in compliance with the License.6# You may obtain a copy of the License at7#8#     http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing, software11# distributed under the License is distributed on an "AS IS" BASIS,12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13# See the License for the specific language governing permissions and14# limitations under the License.15 16"""17Welcome to tests_fetcher V2.18This util is designed to fetch tests to run on a PR so that only the tests impacted by the modifications are run, and19when too many models are being impacted, only run the tests of a subset of core models. It works like this.20 21Stage 1: Identify the modified files. This takes all the files from the branching point to the current commit (so22all modifications in a PR, not just the last commit) but excludes modifications that are on docstrings or comments23only.24 25Stage 2: Extract the tests to run. This is done by looking at the imports in each module and test file: if module A26imports module B, then changing module B impacts module A, so the tests using module A should be run. We thus get the27dependencies of each model and then recursively builds the 'reverse' map of dependencies to get all modules and tests28impacted by a given file. We then only keep the tests (and only the code models tests if there are too many modules).29 30Caveats:31  - This module only filters tests by files (not individual tests) so it's better to have tests for different things32    in different files.33  - This module assumes inits are just importing things, not really building objects, so it's better to structure34    them this way and move objects building in separate submodules.35"""36 37import argparse38import collections39import json40import os41import re42from contextlib import contextmanager43from pathlib import Path44 45from git import Repo46 47 48PATH_TO_REPO = Path(__file__).parent.parent.resolve()49PATH_TO_TRANFORMERS = PATH_TO_REPO / "src/transformers"50PATH_TO_TESTS = PATH_TO_REPO / "tests"51 52# List here the models to always test.53IMPORTANT_MODELS = [54    # Most downloaded models55    "bert",56    "clip",57    "t5",58    "xlm-roberta",59    "gpt2",60    "bart",61    "mpnet",62    "gpt-j",63    "wav2vec2",64    "deberta-v2",65    "layoutlm",66    "opt",67    "longformer",68    "vit",69    # Pipeline-specific model (to be sure each pipeline has one model in this list)70    "tapas",71    "vilt",72    "clap",73    "detr",74    "owlvit",75    "dpt",76    "videomae",77]78 79 80@contextmanager81def checkout_commit(repo, commit_id):82    """83    Context manager that checks out a commit in the repo.84    """85    current_head = repo.head.commit if repo.head.is_detached else repo.head.ref86 87    try:88        repo.git.checkout(commit_id)89        yield90 91    finally:92        repo.git.checkout(current_head)93 94 95def clean_code(content):96    """97    Remove docstrings, empty line or comments from `content`.98    """99    # fmt: off100    # Remove docstrings by splitting on triple " then triple ':101    splits = content.split('\"\"\"')102    content = "".join(splits[::2])103    splits = content.split("\'\'\'")104    # fmt: on105    content = "".join(splits[::2])106 107    # Remove empty lines and comments108    lines_to_keep = []109    for line in content.split("\n"):110        # remove anything that is after a # sign.111        line = re.sub("#.*$", "", line)112        if len(line) == 0 or line.isspace():113            continue114        lines_to_keep.append(line)115    return "\n".join(lines_to_keep)116 117 118def get_all_tests():119    """120    Return a list of paths to all test folders and files under `tests`. All paths are rooted at `tests`.121 122    - folders under `tests`: `tokenization`, `pipelines`, etc. The folder `models` is excluded.123    - folders under `tests/models`: `bert`, `gpt2`, etc.124    - test files under `tests`: `test_modeling_common.py`, `test_tokenization_common.py`, etc.125    """126 127    # test folders/files directly under `tests` folder128    tests = os.listdir(PATH_TO_TESTS)129    tests = [f"tests/{f}" for f in tests if "__pycache__" not in f]130    tests = sorted([f for f in tests if (PATH_TO_REPO / f).is_dir() or f.startswith("tests/test_")])131 132    # model specific test folders133    model_test_folders = os.listdir(PATH_TO_TESTS / "models")134    model_test_folders = [f"tests/models/{f}" for f in model_test_folders if "__pycache__" not in f]135    model_test_folders = sorted([f for f in model_test_folders if (PATH_TO_REPO / f).is_dir()])136 137    tests.remove("tests/models")138    # Sagemaker tests are not meant to be run on the CI.139    if "tests/sagemaker" in tests:140        tests.remove("tests/sagemaker")141    tests = model_test_folders + tests142 143    return tests144 145 146def diff_is_docstring_only(repo, branching_point, filename):147    """148    Check if the diff is only in docstrings in a filename.149    """150    folder = Path(repo.working_dir)151    with checkout_commit(repo, branching_point):152        with open(folder / filename, "r", encoding="utf-8") as f:153            old_content = f.read()154 155    with open(folder / filename, "r", encoding="utf-8") as f:156        new_content = f.read()157 158    old_content_clean = clean_code(old_content)159    new_content_clean = clean_code(new_content)160 161    return old_content_clean == new_content_clean162 163 164def get_diff(repo, base_commit, commits):165    """166    Get's the diff between one or several commits and the head of the repository.167    """168    print("\n### DIFF ###\n")169    code_diff = []170    for commit in commits:171        for diff_obj in commit.diff(base_commit):172            # We always add new python files173            if diff_obj.change_type == "A" and diff_obj.b_path.endswith(".py"):174                code_diff.append(diff_obj.b_path)175            # We check that deleted python files won't break corresponding tests.176            elif diff_obj.change_type == "D" and diff_obj.a_path.endswith(".py"):177                code_diff.append(diff_obj.a_path)178            # Now for modified files179            elif diff_obj.change_type in ["M", "R"] and diff_obj.b_path.endswith(".py"):180                # In case of renames, we'll look at the tests using both the old and new name.181                if diff_obj.a_path != diff_obj.b_path:182                    code_diff.extend([diff_obj.a_path, diff_obj.b_path])183                else:184                    # Otherwise, we check modifications are in code and not docstrings.185                    if diff_is_docstring_only(repo, commit, diff_obj.b_path):186                        print(f"Ignoring diff in {diff_obj.b_path} as it only concerns docstrings or comments.")187                    else:188                        code_diff.append(diff_obj.a_path)189 190    return code_diff191 192 193def get_modified_python_files(diff_with_last_commit=False):194    """195    Return a list of python files that have been modified between:196 197    - the current head and the main branch if `diff_with_last_commit=False` (default)198    - the current head and its parent commit otherwise.199    """200    repo = Repo(PATH_TO_REPO)201 202    if not diff_with_last_commit:203        print(f"main is at {repo.refs.main.commit}")204        print(f"Current head is at {repo.head.commit}")205 206        branching_commits = repo.merge_base(repo.refs.main, repo.head)207        for commit in branching_commits:208            print(f"Branching commit: {commit}")209        return get_diff(repo, repo.head.commit, branching_commits)210    else:211        print(f"main is at {repo.head.commit}")212        parent_commits = repo.head.commit.parents213        for commit in parent_commits:214            print(f"Parent commit: {commit}")215        return get_diff(repo, repo.head.commit, parent_commits)216 217 218# (:?^|\n) -> Non-catching group for the beginning of the doc or a new line.219# \s*from\s+(\.+\S+)\s+import\s+([^\n]+) -> Line only contains from .xxx import yyy and we catch .xxx and yyy220# (?=\n) -> Look-ahead to a new line. We can't just put \n here or using find_all on this re will only catch every221#           other import.222_re_single_line_relative_imports = re.compile(r"(?:^|\n)\s*from\s+(\.+\S+)\s+import\s+([^\n]+)(?=\n)")223# (:?^|\n) -> Non-catching group for the beginning of the doc or a new line.224# \s*from\s+(\.+\S+)\s+import\s+\(([^\)]+)\) -> Line continues with from .xxx import (yyy) and we catch .xxx and yyy225# yyy will take multiple lines otherwise there wouldn't be parenthesis.226_re_multi_line_relative_imports = re.compile(r"(?:^|\n)\s*from\s+(\.+\S+)\s+import\s+\(([^\)]+)\)")227# (:?^|\n) -> Non-catching group for the beginning of the doc or a new line.228# \s*from\s+transformers(\S*)\s+import\s+([^\n]+) -> Line only contains from transformers.xxx import yyy and we catch229#           .xxx and yyy230# (?=\n) -> Look-ahead to a new line. We can't just put \n here or using find_all on this re will only catch every231#           other import.232_re_single_line_direct_imports = re.compile(r"(?:^|\n)\s*from\s+transformers(\S*)\s+import\s+([^\n]+)(?=\n)")233# (:?^|\n) -> Non-catching group for the beginning of the doc or a new line.234# \s*from\s+transformers(\S*)\s+import\s+\(([^\)]+)\) -> Line continues with from transformers.xxx import (yyy) and we235# catch .xxx and yyy. yyy will take multiple lines otherwise there wouldn't be parenthesis.236_re_multi_line_direct_imports = re.compile(r"(?:^|\n)\s*from\s+transformers(\S*)\s+import\s+\(([^\)]+)\)")237 238 239def extract_imports(module_fname, cache=None):240    """241    Get the imports a given module makes. This takes a module filename and returns the list of module filenames242    imported in the module with the objects imported in that module filename.243    """244    if cache is not None and module_fname in cache:245        return cache[module_fname]246 247    with open(PATH_TO_REPO / module_fname, "r", encoding="utf-8") as f:248        content = f.read()249 250    # Filter out all docstrings to not get imports in code examples.251    splits = content.split('"""')252    content = "".join(splits[::2])253 254    module_parts = str(module_fname).split(os.path.sep)255    imported_modules = []256 257    # Let's start with relative imports258    relative_imports = _re_single_line_relative_imports.findall(content)259    relative_imports = [260        (mod, imp) for mod, imp in relative_imports if "# tests_ignore" not in imp and imp.strip() != "("261    ]262    multiline_relative_imports = _re_multi_line_relative_imports.findall(content)263    relative_imports += [(mod, imp) for mod, imp in multiline_relative_imports if "# tests_ignore" not in imp]264 265    for module, imports in relative_imports:266        level = 0267        while module.startswith("."):268            module = module[1:]269            level += 1270 271        if len(module) > 0:272            dep_parts = module_parts[: len(module_parts) - level] + module.split(".")273        else:274            dep_parts = module_parts[: len(module_parts) - level]275        imported_module = os.path.sep.join(dep_parts)276        imported_modules.append((imported_module, [imp.strip() for imp in imports.split(",")]))277 278    # Let's continue with direct imports279    direct_imports = _re_single_line_direct_imports.findall(content)280    direct_imports = [(mod, imp) for mod, imp in direct_imports if "# tests_ignore" not in imp and imp.strip() != "("]281    multiline_direct_imports = _re_multi_line_direct_imports.findall(content)282    direct_imports += [(mod, imp) for mod, imp in multiline_direct_imports if "# tests_ignore" not in imp]283 284    for module, imports in direct_imports:285        import_parts = module.split(".")[1:]  # ignore the first .286        dep_parts = ["src", "transformers"] + import_parts287        imported_module = os.path.sep.join(dep_parts)288        imported_modules.append((imported_module, [imp.strip() for imp in imports.split(",")]))289 290    result = []291    for module_file, imports in imported_modules:292        if (PATH_TO_REPO / f"{module_file}.py").is_file():293            module_file = f"{module_file}.py"294        elif (PATH_TO_REPO / module_file).is_dir() and (PATH_TO_REPO / module_file / "__init__.py").is_file():295            module_file = os.path.sep.join([module_file, "__init__.py"])296        imports = [imp for imp in imports if len(imp) > 0 and re.match("^[A-Za-z0-9_]*$", imp)]297        if len(imports) > 0:298            result.append((module_file, imports))299 300    if cache is not None:301        cache[module_fname] = result302 303    return result304 305 306def get_module_dependencies(module_fname, cache=None):307    """308    Get the dependencies of a module from the module filename as a list of module filenames. This will resolve any309    __init__ we pass: if we import from a submodule utils, the dependencies will be utils/foo.py and utils/bar.py (if310    the objects imported actually come from utils.foo and utils.bar) not utils/__init__.py.311    """312    dependencies = []313    imported_modules = extract_imports(module_fname, cache=cache)314    # The while loop is to recursively traverse all inits we may encounter.315    while len(imported_modules) > 0:316        new_modules = []317        for module, imports in imported_modules:318            # If we end up in an __init__ we are often not actually importing from this init (except in the case where319            # the object is fully defined in the __init__)320            if module.endswith("__init__.py"):321                # So we get the imports from that init then try to find where our objects come from.322                new_imported_modules = extract_imports(module, cache=cache)323                for new_module, new_imports in new_imported_modules:324                    if any([i in new_imports for i in imports]):325                        if new_module not in dependencies:326                            new_modules.append((new_module, [i for i in new_imports if i in imports]))327                        imports = [i for i in imports if i not in new_imports]328                if len(imports) > 0:329                    # If there are any objects lefts, they may be a submodule330                    path_to_module = PATH_TO_REPO / module.replace("__init__.py", "")331                    dependencies.extend(332                        [333                            os.path.join(module.replace("__init__.py", ""), f"{i}.py")334                            for i in imports335                            if (path_to_module / f"{i}.py").is_file()336                        ]337                    )338                    imports = [i for i in imports if not (path_to_module / f"{i}.py").is_file()]339                    if len(imports) > 0:340                        # Then if there are still objects left, they are fully defined in the init, so we keep it as a341                        # dependency.342                        dependencies.append(module)343            else:344                dependencies.append(module)345 346        imported_modules = new_modules347    return dependencies348 349 350def create_reverse_dependency_tree():351    """352    Create a list of all edges (a, b) which mean that modifying a impacts b with a going over all module and test files.353    """354    cache = {}355    all_modules = list(PATH_TO_TRANFORMERS.glob("**/*.py")) + list(PATH_TO_TESTS.glob("**/*.py"))356    all_modules = [str(mod.relative_to(PATH_TO_REPO)) for mod in all_modules]357    edges = [(dep, mod) for mod in all_modules for dep in get_module_dependencies(mod, cache=cache)]358 359    return list(set(edges))360 361 362def get_tree_starting_at(module, edges):363    """364    Returns the tree starting at a given module following all edges in the following format: [module, [list of edges365    starting at module], [list of edges starting at the preceding level], ...]366    """367    vertices_seen = [module]368    new_edges = [edge for edge in edges if edge[0] == module and edge[1] != module and "__init__.py" not in edge[1]]369    tree = [module]370    while len(new_edges) > 0:371        tree.append(new_edges)372        final_vertices = list({edge[1] for edge in new_edges})373        vertices_seen.extend(final_vertices)374        new_edges = [375            edge376            for edge in edges377            if edge[0] in final_vertices and edge[1] not in vertices_seen and "__init__.py" not in edge[1]378        ]379 380    return tree381 382 383def print_tree_deps_of(module, all_edges=None):384    """385    Prints the tree of modules depending on a given module.386    """387    if all_edges is None:388        all_edges = create_reverse_dependency_tree()389    tree = get_tree_starting_at(module, all_edges)390 391    # The list of lines is a list of tuples (line_to_be_printed, module)392    # Keeping the modules lets us know where to insert each new lines in the list.393    lines = [(tree[0], tree[0])]394    for index in range(1, len(tree)):395        edges = tree[index]396        start_edges = {edge[0] for edge in edges}397 398        for start in start_edges:399            end_edges = {edge[1] for edge in edges if edge[0] == start}400            # We will insert all those edges just after the line showing start.401            pos = 0402            while lines[pos][1] != start:403                pos += 1404            lines = lines[: pos + 1] + [(" " * (2 * index) + end, end) for end in end_edges] + lines[pos + 1 :]405 406    for line in lines:407        # We don't print the refs that where just here to help build lines.408        print(line[0])409 410 411def create_reverse_dependency_map():412    """413    Create the dependency map from module/test filename to the list of modules/tests that depend on it (even414    recursively).415    """416    cache = {}417    all_modules = list(PATH_TO_TRANFORMERS.glob("**/*.py")) + list(PATH_TO_TESTS.glob("**/*.py"))418    all_modules = [str(mod.relative_to(PATH_TO_REPO)) for mod in all_modules]419    direct_deps = {m: get_module_dependencies(m, cache=cache) for m in all_modules}420 421    # This recurses the dependencies422    something_changed = True423    while something_changed:424        something_changed = False425        for m in all_modules:426            for d in direct_deps[m]:427                if d.endswith("__init__.py"):428                    continue429                if d not in direct_deps:430                    raise ValueError(f"KeyError:{d}. From {m}")431                new_deps = set(direct_deps[d]) - set(direct_deps[m])432                if len(new_deps) > 0:433                    direct_deps[m].extend(list(new_deps))434                    something_changed = True435 436    # Finally we can build the reverse map.437    reverse_map = collections.defaultdict(list)438    for m in all_modules:439        for d in direct_deps[m]:440            reverse_map[d].append(m)441 442    for m in [f for f in all_modules if f.endswith("__init__.py")]:443        direct_deps = get_module_dependencies(m, cache=cache)444        deps = sum([reverse_map[d] for d in direct_deps if not d.endswith("__init__.py")], direct_deps)445        reverse_map[m] = list(set(deps) - {m})446 447    return reverse_map448 449 450def create_module_to_test_map(reverse_map=None, filter_models=False):451    """452    Extract the tests from the reverse_dependency_map and potentially filters the model tests.453    """454    if reverse_map is None:455        reverse_map = create_reverse_dependency_map()456    test_map = {module: [f for f in deps if f.startswith("tests")] for module, deps in reverse_map.items()}457 458    if not filter_models:459        return test_map460 461    num_model_tests = len(list(PATH_TO_TESTS.glob("models/*")))462 463    def has_many_models(tests):464        model_tests = {Path(t).parts[2] for t in tests if t.startswith("tests/models/")}465        return len(model_tests) > num_model_tests // 2466 467    def filter_tests(tests):468        return [t for t in tests if not t.startswith("tests/models/") or Path(t).parts[2] in IMPORTANT_MODELS]469 470    return {module: (filter_tests(tests) if has_many_models(tests) else tests) for module, tests in test_map.items()}471 472 473def check_imports_all_exist():474    """475    Isn't used per se by the test fetcher but might be used later as a quality check. Putting this here for now so the476    code is not lost.477    """478    cache = {}479    all_modules = list(PATH_TO_TRANFORMERS.glob("**/*.py")) + list(PATH_TO_TESTS.glob("**/*.py"))480    all_modules = [str(mod.relative_to(PATH_TO_REPO)) for mod in all_modules]481    direct_deps = {m: get_module_dependencies(m, cache=cache) for m in all_modules}482 483    for module, deps in direct_deps.items():484        for dep in deps:485            if not (PATH_TO_REPO / dep).is_file():486                print(f"{module} has dependency on {dep} which does not exist.")487 488 489def _print_list(l):490    return "\n".join([f"- {f}" for f in l])491 492 493def create_json_map(test_files_to_run, json_output_file):494    if json_output_file is None:495        return496 497    test_map = {}498    for test_file in test_files_to_run:499        # `test_file` is a path to a test folder/file, starting with `tests/`. For example,500        #   - `tests/models/bert/test_modeling_bert.py` or `tests/models/bert`501        #   - `tests/trainer/test_trainer.py` or `tests/trainer`502        #   - `tests/test_modeling_common.py`503        names = test_file.split(os.path.sep)504        if names[1] == "models":505            # take the part like `models/bert` for modeling tests506            key = os.path.sep.join(names[1:3])507        elif len(names) > 2 or not test_file.endswith(".py"):508            # test folders under `tests` or python files under them509            # take the part like tokenization, `pipeline`, etc. for other test categories510            key = os.path.sep.join(names[1:2])511        else:512            # common test files directly under `tests/`513            key = "common"514 515        if key not in test_map:516            test_map[key] = []517        test_map[key].append(test_file)518 519    # sort the keys & values520    keys = sorted(test_map.keys())521    test_map = {k: " ".join(sorted(test_map[k])) for k in keys}522    with open(json_output_file, "w", encoding="UTF-8") as fp:523        json.dump(test_map, fp, ensure_ascii=False)524 525 526def infer_tests_to_run(527    output_file, diff_with_last_commit=False, filters=None, filter_models=True, json_output_file=None528):529    modified_files = get_modified_python_files(diff_with_last_commit=diff_with_last_commit)530    print(f"\n### MODIFIED FILES ###\n{_print_list(modified_files)}")531 532    # Create the map that will give us all impacted modules.533    reverse_map = create_reverse_dependency_map()534    impacted_files = modified_files.copy()535    for f in modified_files:536        if f in reverse_map:537            impacted_files.extend(reverse_map[f])538 539    # Remove duplicates540    impacted_files = sorted(set(impacted_files))541    print(f"\n### IMPACTED FILES ###\n{_print_list(impacted_files)}")542 543    # Grab the corresponding test files:544    if "setup.py" in modified_files:545        test_files_to_run = ["tests"]546        repo_utils_launch = True547    else:548        # All modified tests need to be run.549        test_files_to_run = [550            f for f in modified_files if f.startswith("tests") and f.split(os.path.sep)[-1].startswith("test")551        ]552        # Then we grab the corresponding test files.553        test_map = create_module_to_test_map(reverse_map=reverse_map, filter_models=filter_models)554        for f in modified_files:555            if f in test_map:556                test_files_to_run.extend(test_map[f])557        test_files_to_run = sorted(set(test_files_to_run))558        # Remove SageMaker tests559        test_files_to_run = [f for f in test_files_to_run if not f.split(os.path.sep)[1] == "sagemaker"]560        # Make sure we did not end up with a test file that was removed561        test_files_to_run = [f for f in test_files_to_run if (PATH_TO_REPO / f).exists()]562        if filters is not None:563            filtered_files = []564            for _filter in filters:565                filtered_files.extend([f for f in test_files_to_run if f.startswith(_filter)])566            test_files_to_run = filtered_files567 568        repo_utils_launch = any(f.split(os.path.sep)[1] == "repo_utils" for f in modified_files)569 570    if repo_utils_launch:571        repo_util_file = Path(output_file).parent / "test_repo_utils.txt"572        with open(repo_util_file, "w", encoding="utf-8") as f:573            f.write("tests/repo_utils")574 575    print(f"\n### TEST TO RUN ###\n{_print_list(test_files_to_run)}")576    if len(test_files_to_run) > 0:577        with open(output_file, "w", encoding="utf-8") as f:578            f.write(" ".join(test_files_to_run))579 580        # Create a map that maps test categories to test files, i.e. `models/bert` -> [...test_modeling_bert.py, ...]581 582        # Get all test directories (and some common test files) under `tests` and `tests/models` if `test_files_to_run`583        # contains `tests` (i.e. when `setup.py` is changed).584        if "tests" in test_files_to_run:585            test_files_to_run = get_all_tests()586 587        create_json_map(test_files_to_run, json_output_file)588 589 590def filter_tests(output_file, filters):591    """592    Reads the content of the output file and filters out all the tests in a list of given folders.593 594    Args:595        output_file (`str` or `os.PathLike`): The path to the output file of the tests fetcher.596        filters (`List[str]`): A list of folders to filter.597    """598    if not os.path.isfile(output_file):599        print("No test file found.")600        return601    with open(output_file, "r", encoding="utf-8") as f:602        test_files = f.read().split(" ")603 604    if len(test_files) == 0 or test_files == [""]:605        print("No tests to filter.")606        return607 608    if test_files == ["tests"]:609        test_files = [os.path.join("tests", f) for f in os.listdir("tests") if f not in ["__init__.py"] + filters]610    else:611        test_files = [f for f in test_files if f.split(os.path.sep)[1] not in filters]612 613    with open(output_file, "w", encoding="utf-8") as f:614        f.write(" ".join(test_files))615 616 617def parse_commit_message(commit_message):618    """619    Parses the commit message to detect if a command is there to skip, force all or part of the CI.620 621    Returns a dictionary of strings to bools with keys skip, test_all_models and test_all.622    """623    if commit_message is None:624        return {"skip": False, "no_filter": False, "test_all": False}625 626    command_search = re.search(r"\[([^\]]*)\]", commit_message)627    if command_search is not None:628        command = command_search.groups()[0]629        command = command.lower().replace("-", " ").replace("_", " ")630        skip = command in ["ci skip", "skip ci", "circleci skip", "skip circleci"]631        no_filter = set(command.split(" ")) == {"no", "filter"}632        test_all = set(command.split(" ")) == {"test", "all"}633        return {"skip": skip, "no_filter": no_filter, "test_all": test_all}634    else:635        return {"skip": False, "no_filter": False, "test_all": False}636 637 638if __name__ == "__main__":639    parser = argparse.ArgumentParser()640    parser.add_argument(641        "--output_file", type=str, default="test_list.txt", help="Where to store the list of tests to run"642    )643    parser.add_argument(644        "--json_output_file",645        type=str,646        default="test_map.json",647        help="Where to store the tests to run in a dictionary format mapping test categories to test files",648    )649    parser.add_argument(650        "--diff_with_last_commit",651        action="store_true",652        help="To fetch the tests between the current commit and the last commit",653    )654    parser.add_argument(655        "--filters",656        type=str,657        nargs="*",658        default=["tests"],659        help="Only keep the test files matching one of those filters.",660    )661    parser.add_argument(662        "--filter_tests",663        action="store_true",664        help="Will filter the pipeline/repo utils tests outside of the generated list of tests.",665    )666    parser.add_argument(667        "--print_dependencies_of",668        type=str,669        help="Will only print the tree of modules depending on the file passed.",670        default=None,671    )672    parser.add_argument(673        "--commit_message",674        type=str,675        help="The commit message (which could contain a command to force all tests or skip the CI).",676        default=None,677    )678    args = parser.parse_args()679    if args.print_dependencies_of is not None:680        print_tree_deps_of(args.print_dependencies_of)681    elif args.filter_tests:682        filter_tests(args.output_file, ["pipelines", "repo_utils"])683    else:684        repo = Repo(PATH_TO_REPO)685        commit_message = repo.head.commit.message686        commit_flags = parse_commit_message(commit_message)687        if commit_flags["skip"]:688            print("Force-skipping the CI")689            quit()690        if commit_flags["no_filter"]:691            print("Running all tests fetched without filtering.")692        if commit_flags["test_all"]:693            print("Force-launching all tests")694 695        diff_with_last_commit = args.diff_with_last_commit696        if not diff_with_last_commit and not repo.head.is_detached and repo.head.ref == repo.refs.main:697            print("main branch detected, fetching tests against last commit.")698            diff_with_last_commit = True699 700        if not commit_flags["test_all"]:701            try:702                infer_tests_to_run(703                    args.output_file,704                    diff_with_last_commit=diff_with_last_commit,705                    filters=args.filters,706                    json_output_file=args.json_output_file,707                    filter_models=not commit_flags["no_filter"],708                )709                filter_tests(args.output_file, ["repo_utils"])710            except Exception as e:711                print(f"\nError when trying to grab the relevant tests: {e}\n\nRunning all tests.")712                commit_flags["test_all"] = True713 714        if commit_flags["test_all"]:715            with open(args.output_file, "w", encoding="utf-8") as f:716                if args.filters is None:717                    f.write("./tests/")718                else:719                    f.write(" ".join(args.filters))720 721            test_files_to_run = get_all_tests()722            create_json_map(test_files_to_run, args.json_output_file)723