# HG changeset patch # User Dan Villiom Podlaski Christiansen <danchr@gmail.com> # Date 1637535242 -3600 # Sun Nov 21 23:54:02 2021 +0100 # Node ID d00e6c90987d3f36b05eb2b3ba4c635b7bb4f6b6 # Parent 265a78f97b7bcce0f524200e5279a97734d55999 tests: sync files from mercurial diff --git a/tests/heredoctest.py b/tests/heredoctest.py --- a/tests/heredoctest.py +++ b/tests/heredoctest.py @@ -1,4 +1,4 @@ -from __future__ import generator_stop +from __future__ import absolute_import, print_function import sys diff --git a/tests/hghave.py b/tests/hghave.py --- a/tests/hghave.py +++ b/tests/hghave.py @@ -1,4 +1,4 @@ -from __future__ import generator_stop +from __future__ import absolute_import, print_function import distutils.version import os @@ -14,6 +14,8 @@ checks = { "true": (lambda: True, "yak shaving"), "false": (lambda: False, "nail clipper"), + "known-bad-output": (lambda: True, "use for currently known bad output"), + "missing-correct-output": (lambda: False, "use for missing good output"), } try: @@ -27,7 +29,8 @@ stdout = getattr(sys.stdout, 'buffer', sys.stdout) stderr = getattr(sys.stderr, 'buffer', sys.stderr) -if sys.version_info[0] >= 3: +is_not_python2 = sys.version_info[0] >= 3 +if is_not_python2: def _sys2bytes(p): if p is None: @@ -102,8 +105,8 @@ check, desc = checks[feature] try: available = check() - except Exception: - result['error'].append('hghave check failed: %s' % feature) + except Exception as e: + result['error'].append('hghave check %s failed: %r' % (feature, e)) continue if not negate and not available: @@ -138,9 +141,22 @@ """Return the match object if cmd executes successfully and its output is matched by the supplied regular expression. """ + + # Tests on Windows have to fake USERPROFILE to point to the test area so + # that `~` is properly expanded on py3.8+. However, some tools like black + # make calls that need the real USERPROFILE in order to run `foo --version`. + env = os.environ + if os.name == 'nt': + env = os.environ.copy() + env['USERPROFILE'] = env['REALUSERPROFILE'] + r = re.compile(regexp) p = subprocess.Popen( - cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env, ) s = p.communicate()[0] ret = p.returncode @@ -152,38 +168,40 @@ return matchoutput('baz --version 2>&1', br'baz Bazaar version') -@check("bzr", "Canonical's Bazaar client") +@check("bzr", "Breezy library and executable version >= 3.1") def has_bzr(): + if not is_not_python2: + return False try: - import bzrlib - import bzrlib.bzrdir - import bzrlib.errors - import bzrlib.revision - import bzrlib.revisionspec + # Test the Breezy python lib + import breezy + import breezy.bzr.bzrdir + import breezy.errors + import breezy.revision + import breezy.revisionspec - bzrlib.revisionspec.RevisionSpec - return bzrlib.__doc__ is not None + breezy.revisionspec.RevisionSpec + if breezy.__doc__ is None or breezy.version_info[:2] < (3, 1): + return False except (AttributeError, ImportError): return False - - -@checkvers("bzr", "Canonical's Bazaar client >= %s", (1.14,)) -def has_bzr_range(v): - major, minor = v.split('rc')[0].split('.')[0:2] - try: - import bzrlib - - return bzrlib.__doc__ is not None and bzrlib.version_info[:2] >= ( - int(major), - int(minor), - ) - except ImportError: - return False + # Test the executable + return matchoutput('brz --version 2>&1', br'Breezy \(brz\) ') @check("chg", "running with chg") def has_chg(): - return 'CHGHG' in os.environ + return 'CHG_INSTALLED_AS_HG' in os.environ + + +@check("rhg", "running with rhg as 'hg'") +def has_rhg(): + return 'RHG_INSTALLED_AS_HG' in os.environ + + +@check("pyoxidizer", "running with pyoxidizer build as 'hg'") +def has_rhg(): + return 'PYOXIDIZED_INSTALLED_AS_HG' in os.environ @check("cvs", "cvs client/server") @@ -246,6 +264,13 @@ return not (new_file_has_exec or exec_flags_cannot_flip) +@check("suidbit", "setuid and setgid bit") +def has_suidbit(): + if getattr(os, "statvfs", None) is None or getattr(os, "ST_NOSUID") is None: + return False + return bool(os.statvfs('.').f_flag & os.ST_NOSUID) + + @check("icasefs", "case insensitive file system") def has_icasefs(): # Stolen from mercurial.util @@ -612,14 +637,21 @@ @check("pylint", "Pylint python linter") def has_pylint(): - return matchoutput("pylint --help", br"Usage: pylint", True) + return matchoutput("pylint --help", br"Usage:[ ]+pylint", True) -@check("clang-format", "clang-format C code formatter") +@check("clang-format", "clang-format C code formatter (>= 11)") def has_clang_format(): m = matchoutput('clang-format --version', br'clang-format version (\d+)') - # style changed somewhere between 4.x and 6.x - return m and int(m.group(1)) >= 6 + # style changed somewhere between 10.x and 11.x + if m: + return int(m.group(1)) >= 11 + # Assist Googler contributors, they have a centrally-maintained version of + # clang-format that is generally very fresh, but unlike most builds (both + # official and unofficial), it does *not* include a version number. + return matchoutput( + 'clang-format --version', br'clang-format .*google3-trunk \([0-9a-f]+\)' + ) @check("jshint", "JSHint static code analysis tool") @@ -726,17 +758,35 @@ return os.path.isdir(os.path.join(t, "..", ".hg")) -@check("tic", "terminfo compiler and curses module") -def has_tic(): +@check("network-io", "whether tests are allowed to access 3rd party services") +def has_test_repo(): + t = os.environ.get("HGTESTS_ALLOW_NETIO") + return t == "1" + + +@check("curses", "terminfo compiler and curses module") +def has_curses(): try: import curses curses.COLOR_BLUE - return matchoutput('test -x "`which tic`"', br'') + + # Windows doesn't have a `tic` executable, but the windows_curses + # package is sufficient to run the tests without it. + if os.name == 'nt': + return True + + return has_tic() + except (ImportError, AttributeError): return False +@check("tic", "terminfo compiler") +def has_tic(): + return matchoutput('test -x "`which tic`"', br'') + + @check("xz", "xz compression utility") def has_xz(): # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which @@ -851,7 +901,10 @@ @check("py3exe", "a Python 3.x interpreter is available") def has_python3exe(): - return matchoutput('python3 -V', br'^Python 3.(5|6|7|8|9)') + py = 'python3' + if os.name == 'nt': + py = 'py -3' + return matchoutput('%s -V' % py, br'^Python 3.(5|6|7|8|9)') @check("pure", "running with pure Python code") @@ -912,17 +965,16 @@ return False -@check("py2virtualenv", "Python2 virtualenv support") -def has_py2virtualenv(): - if sys.version_info[0] != 2: - return False - +@check("virtualenv", "virtualenv support") +def has_virtualenv(): try: import virtualenv - virtualenv.ACTIVATE_SH - return True - except ImportError: + # --no-site-package became the default in 1.7 (Nov 2011), and the + # argument was removed in 20.0 (Feb 2020). Rather than make the + # script complicated, just ignore ancient versions. + return int(virtualenv.__version__.split('.')[0]) > 1 + except (AttributeError, ImportError, IndexError): return False @@ -1031,7 +1083,15 @@ return 'fncache' in getrepofeatures() -@check('sqlite', 'sqlite3 module is available') +@check('dirstate-v2', 'using the v2 format of .hg/dirstate') +def has_dirstate_v2(): + # Keep this logic in sync with `newreporequirements()` in `mercurial/localrepo.py` + return has_rust() and matchoutput( + 'hg config format.exp-rc-dirstate-v2', b'(?i)1|yes|true|on|always' + ) + + +@check('sqlite', 'sqlite3 module and matching cli is available') def has_sqlite(): try: import sqlite3 @@ -1047,7 +1107,7 @@ return matchoutput('sqlite3 -version', br'^3\.\d+') -@check('vcr', 'vcr http mocking library') +@check('vcr', 'vcr http mocking library (pytest-vcr)') def has_vcr(): try: import vcr @@ -1067,13 +1127,13 @@ return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)') -@check('black', 'the black formatter for python') +@check('black', 'the black formatter for python (>= 20.8b1)') def has_black(): blackcmd = 'black --version' version_regex = b'black, version ([0-9a-b.]+)' version = matchoutput(blackcmd, version_regex) sv = distutils.version.StrictVersion - return version and sv(_bytes2sys(version.group(1))) >= sv('19.10b0') + return version and sv(_bytes2sys(version.group(1))) >= sv('20.8b1') @check('pytype', 'the pytype type checker') @@ -1084,14 +1144,20 @@ return version and sv(_bytes2sys(version.group(0))) >= sv('2019.10.17') -@check("rustfmt", "rustfmt tool") +@check("rustfmt", "rustfmt tool at version nightly-2020-10-04") def has_rustfmt(): # We use Nightly's rustfmt due to current unstable config options. return matchoutput( - '`rustup which --toolchain nightly rustfmt` --version', b'rustfmt' + '`rustup which --toolchain nightly-2020-10-04 rustfmt` --version', + b'rustfmt', ) +@check("cargo", "cargo tool") +def has_cargo(): + return matchoutput('`rustup which cargo` --version', b'cargo') + + @check("lzma", "python lzma module") def has_lzma(): try: @@ -1101,3 +1167,8 @@ return True except ImportError: return False + + +@check("bash", "bash shell") +def has_bash(): + return matchoutput("bash -c 'echo hi'", b'^hi$') diff --git a/tests/killdaemons.py b/tests/killdaemons.py --- a/tests/killdaemons.py +++ b/tests/killdaemons.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -from __future__ import generator_stop +from __future__ import absolute_import import errno import os import signal diff --git a/tests/run-tests.py b/tests/run-tests.py --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # run-tests.py - Run a set of tests on Mercurial # -# Copyright 2006 Matt Mackall <mpm@selenic.com> +# Copyright 2006 Olivia Mackall <olivia@selenic.com> # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. @@ -43,10 +43,11 @@ # completes fairly quickly, includes both shell and Python scripts, and # includes some scripts that run daemon processes.) -from __future__ import generator_stop +from __future__ import absolute_import, print_function import argparse import collections +import contextlib import difflib import distutils.version as version import errno @@ -69,6 +70,8 @@ import uuid import xml.dom.minidom as minidom +WINDOWS = os.name == r'nt' + try: import Queue as queue except ImportError: @@ -83,24 +86,35 @@ shellquote = pipes.quote + processlock = threading.Lock() pygmentspresent = False -# ANSI color is unsupported prior to Windows 10 -if os.name != 'nt': - try: # is pygments installed - import pygments - import pygments.lexers as lexers - import pygments.lexer as lexer - import pygments.formatters as formatters - import pygments.token as token - import pygments.style as style - - pygmentspresent = True - difflexer = lexers.DiffLexer() - terminal256formatter = formatters.Terminal256Formatter() - except ImportError: - pass +try: # is pygments installed + import pygments + import pygments.lexers as lexers + import pygments.lexer as lexer + import pygments.formatters as formatters + import pygments.token as token + import pygments.style as style + + if WINDOWS: + hgpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.append(hgpath) + try: + from mercurial import win32 # pytype: disable=import-error + + # Don't check the result code because it fails on heptapod, but + # something is able to convert to color anyway. + win32.enablevtmode() + finally: + sys.path = sys.path[:-1] + + pygmentspresent = True + difflexer = lexers.DiffLexer() + terminal256formatter = formatters.Terminal256Formatter() +except ImportError: + pass if pygmentspresent: @@ -140,6 +154,7 @@ origenviron = os.environ.copy() + if sys.version_info > (3, 5, 0): PYTHON3 = True xrange = range # we use xrange in one place, and we'd rather not use range @@ -192,7 +207,7 @@ osenvironb = environbytes(os.environ) getcwdb = getattr(os, 'getcwdb') - if not getcwdb or os.name == 'nt': + if not getcwdb or WINDOWS: getcwdb = lambda: _sys2bytes(os.getcwd()) elif sys.version_info >= (3, 0, 0): @@ -215,6 +230,18 @@ osenvironb = os.environ getcwdb = os.getcwd +if WINDOWS: + _getcwdb = getcwdb + + def getcwdb(): + cwd = _getcwdb() + if re.match(b'^[a-z]:', cwd): + # os.getcwd() is inconsistent on the capitalization of the drive + # letter, so adjust it. see https://bugs.python.org/issue40368 + cwd = cwd[0:1].upper() + cwd[1:] + return cwd + + # For Windows support wifexited = getattr(os, "WIFEXITED", lambda x: False) @@ -255,11 +282,18 @@ else: family = socket.AF_INET try: - s = socket.socket(family, socket.SOCK_STREAM) - s.bind(('localhost', port)) - s.close() + with contextlib.closing(socket.socket(family, socket.SOCK_STREAM)) as s: + s.bind(('localhost', port)) return True except socket.error as exc: + if WINDOWS and exc.errno == errno.WSAEACCES: + return False + elif PYTHON3: + # TODO: make a proper exception handler after dropping py2. This + # works because socket.error is an alias for OSError on py3, + # which is also the baseclass of PermissionError. + if isinstance(exc, PermissionError): + return False if exc.errno not in ( errno.EADDRINUSE, errno.EADDRNOTAVAIL, @@ -299,6 +333,7 @@ while time.time() - start < timeout and p.returncode is None: time.sleep(0.1) p.timeout = True + vlog('# Timout reached for process %d' % p.pid) if p.returncode is None: terminate(p) @@ -323,7 +358,7 @@ default_defaults = { 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()), - 'timeout': ('HGTEST_TIMEOUT', 180), + 'timeout': ('HGTEST_TIMEOUT', 360), 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500), 'port': ('HGTEST_PORT', 20059), 'shell': ('HGTEST_SHELL', 'sh'), @@ -336,6 +371,21 @@ return os.path.realpath(os.path.expanduser(path)) +def which(exe): + if PYTHON3: + # shutil.which only accept bytes from 3.8 + cmd = _bytes2sys(exe) + real_exec = shutil.which(cmd) + return _sys2bytes(real_exec) + else: + # let us do the os work + for p in osenvironb[b'PATH'].split(os.pathsep): + f = os.path.join(p, exe) + if os.path.isfile(f): + return f + return None + + def parselistfiles(files, listtype, warn=True): entries = dict() for filename in files: @@ -352,7 +402,8 @@ for line in f.readlines(): line = line.split(b'#', 1)[0].strip() if line: - entries[line] = filename + # Ensure path entries are compatible with os.path.relpath() + entries[os.path.normpath(line)] = filename f.close() return entries @@ -534,7 +585,19 @@ help="install and use chg wrapper in place of hg", ) hgconf.add_argument( - "--chg-debug", action="store_true", help="show chg debug logs", + "--chg-debug", + action="store_true", + help="show chg debug logs", + ) + hgconf.add_argument( + "--rhg", + action="store_true", + help="install and use rhg Rust implementation in place of hg", + ) + hgconf.add_argument( + "--pyoxidized", + action="store_true", + help="build the hg binary using pyoxidizer", ) hgconf.add_argument("--compiler", help="compiler to build with") hgconf.add_argument( @@ -548,6 +611,7 @@ "--local", action="store_true", help="shortcut for --with-hg=<testdir>/../hg, " + "--with-rhg=<testdir>/../rust/target/release/rhg if --rhg is set, " "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set", ) hgconf.add_argument( @@ -576,6 +640,11 @@ help="use specified chg wrapper in place of hg", ) hgconf.add_argument( + "--with-rhg", + metavar="RHG", + help="use specified rhg Rust implementation in place of hg", + ) + hgconf.add_argument( "--with-hg", metavar="HG", help="test using specified hg script rather than a " @@ -663,16 +732,22 @@ parser.error('--rust cannot be used with --no-rust') if options.local: - if options.with_hg or options.with_chg: - parser.error('--local cannot be used with --with-hg or --with-chg') + if options.with_hg or options.with_rhg or options.with_chg: + parser.error( + '--local cannot be used with --with-hg or --with-rhg or --with-chg' + ) + if options.pyoxidized: + parser.error('--pyoxidized does not work with --local (yet)') testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0]))) reporootdir = os.path.dirname(testdir) pathandattrs = [(b'hg', 'with_hg')] if options.chg: pathandattrs.append((b'contrib/chg/chg', 'with_chg')) + if options.rhg: + pathandattrs.append((b'rust/target/release/rhg', 'with_rhg')) for relpath, attr in pathandattrs: binpath = os.path.join(reporootdir, relpath) - if os.name != 'nt' and not os.access(binpath, os.X_OK): + if not (WINDOWS or os.access(binpath, os.X_OK)): parser.error( '--local specified, but %r not found or ' 'not executable' % binpath @@ -687,11 +762,17 @@ ): parser.error('--with-hg must specify an executable hg script') if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']: - sys.stderr.write('warning: --with-hg should specify an hg script\n') + msg = 'warning: --with-hg should specify an hg script, not: %s\n' + msg %= _bytes2sys(os.path.basename(options.with_hg)) + sys.stderr.write(msg) sys.stderr.flush() - if (options.chg or options.with_chg) and os.name == 'nt': + if (options.chg or options.with_chg) and WINDOWS: parser.error('chg does not work on %s' % os.name) + if (options.rhg or options.with_rhg) and WINDOWS: + parser.error('rhg does not work on %s' % os.name) + if options.pyoxidized and not WINDOWS: + parser.error('--pyoxidized is currently Windows only') if options.with_chg: options.chg = False # no installation to temporary location options.with_chg = canonpath(_sys2bytes(options.with_chg)) @@ -700,12 +781,28 @@ and os.access(options.with_chg, os.X_OK) ): parser.error('--with-chg must specify a chg executable') + if options.with_rhg: + options.rhg = False # no installation to temporary location + options.with_rhg = canonpath(_sys2bytes(options.with_rhg)) + if not ( + os.path.isfile(options.with_rhg) + and os.access(options.with_rhg, os.X_OK) + ): + parser.error('--with-rhg must specify a rhg executable') if options.chg and options.with_hg: # chg shares installation location with hg parser.error( '--chg does not work when --with-hg is specified ' '(use --with-chg instead)' ) + if options.rhg and options.with_hg: + # rhg shares installation location with hg + parser.error( + '--rhg does not work when --with-hg is specified ' + '(use --with-rhg instead)' + ) + if options.rhg and options.chg: + parser.error('--rhg and --chg do not work together') if options.color == 'always' and not pygmentspresent: sys.stderr.write( @@ -967,6 +1064,7 @@ if slowtimeout is None: slowtimeout = defaults['slowtimeout'] self.path = path + self.relpath = os.path.relpath(path) self.bname = os.path.basename(path) self.name = _bytes2sys(self.bname) self._testdir = os.path.dirname(path) @@ -1192,7 +1290,10 @@ if self._keeptmpdir: log( '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' - % (_bytes2sys(self._testtmp), _bytes2sys(self._threadtmp),) + % ( + _bytes2sys(self._testtmp), + _bytes2sys(self._threadtmp), + ) ) else: try: @@ -1249,6 +1350,11 @@ (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'), ] r.append((self._escapepath(self._testtmp), b'$TESTTMP')) + if WINDOWS: + # JSON output escapes backslashes in Windows paths, so also catch a + # double-escape. + replaced = self._testtmp.replace(b'\\', br'\\') + r.append((self._escapepath(replaced), b'$STR_REPR_TESTTMP')) replacementfile = os.path.join(self._testdir, b'common-pattern.py') @@ -1267,7 +1373,7 @@ return r def _escapepath(self, p): - if os.name == 'nt': + if WINDOWS: return b''.join( c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) @@ -1327,8 +1433,14 @@ env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or '' env['HGEMITWARNINGS'] = '1' env['TESTTMP'] = _bytes2sys(self._testtmp) + uid_file = os.path.join(_bytes2sys(self._testtmp), 'UID') + env['HGTEST_UUIDFILE'] = uid_file env['TESTNAME'] = self.name env['HOME'] = _bytes2sys(self._testtmp) + if WINDOWS: + env['REALUSERPROFILE'] = env['USERPROFILE'] + # py3.8+ ignores HOME: https://bugs.python.org/issue36264 + env['USERPROFILE'] = env['HOME'] formated_timeout = _bytes2sys(b"%d" % default_defaults['timeout'][1]) env['HGTEST_TIMEOUT_DEFAULT'] = formated_timeout env['HGTEST_TIMEOUT'] = _bytes2sys(b"%d" % self._timeout) @@ -1358,14 +1470,14 @@ extraextensions = [] for opt in self._extraconfigopts: - section, key = _sys2bytes(opt).split(b'.', 1) + section, key = opt.split('.', 1) if section != 'extensions': continue - name = key.split(b'=', 1)[0] + name = key.split('=', 1)[0] extraextensions.append(name) if extraextensions: - env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions) + env['HGTESTEXTRAEXTENSIONS'] = ' '.join(extraextensions) # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw # IP addresses. @@ -1374,7 +1486,7 @@ # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c, # but this is needed for testing python instances like dummyssh, # dummysmtpd.py, and dumbhttp.py. - if PYTHON3 and os.name == 'nt': + if PYTHON3 and WINDOWS: env['PYTHONLEGACYWINDOWSSTDIO'] = '1' # Modified HOME in test environment can confuse Rust tools. So set @@ -1438,9 +1550,15 @@ hgrc.write(b'[ui]\n') hgrc.write(b'slash = True\n') hgrc.write(b'interactive = False\n') + hgrc.write(b'detailed-exit-code = True\n') hgrc.write(b'merge = internal:merge\n') hgrc.write(b'mergemarkers = detailed\n') hgrc.write(b'promptecho = True\n') + dummyssh = os.path.join(self._testdir, b'dummyssh') + hgrc.write(b'ssh = "%s" "%s"\n' % (PYTHON, dummyssh)) + hgrc.write(b'timeout.warn=15\n') + hgrc.write(b'[chgserver]\n') + hgrc.write(b'idletimeout=60\n') hgrc.write(b'[defaults]\n') hgrc.write(b'[devel]\n') hgrc.write(b'all-warnings = true\n') @@ -1482,6 +1600,7 @@ proc = subprocess.Popen( _bytes2sys(cmd), shell=True, + close_fds=closefds, cwd=_bytes2sys(self._testtmp), env=env, ) @@ -1537,8 +1656,7 @@ # Quote the python(3) executable for Windows cmd = b'"%s" "%s"' % (PYTHON, self.path) vlog("# Running", cmd.decode("utf-8")) - normalizenewlines = os.name == 'nt' - result = self._runcommand(cmd, env, normalizenewlines=normalizenewlines) + result = self._runcommand(cmd, env, normalizenewlines=WINDOWS) if self._aborted: raise KeyboardInterrupt() @@ -1752,8 +1870,6 @@ if self._debug: script.append(b'set -x\n') - if self._hgcommand != b'hg': - script.append(b'alias hg="%s"\n' % self._hgcommand) if os.getenv('MSYSTEM'): script.append(b'alias pwd="pwd -W"\n') @@ -2011,7 +2127,7 @@ flags = flags or b'' el = flags + b'(?:' + el + b')' # use \Z to ensure that the regex matches to the end of the string - if os.name == 'nt': + if WINDOWS: return re.match(el + br'\r?\n\Z', l) return re.match(el + br'\n\Z', l) except re.error: @@ -2072,7 +2188,7 @@ el = el.encode('latin-1') else: el = el[:-7].decode('string-escape') + '\n' - if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l: + if el == l or WINDOWS and el[:-1] + b'\r\n' == l: return True, True if el.endswith(b" (re)\n"): return (TTest.rematch(el[:-6], l) or retry), False @@ -2083,17 +2199,17 @@ return (TTest.globmatch(el[:-8], l) or retry), False if os.altsep: _l = l.replace(b'\\', b'/') - if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l: + if el == _l or WINDOWS and el[:-1] + b'\r\n' == _l: return True, True return retry, True @staticmethod def parsehghaveoutput(lines): - '''Parse hghave log lines. + """Parse hghave log lines. Return tuple of lists (missing, failed): * the missing/unknown features - * the features for which existence check failed''' + * the features for which existence check failed""" missing = [] failed = [] for line in lines: @@ -2119,12 +2235,15 @@ firstlock = threading.RLock() firsterror = False - -class TestResult(unittest._TextTestResult): +if PYTHON3: + base_class = unittest.TextTestResult +else: + base_class = unittest._TextTestResult + + +class TestResult(base_class): """Holds results when executing via unittest.""" - # Don't worry too much about accessing the non-public _TextTestResult. - # It is relatively common in Python testing tools. def __init__(self, options, *args, **kwargs): super(TestResult, self).__init__(*args, **kwargs) @@ -2146,19 +2265,23 @@ self.faildata = {} if options.color == 'auto': - self.color = pygmentspresent and self.stream.isatty() + isatty = self.stream.isatty() + # For some reason, redirecting stdout on Windows disables the ANSI + # color processing of stderr, which is what is used to print the + # output. Therefore, both must be tty on Windows to enable color. + if WINDOWS: + isatty = isatty and sys.stdout.isatty() + self.color = pygmentspresent and isatty elif options.color == 'never': self.color = False else: # 'always', for testing purposes self.color = pygmentspresent def onStart(self, test): - """ Can be overriden by custom TestResult - """ + """Can be overriden by custom TestResult""" def onEnd(self): - """ Can be overriden by custom TestResult - """ + """Can be overriden by custom TestResult""" def addFailure(self, test, reason): self.failures.append((test, reason)) @@ -2267,7 +2390,7 @@ if test.path.endswith(b'.t'): rename(test.errpath, test.path) else: - rename(test.errpath, '%s.out' % test.path) + rename(test.errpath, b'%s.out' % test.path) accepted = True if not accepted: self.faildata[test.name] = b''.join(lines) @@ -2336,7 +2459,6 @@ jobs=1, whitelist=None, blacklist=None, - retest=False, keywords=None, loop=False, runs_per_test=1, @@ -2364,9 +2486,6 @@ backwards compatible behavior which reports skipped tests as part of the results. - retest denotes whether to retest failed tests. This arguably belongs - outside of TestSuite. - keywords denotes key words that will be used to filter which tests to execute. This arguably belongs outside of TestSuite. @@ -2377,7 +2496,6 @@ self._jobs = jobs self._whitelist = whitelist self._blacklist = blacklist - self._retest = retest self._keywords = keywords self._loop = loop self._runs_per_test = runs_per_test @@ -2402,15 +2520,17 @@ result.addSkip(test, "Doesn't exist") continue - if not (self._whitelist and test.bname in self._whitelist): - if self._blacklist and test.bname in self._blacklist: + is_whitelisted = self._whitelist and ( + test.relpath in self._whitelist or test.bname in self._whitelist + ) + if not is_whitelisted: + is_blacklisted = self._blacklist and ( + test.relpath in self._blacklist + or test.bname in self._blacklist + ) + if is_blacklisted: result.addSkip(test, 'blacklisted') continue - - if self._retest and not os.path.exists(test.errpath): - result.addIgnore(test, 'not retesting') - continue - if self._keywords: with open(test.path, 'rb') as f: t = f.read().lower() + test.bname.lower() @@ -2564,7 +2684,7 @@ ) with os.fdopen(fd, 'w') as fp: for name, ts in sorted(saved.items()): - fp.write('%s %s\n' % (name, ' '.join('%.3f' % (t,) for t in ts))) + fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts]))) timepath = os.path.join(outputdir, b'.testtimes') try: os.unlink(timepath) @@ -2945,8 +3065,11 @@ self._hgtmp = None self._installdir = None self._bindir = None - self._tmpbinddir = None + # a place for run-tests.py to generate executable it needs + self._custom_bin_dir = None self._pythondir = None + # True if we had to infer the pythondir from --with-hg + self._pythondir_inferred = False self._coveragefile = None self._createdfiles = [] self._hgcommand = None @@ -2984,7 +3107,6 @@ def _run(self, testdescs): testdir = getcwdb() - self._testdir = osenvironb[b'TESTDIR'] = getcwdb() # assume all tests in same folder for now if testdescs: pathname = os.path.dirname(testdescs[0]['path']) @@ -3026,7 +3148,7 @@ os.makedirs(tmpdir) else: d = None - if os.name == 'nt': + if WINDOWS: # without this, we get the default temp dir location, but # in all lowercase, which causes troubles with paths (issue3490) d = osenvironb.get(b'TMP', None) @@ -3034,14 +3156,15 @@ self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir) + self._custom_bin_dir = os.path.join(self._hgtmp, b'custom-bin') + os.makedirs(self._custom_bin_dir) + if self.options.with_hg: self._installdir = None whg = self.options.with_hg self._bindir = os.path.dirname(os.path.realpath(whg)) assert isinstance(self._bindir, bytes) self._hgcommand = os.path.basename(whg) - self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin') - os.makedirs(self._tmpbindir) normbin = os.path.normpath(os.path.abspath(whg)) normbin = normbin.replace(_sys2bytes(os.sep), b'/') @@ -3063,33 +3186,71 @@ # Fall back to the legacy behavior. else: self._pythondir = self._bindir + self._pythondir_inferred = True else: self._installdir = os.path.join(self._hgtmp, b"install") self._bindir = os.path.join(self._installdir, b"bin") self._hgcommand = b'hg' - self._tmpbindir = self._bindir self._pythondir = os.path.join(self._installdir, b"lib", b"python") # Force the use of hg.exe instead of relying on MSYS to recognize hg is # a python script and feed it to python.exe. Legacy stdio is force # enabled by hg.exe, and this is a more realistic way to launch hg # anyway. - if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'): + if WINDOWS and not self._hgcommand.endswith(b'.exe'): self._hgcommand += b'.exe' + real_hg = os.path.join(self._bindir, self._hgcommand) + osenvironb[b'HGTEST_REAL_HG'] = real_hg # set CHGHG, then replace "hg" command by "chg" chgbindir = self._bindir if self.options.chg or self.options.with_chg: - osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand) + osenvironb[b'CHG_INSTALLED_AS_HG'] = b'1' + osenvironb[b'CHGHG'] = real_hg else: - osenvironb.pop(b'CHGHG', None) # drop flag for hghave + # drop flag for hghave + osenvironb.pop(b'CHG_INSTALLED_AS_HG', None) if self.options.chg: self._hgcommand = b'chg' elif self.options.with_chg: chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg)) self._hgcommand = os.path.basename(self.options.with_chg) + # configure fallback and replace "hg" command by "rhg" + rhgbindir = self._bindir + if self.options.rhg or self.options.with_rhg: + # Affects hghave.py + osenvironb[b'RHG_INSTALLED_AS_HG'] = b'1' + # Affects configuration. Alternatives would be setting configuration through + # `$HGRCPATH` but some tests override that, or changing `_hgcommand` to include + # `--config` but that disrupts tests that print command lines and check expected + # output. + osenvironb[b'RHG_ON_UNSUPPORTED'] = b'fallback' + osenvironb[b'RHG_FALLBACK_EXECUTABLE'] = real_hg + else: + # drop flag for hghave + osenvironb.pop(b'RHG_INSTALLED_AS_HG', None) + if self.options.rhg: + self._hgcommand = b'rhg' + elif self.options.with_rhg: + rhgbindir = os.path.dirname(os.path.realpath(self.options.with_rhg)) + self._hgcommand = os.path.basename(self.options.with_rhg) + + if self.options.pyoxidized: + testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0]))) + reporootdir = os.path.dirname(testdir) + # XXX we should ideally install stuff instead of using the local build + bin_path = ( + b'build/pyoxidizer/x86_64-pc-windows-msvc/release/app/hg.exe' + ) + full_path = os.path.join(reporootdir, bin_path) + self._hgcommand = full_path + # Affects hghave.py + osenvironb[b'PYOXIDIZED_INSTALLED_AS_HG'] = b'1' + else: + osenvironb.pop(b'PYOXIDIZED_INSTALLED_AS_HG', None) + osenvironb[b"BINDIR"] = self._bindir osenvironb[b"PYTHON"] = PYTHON @@ -3108,10 +3269,11 @@ path.insert(2, realdir) if chgbindir != self._bindir: path.insert(1, chgbindir) + if rhgbindir != self._bindir: + path.insert(1, rhgbindir) if self._testdir != runtestdir: path = [self._testdir] + path - if self._tmpbindir != self._bindir: - path = [self._tmpbindir] + path + path = [self._custom_bin_dir] + path osenvironb[b"PATH"] = sepb.join(path) # Include TESTDIR in PYTHONPATH so that out-of-tree extensions @@ -3169,7 +3331,9 @@ vlog("# Using HGTMP", _bytes2sys(self._hgtmp)) vlog("# Using PATH", os.environ["PATH"]) vlog( - "# Using", _bytes2sys(IMPL_PATH), _bytes2sys(osenvironb[IMPL_PATH]), + "# Using", + _bytes2sys(IMPL_PATH), + _bytes2sys(osenvironb[IMPL_PATH]), ) vlog("# Writing to directory", _bytes2sys(self._outputdir)) @@ -3253,6 +3417,14 @@ tests.append({'path': t}) else: tests.append({'path': t}) + + if self.options.retest: + retest_args = [] + for test in tests: + errpath = self._geterrpath(test) + if os.path.exists(errpath): + retest_args.append(test) + tests = retest_args return tests def _runtests(self, testdescs): @@ -3269,13 +3441,7 @@ orig = list(testdescs) while testdescs: desc = testdescs[0] - # desc['path'] is a relative path - if 'case' in desc: - casestr = b'#'.join(desc['case']) - errpath = b'%s#%s.err' % (desc['path'], casestr) - else: - errpath = b'%s.err' % desc['path'] - errpath = os.path.join(self._outputdir, errpath) + errpath = self._geterrpath(desc) if os.path.exists(errpath): break testdescs.pop(0) @@ -3298,7 +3464,6 @@ jobs=jobs, whitelist=self.options.whitelisted, blacklist=self.options.blacklist, - retest=self.options.retest, keywords=kws, loop=self.options.loop, runs_per_test=self.options.runs_per_test, @@ -3316,14 +3481,19 @@ if self.options.list_tests: result = runner.listtests(suite) else: + self._usecorrectpython() if self._installdir: self._installhg() self._checkhglib("Testing") - else: - self._usecorrectpython() if self.options.chg: assert self._installdir self._installchg() + if self.options.rhg: + assert self._installdir + self._installrhg() + elif self.options.pyoxidized: + self._build_pyoxidized() + self._use_correct_mercurial() log( 'running %d tests using %d parallel processes' @@ -3346,6 +3516,19 @@ if failed: return 1 + def _geterrpath(self, test): + # test['path'] is a relative path + if 'case' in test: + # for multiple dimensions test cases + casestr = b'#'.join(test['case']) + errpath = b'%s#%s.err' % (test['path'], casestr) + else: + errpath = b'%s.err' % test['path'] + if self.options.outputdir: + self._outputdir = canonpath(_sys2bytes(self.options.outputdir)) + errpath = os.path.join(self._outputdir, errpath) + return errpath + def _getport(self, count): port = self._ports.get(count) # do we have a cached entry? if port is None: @@ -3421,43 +3604,101 @@ def _usecorrectpython(self): """Configure the environment to use the appropriate Python in tests.""" # Tests must use the same interpreter as us or bad things will happen. - pyexename = sys.platform == 'win32' and b'python.exe' or b'python' + if WINDOWS and PYTHON3: + pyexe_names = [b'python', b'python3', b'python.exe'] + elif WINDOWS: + pyexe_names = [b'python', b'python.exe'] + elif PYTHON3: + pyexe_names = [b'python', b'python3'] + else: + pyexe_names = [b'python', b'python2'] # os.symlink() is a thing with py3 on Windows, but it requires # Administrator rights. - if getattr(os, 'symlink', None) and os.name != 'nt': - vlog( - "# Making python executable in test path a symlink to '%s'" - % sysexecutable - ) - mypython = os.path.join(self._tmpbindir, pyexename) - try: - if os.readlink(mypython) == sysexecutable: - return - os.unlink(mypython) - except OSError as err: - if err.errno != errno.ENOENT: - raise - if self._findprogram(pyexename) != sysexecutable: + if not WINDOWS and getattr(os, 'symlink', None): + msg = "# Making python executable in test path a symlink to '%s'" + msg %= sysexecutable + vlog(msg) + for pyexename in pyexe_names: + mypython = os.path.join(self._custom_bin_dir, pyexename) try: - os.symlink(sysexecutable, mypython) - self._createdfiles.append(mypython) + if os.readlink(mypython) == sysexecutable: + continue + os.unlink(mypython) except OSError as err: - # child processes may race, which is harmless - if err.errno != errno.EEXIST: + if err.errno != errno.ENOENT: raise + if self._findprogram(pyexename) != sysexecutable: + try: + os.symlink(sysexecutable, mypython) + self._createdfiles.append(mypython) + except OSError as err: + # child processes may race, which is harmless + if err.errno != errno.EEXIST: + raise + elif WINDOWS and not os.getenv('MSYSTEM'): + raise AssertionError('cannot run test on Windows without MSYSTEM') else: - exedir, exename = os.path.split(sysexecutable) - vlog( - "# Modifying search path to find %s as %s in '%s'" - % (exename, pyexename, exedir) - ) - path = os.environ['PATH'].split(os.pathsep) - while exedir in path: - path.remove(exedir) - os.environ['PATH'] = os.pathsep.join([exedir] + path) - if not self._findprogram(pyexename): - print("WARNING: Cannot find %s in search path" % pyexename) + # Generate explicit file instead of symlink + # + # This is especially important as Windows doesn't have + # `python3.exe`, and MSYS cannot understand the reparse point with + # that name provided by Microsoft. Create a simple script on PATH + # with that name that delegates to the py3 launcher so the shebang + # lines work. + esc_executable = _sys2bytes(shellquote(sysexecutable)) + for pyexename in pyexe_names: + stub_exec_path = os.path.join(self._custom_bin_dir, pyexename) + with open(stub_exec_path, 'wb') as f: + f.write(b'#!/bin/sh\n') + f.write(b'%s "$@"\n' % esc_executable) + + if WINDOWS: + if not PYTHON3: + # lets try to build a valid python3 executable for the + # scrip that requires it. + py3exe_name = os.path.join(self._custom_bin_dir, b'python3') + with open(py3exe_name, 'wb') as f: + f.write(b'#!/bin/sh\n') + f.write(b'py -3 "$@"\n') + + # adjust the path to make sur the main python finds it own dll + path = os.environ['PATH'].split(os.pathsep) + main_exec_dir = os.path.dirname(sysexecutable) + extra_paths = [_bytes2sys(self._custom_bin_dir), main_exec_dir] + + # Binaries installed by pip into the user area like pylint.exe may + # not be in PATH by default. + appdata = os.environ.get('APPDATA') + vi = sys.version_info + if appdata is not None: + python_dir = 'Python%d%d' % (vi[0], vi[1]) + scripts_path = [appdata, 'Python', python_dir, 'Scripts'] + if not PYTHON3: + scripts_path = [appdata, 'Python', 'Scripts'] + scripts_dir = os.path.join(*scripts_path) + extra_paths.append(scripts_dir) + + os.environ['PATH'] = os.pathsep.join(extra_paths + path) + + def _use_correct_mercurial(self): + target_exec = os.path.join(self._custom_bin_dir, b'hg') + if self._hgcommand != b'hg': + # shutil.which only accept bytes from 3.8 + real_exec = which(self._hgcommand) + if real_exec is None: + raise ValueError('could not find exec path for "%s"', real_exec) + if real_exec == target_exec: + # do not overwrite something with itself + return + if WINDOWS: + with open(target_exec, 'wb') as f: + f.write(b'#!/bin/sh\n') + escaped_exec = shellquote(_bytes2sys(real_exec)) + f.write(b'%s "$@"\n' % _sys2bytes(escaped_exec)) + else: + os.symlink(real_exec, target_exec) + self._createdfiles.append(target_exec) def _installhg(self): """Install hg into the test environment. @@ -3488,7 +3729,7 @@ self._hgroot = hgroot os.chdir(hgroot) nohome = b'--home=""' - if os.name == 'nt': + if WINDOWS: # The --home="" trick works only on OS where os.sep == '/' # because of a distutils convert_path() fast-path. Avoid it at # least on Windows for now, deal with .pydistutils.cfg bugs @@ -3542,8 +3783,6 @@ sys.exit(1) os.chdir(self._testdir) - self._usecorrectpython() - hgbat = os.path.join(self._bindir, b'hg.bat') if os.path.isfile(hgbat): # hg.bat expects to be put in bin/scripts while run-tests.py @@ -3582,9 +3821,7 @@ def _checkhglib(self, verb): """Ensure that the 'mercurial' package imported by python is the one we expect it to be. If not, print a warning to stderr.""" - if (self._bindir == self._pythondir) and ( - self._bindir != self._tmpbindir - ): + if self._pythondir_inferred: # The pythondir has been inferred from --with-hg flag. # We cannot expect anything sensible here. return @@ -3641,6 +3878,64 @@ sys.stdout.write(out) sys.exit(1) + def _installrhg(self): + """Install rhg into the test environment""" + vlog('# Performing temporary installation of rhg') + assert os.path.dirname(self._bindir) == self._installdir + assert self._hgroot, 'must be called after _installhg()' + cmd = b'"%(make)s" install-rhg PREFIX="%(prefix)s"' % { + b'make': b'make', # TODO: switch by option or environment? + b'prefix': self._installdir, + } + cwd = self._hgroot + vlog("# Running", cmd) + proc = subprocess.Popen( + cmd, + shell=True, + cwd=cwd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + out, _err = proc.communicate() + if proc.returncode != 0: + if PYTHON3: + sys.stdout.buffer.write(out) + else: + sys.stdout.write(out) + sys.exit(1) + + def _build_pyoxidized(self): + """build a pyoxidized version of mercurial into the test environment + + Ideally this function would be `install_pyoxidier` and would both build + and install pyoxidier. However we are starting small to get pyoxidizer + build binary to testing quickly. + """ + vlog('# build a pyoxidized version of Mercurial') + assert os.path.dirname(self._bindir) == self._installdir + assert self._hgroot, 'must be called after _installhg()' + cmd = b'"%(make)s" pyoxidizer-windows-tests' % { + b'make': b'make', + } + cwd = self._hgroot + vlog("# Running", cmd) + proc = subprocess.Popen( + _bytes2sys(cmd), + shell=True, + cwd=_bytes2sys(cwd), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + out, _err = proc.communicate() + if proc.returncode != 0: + if PYTHON3: + sys.stdout.buffer.write(out) + else: + sys.stdout.write(out) + sys.exit(1) + def _outputcoverage(self): """Produce code coverage output.""" import coverage @@ -3680,14 +3975,14 @@ sepb = _sys2bytes(os.pathsep) for p in osenvironb.get(b'PATH', dpb).split(sepb): name = os.path.join(p, program) - if os.name == 'nt' or os.access(name, os.X_OK): + if WINDOWS or os.access(name, os.X_OK): return _bytes2sys(name) return None def _checktools(self): """Ensure tools required to run tests are present.""" for p in self.REQUIREDTOOLS: - if os.name == 'nt' and not p.endswith(b'.exe'): + if WINDOWS and not p.endswith(b'.exe'): p += b'.exe' found = self._findprogram(p) p = p.decode("utf-8") @@ -3754,6 +4049,15 @@ if __name__ == '__main__': + if WINDOWS and not os.getenv('MSYSTEM'): + print('cannot run test on Windows without MSYSTEM', file=sys.stderr) + print( + '(if you need to do so contact the mercurial devs: ' + 'mercurial@mercurial-scm.org)', + file=sys.stderr, + ) + sys.exit(255) + runner = TestRunner() try: diff --git a/tests/sitecustomize.py b/tests/sitecustomize.py new file mode 100644 --- /dev/null +++ b/tests/sitecustomize.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import +import os + +if os.environ.get('COVERAGE_PROCESS_START'): + try: + import coverage + import uuid + + covpath = os.path.join( + os.environ['COVERAGE_DIR'], 'cov.%s' % uuid.uuid1() + ) + cov = coverage.coverage(data_file=covpath, auto_data=True) + cov._warn_no_data = False + cov._warn_unimported_source = False + cov.start() + except ImportError: + pass diff --git a/tests/test-ambiguousprefix.t b/tests/test-ambiguousprefix.t --- a/tests/test-ambiguousprefix.t +++ b/tests/test-ambiguousprefix.t @@ -31,4 +31,4 @@ $ hg log -r 'gitnode(7e)' abort: git-mapfile@7e: ambiguous identifier!? (re) - [255] + [50] diff --git a/tests/test-doctest.py b/tests/test-doctest.py --- a/tests/test-doctest.py +++ b/tests/test-doctest.py @@ -1,29 +1,41 @@ # this is hack to make sure no escape characters are inserted into the output -from __future__ import generator_stop +from __future__ import absolute_import +from __future__ import print_function import doctest import os import re +import subprocess import sys # add hggit/ to sys.path sys.path.insert(0, os.path.join(os.environ["TESTDIR"], "..")) +ispy3 = sys.version_info[0] >= 3 + if 'TERM' in os.environ: del os.environ['TERM'] + class py3docchecker(doctest.OutputChecker): def check_output(self, want, got, optionflags): want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u'' got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b'' # py3: <exc.name>: b'<msg>' -> <name>: <msg> # <exc.name>: <others> -> <name>: <others> - got2 = re.sub(r'''^hggit\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3', - got2, re.MULTILINE) - got2 = re.sub(r'^hggit\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE) - return any(doctest.OutputChecker.check_output(self, w, g, optionflags) - for w, g in [(want, got), (want2, got2)]) + got2 = re.sub( + r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', + r'\1: \3', + got2, + re.MULTILINE, + ) + got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE) + return any( + doctest.OutputChecker.check_output(self, w, g, optionflags) + for w, g in [(want, got), (want2, got2)] + ) + def testmod(name, optionflags=0, testtarget=None): __import__(name) @@ -33,13 +45,90 @@ # minimal copy of doctest.testmod() finder = doctest.DocTestFinder() - checker = py3docchecker() + checker = None + if ispy3: + checker = py3docchecker() runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags) for test in finder.find(mod, name): runner.run(test) runner.summarize() -testmod('hggit.compat') -testmod('hggit.hg2git') -testmod('hggit.util') -testmod('hggit.git_handler') + +DONT_RUN = [] + +# Exceptions to the defaults for a given detected module. The value for each +# module name is a list of dicts that specify the kwargs to pass to testmod. +# testmod is called once per item in the list, so an empty list will cause the +# module to not be tested. +testmod_arg_overrides = {} + +fileset = 'set:(**.py)' + +cwd = os.path.dirname(os.environ["TESTDIR"]) + +if not os.path.isdir(os.path.join(cwd, ".hg")): + sys.exit(0) + +files = subprocess.check_output( + "hg files --print0 \"%s\"" % fileset, + shell=True, + cwd=cwd, + stderr=subprocess.DEVNULL, +).split(b'\0') + +if sys.version_info[0] >= 3: + cwd = os.fsencode(cwd) + +mods_tested = set() +for f in files: + if not f: + continue + + with open(os.path.join(cwd, f), "rb") as fh: + if not re.search(br'\n\s*>>>', fh.read()): + continue + + if ispy3: + f = f.decode() + + modname = f.replace('.py', '').replace('\\', '.').replace('/', '.') + + # Third-party modules aren't our responsibility to test, and the modules in + # contrib generally do not have doctests in a good state, plus they're hard + # to import if this test is running with py2, so we just skip both for now. + if modname.startswith('mercurial.thirdparty.') or modname.startswith( + 'contrib.' + ): + continue + + for kwargs in testmod_arg_overrides.get(modname, [{}]): + mods_tested.add((modname, '%r' % (kwargs,))) + if modname.startswith('tests.'): + # On py2, we can't import from tests.foo, but it works on both py2 + # and py3 with the way that PYTHONPATH is setup to import without + # the 'tests.' prefix, so we do that. + modname = modname[len('tests.') :] + + testmod(modname, **kwargs) + +# Meta-test: let's make sure that we actually ran what we expected to, above. +# Each item in the set is a 2-tuple of module name and stringified kwargs passed +# to testmod. +expected_mods_tested = set( + [ + ('hggit.git_handler', '{}'), + ('hggit.util', '{}'), + ] +) + +unexpectedly_run = mods_tested.difference(expected_mods_tested) +not_run = expected_mods_tested.difference(mods_tested) + +if unexpectedly_run: + print('Unexpectedly ran (probably need to add to list):') + for r in sorted(unexpectedly_run): + print(' %r' % (r,)) +if not_run: + print('Expected to run, but was not run (doctest removed?):') + for r in sorted(not_run): + print(' %r' % (r,)) diff --git a/tests/test-git-tags.t b/tests/test-git-tags.t --- a/tests/test-git-tags.t +++ b/tests/test-git-tags.t @@ -59,7 +59,7 @@ [255] $ hg tag --git beta --remove -r 0 abort: cannot specify both --rev and --remove - [255] + [10] $ hg tag --git alpha abort: git tags require an explicit revision (please specify -r/--rev) @@ -69,16 +69,16 @@ [255] $ hg tag --git alpha -r 0 -e abort: cannot specify both --git and --edit - [255] + [10] $ hg tag --git alpha -r 0 -m 42 abort: cannot specify both --git and --message - [255] + [10] $ hg tag --git alpha -r 0 -d 42 abort: cannot specify both --git and --date - [255] + [10] $ hg tag --git alpha -r 0 -u user@example.com abort: cannot specify both --git and --user - [255] + [10] $ hg tag --git 'with space' -r 0 abort: the name 'with space' is not a valid git tag [255] @@ -90,7 +90,7 @@ [255] $ hg tag --git tip -r 0 abort: the name 'tip' is reserved - [255] + [10] Create a git tag from hg @@ -250,7 +250,7 @@ #endif $ hg tag beta abort: tag 'beta' already exists (use -f to force) - [255] + [10] $ hg tag -f beta #if secret $ hg phase -d diff --git a/tests/test-keywords.t b/tests/test-keywords.t --- a/tests/test-keywords.t +++ b/tests/test-keywords.t @@ -46,5 +46,5 @@ gitnode_existsB 0 $ hg log --rev "gitnode(7e)" abort: git-mapfile@7e: ambiguous identifier!? (re) - [255] + [50] $ hg log --template "gitnode_notexists {rev}\n" --rev "gitnode(1234567890ab)" diff --git a/tests/test-transactions.t b/tests/test-transactions.t --- a/tests/test-transactions.t +++ b/tests/test-transactions.t @@ -71,10 +71,10 @@ $ git init -q hgrepo $ hg clone gitrepo --config git.intree=yes hgrepo abort: destination 'hgrepo' is not empty - [255] + [10] $ hg clone $TESTTMP/gitrepo --config git.intree=yes --cwd hgrepo . abort: destination '.' is not empty - [255] + [10] $ rm -rf hgrepo Map saving