Source

repoman / tests / runtests.py

Full commit
# Copyright 2009-2012 Edlund A/S
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

# runtests.py depends on:
#  setuptools 0.9c6 or later
#  coverage 3 or later
#  nosetests 0.11.1 or later

import difflib
import subprocess
import sys
import os
import tempfile
import shutil
import support
import random
import codecs

try:
    import coverage
    import coverage as coverage_
    no_coverage = False
except ImportError:
    no_coverage = True

def _get_runtests_dir():
    return os.path.abspath(os.path.dirname(__file__))

def _get_unittest_dir():
    return os.path.join(_get_runtests_dir(), 'unit')

def _get_outputtest_dir():
    return os.path.join(_get_runtests_dir(), 'output')

def _collect_unittests():
    args = []
    for root, dirs, files in os.walk(_get_unittest_dir()):
        args.extend([os.path.join(root, f) for f in files if os.path.splitext(f)[1] == '.py'])
    random.shuffle(args)
    return args

def _int_collect_outputtests(x, ordered=False):
    args = []
    exts = ['.py']
    if os.name == 'nt':
        exts.extend(['.bat', '.cmd'])
    else:
        exts.append('.sh')

    for root, dirs, files in os.walk(x):
        args.extend([os.path.join(root, f) for f in files if os.path.splitext(f)[1] in exts])
    if not ordered:
        random.shuffle(args)
    return args

def _collect_outputtests(args, extradirs, ordered):
    tests = _int_collect_outputtests(_get_outputtest_dir(), ordered)

    for dir in extradirs:
        tests.extend(_int_collect_outputtests(os.path.normpath(os.path.abspath(dir.split('=', 1)[1])), ordered))

    for arg in args:
        tests.append(os.path.normpath(os.path.abspath(arg)))

    return tests

def run_unittests(args, opts, all_tests):
    if not args:
        args = all_tests
    else: # only include relevant unittest tests
        allunittests = set(all_tests)
        args = list(set(args) & allunittests)

    import nose, nose.result
    argv = [__file__]
    if '--no-coverage' not in opts:
        argv.extend(['--with-coverage', '--cover-package=repoman'])
    argv.extend(args)

    rv = nose.main(argv=argv, exit=False, env=os.environ)
    return rv.success

def run_command(arg):
    close_fds=os.name != 'nt'
    p = subprocess.Popen(arg, shell=True,
            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT, close_fds=close_fds)
    tochild, fromchild = p.stdin, p.stdout
    tochild.close()
    output = fromchild.read()
    ret = fromchild.close()
    if ret == None:
        ret = 0

    return ret, output

def _splitlines(text):
    return [x for x in text.split('\n')]

def _run_outputtest(arg, opts, coverage=False):
    prefix = ''
    ext = os.path.splitext(arg)[1]
    if ext == '.py':
        if '--no-coverage' not in opts:
            if coverage_.__version__ == '3.2b1':
                prefix = 'coverage run --branch -p '
            else:
                prefix = 'coverage -x -p '
        else:
            prefix = 'python -u '
    elif ext == '.sh':
        prefix = 'sh '

    ret, output = run_command('%s%s' % (prefix, arg))

    if not os.path.exists(arg + '.out'):
        expected = []
    else:
        with open(arg + '.out', 'rb') as f:
            expected = _splitlines(f.read().replace('\r', ''))

    lineterm = '\r\n' if os.path.splitext(arg)[1] != '.sh' else '\n'
    if output.split('\n') != expected:
        print arg, 'failed'

        with open(arg + '.err', 'w') as errf:
            errf.write(output)

        if '--nodiff' not in opts:
            diff = difflib.unified_diff(expected, _splitlines(output), 'Expected output', 'Test output', lineterm=lineterm)
            for line in diff:
                sys.stderr.write(line)
                if line and line[-1] != '\n':
                    sys.stderr.write('\n')
        return -80

    return ret

