| #!/usr/bin/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.6', |
| '3.7', |
| '3.8', |
| '3.9' |
| ] |
| # The rest are just "nice to have". |
| PYTHON_NICE_VERSIONS = [ |
| 'pypy3', |
| '3.10' |
| ] |
| |
| 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) |