diff -r 57875cf423c9 -r 2372284d9457 tests/run-tests.py --- a/tests/run-tests.py Sat Oct 05 10:29:34 2019 -0400 +++ b/tests/run-tests.py Sun Oct 06 09:45:02 2019 -0400 @@ -75,9 +75,11 @@ try: import shlex + shellquote = shlex.quote except (ImportError, AttributeError): import pipes + shellquote = pipes.quote processlock = threading.Lock() @@ -85,13 +87,14 @@ pygmentspresent = False # ANSI color is unsupported prior to Windows 10 if os.name != 'nt': - try: # is pygments installed + 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() @@ -99,6 +102,7 @@ pass if pygmentspresent: + class TestRunnerStyle(style.Style): default_style = "" skipped = token.string_to_tokentype("Token.Generic.Skipped") @@ -106,10 +110,10 @@ skippedname = token.string_to_tokentype("Token.Generic.SName") failedname = token.string_to_tokentype("Token.Generic.FName") styles = { - skipped: '#e5e5e5', - skippedname: '#00ffff', - failed: '#7f0000', - failedname: '#ff0000', + skipped: '#e5e5e5', + skippedname: '#00ffff', + failed: '#7f0000', + failedname: '#ff0000', } class TestRunnerLexer(lexer.RegexLexer): @@ -127,7 +131,7 @@ 'failed': [ (testpattern, token.Generic.FName), (r'(:| ).*', token.Generic.Failed), - ] + ], } runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle) @@ -137,7 +141,8 @@ if sys.version_info > (3, 5, 0): PYTHON3 = True - xrange = range # we use xrange in one place, and we'd rather not use range + xrange = range # we use xrange in one place, and we'd rather not use range + def _bytespath(p): if p is None: return p @@ -158,20 +163,27 @@ self.__len__ = strenv.__len__ self.clear = strenv.clear self._strenv = strenv + def __getitem__(self, k): v = self._strenv.__getitem__(_strpath(k)) return _bytespath(v) + def __setitem__(self, k, v): self._strenv.__setitem__(_strpath(k), _strpath(v)) + def __delitem__(self, k): self._strenv.__delitem__(_strpath(k)) + def __contains__(self, k): return self._strenv.__contains__(_strpath(k)) + def __iter__(self): return iter([_bytespath(k) for k in iter(self._strenv)]) + def get(self, k, default=None): v = self._strenv.get(_strpath(k), _strpath(default)) return _bytespath(v) + def pop(self, k, default=None): v = self._strenv.pop(_strpath(k), _strpath(default)) return _bytespath(v) @@ -183,9 +195,11 @@ getcwdb = lambda: _bytespath(os.getcwd()) elif sys.version_info >= (3, 0, 0): - print('%s is only supported on Python 3.5+ and 2.7, not %s' % - (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))) - sys.exit(70) # EX_SOFTWARE from `man 3 sysexit` + print( + '%s is only supported on Python 3.5+ and 2.7, not %s' + % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])) + ) + sys.exit(70) # EX_SOFTWARE from `man 3 sysexit` else: PYTHON3 = False @@ -228,9 +242,11 @@ else: return False + # useipv6 will be set by parseargs useipv6 = None + def checkportisavailable(port): """return true if a port seems free to bind on localhost""" if useipv6: @@ -243,19 +259,31 @@ s.close() return True except socket.error as exc: - if exc.errno not in (errno.EADDRINUSE, errno.EADDRNOTAVAIL, - errno.EPROTONOSUPPORT): + if exc.errno not in ( + errno.EADDRINUSE, + errno.EADDRNOTAVAIL, + errno.EPROTONOSUPPORT, + ): raise return False + closefds = os.name == 'posix' + + def Popen4(cmd, wd, timeout, env=None): processlock.acquire() - p = subprocess.Popen(_strpath(cmd), shell=True, bufsize=-1, - cwd=_strpath(wd), env=env, - close_fds=closefds, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + p = subprocess.Popen( + _strpath(cmd), + shell=True, + bufsize=-1, + cwd=_strpath(wd), + env=env, + close_fds=closefds, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) processlock.release() p.fromchild = p.stdout @@ -264,17 +292,20 @@ p.timeout = False if timeout: + def t(): start = time.time() while time.time() - start < timeout and p.returncode is None: - time.sleep(.1) + time.sleep(0.1) p.timeout = True if p.returncode is None: terminate(p) + threading.Thread(target=t).start() return p + if sys.executable: sysexecutable = sys.executable elif os.environ.get('PYTHONEXECUTABLE'): @@ -297,9 +328,11 @@ 'shell': ('HGTEST_SHELL', 'sh'), } + def canonpath(path): return os.path.realpath(os.path.expanduser(path)) + def parselistfiles(files, listtype, warn=True): entries = dict() for filename in files: @@ -321,6 +354,7 @@ f.close() return entries + def parsettestcases(path): """read a .t test file, return a set of test case names @@ -337,131 +371,262 @@ raise return cases + def getparser(): """Obtain the OptionParser used by the CLI.""" parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]') selection = parser.add_argument_group('Test Selection') - selection.add_argument('--allow-slow-tests', action='store_true', - help='allow extremely slow tests') - selection.add_argument("--blacklist", action="append", - help="skip tests listed in the specified blacklist file") - selection.add_argument("--changed", - help="run tests that are changed in parent rev or working directory") - selection.add_argument("-k", "--keywords", - help="run tests matching keywords") - selection.add_argument("-r", "--retest", action="store_true", - help = "retest failed tests") - selection.add_argument("--test-list", action="append", - help="read tests to run from the specified file") - selection.add_argument("--whitelist", action="append", - help="always run tests listed in the specified whitelist file") - selection.add_argument('tests', metavar='TESTS', nargs='*', - help='Tests to run') + selection.add_argument( + '--allow-slow-tests', + action='store_true', + help='allow extremely slow tests', + ) + selection.add_argument( + "--blacklist", + action="append", + help="skip tests listed in the specified blacklist file", + ) + selection.add_argument( + "--changed", + help="run tests that are changed in parent rev or working directory", + ) + selection.add_argument( + "-k", "--keywords", help="run tests matching keywords" + ) + selection.add_argument( + "-r", "--retest", action="store_true", help="retest failed tests" + ) + selection.add_argument( + "--test-list", + action="append", + help="read tests to run from the specified file", + ) + selection.add_argument( + "--whitelist", + action="append", + help="always run tests listed in the specified whitelist file", + ) + selection.add_argument( + 'tests', metavar='TESTS', nargs='*', help='Tests to run' + ) harness = parser.add_argument_group('Test Harness Behavior') - harness.add_argument('--bisect-repo', - metavar='bisect_repo', - help=("Path of a repo to bisect. Use together with " - "--known-good-rev")) - harness.add_argument("-d", "--debug", action="store_true", + harness.add_argument( + '--bisect-repo', + metavar='bisect_repo', + help=( + "Path of a repo to bisect. Use together with " "--known-good-rev" + ), + ) + harness.add_argument( + "-d", + "--debug", + action="store_true", help="debug mode: write output of test scripts to console" - " rather than capturing and diffing it (disables timeout)") - harness.add_argument("-f", "--first", action="store_true", - help="exit on the first test failure") - harness.add_argument("-i", "--interactive", action="store_true", - help="prompt to accept changed output") - harness.add_argument("-j", "--jobs", type=int, + " rather than capturing and diffing it (disables timeout)", + ) + harness.add_argument( + "-f", + "--first", + action="store_true", + help="exit on the first test failure", + ) + harness.add_argument( + "-i", + "--interactive", + action="store_true", + help="prompt to accept changed output", + ) + harness.add_argument( + "-j", + "--jobs", + type=int, help="number of jobs to run in parallel" - " (default: $%s or %d)" % defaults['jobs']) - harness.add_argument("--keep-tmpdir", action="store_true", - help="keep temporary directory after running tests") - harness.add_argument('--known-good-rev', - metavar="known_good_rev", - help=("Automatically bisect any failures using this " - "revision as a known-good revision.")) - harness.add_argument("--list-tests", action="store_true", - help="list tests instead of running them") - harness.add_argument("--loop", action="store_true", - help="loop tests repeatedly") - harness.add_argument('--random', action="store_true", - help='run tests in random order') - harness.add_argument('--order-by-runtime', action="store_true", - help='run slowest tests first, according to .testtimes') - harness.add_argument("-p", "--port", type=int, + " (default: $%s or %d)" % defaults['jobs'], + ) + harness.add_argument( + "--keep-tmpdir", + action="store_true", + help="keep temporary directory after running tests", + ) + harness.add_argument( + '--known-good-rev', + metavar="known_good_rev", + help=( + "Automatically bisect any failures using this " + "revision as a known-good revision." + ), + ) + harness.add_argument( + "--list-tests", + action="store_true", + help="list tests instead of running them", + ) + harness.add_argument( + "--loop", action="store_true", help="loop tests repeatedly" + ) + harness.add_argument( + '--random', action="store_true", help='run tests in random order' + ) + harness.add_argument( + '--order-by-runtime', + action="store_true", + help='run slowest tests first, according to .testtimes', + ) + harness.add_argument( + "-p", + "--port", + type=int, help="port on which servers should listen" - " (default: $%s or %d)" % defaults['port']) - harness.add_argument('--profile-runner', action='store_true', - help='run statprof on run-tests') - harness.add_argument("-R", "--restart", action="store_true", - help="restart at last error") - harness.add_argument("--runs-per-test", type=int, dest="runs_per_test", - help="run each test N times (default=1)", default=1) - harness.add_argument("--shell", - help="shell to use (default: $%s or %s)" % defaults['shell']) - harness.add_argument('--showchannels', action='store_true', - help='show scheduling channels') - harness.add_argument("--slowtimeout", type=int, + " (default: $%s or %d)" % defaults['port'], + ) + harness.add_argument( + '--profile-runner', + action='store_true', + help='run statprof on run-tests', + ) + harness.add_argument( + "-R", "--restart", action="store_true", help="restart at last error" + ) + harness.add_argument( + "--runs-per-test", + type=int, + dest="runs_per_test", + help="run each test N times (default=1)", + default=1, + ) + harness.add_argument( + "--shell", help="shell to use (default: $%s or %s)" % defaults['shell'] + ) + harness.add_argument( + '--showchannels', action='store_true', help='show scheduling channels' + ) + harness.add_argument( + "--slowtimeout", + type=int, help="kill errant slow tests after SLOWTIMEOUT seconds" - " (default: $%s or %d)" % defaults['slowtimeout']) - harness.add_argument("-t", "--timeout", type=int, + " (default: $%s or %d)" % defaults['slowtimeout'], + ) + harness.add_argument( + "-t", + "--timeout", + type=int, help="kill errant tests after TIMEOUT seconds" - " (default: $%s or %d)" % defaults['timeout']) - harness.add_argument("--tmpdir", + " (default: $%s or %d)" % defaults['timeout'], + ) + harness.add_argument( + "--tmpdir", help="run tests in the given temporary directory" - " (implies --keep-tmpdir)") - harness.add_argument("-v", "--verbose", action="store_true", - help="output verbose messages") + " (implies --keep-tmpdir)", + ) + harness.add_argument( + "-v", "--verbose", action="store_true", help="output verbose messages" + ) hgconf = parser.add_argument_group('Mercurial Configuration') - hgconf.add_argument("--chg", action="store_true", - help="install and use chg wrapper in place of hg") - hgconf.add_argument("--compiler", - help="compiler to build with") - hgconf.add_argument('--extra-config-opt', action="append", default=[], - help='set the given config opt in the test hgrc') - hgconf.add_argument("-l", "--local", action="store_true", + hgconf.add_argument( + "--chg", + action="store_true", + help="install and use chg wrapper in place of hg", + ) + hgconf.add_argument("--compiler", help="compiler to build with") + hgconf.add_argument( + '--extra-config-opt', + action="append", + default=[], + help='set the given config opt in the test hgrc', + ) + hgconf.add_argument( + "-l", + "--local", + action="store_true", help="shortcut for --with-hg=/../hg, " - "and --with-chg=/../contrib/chg/chg if --chg is set") - hgconf.add_argument("--ipv6", action="store_true", - help="prefer IPv6 to IPv4 for network related tests") - hgconf.add_argument("--pure", action="store_true", - help="use pure Python code instead of C extensions") - hgconf.add_argument("-3", "--py3-warnings", action="store_true", - help="enable Py3k warnings on Python 2.7+") - hgconf.add_argument("--with-chg", metavar="CHG", - help="use specified chg wrapper in place of hg") - hgconf.add_argument("--with-hg", + "and --with-chg=/../contrib/chg/chg if --chg is set", + ) + hgconf.add_argument( + "--ipv6", + action="store_true", + help="prefer IPv6 to IPv4 for network related tests", + ) + hgconf.add_argument( + "--pure", + action="store_true", + help="use pure Python code instead of C extensions", + ) + hgconf.add_argument( + "-3", + "--py3-warnings", + action="store_true", + help="enable Py3k warnings on Python 2.7+", + ) + hgconf.add_argument( + "--with-chg", + metavar="CHG", + help="use specified chg wrapper in place of hg", + ) + hgconf.add_argument( + "--with-hg", metavar="HG", help="test using specified hg script rather than a " - "temporary installation") + "temporary installation", + ) reporting = parser.add_argument_group('Results Reporting') - reporting.add_argument("-C", "--annotate", action="store_true", - help="output files annotated with coverage") - reporting.add_argument("--color", choices=["always", "auto", "never"], + reporting.add_argument( + "-C", + "--annotate", + action="store_true", + help="output files annotated with coverage", + ) + reporting.add_argument( + "--color", + choices=["always", "auto", "never"], default=os.environ.get('HGRUNTESTSCOLOR', 'auto'), - help="colorisation: always|auto|never (default: auto)") - reporting.add_argument("-c", "--cover", action="store_true", - help="print a test coverage report") - reporting.add_argument('--exceptions', action='store_true', - help='log all exceptions and generate an exception report') - reporting.add_argument("-H", "--htmlcov", action="store_true", - help="create an HTML report of the coverage of the files") - reporting.add_argument("--json", action="store_true", - help="store test result data in 'report.json' file") - reporting.add_argument("--outputdir", - help="directory to write error logs to (default=test directory)") - reporting.add_argument("-n", "--nodiff", action="store_true", - help="skip showing test changes") - reporting.add_argument("-S", "--noskips", action="store_true", - help="don't report skip tests verbosely") - reporting.add_argument("--time", action="store_true", - help="time how long each test takes") - reporting.add_argument("--view", - help="external diff viewer") - reporting.add_argument("--xunit", - help="record xunit results at specified path") + help="colorisation: always|auto|never (default: auto)", + ) + reporting.add_argument( + "-c", + "--cover", + action="store_true", + help="print a test coverage report", + ) + reporting.add_argument( + '--exceptions', + action='store_true', + help='log all exceptions and generate an exception report', + ) + reporting.add_argument( + "-H", + "--htmlcov", + action="store_true", + help="create an HTML report of the coverage of the files", + ) + reporting.add_argument( + "--json", + action="store_true", + help="store test result data in 'report.json' file", + ) + reporting.add_argument( + "--outputdir", + help="directory to write error logs to (default=test directory)", + ) + reporting.add_argument( + "-n", "--nodiff", action="store_true", help="skip showing test changes" + ) + reporting.add_argument( + "-S", + "--noskips", + action="store_true", + help="don't report skip tests verbosely", + ) + reporting.add_argument( + "--time", action="store_true", help="time how long each test takes" + ) + reporting.add_argument("--view", help="external diff viewer") + reporting.add_argument( + "--xunit", help="record xunit results at specified path" + ) for option, (envvar, default) in defaults.items(): defaults[option] = type(default)(os.environ.get(envvar, default)) @@ -469,6 +634,7 @@ return parser + def parseargs(args, parser): """Parse arguments with our OptionParser and validate results.""" options = parser.parse_args(args) @@ -488,14 +654,18 @@ for relpath, attr in pathandattrs: binpath = os.path.join(reporootdir, relpath) if os.name != 'nt' and not os.access(binpath, os.X_OK): - parser.error('--local specified, but %r not found or ' - 'not executable' % binpath) + parser.error( + '--local specified, but %r not found or ' + 'not executable' % binpath + ) setattr(options, attr, _strpath(binpath)) if options.with_hg: options.with_hg = canonpath(_bytespath(options.with_hg)) - if not (os.path.isfile(options.with_hg) and - os.access(options.with_hg, os.X_OK)): + if not ( + os.path.isfile(options.with_hg) + and os.access(options.with_hg, os.X_OK) + ): 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') @@ -506,17 +676,23 @@ if options.with_chg: options.chg = False # no installation to temporary location options.with_chg = canonpath(_bytespath(options.with_chg)) - if not (os.path.isfile(options.with_chg) and - os.access(options.with_chg, os.X_OK)): + if not ( + os.path.isfile(options.with_chg) + and os.access(options.with_chg, os.X_OK) + ): parser.error('--with-chg must specify a chg 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)') + parser.error( + '--chg does not work when --with-hg is specified ' + '(use --with-chg instead)' + ) if options.color == 'always' and not pygmentspresent: - sys.stderr.write('warning: --color=always ignored because ' - 'pygments is not installed\n') + sys.stderr.write( + 'warning: --color=always ignored because ' + 'pygments is not installed\n' + ) if options.bisect_repo and not options.known_good_rev: parser.error("--bisect-repo cannot be used without --known-good-rev") @@ -526,13 +702,15 @@ useipv6 = checksocketfamily('AF_INET6') else: # only use IPv6 if IPv4 is unavailable and IPv6 is available - useipv6 = ((not checksocketfamily('AF_INET')) - and checksocketfamily('AF_INET6')) + useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily( + 'AF_INET6' + ) options.anycoverage = options.cover or options.annotate or options.htmlcov if options.anycoverage: try: import coverage + covver = version.StrictVersion(coverage.__version__).version if covver < (3, 3): parser.error('coverage options require coverage 3.3 or later') @@ -541,12 +719,14 @@ if options.anycoverage and options.local: # this needs some path mangling somewhere, I guess - parser.error("sorry, coverage options do not work when --local " - "is specified") + parser.error( + "sorry, coverage options do not work when --local " "is specified" + ) if options.anycoverage and options.with_hg: - parser.error("sorry, coverage options do not work when --with-hg " - "is specified") + parser.error( + "sorry, coverage options do not work when --with-hg " "is specified" + ) global verbose if options.verbose: @@ -561,17 +741,16 @@ parser.error("-i/--interactive and -d/--debug are incompatible") if options.debug: if options.timeout != defaults['timeout']: - sys.stderr.write( - 'warning: --timeout option ignored with --debug\n') + sys.stderr.write('warning: --timeout option ignored with --debug\n') if options.slowtimeout != defaults['slowtimeout']: sys.stderr.write( - 'warning: --slowtimeout option ignored with --debug\n') + 'warning: --slowtimeout option ignored with --debug\n' + ) options.timeout = 0 options.slowtimeout = 0 if options.py3_warnings: if PYTHON3: - parser.error( - '--py3-warnings can only be used on Python 2.7') + parser.error('--py3-warnings can only be used on Python 2.7') if options.blacklist: options.blacklist = parselistfiles(options.blacklist, 'blacklist') @@ -585,6 +764,7 @@ return options + def rename(src, dst): """Like os.rename(), trade atomicity and opened files friendliness for existing destination support. @@ -592,6 +772,7 @@ shutil.copy(src, dst) os.remove(src) + def makecleanable(path): """Try to fix directory permission recursively so that the entire tree can be deleted""" @@ -603,11 +784,14 @@ except OSError: pass + _unified_diff = difflib.unified_diff if PYTHON3: import functools + _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff) + def getdiff(expected, output, ref, err): servefail = False lines = [] @@ -618,12 +802,16 @@ line = line[:-2] + b'\n' lines.append(line) if not servefail and line.startswith( - b'+ abort: child process failed to start'): + b'+ abort: child process failed to start' + ): servefail = True return servefail, lines + verbose = False + + def vlog(*msg): """Log only when in verbose mode.""" if verbose is False: @@ -631,6 +819,7 @@ return log(*msg) + # Bytes that break XML even in a CDATA block: control characters 0-31 # sans \t, \n and \r CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]") @@ -641,6 +830,7 @@ # output..output (feature !)\n optline = re.compile(br'(.*) \((.+?) !\)\n$') + def cdatasafe(data): """Make a string safe to include in a CDATA block. @@ -651,6 +841,7 @@ """ return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>') + def log(*msg): """Log something to stdout. @@ -664,12 +855,15 @@ print() sys.stdout.flush() + def highlightdiff(line, color): if not color: return line assert pygmentspresent - return pygments.highlight(line.decode('latin1'), difflexer, - terminal256formatter).encode('latin1') + return pygments.highlight( + line.decode('latin1'), difflexer, terminal256formatter + ).encode('latin1') + def highlightmsg(msg, color): if not color: @@ -677,6 +871,7 @@ assert pygmentspresent return pygments.highlight(msg, runnerlexer, runnerformatter) + def terminate(proc): """Terminate subprocess""" vlog('# Terminating process %d' % proc.pid) @@ -685,10 +880,12 @@ except OSError: pass + def killdaemons(pidfile): import killdaemons as killmod - return killmod.killdaemons(pidfile, tryhard=False, remove=True, - logfn=vlog) + + return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog) + class Test(unittest.TestCase): """Encapsulates a single, runnable test. @@ -701,14 +898,24 @@ # Status code reserved for skipped tests (used by hghave). SKIPPED_STATUS = 80 - def __init__(self, path, outputdir, tmpdir, keeptmpdir=False, - debug=False, - first=False, - timeout=None, - startport=None, extraconfigopts=None, - py3warnings=False, shell=None, hgcommand=None, - slowtimeout=None, usechg=False, - useipv6=False): + def __init__( + self, + path, + outputdir, + tmpdir, + keeptmpdir=False, + debug=False, + first=False, + timeout=None, + startport=None, + extraconfigopts=None, + py3warnings=False, + shell=None, + hgcommand=None, + slowtimeout=None, + usechg=False, + useipv6=False, + ): """Create a test from parameters. path is the full path to the file defining the test. @@ -783,7 +990,7 @@ # If we're not in --debug mode and reference output file exists, # check test output against it. if self._debug: - return None # to match "out is None" + return None # to match "out is None" elif os.path.exists(self.refpath): with open(self.refpath, 'rb') as f: return f.read().splitlines(True) @@ -830,8 +1037,9 @@ raise if self._usechg: - self._chgsockdir = os.path.join(self._threadtmp, - b'%s.chgsock' % name) + self._chgsockdir = os.path.join( + self._threadtmp, b'%s.chgsock' % name + ) os.mkdir(self._chgsockdir) def run(self, result): @@ -914,7 +1122,7 @@ self._skipped = False if ret == self.SKIPPED_STATUS: - if out is None: # Debug mode, nothing to parse. + if out is None: # Debug mode, nothing to parse. missing = ['unknown'] failed = None else: @@ -934,8 +1142,11 @@ self.fail('no result code from test') elif out != self._refout: # Diff generation may rely on written .err file. - if ((ret != 0 or out != self._refout) and not self._skipped - and not self._debug): + if ( + (ret != 0 or out != self._refout) + and not self._skipped + and not self._debug + ): with open(self.errpath, 'wb') as f: for line in out: f.write(line) @@ -965,9 +1176,13 @@ self._daemonpids = [] if self._keeptmpdir: - log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' % - (self._testtmp.decode('utf-8'), - self._threadtmp.decode('utf-8'))) + log( + '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' + % ( + self._testtmp.decode('utf-8'), + self._threadtmp.decode('utf-8'), + ) + ) else: try: shutil.rmtree(self._testtmp) @@ -983,8 +1198,12 @@ # files are deleted shutil.rmtree(self._chgsockdir, True) - if ((self._ret != 0 or self._out != self._refout) and not self._skipped - and not self._debug and self._out): + if ( + (self._ret != 0 or self._out != self._refout) + and not self._skipped + and not self._debug + and self._out + ): with open(self.errpath, 'wb') as f: for line in self._out: f.write(line) @@ -1017,7 +1236,7 @@ self._portmap(2), (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'), (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'), - ] + ] r.append((self._escapepath(self._testtmp), b'$TESTTMP')) replacementfile = os.path.join(self._testdir, b'common-pattern.py') @@ -1038,10 +1257,15 @@ def _escapepath(self, p): if os.name == 'nt': - return ( - (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or - c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c - for c in [p[i:i + 1] for i in range(len(p))])) + return b''.join( + c.isalpha() + and b'[%s%s]' % (c.lower(), c.upper()) + or c in b'/\\' + and br'[/\\]' + or c.isdigit() + and c + or b'\\' + c + for c in [p[i : i + 1] for i in range(len(p))] ) else: return re.escape(p) @@ -1083,9 +1307,11 @@ def _getenv(self): """Obtain environment variables to use during test execution.""" + def defineport(i): offset = '' if i == 0 else '%s' % i env["HGPORT%s" % offset] = '%s' % (self._startport + i) + env = os.environ.copy() env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or '' env['HGEMITWARNINGS'] = '1' @@ -1097,11 +1323,13 @@ # This list should be parallel to _portmap in _getreplacements defineport(port) env["HGRCPATH"] = _strpath(os.path.join(self._threadtmp, b'.hgrc')) - env["DAEMON_PIDS"] = _strpath(os.path.join(self._threadtmp, - b'daemon.pids')) - env["HGEDITOR"] = ('"' + sysexecutable + '"' - + ' -c "import sys; sys.exit(0)"') - env["HGUSER"] = "test" + env["DAEMON_PIDS"] = _strpath( + os.path.join(self._threadtmp, b'daemon.pids') + ) + env["HGEDITOR"] = ( + '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"' + ) + env["HGUSER"] = "test" env["HGENCODING"] = "ascii" env["HGENCODINGMODE"] = "strict" env["HGHOSTNAME"] = "test-hostname" @@ -1111,7 +1339,8 @@ # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the # non-test one in as a default, otherwise set to devnull env['HGTESTCATAPULTSERVERPIPE'] = env.get( - 'HGCATAPULTSERVERPIPE', os.devnull) + 'HGCATAPULTSERVERPIPE', os.devnull + ) extraextensions = [] for opt in self._extraconfigopts: @@ -1187,11 +1416,15 @@ hgrc.write(b'all-warnings = true\n') hgrc.write(b'default-date = 0 0\n') hgrc.write(b'[largefiles]\n') - hgrc.write(b'usercache = %s\n' % - (os.path.join(self._testtmp, b'.cache/largefiles'))) + hgrc.write( + b'usercache = %s\n' + % (os.path.join(self._testtmp, b'.cache/largefiles')) + ) hgrc.write(b'[lfs]\n') - hgrc.write(b'usercache = %s\n' % - (os.path.join(self._testtmp, b'.cache/lfs'))) + hgrc.write( + b'usercache = %s\n' + % (os.path.join(self._testtmp, b'.cache/lfs')) + ) hgrc.write(b'[web]\n') hgrc.write(b'address = localhost\n') hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii')) @@ -1199,8 +1432,9 @@ for opt in self._extraconfigopts: section, key = opt.encode('utf-8').split(b'.', 1) - assert b'=' in key, ('extra config opt %s must ' - 'have an = for assignment' % opt) + assert b'=' in key, ( + 'extra config opt %s must ' 'have an = for assignment' % opt + ) hgrc.write(b'[%s]\n%s\n' % (section, key)) def fail(self, msg): @@ -1215,13 +1449,14 @@ Return a tuple (exitcode, output). output is None in debug mode. """ if self._debug: - proc = subprocess.Popen(_strpath(cmd), shell=True, - cwd=_strpath(self._testtmp), - env=env) + proc = subprocess.Popen( + _strpath(cmd), shell=True, cwd=_strpath(self._testtmp), env=env + ) ret = proc.wait() return (ret, None) proc = Popen4(cmd, self._testtmp, self._timeout, env) + def cleanup(): terminate(proc) ret = proc.wait() @@ -1257,6 +1492,7 @@ return ret, output.splitlines(True) + class PythonTest(Test): """A Python-based test.""" @@ -1270,13 +1506,13 @@ cmd = b'"%s"%s "%s"' % (PYTHON, py3switch, self.path) vlog("# Running", cmd) normalizenewlines = os.name == 'nt' - result = self._runcommand(cmd, env, - normalizenewlines=normalizenewlines) + result = self._runcommand(cmd, env, normalizenewlines=normalizenewlines) if self._aborted: raise KeyboardInterrupt() return result + # Some glob patterns apply only in some circumstances, so the script # might want to remove (glob) annotations that otherwise should be # retained. @@ -1301,9 +1537,11 @@ MARK_OPTIONAL = b" (?)\n" + def isoptional(line): return line.endswith(MARK_OPTIONAL) + class TTest(Test): """A "t test" is a test backed by a .t file.""" @@ -1376,9 +1614,12 @@ # TODO do something smarter when all other uses of hghave are gone. runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__))) tdir = runtestdir.replace(b'\\', b'/') - proc = Popen4(b'%s -c "%s/hghave %s"' % - (self._shell, tdir, allreqs), - self._testtmp, 0, self._getenv()) + proc = Popen4( + b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs), + self._testtmp, + 0, + self._getenv(), + ) stdout, stderr = proc.communicate() ret = proc.wait() if wifexited(ret): @@ -1419,25 +1660,30 @@ # up script results with our source. These markers include input # line number and the last return code. salt = b"SALT%d" % time.time() + def addsalt(line, inpython): if inpython: script.append(b'%s %d 0\n' % (salt, line)) else: script.append(b'echo %s %d $?\n' % (salt, line)) + activetrace = [] session = str(uuid.uuid4()) if PYTHON3: session = session.encode('ascii') - hgcatapult = (os.getenv('HGTESTCATAPULTSERVERPIPE') or - os.getenv('HGCATAPULTSERVERPIPE')) + hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv( + 'HGCATAPULTSERVERPIPE' + ) + def toggletrace(cmd=None): if not hgcatapult or hgcatapult == os.devnull: return if activetrace: script.append( - b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % ( - session, activetrace[0])) + b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' + % (session, activetrace[0]) + ) if cmd is None: return @@ -1447,8 +1693,9 @@ quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8') quoted = quoted.replace(b'\\', b'\\\\') script.append( - b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % ( - session, quoted)) + b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' + % (session, quoted) + ) activetrace[0:] = [quoted] script = [] @@ -1550,21 +1797,21 @@ after.setdefault(pos, []).append(l) elif skipping: after.setdefault(pos, []).append(l) - elif l.startswith(b' >>> '): # python inlines + elif l.startswith(b' >>> '): # python inlines after.setdefault(pos, []).append(l) prepos = pos pos = n if not inpython: # We've just entered a Python block. Add the header. inpython = True - addsalt(prepos, False) # Make sure we report the exit code. + addsalt(prepos, False) # Make sure we report the exit code. script.append(b'"%s" -m heredoctest < '): # continuations + elif l.startswith(b' > '): # continuations after.setdefault(prepos, []).append(l) script.append(l[4:]) - elif l.startswith(b' '): # results + elif l.startswith(b' '): # results # Queue up a list of expected results. expected.setdefault(pos, []).append(l[2:]) else: @@ -1603,7 +1850,7 @@ def _processoutput(self, exitcode, output, salt, after, expected): # Merge the script output back into a unified test. - warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not + warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not if exitcode != 0: warnonly = WARN_NO @@ -1614,19 +1861,16 @@ if salt in out_rawline: out_line, cmd_line = out_rawline.split(salt, 1) - pos, postout, warnonly = self._process_out_line(out_line, - pos, - postout, - expected, - warnonly) - pos, postout = self._process_cmd_line(cmd_line, pos, postout, - after) + pos, postout, warnonly = self._process_out_line( + out_line, pos, postout, expected, warnonly + ) + pos, postout = self._process_cmd_line(cmd_line, pos, postout, after) if pos in after: postout += after.pop(pos) if warnonly == WARN_YES: - exitcode = False # Set exitcode to warned. + exitcode = False # Set exitcode to warned. return exitcode, postout @@ -1648,7 +1892,7 @@ if isinstance(r, str): if r == '-glob': out_line = ''.join(el.rsplit(' (glob)', 1)) - r = '' # Warn only this line. + r = '' # Warn only this line. elif r == "retry": postout.append(b' ' + el) else: @@ -1663,8 +1907,7 @@ else: m = optline.match(el) if m: - conditions = [ - c for c in m.group(2).split(b' ')] + conditions = [c for c in m.group(2).split(b' ')] if not self._iftest(conditions): optional.append(i) @@ -1685,10 +1928,11 @@ postout.append(b' ' + el) else: if self.NEEDESCAPE(out_line): - out_line = TTest._stringescape(b'%s (esc)\n' % - out_line.rstrip(b'\n')) - postout.append(b' ' + out_line) # Let diff deal with it. - if r != '': # If line failed. + out_line = TTest._stringescape( + b'%s (esc)\n' % out_line.rstrip(b'\n') + ) + postout.append(b' ' + out_line) # Let diff deal with it. + if r != '': # If line failed. warnonly = WARN_NO elif warnonly == WARN_UNDEFINED: warnonly = WARN_YES @@ -1752,10 +1996,10 @@ i, n = 0, len(el) res = b'' while i < n: - c = el[i:i + 1] + c = el[i : i + 1] i += 1 - if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/': - res += el[i - 1:i + 1] + if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/': + res += el[i - 1 : i + 1] i += 1 elif c == b'*': res += b'.*' @@ -1768,12 +2012,12 @@ return TTest.rematch(res, l) def linematch(self, el, l): - if el == l: # perfect match (fast) + if el == l: # perfect match (fast) return True, True retry = False if isoptional(el): retry = "retry" - el = el[:-len(MARK_OPTIONAL)] + b"\n" + el = el[: -len(MARK_OPTIONAL)] + b"\n" else: m = optline.match(el) if m: @@ -1817,10 +2061,12 @@ for line in lines: if line.startswith(TTest.SKIPPED_PREFIX): line = line.splitlines()[0] - missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8')) + missing.append( + line[len(TTest.SKIPPED_PREFIX) :].decode('utf-8') + ) elif line.startswith(TTest.FAILED_PREFIX): line = line.splitlines()[0] - failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8')) + failed.append(line[len(TTest.FAILED_PREFIX) :].decode('utf-8')) return missing, failed @@ -1832,12 +2078,15 @@ def _stringescape(s): return TTest.ESCAPESUB(TTest._escapef, s) + iolock = threading.RLock() firstlock = threading.RLock() firsterror = False + class TestResult(unittest._TextTestResult): """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): @@ -1864,7 +2113,7 @@ self.color = pygmentspresent and self.stream.isatty() elif options.color == 'never': self.color = False - else: # 'always', for testing purposes + else: # 'always', for testing purposes self.color = pygmentspresent def onStart(self, test): @@ -1942,12 +2191,15 @@ pass elif self._options.view: v = self._options.view - subprocess.call(r'"%s" "%s" "%s"' % - (v, _strpath(test.refpath), - _strpath(test.errpath)), shell=True) + subprocess.call( + r'"%s" "%s" "%s"' + % (v, _strpath(test.refpath), _strpath(test.errpath)), + shell=True, + ) else: - servefail, lines = getdiff(expected, got, - test.refpath, test.errpath) + servefail, lines = getdiff( + expected, got, test.refpath, test.errpath + ) self.stream.write('\n') for line in lines: line = highlightdiff(line, self.color) @@ -1961,14 +2213,16 @@ if servefail: raise test.failureException( - 'server failed to start (HGPORT=%s)' % test._startport) + 'server failed to start (HGPORT=%s)' % test._startport + ) # handle interactive prompt without releasing iolock if self._options.interactive: if test.readrefout() != expected: self.stream.write( 'Reference output has changed (run again to prompt ' - 'changes)') + 'changes)' + ) else: self.stream.write('Accept this change? [n] ') self.stream.flush() @@ -1992,7 +2246,7 @@ # This module has one limitation. It can only work for Linux user # and not for Windows. test.started = os.times() - if self._firststarttime is None: # thread racy but irrelevant + if self._firststarttime is None: # thread racy but irrelevant self._firststarttime = test.started[4] def stopTest(self, test, interrupted=False): @@ -2003,18 +2257,24 @@ starttime = test.started endtime = test.stopped origin = self._firststarttime - self.times.append((test.name, - endtime[2] - starttime[2], # user space CPU time - endtime[3] - starttime[3], # sys space CPU time - endtime[4] - starttime[4], # real time - starttime[4] - origin, # start date in run context - endtime[4] - origin, # end date in run context - )) + self.times.append( + ( + test.name, + endtime[2] - starttime[2], # user space CPU time + endtime[3] - starttime[3], # sys space CPU time + endtime[4] - starttime[4], # real time + starttime[4] - origin, # start date in run context + endtime[4] - origin, # end date in run context + ) + ) if interrupted: with iolock: - self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % ( - test.name, self.times[-1][3])) + self.stream.writeln( + 'INTERRUPTED: %s (after %d seconds)' + % (test.name, self.times[-1][3]) + ) + def getTestResult(): """ @@ -2026,13 +2286,25 @@ else: return TestResult + class TestSuite(unittest.TestSuite): """Custom unittest TestSuite that knows how to execute Mercurial tests.""" - def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None, - retest=False, keywords=None, loop=False, runs_per_test=1, - loadtest=None, showchannels=False, - *args, **kwargs): + def __init__( + self, + testdir, + jobs=1, + whitelist=None, + blacklist=None, + retest=False, + keywords=None, + loop=False, + runs_per_test=1, + loadtest=None, + showchannels=False, + *args, + **kwargs + ): """Create a new instance that can run tests with a configuration. testdir specifies the directory where tests are executed from. This @@ -2079,11 +2351,13 @@ tests = [] num_tests = [0] for test in self._tests: + def get(): num_tests[0] += 1 if getattr(test, 'should_reload', False): return self._loadtest(test, num_tests[0]) return test + if not os.path.exists(test.path): result.addSkip(test, "Doesn't exist") continue @@ -2131,7 +2405,7 @@ done.put(None) except KeyboardInterrupt: pass - except: # re-raises + except: # re-raises done.put(('!', test, 'run-test raised an error, see traceback')) raise finally: @@ -2156,7 +2430,7 @@ sys.stdout.flush() for x in xrange(10): if channels: - time.sleep(.1) + time.sleep(0.1) count += 1 stoppedearly = False @@ -2181,15 +2455,15 @@ if self._loop: if getattr(test, 'should_reload', False): num_tests[0] += 1 - tests.append( - self._loadtest(test, num_tests[0])) + tests.append(self._loadtest(test, num_tests[0])) else: tests.append(test) if self._jobs == 1: job(test, result) else: - t = threading.Thread(target=job, name=test.name, - args=(test, result)) + t = threading.Thread( + target=job, name=test.name, args=(test, result) + ) t.start() running += 1 @@ -2212,24 +2486,28 @@ return result + # Save the most recent 5 wall-clock runtimes of each test to a # human-readable text file named .testtimes. Tests are sorted # alphabetically, while times for each test are listed from oldest to # newest. + def loadtimes(outputdir): times = [] try: with open(os.path.join(outputdir, b'.testtimes')) as fp: for line in fp: m = re.match('(.*?) ([0-9. ]+)', line) - times.append((m.group(1), - [float(t) for t in m.group(2).split()])) + times.append( + (m.group(1), [float(t) for t in m.group(2).split()]) + ) except IOError as err: if err.errno != errno.ENOENT: raise return times + def savetimes(outputdir, result): saved = dict(loadtimes(outputdir)) maxruns = 5 @@ -2241,8 +2519,9 @@ ts.append(real) ts[:] = ts[-maxruns:] - fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes', - dir=outputdir, text=True) + fd, tmpname = tempfile.mkstemp( + prefix=b'.testtimes', dir=outputdir, text=True + ) 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]))) @@ -2256,6 +2535,7 @@ except OSError: pass + class TextTestRunner(unittest.TextTestRunner): """Custom unittest test runner that uses appropriate settings.""" @@ -2264,8 +2544,9 @@ self._runner = runner - self._result = getTestResult()(self._runner.options, self.stream, - self.descriptions, self.verbosity) + self._result = getTestResult()( + self._runner.options, self.stream, self.descriptions, self.verbosity + ) def listtests(self, test): test = sorted(test, key=lambda t: t.name) @@ -2299,17 +2580,20 @@ self.stream.writeln('') if not self._runner.options.noskips: - for test, msg in sorted(self._result.skipped, - key=lambda s: s[0].name): + for test, msg in sorted( + self._result.skipped, key=lambda s: s[0].name + ): formatted = 'Skipped %s: %s\n' % (test.name, msg) msg = highlightmsg(formatted, self._result.color) self.stream.write(msg) - for test, msg in sorted(self._result.failures, - key=lambda f: f[0].name): + for test, msg in sorted( + self._result.failures, key=lambda f: f[0].name + ): formatted = 'Failed %s: %s\n' % (test.name, msg) self.stream.write(highlightmsg(formatted, self._result.color)) - for test, msg in sorted(self._result.errors, - key=lambda e: e[0].name): + for test, msg in sorted( + self._result.errors, key=lambda e: e[0].name + ): self.stream.writeln('Errored %s: %s' % (test.name, msg)) if self._runner.options.xunit: @@ -2329,31 +2613,41 @@ self._bisecttests(t for t, m in self._result.failures) self.stream.writeln( '# Ran %d tests, %d skipped, %d failed.' - % (self._result.testsRun, skipped + ignored, failed)) + % (self._result.testsRun, skipped + ignored, failed) + ) if failed: - self.stream.writeln('python hash seed: %s' % - os.environ['PYTHONHASHSEED']) + self.stream.writeln( + 'python hash seed: %s' % os.environ['PYTHONHASHSEED'] + ) if self._runner.options.time: self.printtimes(self._result.times) if self._runner.options.exceptions: exceptions = aggregateexceptions( - os.path.join(self._runner._outputdir, b'exceptions')) + os.path.join(self._runner._outputdir, b'exceptions') + ) self.stream.writeln('Exceptions Report:') - self.stream.writeln('%d total from %d frames' % - (exceptions['total'], - len(exceptions['exceptioncounts']))) + self.stream.writeln( + '%d total from %d frames' + % (exceptions['total'], len(exceptions['exceptioncounts'])) + ) combined = exceptions['combined'] for key in sorted(combined, key=combined.get, reverse=True): frame, line, exc = key totalcount, testcount, leastcount, leasttest = combined[key] - self.stream.writeln('%d (%d tests)\t%s: %s (%s - %d total)' - % (totalcount, - testcount, - frame, exc, - leasttest, leastcount)) + self.stream.writeln( + '%d (%d tests)\t%s: %s (%s - %d total)' + % ( + totalcount, + testcount, + frame, + exc, + leasttest, + leastcount, + ) + ) self.stream.flush() @@ -2364,14 +2658,17 @@ bisectrepo = self._runner.options.bisect_repo if bisectrepo: bisectcmd.extend(['-R', os.path.abspath(bisectrepo)]) + def pread(args): env = os.environ.copy() env['HGPLAIN'] = '1' - p = subprocess.Popen(args, stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, env=env) + p = subprocess.Popen( + args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env + ) data = p.stdout.read() p.wait() return data + for test in tests: pread(bisectcmd + ['--reset']), pread(bisectcmd + ['--bad', '.']) @@ -2382,32 +2679,43 @@ withhg = self._runner.options.with_hg if withhg: opts += ' --with-hg=%s ' % shellquote(_strpath(withhg)) - rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, - test) + rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test) data = pread(bisectcmd + ['--command', rtc]) m = re.search( - (br'\nThe first (?Pbad|good) revision ' - br'is:\nchangeset: +\d+:(?P[a-f0-9]+)\n.*\n' - br'summary: +(?P[^\n]+)\n'), - data, (re.MULTILINE | re.DOTALL)) + ( + br'\nThe first (?Pbad|good) revision ' + br'is:\nchangeset: +\d+:(?P[a-f0-9]+)\n.*\n' + br'summary: +(?P[^\n]+)\n' + ), + data, + (re.MULTILINE | re.DOTALL), + ) if m is None: self.stream.writeln( - 'Failed to identify failure point for %s' % test) + 'Failed to identify failure point for %s' % test + ) continue dat = m.groupdict() verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed' self.stream.writeln( - '%s %s by %s (%s)' % ( - test, verb, dat['node'].decode('ascii'), - dat['summary'].decode('utf8', 'ignore'))) + '%s %s by %s (%s)' + % ( + test, + verb, + dat['node'].decode('ascii'), + dat['summary'].decode('utf8', 'ignore'), + ) + ) def printtimes(self, times): # iolock held by run self.stream.writeln('# Producing time report') times.sort(key=lambda t: (t[3])) cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s' - self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' % - ('start', 'end', 'cuser', 'csys', 'real', 'Test')) + self.stream.writeln( + '%-7s %-7s %-7s %-7s %-7s %s' + % ('start', 'end', 'cuser', 'csys', 'real', 'Test') + ) for tdata in times: test = tdata[0] cuser, csys, real, start, end = tdata[1:6] @@ -2419,11 +2727,12 @@ timesd = dict((t[0], t[3]) for t in result.times) doc = minidom.Document() s = doc.createElement('testsuite') - s.setAttribute('errors', "0") # TODO + s.setAttribute('errors', "0") # TODO s.setAttribute('failures', str(len(result.failures))) s.setAttribute('name', 'run-tests') - s.setAttribute('skipped', str(len(result.skipped) + - len(result.ignored))) + s.setAttribute( + 'skipped', str(len(result.skipped) + len(result.ignored)) + ) s.setAttribute('tests', str(result.testsRun)) doc.appendChild(s) for tc in result.successes: @@ -2474,10 +2783,11 @@ timesd[test] = tdata[1:] outcome = {} - groups = [('success', ((tc, None) - for tc in result.successes)), - ('failure', result.failures), - ('skip', result.skipped)] + groups = [ + ('success', ((tc, None) for tc in result.successes)), + ('failure', result.failures), + ('skip', result.skipped), + ] for res, testcases in groups: for tc, __ in testcases: if tc.name in timesd: @@ -2486,23 +2796,26 @@ diff = diff.decode('unicode_escape') except UnicodeDecodeError as e: diff = '%r decoding diff, sorry' % e - tres = {'result': res, - 'time': ('%0.3f' % timesd[tc.name][2]), - 'cuser': ('%0.3f' % timesd[tc.name][0]), - 'csys': ('%0.3f' % timesd[tc.name][1]), - 'start': ('%0.3f' % timesd[tc.name][3]), - 'end': ('%0.3f' % timesd[tc.name][4]), - 'diff': diff, - } + tres = { + 'result': res, + 'time': ('%0.3f' % timesd[tc.name][2]), + 'cuser': ('%0.3f' % timesd[tc.name][0]), + 'csys': ('%0.3f' % timesd[tc.name][1]), + 'start': ('%0.3f' % timesd[tc.name][3]), + 'end': ('%0.3f' % timesd[tc.name][4]), + 'diff': diff, + } else: # blacklisted test tres = {'result': res} outcome[tc.name] = tres - jsonout = json.dumps(outcome, sort_keys=True, indent=4, - separators=(',', ': ')) + jsonout = json.dumps( + outcome, sort_keys=True, indent=4, separators=(',', ': ') + ) outf.writelines(("testreport =", jsonout)) + def sorttests(testdescs, previoustimes, shuffle=False): """Do an in-place sort of tests.""" if shuffle: @@ -2510,29 +2823,32 @@ return if previoustimes: + def sortkey(f): f = f['path'] if f in previoustimes: # Use most recent time as estimate - return -previoustimes[f][-1] + return -(previoustimes[f][-1]) else: # Default to a rather arbitrary value of 1 second for new tests return -1.0 + else: # keywords for slow tests - slow = {b'svn': 10, - b'cvs': 10, - b'hghave': 10, - b'largefiles-update': 10, - b'run-tests': 10, - b'corruption': 10, - b'race': 10, - b'i18n': 10, - b'check': 100, - b'gendoc': 100, - b'contrib-perf': 200, - b'merge-combination': 100, - } + slow = { + b'svn': 10, + b'cvs': 10, + b'hghave': 10, + b'largefiles-update': 10, + b'run-tests': 10, + b'corruption': 10, + b'race': 10, + b'i18n': 10, + b'check': 100, + b'gendoc': 100, + b'contrib-perf': 200, + b'merge-combination': 100, + } perf = {} def sortkey(f): @@ -2558,6 +2874,7 @@ testdescs.sort(key=sortkey) + class TestRunner(object): """Holds context for executing tests. @@ -2614,6 +2931,7 @@ testdescs = self.findtests(tests) if options.profile_runner: import statprof + statprof.start() result = self._run(testdescs) if options.profile_runner: @@ -2668,8 +2986,7 @@ d = osenvironb.get(b'TMP', None) tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d) - self._hgtmp = osenvironb[b'HGTMP'] = ( - os.path.realpath(tmpdir)) + self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir) if self.options.with_hg: self._installdir = None @@ -2791,7 +3108,8 @@ osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir logexceptions = os.path.join(self._testdir, b'logexceptions.py') self.options.extra_config_opt.append( - 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')) + 'extensions.logexceptions=%s' % logexceptions.decode('utf-8') + ) vlog("# Using TESTDIR", self._testdir) vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR']) @@ -2803,7 +3121,7 @@ try: return self._runtests(testdescs) or 0 finally: - time.sleep(.1) + time.sleep(0.1) self._cleanup() def findtests(self, args): @@ -2814,8 +3132,12 @@ """ if not args: if self.options.changed: - proc = Popen4(b'hg st --rev "%s" -man0 .' % - _bytespath(self.options.changed), None, 0) + proc = Popen4( + b'hg st --rev "%s" -man0 .' + % _bytespath(self.options.changed), + None, + 0, + ) stdout, stderr = proc.communicate() args = stdout.strip(b'\0').split(b'\0') else: @@ -2832,13 +3154,16 @@ args = expanded_args testcasepattern = re.compile( - br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-\.#]+))') + br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-\.#]+))' + ) tests = [] for t in args: case = [] - if not (os.path.basename(t).startswith(b'test-') - and (t.endswith(b'.py') or t.endswith(b'.t'))): + if not ( + os.path.basename(t).startswith(b'test-') + and (t.endswith(b'.py') or t.endswith(b'.t')) + ): m = testcasepattern.match(os.path.basename(t)) if m is not None: @@ -2854,12 +3179,14 @@ casedimensions = parsettestcases(t) if casedimensions: cases = [] + def addcases(case, casedimensions): if not casedimensions: cases.append(case) else: for c in casedimensions[0]: addcases(case + [c], casedimensions[1:]) + addcases([], casedimensions) if case and case in cases: cases = [case] @@ -2913,16 +3240,19 @@ if kws is not None and PYTHON3: kws = kws.encode('utf-8') - suite = TestSuite(self._testdir, - 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, - showchannels=self.options.showchannels, - tests=tests, loadtest=_reloadtest) + suite = TestSuite( + self._testdir, + 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, + showchannels=self.options.showchannels, + tests=tests, + loadtest=_reloadtest, + ) verbosity = 1 if self.options.list_tests: verbosity = 0 @@ -2942,8 +3272,10 @@ assert self._installdir self._installchg() - log('running %d tests using %d parallel processes' % ( - num_tests, jobs)) + log( + 'running %d tests using %d parallel processes' + % (num_tests, jobs) + ) result = runner.run(suite) @@ -2962,7 +3294,7 @@ return 1 def _getport(self, count): - port = self._ports.get(count) # do we have a cached entry? + port = self._ports.get(count) # do we have a cached entry? if port is None: portneeded = 3 # above 100 tries we just give up and let test reports failure @@ -3000,18 +3332,23 @@ # extra keyword parameters. 'case' is used by .t tests kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc) - t = testcls(refpath, self._outputdir, tmpdir, - keeptmpdir=self.options.keep_tmpdir, - debug=self.options.debug, - first=self.options.first, - timeout=self.options.timeout, - startport=self._getport(count), - extraconfigopts=self.options.extra_config_opt, - py3warnings=self.options.py3_warnings, - shell=self.options.shell, - hgcommand=self._hgcommand, - usechg=bool(self.options.with_chg or self.options.chg), - useipv6=useipv6, **kwds) + t = testcls( + refpath, + self._outputdir, + tmpdir, + keeptmpdir=self.options.keep_tmpdir, + debug=self.options.debug, + first=self.options.first, + timeout=self.options.timeout, + startport=self._getport(count), + extraconfigopts=self.options.extra_config_opt, + py3warnings=self.options.py3_warnings, + shell=self.options.shell, + hgcommand=self._hgcommand, + usechg=bool(self.options.with_chg or self.options.chg), + useipv6=useipv6, + **kwds + ) t.should_reload = True return t @@ -3036,8 +3373,10 @@ # 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) + 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: @@ -3056,8 +3395,10 @@ raise else: exedir, exename = os.path.split(sysexecutable) - vlog("# Modifying search path to find %s as %s in '%s'" % - (exename, pyexename, exedir)) + 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) @@ -3097,17 +3438,24 @@ # least on Windows for now, deal with .pydistutils.cfg bugs # when they happen. nohome = b'' - cmd = (b'"%(exe)s" setup.py %(pure)s clean --all' - b' build %(compiler)s --build-base="%(base)s"' - b' install --force --prefix="%(prefix)s"' - b' --install-lib="%(libdir)s"' - b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1' - % {b'exe': exe, b'pure': pure, - b'compiler': compiler, - b'base': os.path.join(self._hgtmp, b"build"), - b'prefix': self._installdir, b'libdir': self._pythondir, - b'bindir': self._bindir, - b'nohome': nohome, b'logfile': installerrs}) + cmd = ( + b'"%(exe)s" setup.py %(pure)s clean --all' + b' build %(compiler)s --build-base="%(base)s"' + b' install --force --prefix="%(prefix)s"' + b' --install-lib="%(libdir)s"' + b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1' + % { + b'exe': exe, + b'pure': pure, + b'compiler': compiler, + b'base': os.path.join(self._hgtmp, b"build"), + b'prefix': self._installdir, + b'libdir': self._pythondir, + b'bindir': self._bindir, + b'nohome': nohome, + b'logfile': installerrs, + } + ) # setuptools requires install directories to exist. def makedirs(p): @@ -3116,6 +3464,7 @@ except OSError as e: if e.errno != errno.EEXIST: raise + makedirs(self._pythondir) makedirs(self._bindir) @@ -3155,8 +3504,10 @@ with open(hgbat, 'rb') as f: data = f.read() if br'"%~dp0..\python" "%~dp0hg" %*' in data: - data = data.replace(br'"%~dp0..\python" "%~dp0hg" %*', - b'"%~dp0python" "%~dp0hg" %*') + data = data.replace( + br'"%~dp0..\python" "%~dp0hg" %*', + b'"%~dp0python" "%~dp0hg" %*', + ) with open(hgbat, 'wb') as f: f.write(data) else: @@ -3182,17 +3533,20 @@ 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._bindir == self._pythondir) and ( + self._bindir != self._tmpbindir + ): # The pythondir has been inferred from --with-hg flag. # We cannot expect anything sensible here. return expecthg = os.path.join(self._pythondir, b'mercurial') actualhg = self._gethgpath() if os.path.abspath(actualhg) != os.path.abspath(expecthg): - sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n' - ' (expected %s)\n' - % (verb, actualhg, expecthg)) + sys.stderr.write( + 'warning: %s with unexpected mercurial lib: %s\n' + ' (expected %s)\n' % (verb, actualhg, expecthg) + ) + def _gethgpath(self): """Return the path to the mercurial package that is actually found by the current Python interpreter.""" @@ -3216,14 +3570,20 @@ vlog('# Performing temporary installation of CHG') assert os.path.dirname(self._bindir) == self._installdir assert self._hgroot, 'must be called after _installhg()' - cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"' - % {b'make': b'make', # TODO: switch by option or environment? - b'prefix': self._installdir}) + cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % { + b'make': b'make', # TODO: switch by option or environment? + b'prefix': self._installdir, + } cwd = os.path.join(self._hgroot, b'contrib', b'chg') vlog("# Running", cmd) - proc = subprocess.Popen(cmd, shell=True, cwd=cwd, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + 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: @@ -3235,6 +3595,7 @@ def _outputcoverage(self): """Produce code coverage output.""" import coverage + coverage = coverage.coverage vlog('# Producing coverage report') @@ -3280,8 +3641,11 @@ if found: vlog("# Found prerequisite", p, "at", found) else: - print("WARNING: Did not find prerequisite tool: %s " % - p.decode("utf-8")) + print( + "WARNING: Did not find prerequisite tool: %s " + % p.decode("utf-8") + ) + def aggregateexceptions(path): exceptioncounts = collections.Counter() @@ -3322,10 +3686,12 @@ # impacted tests. combined = {} for key in exceptioncounts: - combined[key] = (exceptioncounts[key], - len(testsbyfailure[key]), - leastfailing[key][0], - leastfailing[key][1]) + combined[key] = ( + exceptioncounts[key], + len(testsbyfailure[key]), + leastfailing[key][0], + leastfailing[key][1], + ) return { 'exceptioncounts': exceptioncounts, @@ -3336,11 +3702,13 @@ 'bytest': failuresbytest, } + if __name__ == '__main__': runner = TestRunner() try: import msvcrt + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)