| # Lint as: python3 |
| # Copyright 2022 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import dataclasses |
| import datetime as dt |
| import functools |
| import logging |
| import multiprocessing |
| import os |
| import pathlib |
| import sys |
| from typing import List, Optional, Set, Tuple, Union |
| |
| _TOOLS_ANDROID_PATH = pathlib.Path(__file__).resolve(strict=True).parents[1] |
| if str(_TOOLS_ANDROID_PATH) not in sys.path: |
| sys.path.append(str(_TOOLS_ANDROID_PATH)) |
| from python_utils import git_metadata_utils |
| |
| import java_test_utils |
| |
| _CHROMIUM_SRC_PATH = git_metadata_utils.get_chromium_src_path() |
| |
| _IGNORED_DIRS = ('out', 'third_party', 'clank', 'build/linux', 'native_client', |
| 'tools/android/test_health/testdata') |
| |
| _IGNORED_FILES = set() |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class GitRepoInfo: |
| """Holder class for Git repository information.""" |
| |
| git_head: str |
| """The SHA1 hash of the Git repository's commit at HEAD.""" |
| |
| git_head_time: dt.datetime |
| """The datetime of the Git repository's commit at HEAD.""" |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class TestHealthInfo: |
| """Holder class for test health information about a test class.""" |
| |
| test_name: str |
| """The name of the test, e.g., the class name of a Java test.""" |
| |
| test_dir: pathlib.Path |
| """The directory containing the test, relative to the Git repo root.""" |
| |
| test_filename: str |
| """The filename of the test, e.g., FooJavaTest.java.""" |
| |
| java_test_health: Optional[java_test_utils.JavaTestHealth] |
| """Java test health info and counters; this is None if not a Java test.""" |
| |
| git_repo_info: GitRepoInfo |
| """Information about the Git repository being sampled.""" |
| |
| |
| def get_repo_test_health(git_repo: Optional[pathlib.Path] = None, |
| *, |
| test_dir: Union[str, pathlib.Path, None] = None, |
| ignored_dirs: Tuple[str, ...] = _IGNORED_DIRS, |
| ignored_files: Set[str] = _IGNORED_FILES |
| ) -> List[TestHealthInfo]: |
| """Gets test health information and stats for a Git repository. |
| |
| This function checks for Java tests annotated as disabled but could |
| be extended to check other metrics or languages in the future. |
| |
| Args: |
| git_repo: |
| The path to the root of the Git repository being checked; defaults |
| to the Chromium repo. |
| test_dir: |
| The subdirectory, relative to the Git repo root, containing the |
| tests of interest; defaults to the root of the Git repo. |
| ignored_dirs: |
| A list of directories to skip (paths relative to `test_dir`); |
| defaults to a set of directories that should be ignored in the |
| Chromium Git repo. |
| ignored_files: |
| A set of file paths to skip (relative to `test_dir`); defaults to |
| files in the Chromium Git repo with unsupported Java syntax. |
| Returns: |
| A list of `TestHealthInfo` objects, one for each test file processed. |
| """ |
| git_repo = git_repo or _CHROMIUM_SRC_PATH |
| test_dir = test_dir or pathlib.Path('.') |
| tests_root = (git_repo / test_dir).resolve(strict=True) |
| repo_info = _get_git_repo_info(git_repo) |
| |
| logging.debug(f'Starting os.walk in {tests_root}') |
| test_paths = [] |
| for dirpath, _, filenames in os.walk(tests_root): |
| if os.path.relpath(dirpath, tests_root).startswith(ignored_dirs): |
| continue |
| |
| for filename in filenames: |
| if not filename.endswith('Test.java'): |
| continue |
| |
| test_path = pathlib.Path(dirpath) / filename |
| if os.path.relpath(test_path, tests_root) in ignored_files: |
| continue |
| |
| test_paths.append(test_path) |
| |
| logging.debug(f'Parsing {len(test_paths)} test files') |
| with multiprocessing.Pool() as p: |
| test_health_infos: list[Optional[TestHealthInfo]] = p.map( |
| functools.partial(_get_test_health_info, git_repo, repo_info), |
| test_paths) |
| |
| return [t for t in test_health_infos if t is not None] |
| |
| |
| def _get_test_health_info(repo_root: pathlib.Path, repo_info: GitRepoInfo, |
| test_path: pathlib.Path) -> Optional[TestHealthInfo]: |
| test_file = test_path.relative_to(repo_root) |
| try: |
| test_health_stats = java_test_utils.get_java_test_health(test_path) |
| except java_test_utils.JavaSyntaxError as e: |
| # This can occur if the file uses syntax not supported by the underlying |
| # javalang python module used by java_test_utils. These files should be |
| # investigated manually. |
| logging.warning(f'Skipped file "{test_file}" due to' |
| ' Java syntax error:') |
| logging.warning(f' {e}') |
| logging.warning(f' {e.lineno}:{e.offset}: {e.text}') |
| return None |
| |
| return TestHealthInfo(test_name=test_file.stem, |
| test_dir=test_file.parent, |
| test_filename=test_file.name, |
| java_test_health=test_health_stats, |
| git_repo_info=repo_info) |
| |
| |
| def _get_git_repo_info(git_repo: pathlib.Path) -> GitRepoInfo: |
| return GitRepoInfo(git_metadata_utils.get_head_commit_hash(git_repo), |
| git_metadata_utils.get_head_commit_datetime(git_repo)) |