tests/hghave.py
author Yuya Nishihara <yuya@tcha.org>
Tue, 26 Jan 2016 23:26:05 +0900
changeset 27995 834d27c4655d
parent 27702 39122c2442cd
child 28126 562a073a2a1b
permissions -rw-r--r--
test-highlight: unify normalization rule of pygments output We had two variants for unknown reason, s/mf/mi/g and s/mi/mf/g. This patch unifies them to s/mf/mi/g so that we can introduce a utility function.

from __future__ import absolute_import

import errno
import os
import re
import socket
import stat
import subprocess
import sys
import tempfile

tempprefix = 'hg-hghave-'

checks = {
    "true": (lambda: True, "yak shaving"),
    "false": (lambda: False, "nail clipper"),
}

def check(name, desc):
    def decorator(func):
        checks[name] = (func, desc)
        return func
    return decorator

def checkfeatures(features):
    result = {
        'error': [],
        'missing': [],
        'skipped': [],
    }

    for feature in features:
        negate = feature.startswith('no-')
        if negate:
            feature = feature[3:]

        if feature not in checks:
            result['missing'].append(feature)
            continue

        check, desc = checks[feature]
        try:
            available = check()
        except Exception:
            result['error'].append('hghave check failed: %s' % feature)
            continue

        if not negate and not available:
            result['skipped'].append('missing feature: %s' % desc)
        elif negate and available:
            result['skipped'].append('system supports %s' % desc)

    return result

def require(features):
    """Require that features are available, exiting if not."""
    result = checkfeatures(features)

    for missing in result['missing']:
        sys.stderr.write('skipped: unknown feature: %s\n' % missing)
    for msg in result['skipped']:
        sys.stderr.write('skipped: %s\n' % msg)
    for msg in result['error']:
        sys.stderr.write('%s\n' % msg)

    if result['missing']:
        sys.exit(2)

    if result['skipped'] or result['error']:
        sys.exit(1)

def matchoutput(cmd, regexp, ignorestatus=False):
    """Return the match object if cmd executes successfully and its output
    is matched by the supplied regular expression.
    """
    r = re.compile(regexp)
    try:
        p = subprocess.Popen(
            cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    except OSError as e:
        if e.errno != errno.ENOENT:
            raise
        ret = -1
    ret = p.wait()
    s = p.stdout.read()
    return (ignorestatus or not ret) and r.search(s)

@check("baz", "GNU Arch baz client")
def has_baz():
    return matchoutput('baz --version 2>&1', r'baz Bazaar version')

@check("bzr", "Canonical's Bazaar client")
def has_bzr():
    try:
        import bzrlib
        return bzrlib.__doc__ is not None
    except ImportError:
        return False

@check("bzr114", "Canonical's Bazaar client >= 1.14")
def has_bzr114():
    try:
        import bzrlib
        return (bzrlib.__doc__ is not None
                and bzrlib.version_info[:2] >= (1, 14))
    except ImportError:
        return False

@check("cvs", "cvs client/server")
def has_cvs():
    re = r'Concurrent Versions System.*?server'
    return matchoutput('cvs --version 2>&1', re) and not has_msys()

@check("cvs112", "cvs client/server >= 1.12")
def has_cvs112():
    re = r'Concurrent Versions System \(CVS\) 1.12.*?server'
    return matchoutput('cvs --version 2>&1', re) and not has_msys()

@check("darcs", "darcs client")
def has_darcs():
    return matchoutput('darcs --version', r'2\.[2-9]', True)

@check("mtn", "monotone client (>= 1.0)")
def has_mtn():
    return matchoutput('mtn --version', r'monotone', True) and not matchoutput(
        'mtn --version', r'monotone 0\.', True)

@check("eol-in-paths", "end-of-lines in paths")
def has_eol_in_paths():
    try:
        fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
        os.close(fd)
        os.remove(path)
        return True
    except (IOError, OSError):
        return False

@check("execbit", "executable bit")
def has_executablebit():
    try:
        EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
        fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
        try:
            os.close(fh)
            m = os.stat(fn).st_mode & 0o777
            new_file_has_exec = m & EXECFLAGS
            os.chmod(fn, m ^ EXECFLAGS)
            exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0o777) == m)
        finally:
            os.unlink(fn)
    except (IOError, OSError):
        # we don't care, the user probably won't be able to commit anyway
        return False
    return not (new_file_has_exec or exec_flags_cannot_flip)

