comparison tests/run-tests.py @ 1106:cca56bbea143

tests: copy hg test infra from hg repo @5cfdf6137af8
author Tony Tung <ttung@chanzuckerberg.com <mailto:ttung@chanzuckerberg.com>>
date Tue, 06 Feb 2018 16:34:28 -0800
parents e085b381e8e2
children
comparison
equal deleted inserted replaced
1105:bb255eaf3d14 1106:cca56bbea143
43 # completes fairly quickly, includes both shell and Python scripts, and 43 # completes fairly quickly, includes both shell and Python scripts, and
44 # includes some scripts that run daemon processes.) 44 # includes some scripts that run daemon processes.)
45 45
46 from __future__ import absolute_import, print_function 46 from __future__ import absolute_import, print_function
47 47
48 import argparse
49 import collections
48 import difflib 50 import difflib
49 import distutils.version as version 51 import distutils.version as version
50 import errno 52 import errno
51 import json 53 import json
52 import optparse
53 import os 54 import os
54 import random 55 import random
55 import re 56 import re
56 import shutil 57 import shutil
57 import signal 58 import signal
117 failed: '#7f0000', 118 failed: '#7f0000',
118 failedname: '#ff0000', 119 failedname: '#ff0000',
119 } 120 }
120 121
121 class TestRunnerLexer(lexer.RegexLexer): 122 class TestRunnerLexer(lexer.RegexLexer):
123 testpattern = r'[\w-]+\.(t|py)( \(case [\w-]+\))?'
122 tokens = { 124 tokens = {
123 'root': [ 125 'root': [
124 (r'^Skipped', token.Generic.Skipped, 'skipped'), 126 (r'^Skipped', token.Generic.Skipped, 'skipped'),
125 (r'^Failed ', token.Generic.Failed, 'failed'), 127 (r'^Failed ', token.Generic.Failed, 'failed'),
126 (r'^ERROR: ', token.Generic.Failed, 'failed'), 128 (r'^ERROR: ', token.Generic.Failed, 'failed'),
127 ], 129 ],
128 'skipped': [ 130 'skipped': [
129 (r'[\w-]+\.(t|py)', token.Generic.SName), 131 (testpattern, token.Generic.SName),
130 (r':.*', token.Generic.Skipped), 132 (r':.*', token.Generic.Skipped),
131 ], 133 ],
132 'failed': [ 134 'failed': [
133 (r'[\w-]+\.(t|py)', token.Generic.FName), 135 (testpattern, token.Generic.FName),
134 (r'(:| ).*', token.Generic.Failed), 136 (r'(:| ).*', token.Generic.Failed),
135 ] 137 ]
136 } 138 }
137 139
138 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle) 140 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
294 raise 296 raise
295 return cases 297 return cases
296 298
297 def getparser(): 299 def getparser():
298 """Obtain the OptionParser used by the CLI.""" 300 """Obtain the OptionParser used by the CLI."""
299 parser = optparse.OptionParser("%prog [options] [tests]") 301 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
300 302
301 # keep these sorted 303 selection = parser.add_argument_group('Test Selection')
302 parser.add_option("--blacklist", action="append", 304 selection.add_argument('--allow-slow-tests', action='store_true',
305 help='allow extremely slow tests')
306 selection.add_argument("--blacklist", action="append",
303 help="skip tests listed in the specified blacklist file") 307 help="skip tests listed in the specified blacklist file")
304 parser.add_option("--whitelist", action="append", 308 selection.add_argument("--changed",
309 help="run tests that are changed in parent rev or working directory")
310 selection.add_argument("-k", "--keywords",
311 help="run tests matching keywords")
312 selection.add_argument("-r", "--retest", action="store_true",
313 help = "retest failed tests")
314 selection.add_argument("--test-list", action="append",
315 help="read tests to run from the specified file")
316 selection.add_argument("--whitelist", action="append",
305 help="always run tests listed in the specified whitelist file") 317 help="always run tests listed in the specified whitelist file")
306 parser.add_option("--test-list", action="append", 318 selection.add_argument('tests', metavar='TESTS', nargs='*',
307 help="read tests to run from the specified file") 319 help='Tests to run')
308 parser.add_option("--changed", type="string", 320
309 help="run tests that are changed in parent rev or working directory") 321 harness = parser.add_argument_group('Test Harness Behavior')
310 parser.add_option("-C", "--annotate", action="store_true", 322 harness.add_argument('--bisect-repo',
311 help="output files annotated with coverage") 323 metavar='bisect_repo',
312 parser.add_option("-c", "--cover", action="store_true", 324 help=("Path of a repo to bisect. Use together with "
313 help="print a test coverage report") 325 "--known-good-rev"))
314 parser.add_option("--color", choices=["always", "auto", "never"], 326 harness.add_argument("-d", "--debug", action="store_true",
315 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
316 help="colorisation: always|auto|never (default: auto)")
317 parser.add_option("-d", "--debug", action="store_true",
318 help="debug mode: write output of test scripts to console" 327 help="debug mode: write output of test scripts to console"
319 " rather than capturing and diffing it (disables timeout)") 328 " rather than capturing and diffing it (disables timeout)")
320 parser.add_option("-f", "--first", action="store_true", 329 harness.add_argument("-f", "--first", action="store_true",
321 help="exit on the first test failure") 330 help="exit on the first test failure")
322 parser.add_option("-H", "--htmlcov", action="store_true", 331 harness.add_argument("-i", "--interactive", action="store_true",
323 help="create an HTML report of the coverage of the files")
324 parser.add_option("-i", "--interactive", action="store_true",
325 help="prompt to accept changed output") 332 help="prompt to accept changed output")
326 parser.add_option("-j", "--jobs", type="int", 333 harness.add_argument("-j", "--jobs", type=int,
327 help="number of jobs to run in parallel" 334 help="number of jobs to run in parallel"
328 " (default: $%s or %d)" % defaults['jobs']) 335 " (default: $%s or %d)" % defaults['jobs'])
329 parser.add_option("--keep-tmpdir", action="store_true", 336 harness.add_argument("--keep-tmpdir", action="store_true",
330 help="keep temporary directory after running tests") 337 help="keep temporary directory after running tests")
331 parser.add_option("-k", "--keywords", 338 harness.add_argument('--known-good-rev',
332 help="run tests matching keywords") 339 metavar="known_good_rev",
333 parser.add_option("--list-tests", action="store_true", 340 help=("Automatically bisect any failures using this "
341 "revision as a known-good revision."))
342 harness.add_argument("--list-tests", action="store_true",
334 help="list tests instead of running them") 343 help="list tests instead of running them")
335 parser.add_option("-l", "--local", action="store_true", 344 harness.add_argument("--loop", action="store_true",
345 help="loop tests repeatedly")
346 harness.add_argument('--random', action="store_true",
347 help='run tests in random order')
348 harness.add_argument("-p", "--port", type=int,
349 help="port on which servers should listen"
350 " (default: $%s or %d)" % defaults['port'])
351 harness.add_argument('--profile-runner', action='store_true',
352 help='run statprof on run-tests')
353 harness.add_argument("-R", "--restart", action="store_true",
354 help="restart at last error")
355 harness.add_argument("--runs-per-test", type=int, dest="runs_per_test",
356 help="run each test N times (default=1)", default=1)
357 harness.add_argument("--shell",
358 help="shell to use (default: $%s or %s)" % defaults['shell'])
359 harness.add_argument('--showchannels', action='store_true',
360 help='show scheduling channels')
361 harness.add_argument("--slowtimeout", type=int,
362 help="kill errant slow tests after SLOWTIMEOUT seconds"
363 " (default: $%s or %d)" % defaults['slowtimeout'])
364 harness.add_argument("-t", "--timeout", type=int,
365 help="kill errant tests after TIMEOUT seconds"
366 " (default: $%s or %d)" % defaults['timeout'])
367 harness.add_argument("--tmpdir",
368 help="run tests in the given temporary directory"
369 " (implies --keep-tmpdir)")
370 harness.add_argument("-v", "--verbose", action="store_true",
371 help="output verbose messages")
372
373 hgconf = parser.add_argument_group('Mercurial Configuration')
374 hgconf.add_argument("--chg", action="store_true",
375 help="install and use chg wrapper in place of hg")
376 hgconf.add_argument("--compiler",
377 help="compiler to build with")
378 hgconf.add_argument('--extra-config-opt', action="append", default=[],
379 help='set the given config opt in the test hgrc')
380 hgconf.add_argument("-l", "--local", action="store_true",
336 help="shortcut for --with-hg=<testdir>/../hg, " 381 help="shortcut for --with-hg=<testdir>/../hg, "
337 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set") 382 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
338 parser.add_option("--loop", action="store_true", 383 hgconf.add_argument("--ipv6", action="store_true",
339 help="loop tests repeatedly") 384 help="prefer IPv6 to IPv4 for network related tests")
340 parser.add_option("--runs-per-test", type="int", dest="runs_per_test", 385 hgconf.add_argument("--pure", action="store_true",
341 help="run each test N times (default=1)", default=1)
342 parser.add_option("-n", "--nodiff", action="store_true",
343 help="skip showing test changes")
344 parser.add_option("--outputdir", type="string",
345 help="directory to write error logs to (default=test directory)")
346 parser.add_option("-p", "--port", type="int",
347 help="port on which servers should listen"
348 " (default: $%s or %d)" % defaults['port'])
349 parser.add_option("--compiler", type="string",
350 help="compiler to build with")
351 parser.add_option("--pure", action="store_true",
352 help="use pure Python code instead of C extensions") 386 help="use pure Python code instead of C extensions")
353 parser.add_option("-R", "--restart", action="store_true", 387 hgconf.add_argument("-3", "--py3k-warnings", action="store_true",
354 help="restart at last error") 388 help="enable Py3k warnings on Python 2.7+")
355 parser.add_option("-r", "--retest", action="store_true", 389 hgconf.add_argument("--with-chg", metavar="CHG",
356 help="retest failed tests") 390 help="use specified chg wrapper in place of hg")
357 parser.add_option("-S", "--noskips", action="store_true", 391 hgconf.add_argument("--with-hg",
358 help="don't report skip tests verbosely")
359 parser.add_option("--shell", type="string",
360 help="shell to use (default: $%s or %s)" % defaults['shell'])
361 parser.add_option("-t", "--timeout", type="int",
362 help="kill errant tests after TIMEOUT seconds"
363 " (default: $%s or %d)" % defaults['timeout'])
364 parser.add_option("--slowtimeout", type="int",
365 help="kill errant slow tests after SLOWTIMEOUT seconds"
366 " (default: $%s or %d)" % defaults['slowtimeout'])
367 parser.add_option("--time", action="store_true",
368 help="time how long each test takes")
369 parser.add_option("--json", action="store_true",
370 help="store test result data in 'report.json' file")
371 parser.add_option("--tmpdir", type="string",
372 help="run tests in the given temporary directory"
373 " (implies --keep-tmpdir)")
374 parser.add_option("-v", "--verbose", action="store_true",
375 help="output verbose messages")
376 parser.add_option("--xunit", type="string",
377 help="record xunit results at specified path")
378 parser.add_option("--view", type="string",
379 help="external diff viewer")
380 parser.add_option("--with-hg", type="string",
381 metavar="HG", 392 metavar="HG",
382 help="test using specified hg script rather than a " 393 help="test using specified hg script rather than a "
383 "temporary installation") 394 "temporary installation")
384 parser.add_option("--chg", action="store_true",
385 help="install and use chg wrapper in place of hg")
386 parser.add_option("--with-chg", metavar="CHG",
387 help="use specified chg wrapper in place of hg")
388 parser.add_option("--ipv6", action="store_true",
389 help="prefer IPv6 to IPv4 for network related tests")
390 parser.add_option("-3", "--py3k-warnings", action="store_true",
391 help="enable Py3k warnings on Python 2.7+")
392 # This option should be deleted once test-check-py3-compat.t and other 395 # This option should be deleted once test-check-py3-compat.t and other
393 # Python 3 tests run with Python 3. 396 # Python 3 tests run with Python 3.
394 parser.add_option("--with-python3", metavar="PYTHON3", 397 hgconf.add_argument("--with-python3", metavar="PYTHON3",
395 help="Python 3 interpreter (if running under Python 2)" 398 help="Python 3 interpreter (if running under Python 2)"
396 " (TEMPORARY)") 399 " (TEMPORARY)")
397 parser.add_option('--extra-config-opt', action="append", 400
398 help='set the given config opt in the test hgrc') 401 reporting = parser.add_argument_group('Results Reporting')
399 parser.add_option('--random', action="store_true", 402 reporting.add_argument("-C", "--annotate", action="store_true",
400 help='run tests in random order') 403 help="output files annotated with coverage")
401 parser.add_option('--profile-runner', action='store_true', 404 reporting.add_argument("--color", choices=["always", "auto", "never"],
402 help='run statprof on run-tests') 405 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
403 parser.add_option('--allow-slow-tests', action='store_true', 406 help="colorisation: always|auto|never (default: auto)")
404 help='allow extremely slow tests') 407 reporting.add_argument("-c", "--cover", action="store_true",
405 parser.add_option('--showchannels', action='store_true', 408 help="print a test coverage report")
406 help='show scheduling channels') 409 reporting.add_argument('--exceptions', action='store_true',
407 parser.add_option('--known-good-rev', type="string", 410 help='log all exceptions and generate an exception report')
408 metavar="known_good_rev", 411 reporting.add_argument("-H", "--htmlcov", action="store_true",
409 help=("Automatically bisect any failures using this " 412 help="create an HTML report of the coverage of the files")
410 "revision as a known-good revision.")) 413 reporting.add_argument("--json", action="store_true",
411 parser.add_option('--bisect-repo', type="string", 414 help="store test result data in 'report.json' file")
412 metavar='bisect_repo', 415 reporting.add_argument("--outputdir",
413 help=("Path of a repo to bisect. Use together with " 416 help="directory to write error logs to (default=test directory)")
414 "--known-good-rev")) 417 reporting.add_argument("-n", "--nodiff", action="store_true",
418 help="skip showing test changes")
419 reporting.add_argument("-S", "--noskips", action="store_true",
420 help="don't report skip tests verbosely")
421 reporting.add_argument("--time", action="store_true",
422 help="time how long each test takes")
423 reporting.add_argument("--view",
424 help="external diff viewer")
425 reporting.add_argument("--xunit",
426 help="record xunit results at specified path")
415 427
416 for option, (envvar, default) in defaults.items(): 428 for option, (envvar, default) in defaults.items():
417 defaults[option] = type(default)(os.environ.get(envvar, default)) 429 defaults[option] = type(default)(os.environ.get(envvar, default))
418 parser.set_defaults(**defaults) 430 parser.set_defaults(**defaults)
419 431
420 return parser 432 return parser
421 433
422 def parseargs(args, parser): 434 def parseargs(args, parser):
423 """Parse arguments with our OptionParser and validate results.""" 435 """Parse arguments with our OptionParser and validate results."""
424 (options, args) = parser.parse_args(args) 436 options = parser.parse_args(args)
425 437
426 # jython is always pure 438 # jython is always pure
427 if 'java' in sys.platform or '__pypy__' in sys.modules: 439 if 'java' in sys.platform or '__pypy__' in sys.modules:
428 options.pure = True 440 options.pure = True
429 441
548 options.whitelisted = {} 560 options.whitelisted = {}
549 561
550 if options.showchannels: 562 if options.showchannels:
551 options.nodiff = True 563 options.nodiff = True
552 564
553 return (options, args) 565 return options
554 566
555 def rename(src, dst): 567 def rename(src, dst):
556 """Like os.rename(), trade atomicity and opened files friendliness 568 """Like os.rename(), trade atomicity and opened files friendliness
557 for existing destination support. 569 for existing destination support.
558 """ 570 """
657 # Status code reserved for skipped tests (used by hghave). 669 # Status code reserved for skipped tests (used by hghave).
658 SKIPPED_STATUS = 80 670 SKIPPED_STATUS = 80
659 671
660 def __init__(self, path, outputdir, tmpdir, keeptmpdir=False, 672 def __init__(self, path, outputdir, tmpdir, keeptmpdir=False,
661 debug=False, 673 debug=False,
674 first=False,
662 timeout=None, 675 timeout=None,
663 startport=None, extraconfigopts=None, 676 startport=None, extraconfigopts=None,
664 py3kwarnings=False, shell=None, hgcommand=None, 677 py3kwarnings=False, shell=None, hgcommand=None,
665 slowtimeout=None, usechg=False, 678 slowtimeout=None, usechg=False,
666 useipv6=False): 679 useipv6=False):
709 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname) 722 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
710 723
711 self._threadtmp = tmpdir 724 self._threadtmp = tmpdir
712 self._keeptmpdir = keeptmpdir 725 self._keeptmpdir = keeptmpdir
713 self._debug = debug 726 self._debug = debug
727 self._first = first
714 self._timeout = timeout 728 self._timeout = timeout
715 self._slowtimeout = slowtimeout 729 self._slowtimeout = slowtimeout
716 self._startport = startport 730 self._startport = startport
717 self._extraconfigopts = extraconfigopts or [] 731 self._extraconfigopts = extraconfigopts or []
718 self._py3kwarnings = py3kwarnings 732 self._py3kwarnings = py3kwarnings
888 self.fail('no result code from test') 902 self.fail('no result code from test')
889 elif out != self._refout: 903 elif out != self._refout:
890 # Diff generation may rely on written .err file. 904 # Diff generation may rely on written .err file.
891 if (ret != 0 or out != self._refout) and not self._skipped \ 905 if (ret != 0 or out != self._refout) and not self._skipped \
892 and not self._debug: 906 and not self._debug:
893 f = open(self.errpath, 'wb') 907 with open(self.errpath, 'wb') as f:
894 for line in out: 908 for line in out:
895 f.write(line) 909 f.write(line)
896 f.close()
897 910
898 # The result object handles diff calculation for us. 911 # The result object handles diff calculation for us.
899 if self._result.addOutputMismatch(self, ret, out, self._refout): 912 with firstlock:
900 # change was accepted, skip failing 913 if self._result.addOutputMismatch(self, ret, out, self._refout):
901 return 914 # change was accepted, skip failing
915 return
916 if self._first:
917 global firsterror
918 firsterror = True
902 919
903 if ret: 920 if ret:
904 msg = 'output changed and ' + describe(ret) 921 msg = 'output changed and ' + describe(ret)
905 else: 922 else:
906 msg = 'output changed' 923 msg = 'output changed'
928 # files are deleted 945 # files are deleted
929 shutil.rmtree(self._chgsockdir, True) 946 shutil.rmtree(self._chgsockdir, True)
930 947
931 if (self._ret != 0 or self._out != self._refout) and not self._skipped \ 948 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
932 and not self._debug and self._out: 949 and not self._debug and self._out:
933 f = open(self.errpath, 'wb') 950 with open(self.errpath, 'wb') as f:
934 for line in self._out: 951 for line in self._out:
935 f.write(line) 952 f.write(line)
936 f.close()
937 953
938 vlog("# Ret was:", self._ret, '(%s)' % self.name) 954 vlog("# Ret was:", self._ret, '(%s)' % self.name)
939 955
940 def _run(self, env): 956 def _run(self, env):
941 # This should be implemented in child classes to run tests. 957 # This should be implemented in child classes to run tests.
959 r = [ 975 r = [
960 # This list should be parallel to defineport in _getenv 976 # This list should be parallel to defineport in _getenv
961 self._portmap(0), 977 self._portmap(0),
962 self._portmap(1), 978 self._portmap(1),
963 self._portmap(2), 979 self._portmap(2),
964 (br'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
965 br'\1 (glob)'),
966 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'), 980 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
967 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'), 981 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
968 ] 982 ]
969 r.append((self._escapepath(self._testtmp), b'$TESTTMP')) 983 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
970 984
985 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
986
987 if os.path.exists(replacementfile):
988 data = {}
989 with open(replacementfile, mode='rb') as source:
990 # the intermediate 'compile' step help with debugging
991 code = compile(source.read(), replacementfile, 'exec')
992 exec(code, data)
993 r.extend(data.get('substitutions', ()))
971 return r 994 return r
972 995
973 def _escapepath(self, p): 996 def _escapepath(self, p):
974 if os.name == 'nt': 997 if os.name == 'nt':
975 return ( 998 return (
1019 """Obtain environment variables to use during test execution.""" 1042 """Obtain environment variables to use during test execution."""
1020 def defineport(i): 1043 def defineport(i):
1021 offset = '' if i == 0 else '%s' % i 1044 offset = '' if i == 0 else '%s' % i
1022 env["HGPORT%s" % offset] = '%s' % (self._startport + i) 1045 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1023 env = os.environ.copy() 1046 env = os.environ.copy()
1024 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') 1047 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1025 env['HGEMITWARNINGS'] = '1' 1048 env['HGEMITWARNINGS'] = '1'
1026 env['TESTTMP'] = self._testtmp 1049 env['TESTTMP'] = self._testtmp
1027 env['HOME'] = self._testtmp 1050 env['HOME'] = self._testtmp
1028 # This number should match portneeded in _getport 1051 # This number should match portneeded in _getport
1029 for port in xrange(3): 1052 for port in xrange(3):
1067 1090
1068 return env 1091 return env
1069 1092
1070 def _createhgrc(self, path): 1093 def _createhgrc(self, path):
1071 """Create an hgrc file for this test.""" 1094 """Create an hgrc file for this test."""
1072 hgrc = open(path, 'wb') 1095 with open(path, 'wb') as hgrc:
1073 hgrc.write(b'[ui]\n') 1096 hgrc.write(b'[ui]\n')
1074 hgrc.write(b'slash = True\n') 1097 hgrc.write(b'slash = True\n')
1075 hgrc.write(b'interactive = False\n') 1098 hgrc.write(b'interactive = False\n')
1076 hgrc.write(b'mergemarkers = detailed\n') 1099 hgrc.write(b'mergemarkers = detailed\n')
1077 hgrc.write(b'promptecho = True\n') 1100 hgrc.write(b'promptecho = True\n')
1078 hgrc.write(b'[defaults]\n') 1101 hgrc.write(b'[defaults]\n')
1079 hgrc.write(b'[devel]\n') 1102 hgrc.write(b'[devel]\n')
1080 hgrc.write(b'all-warnings = true\n') 1103 hgrc.write(b'all-warnings = true\n')
1081 hgrc.write(b'default-date = 0 0\n') 1104 hgrc.write(b'default-date = 0 0\n')
1082 hgrc.write(b'[largefiles]\n') 1105 hgrc.write(b'[largefiles]\n')
1083 hgrc.write(b'usercache = %s\n' % 1106 hgrc.write(b'usercache = %s\n' %
1084 (os.path.join(self._testtmp, b'.cache/largefiles'))) 1107 (os.path.join(self._testtmp, b'.cache/largefiles')))
1085 hgrc.write(b'[web]\n') 1108 hgrc.write(b'[lfs]\n')
1086 hgrc.write(b'address = localhost\n') 1109 hgrc.write(b'usercache = %s\n' %
1087 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii')) 1110 (os.path.join(self._testtmp, b'.cache/lfs')))
1088 1111 hgrc.write(b'[web]\n')
1089 for opt in self._extraconfigopts: 1112 hgrc.write(b'address = localhost\n')
1090 section, key = opt.split('.', 1) 1113 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1091 assert '=' in key, ('extra config opt %s must ' 1114
1092 'have an = for assignment' % opt) 1115 for opt in self._extraconfigopts:
1093 hgrc.write(b'[%s]\n%s\n' % (section, key)) 1116 section, key = opt.encode('utf-8').split(b'.', 1)
1094 hgrc.close() 1117 assert b'=' in key, ('extra config opt %s must '
1118 'have an = for assignment' % opt)
1119 hgrc.write(b'[%s]\n%s\n' % (section, key))
1095 1120
1096 def fail(self, msg): 1121 def fail(self, msg):
1097 # unittest differentiates between errored and failed. 1122 # unittest differentiates between errored and failed.
1098 # Failed is denoted by AssertionError (by default at least). 1123 # Failed is denoted by AssertionError (by default at least).
1099 raise AssertionError(msg) 1124 raise AssertionError(msg)
1195 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256)) 1220 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
1196 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'}) 1221 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1197 1222
1198 def __init__(self, path, *args, **kwds): 1223 def __init__(self, path, *args, **kwds):
1199 # accept an extra "case" parameter 1224 # accept an extra "case" parameter
1200 case = None 1225 case = kwds.pop('case', None)
1201 if 'case' in kwds:
1202 case = kwds.pop('case')
1203 self._case = case 1226 self._case = case
1204 self._allcases = parsettestcases(path) 1227 self._allcases = parsettestcases(path)
1205 super(TTest, self).__init__(path, *args, **kwds) 1228 super(TTest, self).__init__(path, *args, **kwds)
1206 if case: 1229 if case:
1207 self.name = '%s (case %s)' % (self.name, _strpath(case)) 1230 self.name = '%s (case %s)' % (self.name, _strpath(case))
1211 @property 1234 @property
1212 def refpath(self): 1235 def refpath(self):
1213 return os.path.join(self._testdir, self.bname) 1236 return os.path.join(self._testdir, self.bname)
1214 1237
1215 def _run(self, env): 1238 def _run(self, env):
1216 f = open(self.path, 'rb') 1239 with open(self.path, 'rb') as f:
1217 lines = f.readlines() 1240 lines = f.readlines()
1218 f.close()
1219 1241
1220 # .t file is both reference output and the test input, keep reference 1242 # .t file is both reference output and the test input, keep reference
1221 # output updated with the the test input. This avoids some race 1243 # output updated with the the test input. This avoids some race
1222 # conditions where the reference output does not match the actual test. 1244 # conditions where the reference output does not match the actual test.
1223 if self._refout is not None: 1245 if self._refout is not None:
1225 1247
1226 salt, script, after, expected = self._parsetest(lines) 1248 salt, script, after, expected = self._parsetest(lines)
1227 1249
1228 # Write out the generated script. 1250 # Write out the generated script.
1229 fname = b'%s.sh' % self._testtmp 1251 fname = b'%s.sh' % self._testtmp
1230 f = open(fname, 'wb') 1252 with open(fname, 'wb') as f:
1231 for l in script: 1253 for l in script:
1232 f.write(l) 1254 f.write(l)
1233 f.close()
1234 1255
1235 cmd = b'%s "%s"' % (self._shell, fname) 1256 cmd = b'%s "%s"' % (self._shell, fname)
1236 vlog("# Running", cmd) 1257 vlog("# Running", cmd)
1237 1258
1238 exitcode, output = self._runcommand(cmd, env) 1259 exitcode, output = self._runcommand(cmd, env)
1318 script.append(b'set -x\n') 1339 script.append(b'set -x\n')
1319 if self._hgcommand != b'hg': 1340 if self._hgcommand != b'hg':
1320 script.append(b'alias hg="%s"\n' % self._hgcommand) 1341 script.append(b'alias hg="%s"\n' % self._hgcommand)
1321 if os.getenv('MSYSTEM'): 1342 if os.getenv('MSYSTEM'):
1322 script.append(b'alias pwd="pwd -W"\n') 1343 script.append(b'alias pwd="pwd -W"\n')
1344 if self._case:
1345 if isinstance(self._case, str):
1346 quoted = shellquote(self._case)
1347 else:
1348 quoted = shellquote(self._case.decode('utf8')).encode('utf8')
1349 script.append(b'TESTCASE=%s\n' % quoted)
1350 script.append(b'export TESTCASE\n')
1323 1351
1324 n = 0 1352 n = 0
1325 for n, l in enumerate(lines): 1353 for n, l in enumerate(lines):
1326 if not l.endswith(b'\n'): 1354 if not l.endswith(b'\n'):
1327 l += b'\n' 1355 l += b'\n'
1428 while i < len(els): 1456 while i < len(els):
1429 el = els[i] 1457 el = els[i]
1430 1458
1431 r = self.linematch(el, lout) 1459 r = self.linematch(el, lout)
1432 if isinstance(r, str): 1460 if isinstance(r, str):
1433 if r == '+glob': 1461 if r == '-glob':
1434 lout = el[:-1] + ' (glob)\n'
1435 r = '' # Warn only this line.
1436 elif r == '-glob':
1437 lout = ''.join(el.rsplit(' (glob)', 1)) 1462 lout = ''.join(el.rsplit(' (glob)', 1))
1438 r = '' # Warn only this line. 1463 r = '' # Warn only this line.
1439 elif r == "retry": 1464 elif r == "retry":
1440 postout.append(b' ' + el) 1465 postout.append(b' ' + el)
1441 els.pop(i) 1466 els.pop(i)
1515 return exitcode, postout 1540 return exitcode, postout
1516 1541
1517 @staticmethod 1542 @staticmethod
1518 def rematch(el, l): 1543 def rematch(el, l):
1519 try: 1544 try:
1545 el = b'(?:' + el + b')'
1520 # use \Z to ensure that the regex matches to the end of the string 1546 # use \Z to ensure that the regex matches to the end of the string
1521 if os.name == 'nt': 1547 if os.name == 'nt':
1522 return re.match(el + br'\r?\n\Z', l) 1548 return re.match(el + br'\r?\n\Z', l)
1523 return re.match(el + br'\n\Z', l) 1549 return re.match(el + br'\n\Z', l)
1524 except re.error: 1550 except re.error:
1586 if el.endswith(b" (glob)\n"): 1612 if el.endswith(b" (glob)\n"):
1587 # ignore '(glob)' added to l by 'replacements' 1613 # ignore '(glob)' added to l by 'replacements'
1588 if l.endswith(b" (glob)\n"): 1614 if l.endswith(b" (glob)\n"):
1589 l = l[:-8] + b"\n" 1615 l = l[:-8] + b"\n"
1590 return TTest.globmatch(el[:-8], l) or retry 1616 return TTest.globmatch(el[:-8], l) or retry
1591 if os.altsep and l.replace(b'\\', b'/') == el: 1617 if os.altsep:
1592 return b'+glob' 1618 _l = l.replace(b'\\', b'/')
1619 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1620 return True
1593 return retry 1621 return retry
1594 1622
1595 @staticmethod 1623 @staticmethod
1596 def parsehghaveoutput(lines): 1624 def parsehghaveoutput(lines):
1597 '''Parse hghave log lines. 1625 '''Parse hghave log lines.
1618 @staticmethod 1646 @staticmethod
1619 def _stringescape(s): 1647 def _stringescape(s):
1620 return TTest.ESCAPESUB(TTest._escapef, s) 1648 return TTest.ESCAPESUB(TTest._escapef, s)
1621 1649
1622 iolock = threading.RLock() 1650 iolock = threading.RLock()
1651 firstlock = threading.RLock()
1652 firsterror = False
1623 1653
1624 class TestResult(unittest._TextTestResult): 1654 class TestResult(unittest._TextTestResult):
1625 """Holds results when executing via unittest.""" 1655 """Holds results when executing via unittest."""
1626 # Don't worry too much about accessing the non-public _TextTestResult. 1656 # Don't worry too much about accessing the non-public _TextTestResult.
1627 # It is relatively common in Python testing tools. 1657 # It is relatively common in Python testing tools.
1703 self.testsRun += 1 1733 self.testsRun += 1
1704 self.stream.flush() 1734 self.stream.flush()
1705 1735
1706 def addOutputMismatch(self, test, ret, got, expected): 1736 def addOutputMismatch(self, test, ret, got, expected):
1707 """Record a mismatch in test output for a particular test.""" 1737 """Record a mismatch in test output for a particular test."""
1708 if self.shouldStop: 1738 if self.shouldStop or firsterror:
1709 # don't print, some other test case already failed and 1739 # don't print, some other test case already failed and
1710 # printed, we're just stale and probably failed due to our 1740 # printed, we're just stale and probably failed due to our
1711 # temp dir getting cleaned up. 1741 # temp dir getting cleaned up.
1712 return 1742 return
1713 1743
1863 if self._retest and not os.path.exists(test.errpath): 1893 if self._retest and not os.path.exists(test.errpath):
1864 result.addIgnore(test, 'not retesting') 1894 result.addIgnore(test, 'not retesting')
1865 continue 1895 continue
1866 1896
1867 if self._keywords: 1897 if self._keywords:
1868 f = open(test.path, 'rb') 1898 with open(test.path, 'rb') as f:
1869 t = f.read().lower() + test.bname.lower() 1899 t = f.read().lower() + test.bname.lower()
1870 f.close()
1871 ignored = False 1900 ignored = False
1872 for k in self._keywords.lower().split(): 1901 for k in self._keywords.lower().split():
1873 if k not in t: 1902 if k not in t:
1874 result.addIgnore(test, "doesn't match keyword") 1903 result.addIgnore(test, "doesn't match keyword")
1875 ignored = True 1904 ignored = True
2094 if failed: 2123 if failed:
2095 self.stream.writeln('python hash seed: %s' % 2124 self.stream.writeln('python hash seed: %s' %
2096 os.environ['PYTHONHASHSEED']) 2125 os.environ['PYTHONHASHSEED'])
2097 if self._runner.options.time: 2126 if self._runner.options.time:
2098 self.printtimes(result.times) 2127 self.printtimes(result.times)
2128
2129 if self._runner.options.exceptions:
2130 exceptions = aggregateexceptions(
2131 os.path.join(self._runner._outputdir, b'exceptions'))
2132 total = sum(exceptions.values())
2133
2134 self.stream.writeln('Exceptions Report:')
2135 self.stream.writeln('%d total from %d frames' %
2136 (total, len(exceptions)))
2137 for (frame, line, exc), count in exceptions.most_common():
2138 self.stream.writeln('%d\t%s: %s' % (count, frame, exc))
2139
2099 self.stream.flush() 2140 self.stream.flush()
2100 2141
2101 return result 2142 return result
2102 2143
2103 def _bisecttests(self, tests): 2144 def _bisecttests(self, tests):
2241 outcome[tc.name] = tres 2282 outcome[tc.name] = tres
2242 jsonout = json.dumps(outcome, sort_keys=True, indent=4, 2283 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2243 separators=(',', ': ')) 2284 separators=(',', ': '))
2244 outf.writelines(("testreport =", jsonout)) 2285 outf.writelines(("testreport =", jsonout))
2245 2286
2287 def sorttests(testdescs, shuffle=False):
2288 """Do an in-place sort of tests."""
2289 if shuffle:
2290 random.shuffle(testdescs)
2291 return
2292
2293 # keywords for slow tests
2294 slow = {b'svn': 10,
2295 b'cvs': 10,
2296 b'hghave': 10,
2297 b'largefiles-update': 10,
2298 b'run-tests': 10,
2299 b'corruption': 10,
2300 b'race': 10,
2301 b'i18n': 10,
2302 b'check': 100,
2303 b'gendoc': 100,
2304 b'contrib-perf': 200,
2305 }
2306 perf = {}
2307
2308 def sortkey(f):
2309 # run largest tests first, as they tend to take the longest
2310 f = f['path']
2311 try:
2312 return perf[f]
2313 except KeyError:
2314 try:
2315 val = -os.stat(f).st_size
2316 except OSError as e:
2317 if e.errno != errno.ENOENT:
2318 raise
2319 perf[f] = -1e9 # file does not exist, tell early
2320 return -1e9
2321 for kw, mul in slow.items():
2322 if kw in f:
2323 val *= mul
2324 if f.endswith(b'.py'):
2325 val /= 10.0
2326 perf[f] = val / 1000.0
2327 return perf[f]
2328
2329 testdescs.sort(key=sortkey)
2330
2246 class TestRunner(object): 2331 class TestRunner(object):
2247 """Holds context for executing tests. 2332 """Holds context for executing tests.
2248 2333
2249 Tests rely on a lot of state. This object holds it for them. 2334 Tests rely on a lot of state. This object holds it for them.
2250 """ 2335 """
2285 def run(self, args, parser=None): 2370 def run(self, args, parser=None):
2286 """Run the test suite.""" 2371 """Run the test suite."""
2287 oldmask = os.umask(0o22) 2372 oldmask = os.umask(0o22)
2288 try: 2373 try:
2289 parser = parser or getparser() 2374 parser = parser or getparser()
2290 options, args = parseargs(args, parser) 2375 options = parseargs(args, parser)
2291 # positional arguments are paths to test files to run, so 2376 tests = [_bytespath(a) for a in options.tests]
2292 # we make sure they're all bytestrings
2293 args = [_bytespath(a) for a in args]
2294 if options.test_list is not None: 2377 if options.test_list is not None:
2295 for listfile in options.test_list: 2378 for listfile in options.test_list:
2296 with open(listfile, 'rb') as f: 2379 with open(listfile, 'rb') as f:
2297 args.extend(t for t in f.read().splitlines() if t) 2380 tests.extend(t for t in f.read().splitlines() if t)
2298 self.options = options 2381 self.options = options
2299 2382
2300 self._checktools() 2383 self._checktools()
2301 testdescs = self.findtests(args) 2384 testdescs = self.findtests(tests)
2302 if options.profile_runner: 2385 if options.profile_runner:
2303 import statprof 2386 import statprof
2304 statprof.start() 2387 statprof.start()
2305 result = self._run(testdescs) 2388 result = self._run(testdescs)
2306 if options.profile_runner: 2389 if options.profile_runner:
2310 2393
2311 finally: 2394 finally:
2312 os.umask(oldmask) 2395 os.umask(oldmask)
2313 2396
2314 def _run(self, testdescs): 2397 def _run(self, testdescs):
2315 if self.options.random: 2398 sorttests(testdescs, shuffle=self.options.random)
2316 random.shuffle(testdescs)
2317 else:
2318 # keywords for slow tests
2319 slow = {b'svn': 10,
2320 b'cvs': 10,
2321 b'hghave': 10,
2322 b'largefiles-update': 10,
2323 b'run-tests': 10,
2324 b'corruption': 10,
2325 b'race': 10,
2326 b'i18n': 10,
2327 b'check': 100,
2328 b'gendoc': 100,
2329 b'contrib-perf': 200,
2330 }
2331 perf = {}
2332 def sortkey(f):
2333 # run largest tests first, as they tend to take the longest
2334 f = f['path']
2335 try:
2336 return perf[f]
2337 except KeyError:
2338 try:
2339 val = -os.stat(f).st_size
2340 except OSError as e:
2341 if e.errno != errno.ENOENT:
2342 raise
2343 perf[f] = -1e9 # file does not exist, tell early
2344 return -1e9
2345 for kw, mul in slow.items():
2346 if kw in f:
2347 val *= mul
2348 if f.endswith(b'.py'):
2349 val /= 10.0
2350 perf[f] = val / 1000.0
2351 return perf[f]
2352 testdescs.sort(key=sortkey)
2353 2399
2354 self._testdir = osenvironb[b'TESTDIR'] = getattr( 2400 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2355 os, 'getcwdb', os.getcwd)() 2401 os, 'getcwdb', os.getcwd)()
2402 # assume all tests in same folder for now
2403 if testdescs:
2404 pathname = os.path.dirname(testdescs[0]['path'])
2405 if pathname:
2406 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2407 pathname)
2356 if self.options.outputdir: 2408 if self.options.outputdir:
2357 self._outputdir = canonpath(_bytespath(self.options.outputdir)) 2409 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2358 else: 2410 else:
2359 self._outputdir = self._testdir 2411 self._outputdir = self._testdir
2412 if testdescs and pathname:
2413 self._outputdir = os.path.join(self._outputdir, pathname)
2360 2414
2361 if 'PYTHONHASHSEED' not in os.environ: 2415 if 'PYTHONHASHSEED' not in os.environ:
2362 # use a random python hash seed all the time 2416 # use a random python hash seed all the time
2363 # we do the randomness ourself to know what seed is used 2417 # we do the randomness ourself to know what seed is used
2364 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32)) 2418 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2371 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if 2425 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2372 # tmpdir already exists. 2426 # tmpdir already exists.
2373 print("error: temp dir %r already exists" % tmpdir) 2427 print("error: temp dir %r already exists" % tmpdir)
2374 return 1 2428 return 1
2375 2429
2376 # Automatically removing tmpdir sounds convenient, but could
2377 # really annoy anyone in the habit of using "--tmpdir=/tmp"
2378 # or "--tmpdir=$HOME".
2379 #vlog("# Removing temp dir", tmpdir)
2380 #shutil.rmtree(tmpdir)
2381 os.makedirs(tmpdir) 2430 os.makedirs(tmpdir)
2382 else: 2431 else:
2383 d = None 2432 d = None
2384 if os.name == 'nt': 2433 if os.name == 'nt':
2385 # without this, we get the default temp dir location, but 2434 # without this, we get the default temp dir location, but
2397 assert isinstance(self._bindir, bytes) 2446 assert isinstance(self._bindir, bytes)
2398 self._hgcommand = os.path.basename(whg) 2447 self._hgcommand = os.path.basename(whg)
2399 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin') 2448 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2400 os.makedirs(self._tmpbindir) 2449 os.makedirs(self._tmpbindir)
2401 2450
2402 # This looks redundant with how Python initializes sys.path from 2451 normbin = os.path.normpath(os.path.abspath(whg))
2403 # the location of the script being executed. Needed because the 2452 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2404 # "hg" specified by --with-hg is not the only Python script 2453
2405 # executed in the test suite that needs to import 'mercurial' 2454 # Other Python scripts in the test harness need to
2406 # ... which means it's not really redundant at all. 2455 # `import mercurial`. If `hg` is a Python script, we assume
2407 self._pythondir = self._bindir 2456 # the Mercurial modules are relative to its path and tell the tests
2457 # to load Python modules from its directory.
2458 with open(whg, 'rb') as fh:
2459 initial = fh.read(1024)
2460
2461 if re.match(b'#!.*python', initial):
2462 self._pythondir = self._bindir
2463 # If it looks like our in-repo Rust binary, use the source root.
2464 # This is a bit hacky. But rhg is still not supported outside the
2465 # source directory. So until it is, do the simple thing.
2466 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2467 self._pythondir = os.path.dirname(self._testdir)
2468 # Fall back to the legacy behavior.
2469 else:
2470 self._pythondir = self._bindir
2471
2408 else: 2472 else:
2409 self._installdir = os.path.join(self._hgtmp, b"install") 2473 self._installdir = os.path.join(self._hgtmp, b"install")
2410 self._bindir = os.path.join(self._installdir, b"bin") 2474 self._bindir = os.path.join(self._installdir, b"bin")
2411 self._hgcommand = b'hg' 2475 self._hgcommand = b'hg'
2412 self._tmpbindir = self._bindir 2476 self._tmpbindir = self._bindir
2474 elif 'HGTEST_SLOW' in os.environ: 2538 elif 'HGTEST_SLOW' in os.environ:
2475 del os.environ['HGTEST_SLOW'] 2539 del os.environ['HGTEST_SLOW']
2476 2540
2477 self._coveragefile = os.path.join(self._testdir, b'.coverage') 2541 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2478 2542
2543 if self.options.exceptions:
2544 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2545 try:
2546 os.makedirs(exceptionsdir)
2547 except OSError as e:
2548 if e.errno != errno.EEXIST:
2549 raise
2550
2551 # Remove all existing exception reports.
2552 for f in os.listdir(exceptionsdir):
2553 os.unlink(os.path.join(exceptionsdir, f))
2554
2555 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2556 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2557 self.options.extra_config_opt.append(
2558 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2559
2479 vlog("# Using TESTDIR", self._testdir) 2560 vlog("# Using TESTDIR", self._testdir)
2480 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR']) 2561 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2481 vlog("# Using HGTMP", self._hgtmp) 2562 vlog("# Using HGTMP", self._hgtmp)
2482 vlog("# Using PATH", os.environ["PATH"]) 2563 vlog("# Using PATH", os.environ["PATH"])
2483 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH]) 2564 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2501 self.options.changed, None, 0) 2582 self.options.changed, None, 0)
2502 stdout, stderr = proc.communicate() 2583 stdout, stderr = proc.communicate()
2503 args = stdout.strip(b'\0').split(b'\0') 2584 args = stdout.strip(b'\0').split(b'\0')
2504 else: 2585 else:
2505 args = os.listdir(b'.') 2586 args = os.listdir(b'.')
2587
2588 expanded_args = []
2589 for arg in args:
2590 if os.path.isdir(arg):
2591 if not arg.endswith(b'/'):
2592 arg += b'/'
2593 expanded_args.extend([arg + a for a in os.listdir(arg)])
2594 else:
2595 expanded_args.append(arg)
2596 args = expanded_args
2506 2597
2507 tests = [] 2598 tests = []
2508 for t in args: 2599 for t in args:
2509 if not (os.path.basename(t).startswith(b'test-') 2600 if not (os.path.basename(t).startswith(b'test-')
2510 and (t.endswith(b'.py') or t.endswith(b'.t'))): 2601 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2635 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc) 2726 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2636 2727
2637 t = testcls(refpath, self._outputdir, tmpdir, 2728 t = testcls(refpath, self._outputdir, tmpdir,
2638 keeptmpdir=self.options.keep_tmpdir, 2729 keeptmpdir=self.options.keep_tmpdir,
2639 debug=self.options.debug, 2730 debug=self.options.debug,
2731 first=self.options.first,
2640 timeout=self.options.timeout, 2732 timeout=self.options.timeout,
2641 startport=self._getport(count), 2733 startport=self._getport(count),
2642 extraconfigopts=self.options.extra_config_opt, 2734 extraconfigopts=self.options.extra_config_opt,
2643 py3kwarnings=self.options.py3k_warnings, 2735 py3kwarnings=self.options.py3k_warnings,
2644 shell=self.options.shell, 2736 shell=self.options.shell,
2756 os.remove(installerrs) 2848 os.remove(installerrs)
2757 except OSError as e: 2849 except OSError as e:
2758 if e.errno != errno.ENOENT: 2850 if e.errno != errno.ENOENT:
2759 raise 2851 raise
2760 else: 2852 else:
2761 f = open(installerrs, 'rb') 2853 with open(installerrs, 'rb') as f:
2762 for line in f: 2854 for line in f:
2763 if PYTHON3: 2855 if PYTHON3:
2764 sys.stdout.buffer.write(line) 2856 sys.stdout.buffer.write(line)
2765 else: 2857 else:
2766 sys.stdout.write(line) 2858 sys.stdout.write(line)
2767 f.close()
2768 sys.exit(1) 2859 sys.exit(1)
2769 os.chdir(self._testdir) 2860 os.chdir(self._testdir)
2770 2861
2771 self._usecorrectpython() 2862 self._usecorrectpython()
2772 2863
2773 if self.options.py3k_warnings and not self.options.anycoverage: 2864 if self.options.py3k_warnings and not self.options.anycoverage:
2774 vlog("# Updating hg command to enable Py3k Warnings switch") 2865 vlog("# Updating hg command to enable Py3k Warnings switch")
2775 f = open(os.path.join(self._bindir, 'hg'), 'rb') 2866 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2776 lines = [line.rstrip() for line in f] 2867 lines = [line.rstrip() for line in f]
2777 lines[0] += ' -3' 2868 lines[0] += ' -3'
2778 f.close() 2869 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2779 f = open(os.path.join(self._bindir, 'hg'), 'wb') 2870 for line in lines:
2780 for line in lines: 2871 f.write(line + '\n')
2781 f.write(line + '\n')
2782 f.close()
2783 2872
2784 hgbat = os.path.join(self._bindir, b'hg.bat') 2873 hgbat = os.path.join(self._bindir, b'hg.bat')
2785 if os.path.isfile(hgbat): 2874 if os.path.isfile(hgbat):
2786 # hg.bat expects to be put in bin/scripts while run-tests.py 2875 # hg.bat expects to be put in bin/scripts while run-tests.py
2787 # installation layout put it in bin/ directly. Fix it 2876 # installation layout put it in bin/ directly. Fix it
2788 f = open(hgbat, 'rb') 2877 with open(hgbat, 'rb') as f:
2789 data = f.read() 2878 data = f.read()
2790 f.close()
2791 if b'"%~dp0..\python" "%~dp0hg" %*' in data: 2879 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2792 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*', 2880 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2793 b'"%~dp0python" "%~dp0hg" %*') 2881 b'"%~dp0python" "%~dp0hg" %*')
2794 f = open(hgbat, 'wb') 2882 with open(hgbat, 'wb') as f:
2795 f.write(data) 2883 f.write(data)
2796 f.close()
2797 else: 2884 else:
2798 print('WARNING: cannot fix hg.bat reference to python.exe') 2885 print('WARNING: cannot fix hg.bat reference to python.exe')
2799 2886
2800 if self.options.anycoverage: 2887 if self.options.anycoverage:
2801 custom = os.path.join(self._testdir, 'sitecustomize.py') 2888 custom = os.path.join(self._testdir, 'sitecustomize.py')
2916 vlog("# Found prerequisite", p, "at", found) 3003 vlog("# Found prerequisite", p, "at", found)
2917 else: 3004 else:
2918 print("WARNING: Did not find prerequisite tool: %s " % 3005 print("WARNING: Did not find prerequisite tool: %s " %
2919 p.decode("utf-8")) 3006 p.decode("utf-8"))
2920 3007
3008 def aggregateexceptions(path):
3009 exceptions = collections.Counter()
3010
3011 for f in os.listdir(path):
3012 with open(os.path.join(path, f), 'rb') as fh:
3013 data = fh.read().split(b'\0')
3014 if len(data) != 4:
3015 continue
3016
3017 exc, mainframe, hgframe, hgline = data
3018 exc = exc.decode('utf-8')
3019 mainframe = mainframe.decode('utf-8')
3020 hgframe = hgframe.decode('utf-8')
3021 hgline = hgline.decode('utf-8')
3022 exceptions[(hgframe, hgline, exc)] += 1
3023
3024 return exceptions
3025
2921 if __name__ == '__main__': 3026 if __name__ == '__main__':
2922 runner = TestRunner() 3027 runner = TestRunner()
2923 3028
2924 try: 3029 try:
2925 import msvcrt 3030 import msvcrt