#!/usr/bin/python # Copyright 2010-2015 Gentoo Foundation # 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. """ from __future__ import print_function 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.6', '3.7', '3.8' ] # The rest are just "nice to have". PYTHON_NICE_VERSIONS = [ 'pypy3', '3.9' ] 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)