@check("icasefs", "case insensitive file system")
def has_icasefs():
    # Stolen from mercurial.util
    fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
    os.close(fd)
    try:
        s1 = os.stat(path)
        d, b = os.path.split(path)
        p2 = os.path.join(d, b.upper())
        if path == p2:
            p2 = os.path.join(d, b.lower())
        try:
            s2 = os.stat(p2)
            return s2 == s1
        except OSError:
            return False
    finally:
        os.remove(path)

@check("fifo", "named pipes")
def has_fifo():
    if getattr(os, "mkfifo", None) is None:
        return False
    name = tempfile.mktemp(dir='.', prefix=tempprefix)
    try:
        os.mkfifo(name)
        os.unlink(name)
        return True
    except OSError:
        return False

@check("killdaemons", 'killdaemons.py support')
def has_killdaemons():
    return True

@check("cacheable", "cacheable filesystem")
def has_cacheable_fs():
    from mercurial import util

    fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
    os.close(fd)
    try:
        return util.cachestat(path).cacheable()
    finally:
        os.remove(path)

@check("lsprof", "python lsprof module")
def has_lsprof():
    try:
        import _lsprof
        _lsprof.Profiler # silence unused import warning
        return True
    except ImportError:
        return False

@check("gettext", "GNU Gettext (msgfmt)")
def has_gettext():
    return matchoutput('msgfmt --version', 'GNU gettext-tools')

@check("git", "git command line client")
def has_git():
    return matchoutput('git --version 2>&1', r'^git version')

@check("docutils", "Docutils text processing library")
def has_docutils():
    try:
        from docutils.core import publish_cmdline
        publish_cmdline # silence unused import
        return True
    except ImportError:
        return False

def getsvnversion():
    m = matchoutput('svn --version --quiet 2>&1', r'^(\d+)\.(\d+)')
    if not m:
        return (0, 0)
    return (int(m.group(1)), int(m.group(2)))

@check("svn15", "subversion client and admin tools >= 1.5")
def has_svn15():
    return getsvnversion() >= (1, 5)

@check("svn13", "subversion client and admin tools >= 1.3")
def has_svn13():
    return getsvnversion() >= (1, 3)

@check("svn", "subversion client and admin tools")
def has_svn():
    return matchoutput('svn --version 2>&1', r'^svn, version') and \
        matchoutput('svnadmin --version 2>&1', r'^svnadmin, version')

@check("svn-bindings", "subversion python bindings")
def has_svn_bindings():
    try:
        import svn.core
        version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
        if version < (1, 4):
            return False
        return True
    except ImportError:
        return False

@check("p4", "Perforce server and client")
def has_p4():
    return (matchoutput('p4 -V', r'Rev\. P4/') and
            matchoutput('p4d -V', r'Rev\. P4D/'))

@check("symlink", "symbolic links")
def has_symlink():
    if getattr(os, "symlink", None) is None:
        return False
    name = tempfile.mktemp(dir='.', prefix=tempprefix)
    try:
        os.symlink(".", name)
        os.unlink(name)
        return True
    except (OSError, AttributeError):
        return False

@check("hardlink", "hardlinks")
def has_hardlink():
    from mercurial import util
    fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
    os.close(fh)
    name = tempfile.mktemp(dir='.', prefix=tempprefix)
    try:
        util.oslink(fn, name)
        os.unlink(name)
        return True
    except OSError:
        return False
    finally:
        os.unlink(fn)

@check("tla", "GNU Arch tla client")
def has_tla():
    return matchoutput('tla --version 2>&1', r'The GNU Arch Revision')

@check("gpg", "gpg client")
def has_gpg():
    return matchoutput('gpg --version 2>&1', r'GnuPG')

@check("unix-permissions", "unix-style permissions")
def has_unix_permissions():
    d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
    try:
        fname = os.path.join(d, 'foo')
        for umask in (0o77, 0o07, 0o22):
            os.umask(umask)
            f = open(fname, 'w')
            f.close()
            mode = os.stat(fname).st_mode
            os.unlink(fname)
            if mode & 0o777 != ~umask & 0o666:
                return False
        return True
    finally:
        os.rmdir(d)

@check("unix-socket", "AF_UNIX socket family")
def has_unix_socket():
    return getattr(socket, 'AF_UNIX', None) is not None

