#!/usr/bin/env python3 # Copyright (C) 2019 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Wrapper to run linters and pytest with the right settings.""" import functools import os import shlex import shutil import subprocess import sys # NB: While tests/* support Python >=3.6 to match requirements.json for `repo`, # the higher level runner logic does not need to be held back. assert sys.version_info >= (3, 9), "Test/release framework requires Python 3.9+" ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) def log_cmd(cmd: str, argv: list[str]) -> None: """Log a debug message to make history easier to track.""" print("+", cmd, shlex.join(argv), file=sys.stderr) @functools.lru_cache() def is_ci() -> bool: """Whether we're running in our CI system.""" return os.getenv("LUCI_CQ") == "yes" def run_pytest(argv: list[str]) -> int: """Returns the exit code from pytest.""" if is_ci(): argv = ["-m", "not skip_cq"] + argv log_cmd("pytest", argv) return subprocess.run( [sys.executable, "-m", "pytest"] + argv, check=False, cwd=ROOT_DIR, ).returncode def run_pytest_py38(argv: list[str]) -> int: """Returns the exit code from pytest under Python 3.8.""" if is_ci(): argv = ["-m", "not skip_cq"] + argv log_cmd("[vpython 3.8] pytest", argv) try: return subprocess.run( [ "vpython3", "-vpython-spec", "run_tests.vpython3.8", "-m", "pytest", ] + argv, check=False, cwd=ROOT_DIR, ).returncode except FileNotFoundError: # Skip if the user doesn't have vpython from depot_tools. return 0 def run_black(): """Returns the exit code from black.""" # Black by default only matches .py files. We have to list standalone # scripts manually. extra_programs = [ "repo", "run_tests", "release/update-hooks", "release/update-manpages", ] argv = ["--diff", "--check", ROOT_DIR] + extra_programs log_cmd("black", argv) return subprocess.run( [sys.executable, "-m", "black"] + argv, check=False, cwd=ROOT_DIR, ).returncode def run_flake8(): """Returns the exit code from flake8.""" argv = [ROOT_DIR] log_cmd("flake8", argv) return subprocess.run( [sys.executable, "-m", "flake8"] + argv, check=False, cwd=ROOT_DIR, ).returncode def run_isort(): """Returns the exit code from isort.""" argv = ["--check", ROOT_DIR] log_cmd("isort", argv) return subprocess.run( [sys.executable, "-m", "isort"] + argv, check=False, cwd=ROOT_DIR, ).returncode def run_check_metadata(): """Returns the exit code from check-metadata.""" argv = [] log_cmd("release/check-metadata.py", argv) return subprocess.run( [sys.executable, "release/check-metadata.py"] + argv, check=False, cwd=ROOT_DIR, ).returncode def run_update_manpages() -> int: """Returns the exit code from release/update-manpages.""" # Allow this to fail on CI, but not local devs. if is_ci() and not shutil.which("help2man"): print("update-manpages: help2man not found; skipping test") return 0 argv = ["--check"] log_cmd("release/update-manpages", argv) return subprocess.run( [sys.executable, "release/update-manpages"] + argv, check=False, cwd=ROOT_DIR, ).returncode def main(argv): """The main entry.""" checks = ( functools.partial(run_pytest, argv), functools.partial(run_pytest_py38, argv), run_black, run_flake8, run_isort, run_check_metadata, run_update_manpages, ) # Run all the tests all the time to get full feedback. Don't exit on the # first error as that makes it more difficult to iterate in the CQ. return 1 if sum(c() for c in checks) else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))