| #!/usr/bin/env python |
| # Copyright 2010-2020 Gentoo Authors |
| # Distributed under the terms of the GNU General Public License v2 |
| # |
| # Note: We don't want to import portage modules directly because we do things |
| # like run the testsuite through multiple versions of python. |
| |
| """Helper script to run portage unittests against different python versions. |
| |
| Note: Any additional arguments will be passed down directly to the underlying |
| unittest runner. This lets you select specific tests to execute. |
| """ |
| |
| import argparse |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| |
| |
| # These are the versions we fully support and require to pass tests. |
| PYTHON_SUPPORTED_VERSIONS = ["3.7", "3.8", "3.9", "3.10"] |
| # The rest are just "nice to have". |
| PYTHON_NICE_VERSIONS = ["pypy3", "3.11"] |
| |
| EPREFIX = os.environ.get("PORTAGE_OVERRIDE_EPREFIX", "/") |
| |
| |
| class Colors: |
| """Simple object holding color constants.""" |
| |
| _COLORS_YES = ("y", "yes", "true") |
| _COLORS_NO = ("n", "no", "false") |
| |
| WARN = GOOD = BAD = NORMAL = "" |
| |
| def __init__(self, colorize=None): |
| if colorize is None: |
| nocolors = os.environ.get("NOCOLOR", "false") |
| # Ugh, look away, for here we invert the world! |
| if nocolors in self._COLORS_YES: |
| colorize = False |
| elif nocolors in self._COLORS_NO: |
| colorize = True |
| else: |
| raise ValueError("$NOCOLORS is invalid: %s" % nocolors) |
| else: |
| if colorize in self._COLORS_YES: |
| colorize = True |
| elif colorize in self._COLORS_NO: |
| colorize = False |
| else: |
| raise ValueError("--colors is invalid: %s" % colorize) |
| |
| if colorize: |
| self.WARN = "\033[1;33m" |
| self.GOOD = "\033[1;32m" |
| self.BAD = "\033[1;31m" |
| self.NORMAL = "\033[0m" |
| |
| |
| def get_python_executable(ver): |
| """Find the right python executable for |ver|""" |
| if ver in ("pypy", "pypy3"): |
| prog = ver |
| else: |
| prog = "python" + ver |
| return os.path.join(EPREFIX, "usr", "bin", prog) |
| |
| |
| def get_parser(): |
| """Return a argument parser for this module""" |
| epilog = """Examples: |
| List all the available unittests. |
| $ %(prog)s --list |
| |
| Run against specific versions of python. |
| $ %(prog)s --python-versions '2.7 3.3' |
| |
| Run just one unittest. |
| $ %(prog)s lib/portage/tests/xpak/test_decodeint.py |
| """ |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=epilog, |
| ) |
| parser.add_argument( |
| "--keep-temp", |
| default=False, |
| action="store_true", |
| help="Do not delete the temporary directory when exiting", |
| ) |
| parser.add_argument( |
| "--color", |
| type=str, |
| default=None, |
| help="Whether to use colorized output (default is auto)", |
| ) |
| parser.add_argument( |
| "--python-versions", |
| action="append", |
| help="Versions of python to test (default is test available)", |
| ) |
| return parser |
| |
| |
| def main(argv): |
| parser = get_parser() |
| opts, args = parser.parse_known_args(argv) |
| colors = Colors(colorize=opts.color) |
| |
| # Figure out all the versions we want to test. |
| if opts.python_versions is None: |
| ignore_missing = True |
| pyversions = PYTHON_SUPPORTED_VERSIONS + PYTHON_NICE_VERSIONS |
| else: |
| ignore_missing = False |
| pyversions = [] |
| for ver in opts.python_versions: |
| if ver == "supported": |
| pyversions.extend(PYTHON_SUPPORTED_VERSIONS) |
| else: |
| pyversions.extend(ver.split()) |
| |
| tempdir = None |
| try: |
| # Set up a single tempdir for all the tests to use. |
| # This way we know the tests won't leak things on us. |
| tempdir = tempfile.mkdtemp(prefix="portage.runtests.") |
| os.environ["TMPDIR"] = tempdir |
| |
| # Actually test those versions now. |
| statuses = [] |
| for ver in pyversions: |
| prog = get_python_executable(ver) |
| cmd = [prog, "-b", "-Wd", "lib/portage/tests/runTests.py"] + args |
| if os.access(prog, os.X_OK): |
| print( |
| "%sTesting with Python %s...%s" % (colors.GOOD, ver, colors.NORMAL) |
| ) |
| statuses.append((ver, subprocess.call(cmd))) |
| elif not ignore_missing: |
| print( |
| "%sCould not find requested Python %s%s" |
| % (colors.BAD, ver, colors.NORMAL) |
| ) |
| statuses.append((ver, 1)) |
| else: |
| print("%sSkip Python %s...%s" % (colors.WARN, ver, colors.NORMAL)) |
| print() |
| finally: |
| if tempdir is not None: |
| if opts.keep_temp: |
| print("Temporary directory left behind:\n%s" % tempdir) |
| else: |
| # Nuke our tempdir and anything that might be under it. |
| shutil.rmtree(tempdir, True) |
| |
| # Then summarize it all. |
| print("\nSummary:\n") |
| width = 10 |
| header = "| %-*s | %s" % (width, "Version", "Status") |
| print("%s\n|%s" % (header, "-" * (len(header) - 1))) |
| exit_status = 0 |
| for ver, status in statuses: |
| exit_status += status |
| if status: |
| color = colors.BAD |
| msg = "FAIL" |
| else: |
| color = colors.GOOD |
| msg = "PASS" |
| print( |
| "| %s%-*s%s | %s%s%s" |
| % (color, width, ver, colors.NORMAL, color, msg, colors.NORMAL) |
| ) |
| exit(exit_status) |
| |
| |
| if __name__ == "__main__": |
| try: |
| main(sys.argv[1:]) |
| except KeyboardInterrupt: |
| print("interrupted ...", file=sys.stderr) |
| exit(1) |