def run_outputtests(args, opts, all_tests):
    if not args:
        args = all_tests
    else: # only include relevant output test tests
        alloutputtests = set(all_tests)
        argsz = list(set(args) & alloutputtests)
        args = [arg for arg in args if arg in argsz]

    cov = None
    repo_bat_path = os.path.abspath(os.path.join(os.environ['REPOTESTSPATH'], '..', 'repo.bat'))
    repo_path = os.path.abspath(os.path.join(os.environ['REPOTESTSPATH'], '..', 'repo'))
    if '--no-coverage' not in opts:
        os.environ['COVERAGE_FILE'] = os.path.join(os.environ['REPOTESTSPATH'], '.coverage')
        with open(repo_bat_path, 'w') as f:
            if coverage.__version__ == '3.2b1':
                f.write('@echo off\ncoverage run -p %s --simpleui %%*\n' % repo_path)
            else:
                f.write('@echo off\ncoverage -x -p %s --simpleui %%*\n' % repo_path)
    else:
        with open(repo_bat_path, 'w') as f:
            f.write('@echo off\npython -u %s --simpleui %%*\n' % repo_path)

    failed = []

    cwd = os.getcwd()
    numtest = 0
    for arg in args:
        os.chdir(cwd)
        numtest += 1

        try:
            os.mkdir(str(numtest))
            os.chdir(str(numtest))

            rv = _run_outputtest(arg, opts, coverage='--no-coverage' not in opts)
            if rv != 0:
                sys.stderr.write('!')
                failed.append(arg)
            else:
                if '--debug' not in opts:
                    sys.stderr.write('.')
                else:
                    sys.stderr.write('%s\n' % (arg,))
        finally:
            os.chdir('..')
    if args:
        sys.stderr.write('\n')

    os.unlink(repo_bat_path)
    return failed

def usage():
    print 'Usage: %s [FILE...] [OPTIONS]' % (os.path.basename(__file__))
    print
    print 'Options:'
    print '  --no-coverage            don\'t generate coverage information'
    print '  --no-html                don\'t generate coverage html report'
    print '  --nodiff                 suppress diff on output test failures'
    print '  --keep-tmpdir            don\'t clean up after running tests'
    print '  --ext=DIR                directory base for extensions to report coverage'
    print '  --add-tests=DIR          directory with extra tests to run'
    print '  --with-ext=IMPORT        extension to enable across all tests'
    print '  --debug                  write each test name rather than .'

envsep = ';' if os.name == 'nt' else ':'

def _get_coverage_modules(cov, repomanpath, extopts):
    from mercurial.extensions import loadpath

    filterpath = [os.path.normcase(os.path.abspath(os.path.join(repomanpath, 'repoman')))]
    for opt in extopts:
        filterpath.append(os.path.normcase(os.path.abspath(opt.split('=', 1)[1])))
    filterpath = [x + os.path.sep for x in filterpath]

    realargv = sys.argv
    sys.argv = [sys.argv[0]]

    mods = []
    for morf in (cov.data.measured_files() if hasattr(cov.data, 'measured_files') else cov.data.executed_files()):
        if os.path.splitext(morf)[1] != '.py':
            continue

        morffile = os.path.normcase(os.path.abspath(morf))
        match = None
        for x in filterpath:
            if morffile.startswith(x):
                match = x
                break

        if match is None:
            continue

        if morffile.startswith(filterpath[0]):
            modname = 'repoman' + os.path.splitext(morf[morf.find('repoman')+len('repoman'):])[0].replace(os.path.sep, '.')
        else:
            modname = 'ext.' + os.path.splitext(morf[len(match):])[0].replace(os.path.sep, '.')
        modu = loadpath(morf, modname)
        modu.__name__ = modname
        mods.append(modu)

    sys.argv = realargv

    return mods

