Mercurial > hg-git
diff tests/hghave.py @ 1106:cca56bbea143
tests: copy hg test infra from hg repo @5cfdf6137af8
author | Tony Tung <ttung@chanzuckerberg.com <mailto:ttung@chanzuckerberg.com>> |
---|---|
date | Tue, 06 Feb 2018 16:34:28 -0800 |
parents | 8c6dc6a6f5d8 |
children |
line wrap: on
line diff
--- a/tests/hghave.py Tue Feb 06 12:12:49 2018 -0600 +++ b/tests/hghave.py Tue Feb 06 16:34:28 2018 -0800 @@ -1,53 +1,163 @@ -import os, stat, socket +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): + """Registers a check function for a feature.""" + def decorator(func): + checks[name] = (func, desc) + return func + return decorator + +def checkvers(name, desc, vers): + """Registers a check function for each of a series of versions. + + vers can be a list or an iterator""" + def decorator(func): + def funcv(v): + def f(): + return func(v) + return f + for v in vers: + v = str(v) + f = funcv(v) + checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v) + 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 True if cmd executes successfully and its output + """Return the match object if cmd executes successfully and its output is matched by the supplied regular expression. """ r = re.compile(regexp) - fh = os.popen(cmd) - s = fh.read() try: - ret = fh.close() - except IOError: - # Happen in Windows test environment - ret = 1 - return (ignorestatus or ret is None) and r.search(s) + 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') + return matchoutput('baz --version 2>&1', br'baz Bazaar version') +@check("bzr", "Canonical's Bazaar client") def has_bzr(): try: import bzrlib + import bzrlib.bzrdir + import bzrlib.errors + import bzrlib.revision + import bzrlib.revisionspec + bzrlib.revisionspec.RevisionSpec return bzrlib.__doc__ is not None - except ImportError: + except (AttributeError, ImportError): return False -def has_bzr114(): +@checkvers("bzr", "Canonical's Bazaar client >= %s", (1.14,)) +def has_bzr_range(v): + major, minor = v.split('.')[0:2] try: import bzrlib return (bzrlib.__doc__ is not None - and bzrlib.version_info[:2] >= (1, 14)) + and bzrlib.version_info[:2] >= (int(major), int(minor))) except ImportError: return False +@check("chg", "running with chg") +def has_chg(): + return 'CHGHG' in os.environ + +@check("cvs", "cvs client/server") def has_cvs(): - re = r'Concurrent Versions System.*?server' + re = br'Concurrent Versions System.*?server' + return matchoutput('cvs --version 2>&1', re) and not has_msys() + +@check("cvs112", "cvs client/server 1.12.* (not cvsnt)") +def has_cvs112(): + re = br'Concurrent Versions System \(CVS\) 1.12.*?server' return matchoutput('cvs --version 2>&1', re) and not has_msys() +@check("cvsnt", "cvsnt client/server") +def has_cvsnt(): + re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)' + return matchoutput('cvsnt --version 2>&1', re) + +@check("darcs", "darcs client") def has_darcs(): - return matchoutput('darcs --version', r'2\.[2-9]', True) + return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', 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) + return matchoutput('mtn --version', br'monotone', True) and not matchoutput( + 'mtn --version', br'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') @@ -57,16 +167,17 @@ 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 & 0777 + 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 & 0777) == m) + exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0o777) == m) finally: os.unlink(fn) except (IOError, OSError): @@ -74,6 +185,7 @@ 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) @@ -92,21 +204,7 @@ finally: os.remove(path) -def has_inotify(): - try: - import hgext.inotify.linux.watcher - except ImportError: - return False - name = tempfile.mktemp(dir='.', prefix=tempprefix) - sock = socket.socket(socket.AF_UNIX) - try: - sock.bind(name) - except socket.error, err: - return False - sock.close() - os.unlink(name) - return True - +@check("fifo", "named pipes") def has_fifo(): if getattr(os, "mkfifo", None) is None: return False @@ -118,6 +216,11 @@ 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 @@ -128,42 +231,101 @@ 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 -def has_gettext(): - return matchoutput('msgfmt --version', 'GNU gettext-tools') +def gethgversion(): + m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)') + if not m: + return (0, 0) + return (int(m.group(1)), int(m.group(2))) + +@checkvers("hg", "Mercurial >= %s", + list([(1.0 * x) / 10 for x in range(9, 99)])) +def has_hg_range(v): + major, minor = v.split('.')[0:2] + return gethgversion() >= (int(major), int(minor)) + +@check("hg08", "Mercurial >= 0.8") +def has_hg08(): + if checks["hg09"][0](): + return True + return matchoutput('hg help annotate 2>&1', '--date') + +@check("hg07", "Mercurial >= 0.7") +def has_hg07(): + if checks["hg08"][0](): + return True + return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM') + +@check("hg06", "Mercurial >= 0.6") +def has_hg06(): + if checks["hg07"][0](): + return True + return matchoutput('hg --version --quiet 2>&1', 'Mercurial version') +@check("gettext", "GNU Gettext (msgfmt)") +def has_gettext(): + return matchoutput('msgfmt --version', br'GNU gettext-tools') + +@check("git", "git command line client") def has_git(): - return matchoutput('git --version 2>&1', r'^git version') + return matchoutput('git --version 2>&1', br'^git version') + +def getgitversion(): + m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)') + if not m: + return (0, 0) + return (int(m.group(1)), int(m.group(2))) +# https://github.com/git-lfs/lfs-test-server <https://github.com/git-lfs/lfs-test-server> +@check("lfs-test-server", "git-lfs test server") +def has_lfsserver(): + exe = 'lfs-test-server' + if has_windows(): + exe = 'lfs-test-server.exe' + return any( + os.access(os.path.join(path, exe), os.X_OK) + for path in os.environ["PATH"].split(os.pathsep) + ) + +@checkvers("git", "git client (with ext::sh support) version >= %s", (1.9,)) +def has_git_range(v): + major, minor = v.split('.')[0:2] + return getgitversion() >= (int(major), int(minor)) + +@check("docutils", "Docutils text processing library") def has_docutils(): try: - from docutils.core import publish_cmdline + import docutils.core + docutils.core.publish_cmdline # silence unused import return True except ImportError: return False def getsvnversion(): - m = matchoutput('svn --version 2>&1', r'^svn,\s+version\s+(\d+)\.(\d+)') + m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)') if not m: return (0, 0) return (int(m.group(1)), int(m.group(2))) -def has_svn15(): - return getsvnversion() >= (1, 5) +@checkvers("svn", "subversion client and admin tools >= %s", (1.3, 1.5)) +def has_svn_range(v): + major, minor = v.split('.')[0:2] + return getsvnversion() >= (int(major), int(minor)) -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') + return matchoutput('svn --version 2>&1', br'^svn, version') and \ + matchoutput('svnadmin --version 2>&1', br'^svnadmin, version') +@check("svn-bindings", "subversion python bindings") def has_svn_bindings(): try: import svn.core @@ -174,10 +336,12 @@ 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/')) + return (matchoutput('p4 -V', br'Rev\. P4/') and + matchoutput('p4d -V', br'Rev\. P4D/')) +@check("symlink", "symbolic links") def has_symlink(): if getattr(os, "symlink", None) is None: return False @@ -189,120 +353,358 @@ 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: - try: - util.oslink(fn, name) - os.unlink(name) - return True - except OSError: - return False + util.oslink(fn, name) + os.unlink(name) + return True + except OSError: + return False finally: os.unlink(fn) -def has_tla(): - return matchoutput('tla --version 2>&1', r'The GNU Arch Revision') +@check("hardlink-whitelisted", "hardlinks on whitelisted filesystems") +def has_hardlink_whitelisted(): + from mercurial import util + try: + fstype = util.getfstype('.') + except OSError: + return False + return fstype in util._hardlinkfswhitelist +@check("rmcwd", "can remove current working directory") +def has_rmcwd(): + ocwd = os.getcwd() + temp = tempfile.mkdtemp(dir='.', prefix=tempprefix) + try: + os.chdir(temp) + # On Linux, 'rmdir .' isn't allowed, but the other names are okay. + # On Solaris and Windows, the cwd can't be removed by any names. + os.rmdir(os.getcwd()) + return True + except OSError: + return False + finally: + os.chdir(ocwd) + # clean up temp dir on platforms where cwd can't be removed + try: + os.rmdir(temp) + except OSError: + pass + +@check("tla", "GNU Arch tla client") +def has_tla(): + return matchoutput('tla --version 2>&1', br'The GNU Arch Revision') + +@check("gpg", "gpg client") def has_gpg(): - return matchoutput('gpg --version 2>&1', r'GnuPG') + return matchoutput('gpg --version 2>&1', br'GnuPG') + +@check("gpg2", "gpg client v2") +def has_gpg2(): + return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.') +@check("gpg21", "gpg client v2.1+") +def has_gpg21(): + return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)') + +@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 (077, 007, 022): + 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 & 0777 != ~umask & 0666: + 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", + br"<stdin>:1: 're' imported but unused", + True) + +@check("pylint", "Pylint python linter") +def has_pylint(): + return matchoutput("pylint --help", + br"Usage: pylint", True) +@check("clang-format", "clang-format C code formatter") +def has_clang_format(): + return matchoutput("clang-format --help", + br"^OVERVIEW: A tool to format C/C\+\+[^ ]+ code.") + +@check("jshint", "JSHint static code analysis tool") +def has_jshint(): + return matchoutput("jshint --version 2>&1", br"jshint v") + +@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("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) + br'abort: no repository found', True) +@check("ssl", "ssl module available") def has_ssl(): try: import ssl - import OpenSSL - OpenSSL.SSL.Context + ssl.CERT_NONE 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, ui as uimod + ui = uimod.ui.load() + return sslutil._defaultcacerts(ui) or sslutil._canloaddefaultcerts + +@check("defaultcacertsloaded", "detected presence of loaded system CA certs") +def has_defaultcacertsloaded(): + import ssl + from mercurial import sslutil, ui as uimod + + if not has_defaultcacerts(): + return False + if not has_sslcontext(): + return False + + ui = uimod.ui.load() + cafile = sslutil._defaultcacerts(ui) + ctx = ssl.create_default_context() + if cafile: + ctx.load_verify_locations(cafile=cafile) + else: + ctx.load_default_certs() + + return len(ctx.get_ca_certs()) > 0 + +@check("tls1.2", "TLS 1.2 protocol support") +def has_tls1_2(): + from mercurial import sslutil + return 'tls1.2' in sslutil.supportedprotocols + +@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 + return True + +@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(): - return matchoutput('test -x "`which tic`"', '') + try: + import curses + curses.COLOR_BLUE + return matchoutput('test -x "`which tic`"', br'') + except ImportError: + return False +@check("msys", "Windows with MSYS") def has_msys(): return os.getenv('MSYSTEM') -checks = { - "true": (lambda: True, "yak shaving"), - "false": (lambda: False, "nail clipper"), - "baz": (has_baz, "GNU Arch baz client"), - "bzr": (has_bzr, "Canonical's Bazaar client"), - "bzr114": (has_bzr114, "Canonical's Bazaar client >= 1.14"), - "cacheable": (has_cacheable_fs, "cacheable filesystem"), - "cvs": (has_cvs, "cvs client/server"), - "darcs": (has_darcs, "darcs client"), - "docutils": (has_docutils, "Docutils text processing library"), - "eol-in-paths": (has_eol_in_paths, "end-of-lines in paths"), - "execbit": (has_executablebit, "executable bit"), - "fifo": (has_fifo, "named pipes"), - "gettext": (has_gettext, "GNU Gettext (msgfmt)"), - "git": (has_git, "git command line client"), - "gpg": (has_gpg, "gpg client"), - "hardlink": (has_hardlink, "hardlinks"), - "icasefs": (has_icasefs, "case insensitive file system"), - "inotify": (has_inotify, "inotify extension support"), - "lsprof": (has_lsprof, "python lsprof module"), - "mtn": (has_mtn, "monotone client (>= 1.0)"), - "outer-repo": (has_outer_repo, "outer repo"), - "p4": (has_p4, "Perforce server and client"), - "pyflakes": (has_pyflakes, "Pyflakes python linter"), - "pygments": (has_pygments, "Pygments source highlighting library"), - "serve": (has_serve, "platform and python can manage 'hg serve -d'"), - "ssl": (has_ssl, "python >= 2.6 ssl module and python OpenSSL"), - "svn": (has_svn, "subversion client and admin tools"), - "svn13": (has_svn13, "subversion client and admin tools >= 1.3"), - "svn15": (has_svn15, "subversion client and admin tools >= 1.5"), - "svn-bindings": (has_svn_bindings, "subversion python bindings"), - "symlink": (has_symlink, "symbolic links"), - "system-sh": (has_system_sh, "system() uses sh"), - "tic": (has_tic, "terminfo compiler"), - "tla": (has_tla, "GNU Arch tla client"), - "unix-permissions": (has_unix_permissions, "unix-style permissions"), - "windows": (has_windows, "Windows"), - "msys": (has_msys, "Windows with MSYS"), -} +@check("aix", "AIX") +def has_aix(): + return sys.platform.startswith("aix") + +@check("osx", "OS X") +def has_osx(): + return sys.platform == 'darwin' + +@check("osxpackaging", "OS X packaging tools") +def has_osxpackaging(): + try: + return (matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1) + and matchoutput( + 'productbuild', br'Usage: productbuild ', + ignorestatus=1) + and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1) + and matchoutput( + 'xar --help', br'Usage: xar', ignorestatus=1)) + except ImportError: + return False + +@check('linuxormacos', 'Linux or MacOS') +def has_linuxormacos(): + # This isn't a perfect test for MacOS. But it is sufficient for our needs. + return sys.platform.startswith(('linux', 'darwin')) + +@check("docker", "docker support") +def has_docker(): + pat = br'A self-sufficient runtime for' + 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(): + # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first + # quote), so just accept anything in that spot. + dpkg = matchoutput('dpkg --version', + br"Debian .dpkg' package management program") + dh = matchoutput('dh --help', + br'dh is a part of debhelper.', ignorestatus=True) + dh_py2 = matchoutput('dh_python2 --help', + br'other supported Python versions') + # debuild comes from the 'devscripts' package, though you might want + # the 'build-debs' package instead, which has a dependency on devscripts. + debuild = matchoutput('debuild --help', + br'to run debian/rules with given parameter') + return dpkg and dh and dh_py2 and debuild + +@check("debdeps", + "debian build dependencies (run dpkg-checkbuilddeps in contrib/)") +def has_debdeps(): + # just check exit status (ignoring output) + path = '%s/../contrib/debian/control' % os.environ['TESTDIR'] + return matchoutput('dpkg-checkbuilddeps %s' % path, br'') + +@check("demandimport", "demandimport enabled") +def has_demandimport(): + # chg disables demandimport intentionally for performance wins. + return ((not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable') + +@check("py3k", "running with Python 3.x") +def has_py3k(): + return 3 == sys.version_info[0] + +@check("py3exe", "a Python 3.x interpreter is available") +def has_python3exe(): + return 'PYTHON3' in os.environ + +@check("py3pygments", "Pygments available on Python 3.x") +def has_py3pygments(): + if has_py3k(): + return has_pygments() + elif has_python3exe(): + # just check exit status (ignoring output) + py3 = os.environ['PYTHON3'] + return matchoutput('%s -c "import pygments"' % py3, br'') + return False + +@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 (use --allow-slow-tests)") +def has_slow(): + return os.environ.get('HGTEST_SLOW') == 'slow' + +@check("hypothesis", "Hypothesis automated test generation") +def has_hypothesis(): + try: + import hypothesis + hypothesis.given + return True + except ImportError: + return False + +@check("unziplinks", "unzip(1) understands and extracts symlinks") +def unzip_understands_symlinks(): + return matchoutput('unzip --help', br'Info-ZIP') + +@check("zstd", "zstd Python module available") +def has_zstd(): + try: + import mercurial.zstd + mercurial.zstd.__version__ + return True + except ImportError: + return False + +@check("devfull", "/dev/full special file") +def has_dev_full(): + return os.path.exists('/dev/full') + +@check("virtualenv", "Python virtualenv support") +def has_virtualenv(): + try: + import virtualenv + virtualenv.ACTIVATE_SH + return True + except ImportError: + return False + +@check("fsmonitor", "running tests with fsmonitor") +def has_fsmonitor(): + return 'HGFSMONITOR_TESTS' in os.environ + +@check("fuzzywuzzy", "Fuzzy string matching library") +def has_fuzzywuzzy(): + try: + import fuzzywuzzy + fuzzywuzzy.__version__ + return True + except ImportError: + return False + +@check("clang-libfuzzer", "clang new enough to include libfuzzer") +def has_clang_libfuzzer(): + mat = matchoutput('clang --version', 'clang version (\d)') + if mat: + # libfuzzer is new in clang 6 + return int(mat.group(1)) > 5 + return False