@check("root", "root permissions")
def has_root():
    return getattr(os, 'geteuid', None) and os.geteuid() == 0

@check("pyflakes", "Pyflakes python linter")
def has_pyflakes():
    return matchoutput("sh -c \"echo 'import re' 2>&1 | pyflakes\"",
                       r"<stdin>:1: 're' imported but unused",
                       True)

@check("pygments", "Pygments source highlighting library")
def has_pygments():
    try:
        import pygments
        pygments.highlight # silence unused import warning
        return True
    except ImportError:
        return False

@check("json", "some json module available")
def has_json():
    try:
        import json
        json.dumps
        return True
    except ImportError:
        try:
            import simplejson as json
            json.dumps
            return True
        except ImportError:
            pass
    return False

@check("outer-repo", "outer repo")
def has_outer_repo():
    # failing for other reasons than 'no repo' imply that there is a repo
    return not matchoutput('hg root 2>&1',
                           r'abort: no repository found', True)

@check("ssl", ("(python >= 2.6 ssl module and python OpenSSL) "
               "OR python >= 2.7.9 ssl"))
def has_ssl():
    try:
        import ssl
        if getattr(ssl, 'create_default_context', False):
            return True
        import OpenSSL
        OpenSSL.SSL.Context
        return True
    except ImportError:
        return False

@check("sslcontext", "python >= 2.7.9 ssl")
def has_sslcontext():
    try:
        import ssl
        ssl.SSLContext
        return True
    except (ImportError, AttributeError):
        return False

@check("defaultcacerts", "can verify SSL certs by system's CA certs store")
def has_defaultcacerts():
    from mercurial import sslutil
    return sslutil._defaultcacerts() != '!'

@check("windows", "Windows")
def has_windows():
    return os.name == 'nt'

@check("system-sh", "system() uses sh")
def has_system_sh():
    return os.name != 'nt'

@check("serve", "platform and python can manage 'hg serve -d'")
def has_serve():
    return os.name != 'nt' # gross approximation

@check("test-repo", "running tests from repository")
def has_test_repo():
    t = os.environ["TESTDIR"]
    return os.path.isdir(os.path.join(t, "..", ".hg"))

@check("tic", "terminfo compiler and curses module")
def has_tic():
    try:
        import curses
        curses.COLOR_BLUE
        return matchoutput('test -x "`which tic`"', '')
    except ImportError:
        return False

@check("msys", "Windows with MSYS")
def has_msys():
    return os.getenv('MSYSTEM')

@check("aix", "AIX")
def has_aix():
    return sys.platform.startswith("aix")

@check("osx", "OS X")
def has_osx():
    return sys.platform == 'darwin'

@check("docker", "docker support")
def has_docker():
    pat = r'A self-sufficient runtime for linux containers\.'
    if matchoutput('docker --help', pat):
        if 'linux' not in sys.platform:
            # TODO: in theory we should be able to test docker-based
            # package creation on non-linux using boot2docker, but in
            # practice that requires extra coordination to make sure
            # $TESTTEMP is going to be visible at the same path to the
            # boot2docker VM. If we figure out how to verify that, we
            # can use the following instead of just saying False:
            # return 'DOCKER_HOST' in os.environ
            return False

        return True
    return False

@check("debhelper", "debian packaging tools")
def has_debhelper():
    dpkg = matchoutput('dpkg --version',
                       "Debian `dpkg' package management program")
    dh = matchoutput('dh --help',
                     'dh is a part of debhelper.', ignorestatus=True)
    dh_py2 = matchoutput('dh_python2 --help',
                         'other supported Python versions')
    return dpkg and dh and dh_py2

@check("absimport", "absolute_import in __future__")
def has_absimport():
    import __future__
    from mercurial import util
    return util.safehasattr(__future__, "absolute_import")

@check("py3k", "running with Python 3.x")
def has_py3k():
    return 3 == sys.version_info[0]

@check("pure", "running with pure Python code")
def has_pure():
    return any([
        os.environ.get("HGMODULEPOLICY") == "py",
        os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
    ])

@check("slow", "allow slow tests")
def has_slow():
    return os.environ.get('HGTEST_SLOW') == 'slow'

@check("hypothesis", "is Hypothesis installed")
def has_hypothesis():
    try:
        import hypothesis
        hypothesis.given
        return True
    except ImportError:
        return False