def main(args):
    repomanpath = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))

    if 'PYTHONPATH' in os.environ:
        os.environ['PYTHONPATH'] += envsep + repomanpath
    else:
        os.environ['PYTHONPATH'] = repomanpath
    sys.path.append(repomanpath)
    os.environ['PATH'] = repomanpath + envsep + os.environ['PATH']
    os.environ['LANG'] = os.environ['LANGUAGE'] = 'C'
    os.environ['HGENCODING'] = 'cp1252'
    os.environ['REPOTESTSPATH'] = os.path.join(repomanpath, 'tests')
#    os.environ['HGUSER'] = 'Jane Doe <jane.doe@rlyeh.org>'

    opts = [arg for arg in args if arg.startswith('-')]
    if no_coverage:
        opts.append('--no-coverage')
    args = [os.path.normpath(os.path.abspath(arg)) for arg in args if not arg.startswith('-')]

    if '--no-coverage' not in opts:
        os.environ['REPOWITHCOVERAGE'] = 'coverage -x -p %s'
    else:
        os.environ['REPOWITHCOVERAGE'] = 'python -u %s'
    os.environ['REPOWITHCOVERAGE'] = os.environ['REPOWITHCOVERAGE'] % os.path.join(repomanpath, 'repo')
    extopts = [opt for opt in opts if opt.startswith('--ext=')]
    withext = [opt for opt in opts if opt.startswith('--with-ext=')]

    if '--help' in opts:
        usage()
        sys.exit(1)

    if '--no-coverage' not in opts:
        for root, dirs, files in os.walk(os.path.dirname(__file__)):
            for f in files:
                if f.startswith('.coverage'):
                    os.unlink(os.path.join(root, f))
            break

    outputtests = _collect_outputtests(args, [opt for opt in opts if opt.startswith('--add-tests=')], '--ordered' in opts)

    tmpdir = tempfile.mkdtemp(prefix='tmp.repo')
    curdir = os.getcwd()
    os.chdir(tmpdir)

    os.environ['HGRCPATH'] = os.path.join(tmpdir, '.hgrc')
    with codecs.open(os.environ['HGRCPATH'], 'w', 'utf-8') as fh:
        fh.write('[extensions]\nhgext.mq =\nhgext.rebase =\nhgext.graphlog =\n')
        fh.write(u'[ui]\nusername = Jane Do\xeb <jane.doe@rlyeh.org>\nslash = True\n')

        if withext:
            fh.write(u'[repo-extensions]\n')
        for ext in withext:
            fh.write(ext[11:])
    
    sys.stdout.write('Creating test forest... ')
    support.create_test_forest()
    sys.stdout.write('DONE\n')

    failures = run_outputtests(args, opts, outputtests)
    with open(os.path.join(repomanpath, 'tests', '.output.order'), 'w') as fh:
        fh.write('\n'.join(outputtests))

    os.chdir(curdir)
    try:
        if '--keep-tmpdir' not in opts:
            shutil.rmtree(tmpdir)
        else:
            print 'tmpdir was:', tmpdir
    except Exception:
        print 'Unable to perform cleanup when removing directory "%s"' % (tmpdir,)

    sys.stderr.write('\n\n')
    if not failures:
        sys.stderr.write('Output tests: SUCCESS\n')
    else:
        sys.stderr.write('Failed output tests:\n')
        for f in failures:
            sys.stderr.write('  %s\n' % f)
    sys.stderr.write('\n')

    if '--no-coverage' not in opts:
        cover_file = os.environ['COVERAGE_FILE']
        import coverage
        cov = coverage.coverage(cover_file)
        cov.combine()
        morfs = _get_coverage_modules(cov, repomanpath, extopts)
        cov.report(morfs=morfs)

        if '--no-html' not in opts:
            cov.html_report(morfs=morfs, directory=os.path.abspath(os.path.join(os.path.dirname(__file__), 'html')))

if __name__ == '__main__':
    main(sys.argv[1:])