comparison tests/run-tests.py @ 1054:e085b381e8e2

tests: update run-tests.py It's been three and a half years or so, so I'd say we're due. This brings us up to the version from hg @ 274811627808 (during the 4.4 code freeze). In particular, we'll need support for optional output lines added in 6025cac3d02f.
author Kevin Bullock <kbullock@ringworld.org>
date Mon, 23 Oct 2017 15:01:03 -0500
parents 0b33ab75e3cb
children cca56bbea143
comparison
equal deleted inserted replaced
1053:6bb4c99362f0 1054:e085b381e8e2
33 # ./run-tests.py -j2 -c test-s* # currently broken 33 # ./run-tests.py -j2 -c test-s* # currently broken
34 # 8) parallel, coverage, local install: 34 # 8) parallel, coverage, local install:
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) 35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 # 9) parallel, custom tmp dir: 36 # 9) parallel, custom tmp dir:
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests 37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 # 10) parallel, pure, tests that call run-tests:
39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
38 # 40 #
39 # (You could use any subset of the tests: test-s* happens to match 41 # (You could use any subset of the tests: test-s* happens to match
40 # enough that it's worth doing parallel runs, few enough that it 42 # enough that it's worth doing parallel runs, few enough that it
41 # completes fairly quickly, includes both shell and Python scripts, and 43 # completes fairly quickly, includes both shell and Python scripts, and
42 # includes some scripts that run daemon processes.) 44 # includes some scripts that run daemon processes.)
43 45
44 from distutils import version 46 from __future__ import absolute_import, print_function
47
45 import difflib 48 import difflib
49 import distutils.version as version
46 import errno 50 import errno
51 import json
47 import optparse 52 import optparse
48 import os 53 import os
49 import shutil
50 import subprocess
51 import signal
52 import sys
53 import tempfile
54 import time
55 import random 54 import random
56 import re 55 import re
56 import shutil
57 import signal
58 import socket
59 import subprocess
60 import sys
61 import sysconfig
62 import tempfile
57 import threading 63 import threading
58 import killdaemons as killmod 64 import time
59 import Queue as queue 65 import unittest
60 66 import xml.dom.minidom as minidom
67
68 try:
69 import Queue as queue
70 except ImportError:
71 import queue
72
73 try:
74 import shlex
75 shellquote = shlex.quote
76 except (ImportError, AttributeError):
77 import pipes
78 shellquote = pipes.quote
79
80 if os.environ.get('RTUNICODEPEDANTRY', False):
81 try:
82 reload(sys)
83 sys.setdefaultencoding("undefined")
84 except NameError:
85 pass
86
87 origenviron = os.environ.copy()
88 osenvironb = getattr(os, 'environb', os.environ)
61 processlock = threading.Lock() 89 processlock = threading.Lock()
62 90
63 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24 91 pygmentspresent = False
64 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing 92 # ANSI color is unsupported prior to Windows 10
65 # zombies but it's pretty harmless even if we do. 93 if os.name != 'nt':
66 if sys.version_info < (2, 5): 94 try: # is pygments installed
67 subprocess._cleanup = lambda: None 95 import pygments
96 import pygments.lexers as lexers
97 import pygments.lexer as lexer
98 import pygments.formatters as formatters
99 import pygments.token as token
100 import pygments.style as style
101 pygmentspresent = True
102 difflexer = lexers.DiffLexer()
103 terminal256formatter = formatters.Terminal256Formatter()
104 except ImportError:
105 pass
106
107 if pygmentspresent:
108 class TestRunnerStyle(style.Style):
109 default_style = ""
110 skipped = token.string_to_tokentype("Token.Generic.Skipped")
111 failed = token.string_to_tokentype("Token.Generic.Failed")
112 skippedname = token.string_to_tokentype("Token.Generic.SName")
113 failedname = token.string_to_tokentype("Token.Generic.FName")
114 styles = {
115 skipped: '#e5e5e5',
116 skippedname: '#00ffff',
117 failed: '#7f0000',
118 failedname: '#ff0000',
119 }
120
121 class TestRunnerLexer(lexer.RegexLexer):
122 tokens = {
123 'root': [
124 (r'^Skipped', token.Generic.Skipped, 'skipped'),
125 (r'^Failed ', token.Generic.Failed, 'failed'),
126 (r'^ERROR: ', token.Generic.Failed, 'failed'),
127 ],
128 'skipped': [
129 (r'[\w-]+\.(t|py)', token.Generic.SName),
130 (r':.*', token.Generic.Skipped),
131 ],
132 'failed': [
133 (r'[\w-]+\.(t|py)', token.Generic.FName),
134 (r'(:| ).*', token.Generic.Failed),
135 ]
136 }
137
138 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
139 runnerlexer = TestRunnerLexer()
140
141 if sys.version_info > (3, 5, 0):
142 PYTHON3 = True
143 xrange = range # we use xrange in one place, and we'd rather not use range
144 def _bytespath(p):
145 if p is None:
146 return p
147 return p.encode('utf-8')
148
149 def _strpath(p):
150 if p is None:
151 return p
152 return p.decode('utf-8')
153
154 elif sys.version_info >= (3, 0, 0):
155 print('%s is only supported on Python 3.5+ and 2.7, not %s' %
156 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
157 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
158 else:
159 PYTHON3 = False
160
161 # In python 2.x, path operations are generally done using
162 # bytestrings by default, so we don't have to do any extra
163 # fiddling there. We define the wrapper functions anyway just to
164 # help keep code consistent between platforms.
165 def _bytespath(p):
166 return p
167
168 _strpath = _bytespath
169
170 # For Windows support
171 wifexited = getattr(os, "WIFEXITED", lambda x: False)
172
173 # Whether to use IPv6
174 def checksocketfamily(name, port=20058):
175 """return true if we can listen on localhost using family=name
176
177 name should be either 'AF_INET', or 'AF_INET6'.
178 port being used is okay - EADDRINUSE is considered as successful.
179 """
180 family = getattr(socket, name, None)
181 if family is None:
182 return False
183 try:
184 s = socket.socket(family, socket.SOCK_STREAM)
185 s.bind(('localhost', port))
186 s.close()
187 return True
188 except socket.error as exc:
189 if exc.errno == errno.EADDRINUSE:
190 return True
191 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
192 return False
193 else:
194 raise
195 else:
196 return False
197
198 # useipv6 will be set by parseargs
199 useipv6 = None
200
201 def checkportisavailable(port):
202 """return true if a port seems free to bind on localhost"""
203 if useipv6:
204 family = socket.AF_INET6
205 else:
206 family = socket.AF_INET
207 try:
208 s = socket.socket(family, socket.SOCK_STREAM)
209 s.bind(('localhost', port))
210 s.close()
211 return True
212 except socket.error as exc:
213 if exc.errno not in (errno.EADDRINUSE, errno.EADDRNOTAVAIL,
214 errno.EPROTONOSUPPORT):
215 raise
216 return False
68 217
69 closefds = os.name == 'posix' 218 closefds = os.name == 'posix'
70 def Popen4(cmd, wd, timeout, env=None): 219 def Popen4(cmd, wd, timeout, env=None):
71 processlock.acquire() 220 processlock.acquire()
72 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env, 221 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
90 terminate(p) 239 terminate(p)
91 threading.Thread(target=t).start() 240 threading.Thread(target=t).start()
92 241
93 return p 242 return p
94 243
95 # reserved exit code to skip test (used by hghave) 244 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
96 SKIPPED_STATUS = 80 245 IMPL_PATH = b'PYTHONPATH'
97 SKIPPED_PREFIX = 'skipped: '
98 FAILED_PREFIX = 'hghave check failed: '
99 PYTHON = sys.executable.replace('\\', '/')
100 IMPL_PATH = 'PYTHONPATH'
101 if 'java' in sys.platform: 246 if 'java' in sys.platform:
102 IMPL_PATH = 'JYTHONPATH' 247 IMPL_PATH = b'JYTHONPATH'
103
104 requiredtools = [os.path.basename(sys.executable), "diff", "grep", "unzip",
105 "gunzip", "bunzip2", "sed"]
106 createdfiles = []
107 248
108 defaults = { 249 defaults = {
109 'jobs': ('HGTEST_JOBS', 1), 250 'jobs': ('HGTEST_JOBS', 1),
110 'timeout': ('HGTEST_TIMEOUT', 180), 251 'timeout': ('HGTEST_TIMEOUT', 180),
252 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
111 'port': ('HGTEST_PORT', 20059), 253 'port': ('HGTEST_PORT', 20059),
112 'shell': ('HGTEST_SHELL', 'sh'), 254 'shell': ('HGTEST_SHELL', 'sh'),
113 } 255 }
256
257 def canonpath(path):
258 return os.path.realpath(os.path.expanduser(path))
114 259
115 def parselistfiles(files, listtype, warn=True): 260 def parselistfiles(files, listtype, warn=True):
116 entries = dict() 261 entries = dict()
117 for filename in files: 262 for filename in files:
118 try: 263 try:
119 path = os.path.expanduser(os.path.expandvars(filename)) 264 path = os.path.expanduser(os.path.expandvars(filename))
120 f = open(path, "r") 265 f = open(path, "rb")
121 except IOError, err: 266 except IOError as err:
122 if err.errno != errno.ENOENT: 267 if err.errno != errno.ENOENT:
123 raise 268 raise
124 if warn: 269 if warn:
125 print "warning: no such %s file: %s" % (listtype, filename) 270 print("warning: no such %s file: %s" % (listtype, filename))
126 continue 271 continue
127 272
128 for line in f.readlines(): 273 for line in f.readlines():
129 line = line.split('#', 1)[0].strip() 274 line = line.split(b'#', 1)[0].strip()
130 if line: 275 if line:
131 entries[line] = filename 276 entries[line] = filename
132 277
133 f.close() 278 f.close()
134 return entries 279 return entries
135 280
136 def parseargs(): 281 def parsettestcases(path):
282 """read a .t test file, return a set of test case names
283
284 If path does not exist, return an empty set.
285 """
286 cases = set()
287 try:
288 with open(path, 'rb') as f:
289 for l in f:
290 if l.startswith(b'#testcases '):
291 cases.update(l[11:].split())
292 except IOError as ex:
293 if ex.errno != errno.ENOENT:
294 raise
295 return cases
296
297 def getparser():
298 """Obtain the OptionParser used by the CLI."""
137 parser = optparse.OptionParser("%prog [options] [tests]") 299 parser = optparse.OptionParser("%prog [options] [tests]")
138 300
139 # keep these sorted 301 # keep these sorted
140 parser.add_option("--blacklist", action="append", 302 parser.add_option("--blacklist", action="append",
141 help="skip tests listed in the specified blacklist file") 303 help="skip tests listed in the specified blacklist file")
142 parser.add_option("--whitelist", action="append", 304 parser.add_option("--whitelist", action="append",
143 help="always run tests listed in the specified whitelist file") 305 help="always run tests listed in the specified whitelist file")
306 parser.add_option("--test-list", action="append",
307 help="read tests to run from the specified file")
308 parser.add_option("--changed", type="string",
309 help="run tests that are changed in parent rev or working directory")
144 parser.add_option("-C", "--annotate", action="store_true", 310 parser.add_option("-C", "--annotate", action="store_true",
145 help="output files annotated with coverage") 311 help="output files annotated with coverage")
146 parser.add_option("-c", "--cover", action="store_true", 312 parser.add_option("-c", "--cover", action="store_true",
147 help="print a test coverage report") 313 help="print a test coverage report")
314 parser.add_option("--color", choices=["always", "auto", "never"],
315 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
316 help="colorisation: always|auto|never (default: auto)")
148 parser.add_option("-d", "--debug", action="store_true", 317 parser.add_option("-d", "--debug", action="store_true",
149 help="debug mode: write output of test scripts to console" 318 help="debug mode: write output of test scripts to console"
150 " rather than capturing and diff'ing it (disables timeout)") 319 " rather than capturing and diffing it (disables timeout)")
151 parser.add_option("-f", "--first", action="store_true", 320 parser.add_option("-f", "--first", action="store_true",
152 help="exit on the first test failure") 321 help="exit on the first test failure")
153 parser.add_option("-H", "--htmlcov", action="store_true", 322 parser.add_option("-H", "--htmlcov", action="store_true",
154 help="create an HTML report of the coverage of the files") 323 help="create an HTML report of the coverage of the files")
155 parser.add_option("--inotify", action="store_true",
156 help="enable inotify extension when running tests")
157 parser.add_option("-i", "--interactive", action="store_true", 324 parser.add_option("-i", "--interactive", action="store_true",
158 help="prompt to accept changed output") 325 help="prompt to accept changed output")
159 parser.add_option("-j", "--jobs", type="int", 326 parser.add_option("-j", "--jobs", type="int",
160 help="number of jobs to run in parallel" 327 help="number of jobs to run in parallel"
161 " (default: $%s or %d)" % defaults['jobs']) 328 " (default: $%s or %d)" % defaults['jobs'])
162 parser.add_option("--keep-tmpdir", action="store_true", 329 parser.add_option("--keep-tmpdir", action="store_true",
163 help="keep temporary directory after running tests") 330 help="keep temporary directory after running tests")
164 parser.add_option("-k", "--keywords", 331 parser.add_option("-k", "--keywords",
165 help="run tests matching keywords") 332 help="run tests matching keywords")
333 parser.add_option("--list-tests", action="store_true",
334 help="list tests instead of running them")
166 parser.add_option("-l", "--local", action="store_true", 335 parser.add_option("-l", "--local", action="store_true",
167 help="shortcut for --with-hg=<testdir>/../hg") 336 help="shortcut for --with-hg=<testdir>/../hg, "
337 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
168 parser.add_option("--loop", action="store_true", 338 parser.add_option("--loop", action="store_true",
169 help="loop tests repeatedly") 339 help="loop tests repeatedly")
340 parser.add_option("--runs-per-test", type="int", dest="runs_per_test",
341 help="run each test N times (default=1)", default=1)
170 parser.add_option("-n", "--nodiff", action="store_true", 342 parser.add_option("-n", "--nodiff", action="store_true",
171 help="skip showing test changes") 343 help="skip showing test changes")
344 parser.add_option("--outputdir", type="string",
345 help="directory to write error logs to (default=test directory)")
172 parser.add_option("-p", "--port", type="int", 346 parser.add_option("-p", "--port", type="int",
173 help="port on which servers should listen" 347 help="port on which servers should listen"
174 " (default: $%s or %d)" % defaults['port']) 348 " (default: $%s or %d)" % defaults['port'])
175 parser.add_option("--compiler", type="string", 349 parser.add_option("--compiler", type="string",
176 help="compiler to build with") 350 help="compiler to build with")
185 parser.add_option("--shell", type="string", 359 parser.add_option("--shell", type="string",
186 help="shell to use (default: $%s or %s)" % defaults['shell']) 360 help="shell to use (default: $%s or %s)" % defaults['shell'])
187 parser.add_option("-t", "--timeout", type="int", 361 parser.add_option("-t", "--timeout", type="int",
188 help="kill errant tests after TIMEOUT seconds" 362 help="kill errant tests after TIMEOUT seconds"
189 " (default: $%s or %d)" % defaults['timeout']) 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'])
190 parser.add_option("--time", action="store_true", 367 parser.add_option("--time", action="store_true",
191 help="time how long each test takes") 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")
192 parser.add_option("--tmpdir", type="string", 371 parser.add_option("--tmpdir", type="string",
193 help="run tests in the given temporary directory" 372 help="run tests in the given temporary directory"
194 " (implies --keep-tmpdir)") 373 " (implies --keep-tmpdir)")
195 parser.add_option("-v", "--verbose", action="store_true", 374 parser.add_option("-v", "--verbose", action="store_true",
196 help="output verbose messages") 375 help="output verbose messages")
376 parser.add_option("--xunit", type="string",
377 help="record xunit results at specified path")
197 parser.add_option("--view", type="string", 378 parser.add_option("--view", type="string",
198 help="external diff viewer") 379 help="external diff viewer")
199 parser.add_option("--with-hg", type="string", 380 parser.add_option("--with-hg", type="string",
200 metavar="HG", 381 metavar="HG",
201 help="test using specified hg script rather than a " 382 help="test using specified hg script rather than a "
202 "temporary installation") 383 "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")
203 parser.add_option("-3", "--py3k-warnings", action="store_true", 390 parser.add_option("-3", "--py3k-warnings", action="store_true",
204 help="enable Py3k warnings on Python 2.6+") 391 help="enable Py3k warnings on Python 2.7+")
392 # This option should be deleted once test-check-py3-compat.t and other
393 # Python 3 tests run with Python 3.
394 parser.add_option("--with-python3", metavar="PYTHON3",
395 help="Python 3 interpreter (if running under Python 2)"
396 " (TEMPORARY)")
205 parser.add_option('--extra-config-opt', action="append", 397 parser.add_option('--extra-config-opt', action="append",
206 help='set the given config opt in the test hgrc') 398 help='set the given config opt in the test hgrc')
207 parser.add_option('--random', action="store_true", 399 parser.add_option('--random', action="store_true",
208 help='run tests in random order') 400 help='run tests in random order')
401 parser.add_option('--profile-runner', action='store_true',
402 help='run statprof on run-tests')
403 parser.add_option('--allow-slow-tests', action='store_true',
404 help='allow extremely slow tests')
405 parser.add_option('--showchannels', action='store_true',
406 help='show scheduling channels')
407 parser.add_option('--known-good-rev', type="string",
408 metavar="known_good_rev",
409 help=("Automatically bisect any failures using this "
410 "revision as a known-good revision."))
411 parser.add_option('--bisect-repo', type="string",
412 metavar='bisect_repo',
413 help=("Path of a repo to bisect. Use together with "
414 "--known-good-rev"))
209 415
210 for option, (envvar, default) in defaults.items(): 416 for option, (envvar, default) in defaults.items():
211 defaults[option] = type(default)(os.environ.get(envvar, default)) 417 defaults[option] = type(default)(os.environ.get(envvar, default))
212 parser.set_defaults(**defaults) 418 parser.set_defaults(**defaults)
213 (options, args) = parser.parse_args() 419
420 return parser
421
422 def parseargs(args, parser):
423 """Parse arguments with our OptionParser and validate results."""
424 (options, args) = parser.parse_args(args)
214 425
215 # jython is always pure 426 # jython is always pure
216 if 'java' in sys.platform or '__pypy__' in sys.modules: 427 if 'java' in sys.platform or '__pypy__' in sys.modules:
217 options.pure = True 428 options.pure = True
218 429
219 if options.with_hg: 430 if options.with_hg:
220 options.with_hg = os.path.expanduser(options.with_hg) 431 options.with_hg = canonpath(_bytespath(options.with_hg))
221 if not (os.path.isfile(options.with_hg) and 432 if not (os.path.isfile(options.with_hg) and
222 os.access(options.with_hg, os.X_OK)): 433 os.access(options.with_hg, os.X_OK)):
223 parser.error('--with-hg must specify an executable hg script') 434 parser.error('--with-hg must specify an executable hg script')
224 if not os.path.basename(options.with_hg) == 'hg': 435 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
225 sys.stderr.write('warning: --with-hg should specify an hg script\n') 436 sys.stderr.write('warning: --with-hg should specify an hg script\n')
226 if options.local: 437 if options.local:
227 testdir = os.path.dirname(os.path.realpath(sys.argv[0])) 438 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
228 hgbin = os.path.join(os.path.dirname(testdir), 'hg') 439 reporootdir = os.path.dirname(testdir)
229 if os.name != 'nt' and not os.access(hgbin, os.X_OK): 440 pathandattrs = [(b'hg', 'with_hg')]
230 parser.error('--local specified, but %r not found or not executable' 441 if options.chg:
231 % hgbin) 442 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
232 options.with_hg = hgbin 443 for relpath, attr in pathandattrs:
444 binpath = os.path.join(reporootdir, relpath)
445 if os.name != 'nt' and not os.access(binpath, os.X_OK):
446 parser.error('--local specified, but %r not found or '
447 'not executable' % binpath)
448 setattr(options, attr, binpath)
449
450 if (options.chg or options.with_chg) and os.name == 'nt':
451 parser.error('chg does not work on %s' % os.name)
452 if options.with_chg:
453 options.chg = False # no installation to temporary location
454 options.with_chg = canonpath(_bytespath(options.with_chg))
455 if not (os.path.isfile(options.with_chg) and
456 os.access(options.with_chg, os.X_OK)):
457 parser.error('--with-chg must specify a chg executable')
458 if options.chg and options.with_hg:
459 # chg shares installation location with hg
460 parser.error('--chg does not work when --with-hg is specified '
461 '(use --with-chg instead)')
462
463 if options.color == 'always' and not pygmentspresent:
464 sys.stderr.write('warning: --color=always ignored because '
465 'pygments is not installed\n')
466
467 if options.bisect_repo and not options.known_good_rev:
468 parser.error("--bisect-repo cannot be used without --known-good-rev")
469
470 global useipv6
471 if options.ipv6:
472 useipv6 = checksocketfamily('AF_INET6')
473 else:
474 # only use IPv6 if IPv4 is unavailable and IPv6 is available
475 useipv6 = ((not checksocketfamily('AF_INET'))
476 and checksocketfamily('AF_INET6'))
233 477
234 options.anycoverage = options.cover or options.annotate or options.htmlcov 478 options.anycoverage = options.cover or options.annotate or options.htmlcov
235 if options.anycoverage: 479 if options.anycoverage:
236 try: 480 try:
237 import coverage 481 import coverage
244 if options.anycoverage and options.local: 488 if options.anycoverage and options.local:
245 # this needs some path mangling somewhere, I guess 489 # this needs some path mangling somewhere, I guess
246 parser.error("sorry, coverage options do not work when --local " 490 parser.error("sorry, coverage options do not work when --local "
247 "is specified") 491 "is specified")
248 492
493 if options.anycoverage and options.with_hg:
494 parser.error("sorry, coverage options do not work when --with-hg "
495 "is specified")
496
249 global verbose 497 global verbose
250 if options.verbose: 498 if options.verbose:
251 verbose = '' 499 verbose = ''
252 500
253 if options.tmpdir: 501 if options.tmpdir:
254 options.tmpdir = os.path.expanduser(options.tmpdir) 502 options.tmpdir = canonpath(options.tmpdir)
255 503
256 if options.jobs < 1: 504 if options.jobs < 1:
257 parser.error('--jobs must be positive') 505 parser.error('--jobs must be positive')
258 if options.interactive and options.debug: 506 if options.interactive and options.debug:
259 parser.error("-i/--interactive and -d/--debug are incompatible") 507 parser.error("-i/--interactive and -d/--debug are incompatible")
260 if options.debug: 508 if options.debug:
261 if options.timeout != defaults['timeout']: 509 if options.timeout != defaults['timeout']:
262 sys.stderr.write( 510 sys.stderr.write(
263 'warning: --timeout option ignored with --debug\n') 511 'warning: --timeout option ignored with --debug\n')
512 if options.slowtimeout != defaults['slowtimeout']:
513 sys.stderr.write(
514 'warning: --slowtimeout option ignored with --debug\n')
264 options.timeout = 0 515 options.timeout = 0
516 options.slowtimeout = 0
265 if options.py3k_warnings: 517 if options.py3k_warnings:
266 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): 518 if PYTHON3:
267 parser.error('--py3k-warnings can only be used on Python 2.6+') 519 parser.error(
520 '--py3k-warnings can only be used on Python 2.7')
521 if options.with_python3:
522 if PYTHON3:
523 parser.error('--with-python3 cannot be used when executing with '
524 'Python 3')
525
526 options.with_python3 = canonpath(options.with_python3)
527 # Verify Python3 executable is acceptable.
528 proc = subprocess.Popen([options.with_python3, b'--version'],
529 stdout=subprocess.PIPE,
530 stderr=subprocess.STDOUT)
531 out, _err = proc.communicate()
532 ret = proc.wait()
533 if ret != 0:
534 parser.error('could not determine version of python 3')
535 if not out.startswith('Python '):
536 parser.error('unexpected output from python3 --version: %s' %
537 out)
538 vers = version.LooseVersion(out[len('Python '):])
539 if vers < version.LooseVersion('3.5.0'):
540 parser.error('--with-python3 version must be 3.5.0 or greater; '
541 'got %s' % out)
542
268 if options.blacklist: 543 if options.blacklist:
269 options.blacklist = parselistfiles(options.blacklist, 'blacklist') 544 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
270 if options.whitelist: 545 if options.whitelist:
271 options.whitelisted = parselistfiles(options.whitelist, 'whitelist') 546 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
272 else: 547 else:
273 options.whitelisted = {} 548 options.whitelisted = {}
274 549
550 if options.showchannels:
551 options.nodiff = True
552
275 return (options, args) 553 return (options, args)
276 554
277 def rename(src, dst): 555 def rename(src, dst):
278 """Like os.rename(), trade atomicity and opened files friendliness 556 """Like os.rename(), trade atomicity and opened files friendliness
279 for existing destination support. 557 for existing destination support.
280 """ 558 """
281 shutil.copy(src, dst) 559 shutil.copy(src, dst)
282 os.remove(src) 560 os.remove(src)
283 561
284 def parsehghaveoutput(lines): 562 _unified_diff = difflib.unified_diff
285 '''Parse hghave log lines. 563 if PYTHON3:
286 Return tuple of lists (missing, failed): 564 import functools
287 * the missing/unknown features 565 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
288 * the features for which existence check failed''' 566
289 missing = [] 567 def getdiff(expected, output, ref, err):
290 failed = [] 568 servefail = False
291 for line in lines: 569 lines = []
292 if line.startswith(SKIPPED_PREFIX): 570 for line in _unified_diff(expected, output, ref, err):
293 line = line.splitlines()[0] 571 if line.startswith(b'+++') or line.startswith(b'---'):
294 missing.append(line[len(SKIPPED_PREFIX):]) 572 line = line.replace(b'\\', b'/')
295 elif line.startswith(FAILED_PREFIX): 573 if line.endswith(b' \n'):
296 line = line.splitlines()[0] 574 line = line[:-2] + b'\n'
297 failed.append(line[len(FAILED_PREFIX):]) 575 lines.append(line)
298 576 if not servefail and line.startswith(
299 return missing, failed 577 b'+ abort: child process failed to start'):
300 578 servefail = True
301 def showdiff(expected, output, ref, err): 579
302 print 580 return servefail, lines
303 for line in difflib.unified_diff(expected, output, ref, err):
304 sys.stdout.write(line)
305 581
306 verbose = False 582 verbose = False
307 def vlog(*msg): 583 def vlog(*msg):
308 if verbose is not False: 584 """Log only when in verbose mode."""
309 iolock.acquire() 585 if verbose is False:
586 return
587
588 return log(*msg)
589
590 # Bytes that break XML even in a CDATA block: control characters 0-31
591 # sans \t, \n and \r
592 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
593
594 # Match feature conditionalized output lines in the form, capturing the feature
595 # list in group 2, and the preceeding line output in group 1:
596 #
597 # output..output (feature !)\n
598 optline = re.compile(b'(.*) \((.+?) !\)\n$')
599
600 def cdatasafe(data):
601 """Make a string safe to include in a CDATA block.
602
603 Certain control characters are illegal in a CDATA block, and
604 there's no way to include a ]]> in a CDATA either. This function
605 replaces illegal bytes with ? and adds a space between the ]] so
606 that it won't break the CDATA block.
607 """
608 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
609
610 def log(*msg):
611 """Log something to stdout.
612
613 Arguments are strings to print.
614 """
615 with iolock:
310 if verbose: 616 if verbose:
311 print verbose, 617 print(verbose, end=' ')
312 for m in msg: 618 for m in msg:
313 print m, 619 print(m, end=' ')
314 print 620 print()
315 sys.stdout.flush() 621 sys.stdout.flush()
316 iolock.release() 622
317 623 def highlightdiff(line, color):
318 def log(*msg): 624 if not color:
319 iolock.acquire() 625 return line
320 if verbose: 626 assert pygmentspresent
321 print verbose, 627 return pygments.highlight(line.decode('latin1'), difflexer,
322 for m in msg: 628 terminal256formatter).encode('latin1')
323 print m, 629
324 print 630 def highlightmsg(msg, color):
325 sys.stdout.flush() 631 if not color:
326 iolock.release() 632 return msg
327 633 assert pygmentspresent
328 def findprogram(program): 634 return pygments.highlight(msg, runnerlexer, runnerformatter)
329 """Search PATH for a executable program""" 635
330 for p in os.environ.get('PATH', os.defpath).split(os.pathsep): 636 def terminate(proc):
331 name = os.path.join(p, program) 637 """Terminate subprocess"""
332 if os.name == 'nt' or os.access(name, os.X_OK): 638 vlog('# Terminating process %d' % proc.pid)
333 return name 639 try:
334 return None 640 proc.terminate()
335 641 except OSError:
336 def createhgrc(path, options): 642 pass
337 # create a fresh hgrc 643
338 hgrc = open(path, 'w') 644 def killdaemons(pidfile):
339 hgrc.write('[ui]\n') 645 import killdaemons as killmod
340 hgrc.write('slash = True\n') 646 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
341 hgrc.write('interactive = False\n') 647 logfn=vlog)
342 hgrc.write('[defaults]\n') 648
343 hgrc.write('backout = -d "0 0"\n') 649 class Test(unittest.TestCase):
344 hgrc.write('commit = -d "0 0"\n') 650 """Encapsulates a single, runnable test.
345 hgrc.write('shelve = --date "0 0"\n') 651
346 hgrc.write('tag = -d "0 0"\n') 652 While this class conforms to the unittest.TestCase API, it differs in that
347 if options.inotify: 653 instances need to be instantiated manually. (Typically, unittest.TestCase
348 hgrc.write('[extensions]\n') 654 classes are instantiated automatically by scanning modules.)
349 hgrc.write('inotify=\n') 655 """
350 hgrc.write('[inotify]\n') 656
351 hgrc.write('pidfile=daemon.pids') 657 # Status code reserved for skipped tests (used by hghave).
352 hgrc.write('appendpid=True\n') 658 SKIPPED_STATUS = 80
353 if options.extra_config_opt: 659
354 for opt in options.extra_config_opt: 660 def __init__(self, path, outputdir, tmpdir, keeptmpdir=False,
661 debug=False,
662 timeout=None,
663 startport=None, extraconfigopts=None,
664 py3kwarnings=False, shell=None, hgcommand=None,
665 slowtimeout=None, usechg=False,
666 useipv6=False):
667 """Create a test from parameters.
668
669 path is the full path to the file defining the test.
670
671 tmpdir is the main temporary directory to use for this test.
672
673 keeptmpdir determines whether to keep the test's temporary directory
674 after execution. It defaults to removal (False).
675
676 debug mode will make the test execute verbosely, with unfiltered
677 output.
678
679 timeout controls the maximum run time of the test. It is ignored when
680 debug is True. See slowtimeout for tests with #require slow.
681
682 slowtimeout overrides timeout if the test has #require slow.
683
684 startport controls the starting port number to use for this test. Each
685 test will reserve 3 port numbers for execution. It is the caller's
686 responsibility to allocate a non-overlapping port range to Test
687 instances.
688
689 extraconfigopts is an iterable of extra hgrc config options. Values
690 must have the form "key=value" (something understood by hgrc). Values
691 of the form "foo.key=value" will result in "[foo] key=value".
692
693 py3kwarnings enables Py3k warnings.
694
695 shell is the shell to execute tests in.
696 """
697 if timeout is None:
698 timeout = defaults['timeout']
699 if startport is None:
700 startport = defaults['port']
701 if slowtimeout is None:
702 slowtimeout = defaults['slowtimeout']
703 self.path = path
704 self.bname = os.path.basename(path)
705 self.name = _strpath(self.bname)
706 self._testdir = os.path.dirname(path)
707 self._outputdir = outputdir
708 self._tmpname = os.path.basename(path)
709 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
710
711 self._threadtmp = tmpdir
712 self._keeptmpdir = keeptmpdir
713 self._debug = debug
714 self._timeout = timeout
715 self._slowtimeout = slowtimeout
716 self._startport = startport
717 self._extraconfigopts = extraconfigopts or []
718 self._py3kwarnings = py3kwarnings
719 self._shell = _bytespath(shell)
720 self._hgcommand = hgcommand or b'hg'
721 self._usechg = usechg
722 self._useipv6 = useipv6
723
724 self._aborted = False
725 self._daemonpids = []
726 self._finished = None
727 self._ret = None
728 self._out = None
729 self._skipped = None
730 self._testtmp = None
731 self._chgsockdir = None
732
733 self._refout = self.readrefout()
734
735 def readrefout(self):
736 """read reference output"""
737 # If we're not in --debug mode and reference output file exists,
738 # check test output against it.
739 if self._debug:
740 return None # to match "out is None"
741 elif os.path.exists(self.refpath):
742 with open(self.refpath, 'rb') as f:
743 return f.read().splitlines(True)
744 else:
745 return []
746
747 # needed to get base class __repr__ running
748 @property
749 def _testMethodName(self):
750 return self.name
751
752 def __str__(self):
753 return self.name
754
755 def shortDescription(self):
756 return self.name
757
758 def setUp(self):
759 """Tasks to perform before run()."""
760 self._finished = False
761 self._ret = None
762 self._out = None
763 self._skipped = None
764
765 try:
766 os.mkdir(self._threadtmp)
767 except OSError as e:
768 if e.errno != errno.EEXIST:
769 raise
770
771 name = self._tmpname
772 self._testtmp = os.path.join(self._threadtmp, name)
773 os.mkdir(self._testtmp)
774
775 # Remove any previous output files.
776 if os.path.exists(self.errpath):
777 try:
778 os.remove(self.errpath)
779 except OSError as e:
780 # We might have raced another test to clean up a .err
781 # file, so ignore ENOENT when removing a previous .err
782 # file.
783 if e.errno != errno.ENOENT:
784 raise
785
786 if self._usechg:
787 self._chgsockdir = os.path.join(self._threadtmp,
788 b'%s.chgsock' % name)
789 os.mkdir(self._chgsockdir)
790
791 def run(self, result):
792 """Run this test and report results against a TestResult instance."""
793 # This function is extremely similar to unittest.TestCase.run(). Once
794 # we require Python 2.7 (or at least its version of unittest), this
795 # function can largely go away.
796 self._result = result
797 result.startTest(self)
798 try:
799 try:
800 self.setUp()
801 except (KeyboardInterrupt, SystemExit):
802 self._aborted = True
803 raise
804 except Exception:
805 result.addError(self, sys.exc_info())
806 return
807
808 success = False
809 try:
810 self.runTest()
811 except KeyboardInterrupt:
812 self._aborted = True
813 raise
814 except unittest.SkipTest as e:
815 result.addSkip(self, str(e))
816 # The base class will have already counted this as a
817 # test we "ran", but we want to exclude skipped tests
818 # from those we count towards those run.
819 result.testsRun -= 1
820 except self.failureException as e:
821 # This differs from unittest in that we don't capture
822 # the stack trace. This is for historical reasons and
823 # this decision could be revisited in the future,
824 # especially for PythonTest instances.
825 if result.addFailure(self, str(e)):
826 success = True
827 except Exception:
828 result.addError(self, sys.exc_info())
829 else:
830 success = True
831
832 try:
833 self.tearDown()
834 except (KeyboardInterrupt, SystemExit):
835 self._aborted = True
836 raise
837 except Exception:
838 result.addError(self, sys.exc_info())
839 success = False
840
841 if success:
842 result.addSuccess(self)
843 finally:
844 result.stopTest(self, interrupted=self._aborted)
845
846 def runTest(self):
847 """Run this test instance.
848
849 This will return a tuple describing the result of the test.
850 """
851 env = self._getenv()
852 self._genrestoreenv(env)
853 self._daemonpids.append(env['DAEMON_PIDS'])
854 self._createhgrc(env['HGRCPATH'])
855
856 vlog('# Test', self.name)
857
858 ret, out = self._run(env)
859 self._finished = True
860 self._ret = ret
861 self._out = out
862
863 def describe(ret):
864 if ret < 0:
865 return 'killed by signal: %d' % -ret
866 return 'returned error code %d' % ret
867
868 self._skipped = False
869
870 if ret == self.SKIPPED_STATUS:
871 if out is None: # Debug mode, nothing to parse.
872 missing = ['unknown']
873 failed = None
874 else:
875 missing, failed = TTest.parsehghaveoutput(out)
876
877 if not missing:
878 missing = ['skipped']
879
880 if failed:
881 self.fail('hg have failed checking for %s' % failed[-1])
882 else:
883 self._skipped = True
884 raise unittest.SkipTest(missing[-1])
885 elif ret == 'timeout':
886 self.fail('timed out')
887 elif ret is False:
888 self.fail('no result code from test')
889 elif out != self._refout:
890 # Diff generation may rely on written .err file.
891 if (ret != 0 or out != self._refout) and not self._skipped \
892 and not self._debug:
893 f = open(self.errpath, 'wb')
894 for line in out:
895 f.write(line)
896 f.close()
897
898 # The result object handles diff calculation for us.
899 if self._result.addOutputMismatch(self, ret, out, self._refout):
900 # change was accepted, skip failing
901 return
902
903 if ret:
904 msg = 'output changed and ' + describe(ret)
905 else:
906 msg = 'output changed'
907
908 self.fail(msg)
909 elif ret:
910 self.fail(describe(ret))
911
912 def tearDown(self):
913 """Tasks to perform after run()."""
914 for entry in self._daemonpids:
915 killdaemons(entry)
916 self._daemonpids = []
917
918 if self._keeptmpdir:
919 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
920 (self._testtmp.decode('utf-8'),
921 self._threadtmp.decode('utf-8')))
922 else:
923 shutil.rmtree(self._testtmp, True)
924 shutil.rmtree(self._threadtmp, True)
925
926 if self._usechg:
927 # chgservers will stop automatically after they find the socket
928 # files are deleted
929 shutil.rmtree(self._chgsockdir, True)
930
931 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
932 and not self._debug and self._out:
933 f = open(self.errpath, 'wb')
934 for line in self._out:
935 f.write(line)
936 f.close()
937
938 vlog("# Ret was:", self._ret, '(%s)' % self.name)
939
940 def _run(self, env):
941 # This should be implemented in child classes to run tests.
942 raise unittest.SkipTest('unknown test type')
943
944 def abort(self):
945 """Terminate execution of this test."""
946 self._aborted = True
947
948 def _portmap(self, i):
949 offset = b'' if i == 0 else b'%d' % i
950 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
951
952 def _getreplacements(self):
953 """Obtain a mapping of text replacements to apply to test output.
954
955 Test output needs to be normalized so it can be compared to expected
956 output. This function defines how some of that normalization will
957 occur.
958 """
959 r = [
960 # This list should be parallel to defineport in _getenv
961 self._portmap(0),
962 self._portmap(1),
963 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'),
967 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
968 ]
969 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
970
971 return r
972
973 def _escapepath(self, p):
974 if os.name == 'nt':
975 return (
976 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
977 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
978 for c in p))
979 )
980 else:
981 return re.escape(p)
982
983 def _localip(self):
984 if self._useipv6:
985 return b'::1'
986 else:
987 return b'127.0.0.1'
988
989 def _genrestoreenv(self, testenv):
990 """Generate a script that can be used by tests to restore the original
991 environment."""
992 # Put the restoreenv script inside self._threadtmp
993 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
994 testenv['HGTEST_RESTOREENV'] = scriptpath
995
996 # Only restore environment variable names that the shell allows
997 # us to export.
998 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
999
1000 # Do not restore these variables; otherwise tests would fail.
1001 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1002
1003 with open(scriptpath, 'w') as envf:
1004 for name, value in origenviron.items():
1005 if not name_regex.match(name):
1006 # Skip environment variables with unusual names not
1007 # allowed by most shells.
1008 continue
1009 if name in reqnames:
1010 continue
1011 envf.write('%s=%s\n' % (name, shellquote(value)))
1012
1013 for name in testenv:
1014 if name in origenviron or name in reqnames:
1015 continue
1016 envf.write('unset %s\n' % (name,))
1017
1018 def _getenv(self):
1019 """Obtain environment variables to use during test execution."""
1020 def defineport(i):
1021 offset = '' if i == 0 else '%s' % i
1022 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1023 env = os.environ.copy()
1024 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase')
1025 env['HGEMITWARNINGS'] = '1'
1026 env['TESTTMP'] = self._testtmp
1027 env['HOME'] = self._testtmp
1028 # This number should match portneeded in _getport
1029 for port in xrange(3):
1030 # This list should be parallel to _portmap in _getreplacements
1031 defineport(port)
1032 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
1033 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
1034 env["HGEDITOR"] = ('"' + sys.executable + '"'
1035 + ' -c "import sys; sys.exit(0)"')
1036 env["HGMERGE"] = "internal:merge"
1037 env["HGUSER"] = "test"
1038 env["HGENCODING"] = "ascii"
1039 env["HGENCODINGMODE"] = "strict"
1040 env['HGIPV6'] = str(int(self._useipv6))
1041
1042 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1043 # IP addresses.
1044 env['LOCALIP'] = self._localip()
1045
1046 # Reset some environment variables to well-known values so that
1047 # the tests produce repeatable output.
1048 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1049 env['TZ'] = 'GMT'
1050 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1051 env['COLUMNS'] = '80'
1052 env['TERM'] = 'xterm'
1053
1054 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
1055 'HGPLAIN HGPLAINEXCEPT EDITOR VISUAL PAGER ' +
1056 'NO_PROXY CHGDEBUG').split():
1057 if k in env:
1058 del env[k]
1059
1060 # unset env related to hooks
1061 for k in env.keys():
1062 if k.startswith('HG_'):
1063 del env[k]
1064
1065 if self._usechg:
1066 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1067
1068 return env
1069
1070 def _createhgrc(self, path):
1071 """Create an hgrc file for this test."""
1072 hgrc = open(path, 'wb')
1073 hgrc.write(b'[ui]\n')
1074 hgrc.write(b'slash = True\n')
1075 hgrc.write(b'interactive = False\n')
1076 hgrc.write(b'mergemarkers = detailed\n')
1077 hgrc.write(b'promptecho = True\n')
1078 hgrc.write(b'[defaults]\n')
1079 hgrc.write(b'[devel]\n')
1080 hgrc.write(b'all-warnings = true\n')
1081 hgrc.write(b'default-date = 0 0\n')
1082 hgrc.write(b'[largefiles]\n')
1083 hgrc.write(b'usercache = %s\n' %
1084 (os.path.join(self._testtmp, b'.cache/largefiles')))
1085 hgrc.write(b'[web]\n')
1086 hgrc.write(b'address = localhost\n')
1087 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1088
1089 for opt in self._extraconfigopts:
355 section, key = opt.split('.', 1) 1090 section, key = opt.split('.', 1)
356 assert '=' in key, ('extra config opt %s must ' 1091 assert '=' in key, ('extra config opt %s must '
357 'have an = for assignment' % opt) 1092 'have an = for assignment' % opt)
358 hgrc.write('[%s]\n%s\n' % (section, key)) 1093 hgrc.write(b'[%s]\n%s\n' % (section, key))
359 hgrc.close() 1094 hgrc.close()
360 1095
361 def createenv(options, testtmp, threadtmp, port): 1096 def fail(self, msg):
362 env = os.environ.copy() 1097 # unittest differentiates between errored and failed.
363 env['TESTTMP'] = testtmp 1098 # Failed is denoted by AssertionError (by default at least).
364 env['HOME'] = testtmp 1099 raise AssertionError(msg)
365 env["HGPORT"] = str(port) 1100
366 env["HGPORT1"] = str(port + 1) 1101 def _runcommand(self, cmd, env, normalizenewlines=False):
367 env["HGPORT2"] = str(port + 2) 1102 """Run command in a sub-process, capturing the output (stdout and
368 env["HGRCPATH"] = os.path.join(threadtmp, '.hgrc') 1103 stderr).
369 env["DAEMON_PIDS"] = os.path.join(threadtmp, 'daemon.pids') 1104
370 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' 1105 Return a tuple (exitcode, output). output is None in debug mode.
371 env["HGMERGE"] = "internal:merge" 1106 """
372 env["HGUSER"] = "test" 1107 if self._debug:
373 env["HGENCODING"] = "ascii" 1108 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
374 env["HGENCODINGMODE"] = "strict" 1109 env=env)
375 1110 ret = proc.wait()
376 # Reset some environment variables to well-known values so that 1111 return (ret, None)
377 # the tests produce repeatable output. 1112
378 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C' 1113 proc = Popen4(cmd, self._testtmp, self._timeout, env)
379 env['TZ'] = 'GMT' 1114 def cleanup():
380 env["EMAIL"] = "Foo Bar <foo.bar@example.com>" 1115 terminate(proc)
381 env['COLUMNS'] = '80' 1116 ret = proc.wait()
382 env['TERM'] = 'xterm' 1117 if ret == 0:
383 1118 ret = signal.SIGTERM << 8
384 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' + 1119 killdaemons(env['DAEMON_PIDS'])
385 'NO_PROXY').split(): 1120 return ret
386 if k in env: 1121
387 del env[k] 1122 output = ''
388 1123 proc.tochild.close()
389 # unset env related to hooks 1124
390 for k in env.keys():
391 if k.startswith('HG_'):
392 del env[k]
393
394 return env
395
396 def checktools():
397 # Before we go any further, check for pre-requisite tools
398 # stuff from coreutils (cat, rm, etc) are not tested
399 for p in requiredtools:
400 if os.name == 'nt' and not p.endswith('.exe'):
401 p += '.exe'
402 found = findprogram(p)
403 if found:
404 vlog("# Found prerequisite", p, "at", found)
405 else:
406 print "WARNING: Did not find prerequisite tool: "+p
407
408 def terminate(proc):
409 """Terminate subprocess (with fallback for Python versions < 2.6)"""
410 vlog('# Terminating process %d' % proc.pid)
411 try:
412 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
413 except OSError:
414 pass
415
416 def killdaemons(pidfile):
417 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
418 logfn=vlog)
419
420 def cleanup(options):
421 if not options.keep_tmpdir:
422 vlog("# Cleaning up HGTMP", HGTMP)
423 shutil.rmtree(HGTMP, True)
424 for f in createdfiles:
425 try:
426 os.remove(f)
427 except OSError:
428 pass
429
430 def usecorrectpython():
431 # some tests run python interpreter. they must use same
432 # interpreter we use or bad things will happen.
433 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
434 if getattr(os, 'symlink', None):
435 vlog("# Making python executable in test path a symlink to '%s'" %
436 sys.executable)
437 mypython = os.path.join(TMPBINDIR, pyexename)
438 try: 1125 try:
439 if os.readlink(mypython) == sys.executable: 1126 output = proc.fromchild.read()
440 return 1127 except KeyboardInterrupt:
441 os.unlink(mypython) 1128 vlog('# Handling keyboard interrupt')
442 except OSError, err: 1129 cleanup()
443 if err.errno != errno.ENOENT: 1130 raise
444 raise 1131
445 if findprogram(pyexename) != sys.executable: 1132 ret = proc.wait()
446 try: 1133 if wifexited(ret):
447 os.symlink(sys.executable, mypython) 1134 ret = os.WEXITSTATUS(ret)
448 createdfiles.append(mypython) 1135
449 except OSError, err: 1136 if proc.timeout:
450 # child processes may race, which is harmless 1137 ret = 'timeout'
451 if err.errno != errno.EEXIST: 1138
452 raise 1139 if ret:
453 else: 1140 killdaemons(env['DAEMON_PIDS'])
454 exedir, exename = os.path.split(sys.executable) 1141
455 vlog("# Modifying search path to find %s as %s in '%s'" % 1142 for s, r in self._getreplacements():
456 (exename, pyexename, exedir)) 1143 output = re.sub(s, r, output)
457 path = os.environ['PATH'].split(os.pathsep) 1144
458 while exedir in path: 1145 if normalizenewlines:
459 path.remove(exedir) 1146 output = output.replace('\r\n', '\n')
460 os.environ['PATH'] = os.pathsep.join([exedir] + path) 1147
461 if not findprogram(pyexename): 1148 return ret, output.splitlines(True)
462 print "WARNING: Cannot find %s in search path" % pyexename 1149
463 1150 class PythonTest(Test):
464 def installhg(options): 1151 """A Python-based test."""
465 vlog("# Performing temporary installation of HG") 1152
466 installerrs = os.path.join("tests", "install.err") 1153 @property
467 compiler = '' 1154 def refpath(self):
468 if options.compiler: 1155 return os.path.join(self._testdir, b'%s.out' % self.bname)
469 compiler = '--compiler ' + options.compiler 1156
470 pure = options.pure and "--pure" or "" 1157 def _run(self, env):
471 py3 = '' 1158 py3kswitch = self._py3kwarnings and b' -3' or b''
472 if sys.version_info[0] == 3: 1159 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
473 py3 = '--c2to3' 1160 vlog("# Running", cmd)
474 1161 normalizenewlines = os.name == 'nt'
475 # Run installer in hg root 1162 result = self._runcommand(cmd, env,
476 script = os.path.realpath(sys.argv[0]) 1163 normalizenewlines=normalizenewlines)
477 hgroot = os.path.dirname(os.path.dirname(script)) 1164 if self._aborted:
478 os.chdir(hgroot) 1165 raise KeyboardInterrupt()
479 nohome = '--home=""' 1166
480 if os.name == 'nt': 1167 return result
481 # The --home="" trick works only on OS where os.sep == '/' 1168
482 # because of a distutils convert_path() fast-path. Avoid it at 1169 # Some glob patterns apply only in some circumstances, so the script
483 # least on Windows for now, deal with .pydistutils.cfg bugs 1170 # might want to remove (glob) annotations that otherwise should be
484 # when they happen. 1171 # retained.
485 nohome = '' 1172 checkcodeglobpats = [
486 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all' 1173 # On Windows it looks like \ doesn't require a (glob), but we know
487 ' build %(compiler)s --build-base="%(base)s"' 1174 # better.
488 ' install --force --prefix="%(prefix)s" --install-lib="%(libdir)s"' 1175 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
489 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1' 1176 re.compile(br'^moving \S+/.*[^)]$'),
490 % dict(exe=sys.executable, py3=py3, pure=pure, compiler=compiler, 1177 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
491 base=os.path.join(HGTMP, "build"), 1178 # Not all platforms have 127.0.0.1 as loopback (though most do),
492 prefix=INST, libdir=PYTHONDIR, bindir=BINDIR, 1179 # so we always glob that too.
493 nohome=nohome, logfile=installerrs)) 1180 re.compile(br'.*\$LOCALIP.*$'),
494 vlog("# Running", cmd) 1181 ]
495 if os.system(cmd) == 0: 1182
496 if not options.verbose: 1183 bchr = chr
497 os.remove(installerrs) 1184 if PYTHON3:
498 else: 1185 bchr = lambda x: bytes([x])
499 f = open(installerrs) 1186
500 for line in f: 1187 class TTest(Test):
501 print line, 1188 """A "t test" is a test backed by a .t file."""
1189
1190 SKIPPED_PREFIX = b'skipped: '
1191 FAILED_PREFIX = b'hghave check failed: '
1192 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1193
1194 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1195 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
1196 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1197
1198 def __init__(self, path, *args, **kwds):
1199 # accept an extra "case" parameter
1200 case = None
1201 if 'case' in kwds:
1202 case = kwds.pop('case')
1203 self._case = case
1204 self._allcases = parsettestcases(path)
1205 super(TTest, self).__init__(path, *args, **kwds)
1206 if case:
1207 self.name = '%s (case %s)' % (self.name, _strpath(case))
1208 self.errpath = b'%s.%s.err' % (self.errpath[:-4], case)
1209 self._tmpname += b'-%s' % case
1210
1211 @property
1212 def refpath(self):
1213 return os.path.join(self._testdir, self.bname)
1214
1215 def _run(self, env):
1216 f = open(self.path, 'rb')
1217 lines = f.readlines()
502 f.close() 1218 f.close()
503 sys.exit(1) 1219
504 os.chdir(TESTDIR) 1220 # .t file is both reference output and the test input, keep reference
505 1221 # output updated with the the test input. This avoids some race
506 usecorrectpython() 1222 # conditions where the reference output does not match the actual test.
507 1223 if self._refout is not None:
508 if options.py3k_warnings and not options.anycoverage: 1224 self._refout = lines
509 vlog("# Updating hg command to enable Py3k Warnings switch") 1225
510 f = open(os.path.join(BINDIR, 'hg'), 'r') 1226 salt, script, after, expected = self._parsetest(lines)
511 lines = [line.rstrip() for line in f] 1227
512 lines[0] += ' -3' 1228 # Write out the generated script.
1229 fname = b'%s.sh' % self._testtmp
1230 f = open(fname, 'wb')
1231 for l in script:
1232 f.write(l)
513 f.close() 1233 f.close()
514 f = open(os.path.join(BINDIR, 'hg'), 'w') 1234
515 for line in lines: 1235 cmd = b'%s "%s"' % (self._shell, fname)
516 f.write(line + '\n') 1236 vlog("# Running", cmd)
517 f.close() 1237
518 1238 exitcode, output = self._runcommand(cmd, env)
519 hgbat = os.path.join(BINDIR, 'hg.bat') 1239
520 if os.path.isfile(hgbat): 1240 if self._aborted:
521 # hg.bat expects to be put in bin/scripts while run-tests.py 1241 raise KeyboardInterrupt()
522 # installation layout put it in bin/ directly. Fix it 1242
523 f = open(hgbat, 'rb') 1243 # Do not merge output if skipped. Return hghave message instead.
524 data = f.read() 1244 # Similarly, with --debug, output is None.
525 f.close() 1245 if exitcode == self.SKIPPED_STATUS or output is None:
526 if '"%~dp0..\python" "%~dp0hg" %*' in data: 1246 return exitcode, output
527 data = data.replace('"%~dp0..\python" "%~dp0hg" %*', 1247
528 '"%~dp0python" "%~dp0hg" %*') 1248 return self._processoutput(exitcode, output, salt, after, expected)
529 f = open(hgbat, 'wb') 1249
530 f.write(data) 1250 def _hghave(self, reqs):
531 f.close() 1251 # TODO do something smarter when all other uses of hghave are gone.
532 else: 1252 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
533 print 'WARNING: cannot fix hg.bat reference to python.exe' 1253 tdir = runtestdir.replace(b'\\', b'/')
534 1254 proc = Popen4(b'%s -c "%s/hghave %s"' %
535 if options.anycoverage: 1255 (self._shell, tdir, b' '.join(reqs)),
536 custom = os.path.join(TESTDIR, 'sitecustomize.py') 1256 self._testtmp, 0, self._getenv())
537 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
538 vlog('# Installing coverage trigger to %s' % target)
539 shutil.copyfile(custom, target)
540 rc = os.path.join(TESTDIR, '.coveragerc')
541 vlog('# Installing coverage rc to %s' % rc)
542 os.environ['COVERAGE_PROCESS_START'] = rc
543 fn = os.path.join(INST, '..', '.coverage')
544 os.environ['COVERAGE_FILE'] = fn
545
546 def outputtimes(options):
547 vlog('# Producing time report')
548 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
549 cols = '%7.3f %s'
550 print '\n%-7s %s' % ('Time', 'Test')
551 for test, timetaken in times:
552 print cols % (timetaken, test)
553
554 def outputcoverage(options):
555
556 vlog('# Producing coverage report')
557 os.chdir(PYTHONDIR)
558
559 def covrun(*args):
560 cmd = 'coverage %s' % ' '.join(args)
561 vlog('# Running: %s' % cmd)
562 os.system(cmd)
563
564 covrun('-c')
565 omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR])
566 covrun('-i', '-r', '"--omit=%s"' % omit) # report
567 if options.htmlcov:
568 htmldir = os.path.join(TESTDIR, 'htmlcov')
569 covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit)
570 if options.annotate:
571 adir = os.path.join(TESTDIR, 'annotated')
572 if not os.path.isdir(adir):
573 os.mkdir(adir)
574 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
575
576 def pytest(test, wd, options, replacements, env):
577 py3kswitch = options.py3k_warnings and ' -3' or ''
578 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
579 vlog("# Running", cmd)
580 if os.name == 'nt':
581 replacements.append((r'\r\n', '\n'))
582 return run(cmd, wd, options, replacements, env)
583
584 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
585 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
586 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
587 escapemap.update({'\\': '\\\\', '\r': r'\r'})
588 def escapef(m):
589 return escapemap[m.group(0)]
590 def stringescape(s):
591 return escapesub(escapef, s)
592
593 def rematch(el, l):
594 try:
595 # use \Z to ensure that the regex matches to the end of the string
596 if os.name == 'nt':
597 return re.match(el + r'\r?\n\Z', l)
598 return re.match(el + r'\n\Z', l)
599 except re.error:
600 # el is an invalid regex
601 return False
602
603 def globmatch(el, l):
604 # The only supported special characters are * and ? plus / which also
605 # matches \ on windows. Escaping of these caracters is supported.
606 if el + '\n' == l:
607 if os.altsep:
608 # matching on "/" is not needed for this line
609 return '-glob'
610 return True
611 i, n = 0, len(el)
612 res = ''
613 while i < n:
614 c = el[i]
615 i += 1
616 if c == '\\' and el[i] in '*?\\/':
617 res += el[i - 1:i + 1]
618 i += 1
619 elif c == '*':
620 res += '.*'
621 elif c == '?':
622 res += '.'
623 elif c == '/' and os.altsep:
624 res += '[/\\\\]'
625 else:
626 res += re.escape(c)
627 return rematch(res, l)
628
629 def linematch(el, l):
630 if el == l: # perfect match (fast)
631 return True
632 if el:
633 if el.endswith(" (esc)\n"):
634 el = el[:-7].decode('string-escape') + '\n'
635 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
636 return True
637 if el.endswith(" (re)\n"):
638 return rematch(el[:-6], l)
639 if el.endswith(" (glob)\n"):
640 return globmatch(el[:-8], l)
641 if os.altsep and l.replace('\\', '/') == el:
642 return '+glob'
643 return False
644
645 def tsttest(test, wd, options, replacements, env):
646 # We generate a shell script which outputs unique markers to line
647 # up script results with our source. These markers include input
648 # line number and the last return code
649 salt = "SALT" + str(time.time())
650 def addsalt(line, inpython):
651 if inpython:
652 script.append('%s %d 0\n' % (salt, line))
653 else:
654 script.append('echo %s %s $?\n' % (salt, line))
655
656 # After we run the shell script, we re-unify the script output
657 # with non-active parts of the source, with synchronization by our
658 # SALT line number markers. The after table contains the
659 # non-active components, ordered by line number
660 after = {}
661 pos = prepos = -1
662
663 # Expected shellscript output
664 expected = {}
665
666 # We keep track of whether or not we're in a Python block so we
667 # can generate the surrounding doctest magic
668 inpython = False
669
670 # True or False when in a true or false conditional section
671 skipping = None
672
673 def hghave(reqs):
674 # TODO: do something smarter when all other uses of hghave is gone
675 tdir = TESTDIR.replace('\\', '/')
676 proc = Popen4('%s -c "%s/hghave %s"' %
677 (options.shell, tdir, ' '.join(reqs)), wd, 0)
678 stdout, stderr = proc.communicate() 1257 stdout, stderr = proc.communicate()
679 ret = proc.wait() 1258 ret = proc.wait()
680 if wifexited(ret): 1259 if wifexited(ret):
681 ret = os.WEXITSTATUS(ret) 1260 ret = os.WEXITSTATUS(ret)
682 if ret == 2: 1261 if ret == 2:
683 print stdout 1262 print(stdout.decode('utf-8'))
684 sys.exit(1) 1263 sys.exit(1)
685 return ret == 0 1264
686 1265 if ret != 0:
687 f = open(test) 1266 return False, stdout
688 t = f.readlines() 1267
689 f.close() 1268 if b'slow' in reqs:
690 1269 self._timeout = self._slowtimeout
691 script = [] 1270 return True, None
692 if options.debug: 1271
693 script.append('set -x\n') 1272 def _iftest(self, args):
694 if os.getenv('MSYSTEM'): 1273 # implements "#if"
695 script.append('alias pwd="pwd -W"\n') 1274 reqs = []
696 n = 0 1275 for arg in args:
697 for n, l in enumerate(t): 1276 if arg.startswith(b'no-') and arg[3:] in self._allcases:
698 if not l.endswith('\n'): 1277 if arg[3:] == self._case:
699 l += '\n' 1278 return False
700 if l.startswith('#if'): 1279 elif arg in self._allcases:
701 if skipping is not None: 1280 if arg != self._case:
702 after.setdefault(pos, []).append(' !!! nested #if\n') 1281 return False
703 skipping = not hghave(l.split()[1:]) 1282 else:
704 after.setdefault(pos, []).append(l) 1283 reqs.append(arg)
705 elif l.startswith('#else'): 1284 return self._hghave(reqs)[0]
706 if skipping is None: 1285
707 after.setdefault(pos, []).append(' !!! missing #if\n') 1286 def _parsetest(self, lines):
708 skipping = not skipping 1287 # We generate a shell script which outputs unique markers to line
709 after.setdefault(pos, []).append(l) 1288 # up script results with our source. These markers include input
710 elif l.startswith('#endif'): 1289 # line number and the last return code.
711 if skipping is None: 1290 salt = b"SALT%d" % time.time()
712 after.setdefault(pos, []).append(' !!! missing #if\n') 1291 def addsalt(line, inpython):
713 skipping = None
714 after.setdefault(pos, []).append(l)
715 elif skipping:
716 after.setdefault(pos, []).append(l)
717 elif l.startswith(' >>> '): # python inlines
718 after.setdefault(pos, []).append(l)
719 prepos = pos
720 pos = n
721 if not inpython:
722 # we've just entered a Python block, add the header
723 inpython = True
724 addsalt(prepos, False) # make sure we report the exit code
725 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
726 addsalt(n, True)
727 script.append(l[2:])
728 elif l.startswith(' ... '): # python inlines
729 after.setdefault(prepos, []).append(l)
730 script.append(l[2:])
731 elif l.startswith(' $ '): # commands
732 if inpython: 1292 if inpython:
733 script.append("EOF\n") 1293 script.append(b'%s %d 0\n' % (salt, line))
734 inpython = False 1294 else:
735 after.setdefault(pos, []).append(l) 1295 script.append(b'echo %s %d $?\n' % (salt, line))
736 prepos = pos 1296
737 pos = n 1297 script = []
738 addsalt(n, False) 1298
739 cmd = l[4:].split() 1299 # After we run the shell script, we re-unify the script output
740 if len(cmd) == 2 and cmd[0] == 'cd': 1300 # with non-active parts of the source, with synchronization by our
741 l = ' $ cd %s || exit 1\n' % cmd[1] 1301 # SALT line number markers. The after table contains the non-active
742 script.append(l[4:]) 1302 # components, ordered by line number.
743 elif l.startswith(' > '): # continuations 1303 after = {}
744 after.setdefault(prepos, []).append(l) 1304
745 script.append(l[4:]) 1305 # Expected shell script output.
746 elif l.startswith(' '): # results 1306 expected = {}
747 # queue up a list of expected results 1307
748 expected.setdefault(pos, []).append(l[2:]) 1308 pos = prepos = -1
1309
1310 # True or False when in a true or false conditional section
1311 skipping = None
1312
1313 # We keep track of whether or not we're in a Python block so we
1314 # can generate the surrounding doctest magic.
1315 inpython = False
1316
1317 if self._debug:
1318 script.append(b'set -x\n')
1319 if self._hgcommand != b'hg':
1320 script.append(b'alias hg="%s"\n' % self._hgcommand)
1321 if os.getenv('MSYSTEM'):
1322 script.append(b'alias pwd="pwd -W"\n')
1323
1324 n = 0
1325 for n, l in enumerate(lines):
1326 if not l.endswith(b'\n'):
1327 l += b'\n'
1328 if l.startswith(b'#require'):
1329 lsplit = l.split()
1330 if len(lsplit) < 2 or lsplit[0] != b'#require':
1331 after.setdefault(pos, []).append(' !!! invalid #require\n')
1332 haveresult, message = self._hghave(lsplit[1:])
1333 if not haveresult:
1334 script = [b'echo "%s"\nexit 80\n' % message]
1335 break
1336 after.setdefault(pos, []).append(l)
1337 elif l.startswith(b'#if'):
1338 lsplit = l.split()
1339 if len(lsplit) < 2 or lsplit[0] != b'#if':
1340 after.setdefault(pos, []).append(' !!! invalid #if\n')
1341 if skipping is not None:
1342 after.setdefault(pos, []).append(' !!! nested #if\n')
1343 skipping = not self._iftest(lsplit[1:])
1344 after.setdefault(pos, []).append(l)
1345 elif l.startswith(b'#else'):
1346 if skipping is None:
1347 after.setdefault(pos, []).append(' !!! missing #if\n')
1348 skipping = not skipping
1349 after.setdefault(pos, []).append(l)
1350 elif l.startswith(b'#endif'):
1351 if skipping is None:
1352 after.setdefault(pos, []).append(' !!! missing #if\n')
1353 skipping = None
1354 after.setdefault(pos, []).append(l)
1355 elif skipping:
1356 after.setdefault(pos, []).append(l)
1357 elif l.startswith(b' >>> '): # python inlines
1358 after.setdefault(pos, []).append(l)
1359 prepos = pos
1360 pos = n
1361 if not inpython:
1362 # We've just entered a Python block. Add the header.
1363 inpython = True
1364 addsalt(prepos, False) # Make sure we report the exit code.
1365 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1366 addsalt(n, True)
1367 script.append(l[2:])
1368 elif l.startswith(b' ... '): # python inlines
1369 after.setdefault(prepos, []).append(l)
1370 script.append(l[2:])
1371 elif l.startswith(b' $ '): # commands
1372 if inpython:
1373 script.append(b'EOF\n')
1374 inpython = False
1375 after.setdefault(pos, []).append(l)
1376 prepos = pos
1377 pos = n
1378 addsalt(n, False)
1379 cmd = l[4:].split()
1380 if len(cmd) == 2 and cmd[0] == b'cd':
1381 l = b' $ cd %s || exit 1\n' % cmd[1]
1382 script.append(l[4:])
1383 elif l.startswith(b' > '): # continuations
1384 after.setdefault(prepos, []).append(l)
1385 script.append(l[4:])
1386 elif l.startswith(b' '): # results
1387 # Queue up a list of expected results.
1388 expected.setdefault(pos, []).append(l[2:])
1389 else:
1390 if inpython:
1391 script.append(b'EOF\n')
1392 inpython = False
1393 # Non-command/result. Queue up for merged output.
1394 after.setdefault(pos, []).append(l)
1395
1396 if inpython:
1397 script.append(b'EOF\n')
1398 if skipping is not None:
1399 after.setdefault(pos, []).append(' !!! missing #endif\n')
1400 addsalt(n + 1, False)
1401
1402 return salt, script, after, expected
1403
1404 def _processoutput(self, exitcode, output, salt, after, expected):
1405 # Merge the script output back into a unified test.
1406 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1407 if exitcode != 0:
1408 warnonly = 3
1409
1410 pos = -1
1411 postout = []
1412 for l in output:
1413 lout, lcmd = l, None
1414 if salt in l:
1415 lout, lcmd = l.split(salt, 1)
1416
1417 while lout:
1418 if not lout.endswith(b'\n'):
1419 lout += b' (no-eol)\n'
1420
1421 # Find the expected output at the current position.
1422 els = [None]
1423 if expected.get(pos, None):
1424 els = expected[pos]
1425
1426 i = 0
1427 optional = []
1428 while i < len(els):
1429 el = els[i]
1430
1431 r = self.linematch(el, lout)
1432 if isinstance(r, str):
1433 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))
1438 r = '' # Warn only this line.
1439 elif r == "retry":
1440 postout.append(b' ' + el)
1441 els.pop(i)
1442 break
1443 else:
1444 log('\ninfo, unknown linematch result: %r\n' % r)
1445 r = False
1446 if r:
1447 els.pop(i)
1448 break
1449 if el:
1450 if el.endswith(b" (?)\n"):
1451 optional.append(i)
1452 else:
1453 m = optline.match(el)
1454 if m:
1455 conditions = [
1456 c for c in m.group(2).split(b' ')]
1457
1458 if not self._iftest(conditions):
1459 optional.append(i)
1460
1461 i += 1
1462
1463 if r:
1464 if r == "retry":
1465 continue
1466 # clean up any optional leftovers
1467 for i in optional:
1468 postout.append(b' ' + els[i])
1469 for i in reversed(optional):
1470 del els[i]
1471 postout.append(b' ' + el)
1472 else:
1473 if self.NEEDESCAPE(lout):
1474 lout = TTest._stringescape(b'%s (esc)\n' %
1475 lout.rstrip(b'\n'))
1476 postout.append(b' ' + lout) # Let diff deal with it.
1477 if r != '': # If line failed.
1478 warnonly = 3 # for sure not
1479 elif warnonly == 1: # Is "not yet" and line is warn only.
1480 warnonly = 2 # Yes do warn.
1481 break
1482 else:
1483 # clean up any optional leftovers
1484 while expected.get(pos, None):
1485 el = expected[pos].pop(0)
1486 if el:
1487 if not el.endswith(b" (?)\n"):
1488 m = optline.match(el)
1489 if m:
1490 conditions = [c for c in m.group(2).split(b' ')]
1491
1492 if self._iftest(conditions):
1493 # Don't append as optional line
1494 continue
1495 else:
1496 continue
1497 postout.append(b' ' + el)
1498
1499 if lcmd:
1500 # Add on last return code.
1501 ret = int(lcmd.split()[1])
1502 if ret != 0:
1503 postout.append(b' [%d]\n' % ret)
1504 if pos in after:
1505 # Merge in non-active test bits.
1506 postout += after.pop(pos)
1507 pos = int(lcmd.split()[0])
1508
1509 if pos in after:
1510 postout += after.pop(pos)
1511
1512 if warnonly == 2:
1513 exitcode = False # Set exitcode to warned.
1514
1515 return exitcode, postout
1516
1517 @staticmethod
1518 def rematch(el, l):
1519 try:
1520 # use \Z to ensure that the regex matches to the end of the string
1521 if os.name == 'nt':
1522 return re.match(el + br'\r?\n\Z', l)
1523 return re.match(el + br'\n\Z', l)
1524 except re.error:
1525 # el is an invalid regex
1526 return False
1527
1528 @staticmethod
1529 def globmatch(el, l):
1530 # The only supported special characters are * and ? plus / which also
1531 # matches \ on windows. Escaping of these characters is supported.
1532 if el + b'\n' == l:
1533 if os.altsep:
1534 # matching on "/" is not needed for this line
1535 for pat in checkcodeglobpats:
1536 if pat.match(el):
1537 return True
1538 return b'-glob'
1539 return True
1540 el = el.replace(b'$LOCALIP', b'*')
1541 i, n = 0, len(el)
1542 res = b''
1543 while i < n:
1544 c = el[i:i + 1]
1545 i += 1
1546 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1547 res += el[i - 1:i + 1]
1548 i += 1
1549 elif c == b'*':
1550 res += b'.*'
1551 elif c == b'?':
1552 res += b'.'
1553 elif c == b'/' and os.altsep:
1554 res += b'[/\\\\]'
1555 else:
1556 res += re.escape(c)
1557 return TTest.rematch(res, l)
1558
1559 def linematch(self, el, l):
1560 retry = False
1561 if el == l: # perfect match (fast)
1562 return True
1563 if el:
1564 if el.endswith(b" (?)\n"):
1565 retry = "retry"
1566 el = el[:-5] + b"\n"
1567 else:
1568 m = optline.match(el)
1569 if m:
1570 conditions = [c for c in m.group(2).split(b' ')]
1571
1572 el = m.group(1) + b"\n"
1573 if not self._iftest(conditions):
1574 retry = "retry" # Not required by listed features
1575
1576 if el.endswith(b" (esc)\n"):
1577 if PYTHON3:
1578 el = el[:-7].decode('unicode_escape') + '\n'
1579 el = el.encode('utf-8')
1580 else:
1581 el = el[:-7].decode('string-escape') + '\n'
1582 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1583 return True
1584 if el.endswith(b" (re)\n"):
1585 return TTest.rematch(el[:-6], l) or retry
1586 if el.endswith(b" (glob)\n"):
1587 # ignore '(glob)' added to l by 'replacements'
1588 if l.endswith(b" (glob)\n"):
1589 l = l[:-8] + b"\n"
1590 return TTest.globmatch(el[:-8], l) or retry
1591 if os.altsep and l.replace(b'\\', b'/') == el:
1592 return b'+glob'
1593 return retry
1594
1595 @staticmethod
1596 def parsehghaveoutput(lines):
1597 '''Parse hghave log lines.
1598
1599 Return tuple of lists (missing, failed):
1600 * the missing/unknown features
1601 * the features for which existence check failed'''
1602 missing = []
1603 failed = []
1604 for line in lines:
1605 if line.startswith(TTest.SKIPPED_PREFIX):
1606 line = line.splitlines()[0]
1607 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1608 elif line.startswith(TTest.FAILED_PREFIX):
1609 line = line.splitlines()[0]
1610 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1611
1612 return missing, failed
1613
1614 @staticmethod
1615 def _escapef(m):
1616 return TTest.ESCAPEMAP[m.group(0)]
1617
1618 @staticmethod
1619 def _stringescape(s):
1620 return TTest.ESCAPESUB(TTest._escapef, s)
1621
1622 iolock = threading.RLock()
1623
1624 class TestResult(unittest._TextTestResult):
1625 """Holds results when executing via unittest."""
1626 # Don't worry too much about accessing the non-public _TextTestResult.
1627 # It is relatively common in Python testing tools.
1628 def __init__(self, options, *args, **kwargs):
1629 super(TestResult, self).__init__(*args, **kwargs)
1630
1631 self._options = options
1632
1633 # unittest.TestResult didn't have skipped until 2.7. We need to
1634 # polyfill it.
1635 self.skipped = []
1636
1637 # We have a custom "ignored" result that isn't present in any Python
1638 # unittest implementation. It is very similar to skipped. It may make
1639 # sense to map it into skip some day.
1640 self.ignored = []
1641
1642 self.times = []
1643 self._firststarttime = None
1644 # Data stored for the benefit of generating xunit reports.
1645 self.successes = []
1646 self.faildata = {}
1647
1648 if options.color == 'auto':
1649 self.color = pygmentspresent and self.stream.isatty()
1650 elif options.color == 'never':
1651 self.color = False
1652 else: # 'always', for testing purposes
1653 self.color = pygmentspresent
1654
1655 def addFailure(self, test, reason):
1656 self.failures.append((test, reason))
1657
1658 if self._options.first:
1659 self.stop()
749 else: 1660 else:
750 if inpython: 1661 with iolock:
751 script.append("EOF\n") 1662 if reason == "timed out":
752 inpython = False 1663 self.stream.write('t')
753 # non-command/result - queue up for merged output
754 after.setdefault(pos, []).append(l)
755
756 if inpython:
757 script.append("EOF\n")
758 if skipping is not None:
759 after.setdefault(pos, []).append(' !!! missing #endif\n')
760 addsalt(n + 1, False)
761
762 # Write out the script and execute it
763 name = wd + '.sh'
764 f = open(name, 'w')
765 for l in script:
766 f.write(l)
767 f.close()
768
769 cmd = '%s "%s"' % (options.shell, name)
770 vlog("# Running", cmd)
771 exitcode, output = run(cmd, wd, options, replacements, env)
772 # do not merge output if skipped, return hghave message instead
773 # similarly, with --debug, output is None
774 if exitcode == SKIPPED_STATUS or output is None:
775 return exitcode, output
776
777 # Merge the script output back into a unified test
778
779 warnonly = True
780 pos = -1
781 postout = []
782 for l in output:
783 lout, lcmd = l, None
784 if salt in l:
785 lout, lcmd = l.split(salt, 1)
786
787 if lout:
788 if not lout.endswith('\n'):
789 lout += ' (no-eol)\n'
790
791 # find the expected output at the current position
792 el = None
793 if pos in expected and expected[pos]:
794 el = expected[pos].pop(0)
795
796 r = linematch(el, lout)
797 if isinstance(r, str):
798 if r == '+glob':
799 lout = el[:-1] + ' (glob)\n'
800 r = 0 # warn only
801 elif r == '-glob':
802 lout = ''.join(el.rsplit(' (glob)', 1))
803 r = 0 # warn only
804 else: 1664 else:
805 log('\ninfo, unknown linematch result: %r\n' % r) 1665 if not self._options.nodiff:
806 r = False 1666 self.stream.write('\n')
807 if r: 1667 # Exclude the '\n' from highlighting to lex correctly
808 postout.append(" " + el) 1668 formatted = 'ERROR: %s output changed\n' % test
1669 self.stream.write(highlightmsg(formatted, self.color))
1670 self.stream.write('!')
1671
1672 self.stream.flush()
1673
1674 def addSuccess(self, test):
1675 with iolock:
1676 super(TestResult, self).addSuccess(test)
1677 self.successes.append(test)
1678
1679 def addError(self, test, err):
1680 super(TestResult, self).addError(test, err)
1681 if self._options.first:
1682 self.stop()
1683
1684 # Polyfill.
1685 def addSkip(self, test, reason):
1686 self.skipped.append((test, reason))
1687 with iolock:
1688 if self.showAll:
1689 self.stream.writeln('skipped %s' % reason)
809 else: 1690 else:
810 if needescape(lout): 1691 self.stream.write('s')
811 lout = stringescape(lout.rstrip('\n')) + " (esc)\n" 1692 self.stream.flush()
812 postout.append(" " + lout) # let diff deal with it 1693
813 if r != 0: # != warn only 1694 def addIgnore(self, test, reason):
814 warnonly = False 1695 self.ignored.append((test, reason))
815 1696 with iolock:
816 if lcmd: 1697 if self.showAll:
817 # add on last return code 1698 self.stream.writeln('ignored %s' % reason)
818 ret = int(lcmd.split()[1]) 1699 else:
819 if ret != 0: 1700 if reason not in ('not retesting', "doesn't match keyword"):
820 postout.append(" [%s]\n" % ret) 1701 self.stream.write('i')
821 if pos in after: 1702 else:
822 # merge in non-active test bits 1703 self.testsRun += 1
823 postout += after.pop(pos) 1704 self.stream.flush()
824 pos = int(lcmd.split()[0]) 1705
825 1706 def addOutputMismatch(self, test, ret, got, expected):
826 if pos in after: 1707 """Record a mismatch in test output for a particular test."""
827 postout += after.pop(pos) 1708 if self.shouldStop:
828 1709 # don't print, some other test case already failed and
829 if warnonly and exitcode == 0: 1710 # printed, we're just stale and probably failed due to our
830 exitcode = False 1711 # temp dir getting cleaned up.
831 return exitcode, postout 1712 return
832 1713
833 wifexited = getattr(os, "WIFEXITED", lambda x: False) 1714 accepted = False
834 def run(cmd, wd, options, replacements, env): 1715 lines = []
835 """Run command in a sub-process, capturing the output (stdout and stderr). 1716
836 Return a tuple (exitcode, output). output is None in debug mode.""" 1717 with iolock:
837 # TODO: Use subprocess.Popen if we're running on Python 2.4 1718 if self._options.nodiff:
838 if options.debug: 1719 pass
839 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env) 1720 elif self._options.view:
840 ret = proc.wait() 1721 v = self._options.view
841 return (ret, None) 1722 if PYTHON3:
842 1723 v = _bytespath(v)
843 proc = Popen4(cmd, wd, options.timeout, env) 1724 os.system(b"%s %s %s" %
844 def cleanup(): 1725 (v, test.refpath, test.errpath))
845 terminate(proc) 1726 else:
846 ret = proc.wait() 1727 servefail, lines = getdiff(expected, got,
847 if ret == 0: 1728 test.refpath, test.errpath)
848 ret = signal.SIGTERM << 8 1729 if servefail:
849 killdaemons(env['DAEMON_PIDS']) 1730 raise test.failureException(
850 return ret 1731 'server failed to start (HGPORT=%s)' % test._startport)
851 1732 else:
852 output = '' 1733 self.stream.write('\n')
853 proc.tochild.close() 1734 for line in lines:
854 1735 line = highlightdiff(line, self.color)
1736 if PYTHON3:
1737 self.stream.flush()
1738 self.stream.buffer.write(line)
1739 self.stream.buffer.flush()
1740 else:
1741 self.stream.write(line)
1742 self.stream.flush()
1743
1744 # handle interactive prompt without releasing iolock
1745 if self._options.interactive:
1746 if test.readrefout() != expected:
1747 self.stream.write(
1748 'Reference output has changed (run again to prompt '
1749 'changes)')
1750 else:
1751 self.stream.write('Accept this change? [n] ')
1752 answer = sys.stdin.readline().strip()
1753 if answer.lower() in ('y', 'yes'):
1754 if test.path.endswith(b'.t'):
1755 rename(test.errpath, test.path)
1756 else:
1757 rename(test.errpath, '%s.out' % test.path)
1758 accepted = True
1759 if not accepted:
1760 self.faildata[test.name] = b''.join(lines)
1761
1762 return accepted
1763
1764 def startTest(self, test):
1765 super(TestResult, self).startTest(test)
1766
1767 # os.times module computes the user time and system time spent by
1768 # child's processes along with real elapsed time taken by a process.
1769 # This module has one limitation. It can only work for Linux user
1770 # and not for Windows.
1771 test.started = os.times()
1772 if self._firststarttime is None: # thread racy but irrelevant
1773 self._firststarttime = test.started[4]
1774
1775 def stopTest(self, test, interrupted=False):
1776 super(TestResult, self).stopTest(test)
1777
1778 test.stopped = os.times()
1779
1780 starttime = test.started
1781 endtime = test.stopped
1782 origin = self._firststarttime
1783 self.times.append((test.name,
1784 endtime[2] - starttime[2], # user space CPU time
1785 endtime[3] - starttime[3], # sys space CPU time
1786 endtime[4] - starttime[4], # real time
1787 starttime[4] - origin, # start date in run context
1788 endtime[4] - origin, # end date in run context
1789 ))
1790
1791 if interrupted:
1792 with iolock:
1793 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1794 test.name, self.times[-1][3]))
1795
1796 class TestSuite(unittest.TestSuite):
1797 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1798
1799 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1800 retest=False, keywords=None, loop=False, runs_per_test=1,
1801 loadtest=None, showchannels=False,
1802 *args, **kwargs):
1803 """Create a new instance that can run tests with a configuration.
1804
1805 testdir specifies the directory where tests are executed from. This
1806 is typically the ``tests`` directory from Mercurial's source
1807 repository.
1808
1809 jobs specifies the number of jobs to run concurrently. Each test
1810 executes on its own thread. Tests actually spawn new processes, so
1811 state mutation should not be an issue.
1812
1813 If there is only one job, it will use the main thread.
1814
1815 whitelist and blacklist denote tests that have been whitelisted and
1816 blacklisted, respectively. These arguments don't belong in TestSuite.
1817 Instead, whitelist and blacklist should be handled by the thing that
1818 populates the TestSuite with tests. They are present to preserve
1819 backwards compatible behavior which reports skipped tests as part
1820 of the results.
1821
1822 retest denotes whether to retest failed tests. This arguably belongs
1823 outside of TestSuite.
1824
1825 keywords denotes key words that will be used to filter which tests
1826 to execute. This arguably belongs outside of TestSuite.
1827
1828 loop denotes whether to loop over tests forever.
1829 """
1830 super(TestSuite, self).__init__(*args, **kwargs)
1831
1832 self._jobs = jobs
1833 self._whitelist = whitelist
1834 self._blacklist = blacklist
1835 self._retest = retest
1836 self._keywords = keywords
1837 self._loop = loop
1838 self._runs_per_test = runs_per_test
1839 self._loadtest = loadtest
1840 self._showchannels = showchannels
1841
1842 def run(self, result):
1843 # We have a number of filters that need to be applied. We do this
1844 # here instead of inside Test because it makes the running logic for
1845 # Test simpler.
1846 tests = []
1847 num_tests = [0]
1848 for test in self._tests:
1849 def get():
1850 num_tests[0] += 1
1851 if getattr(test, 'should_reload', False):
1852 return self._loadtest(test, num_tests[0])
1853 return test
1854 if not os.path.exists(test.path):
1855 result.addSkip(test, "Doesn't exist")
1856 continue
1857
1858 if not (self._whitelist and test.bname in self._whitelist):
1859 if self._blacklist and test.bname in self._blacklist:
1860 result.addSkip(test, 'blacklisted')
1861 continue
1862
1863 if self._retest and not os.path.exists(test.errpath):
1864 result.addIgnore(test, 'not retesting')
1865 continue
1866
1867 if self._keywords:
1868 f = open(test.path, 'rb')
1869 t = f.read().lower() + test.bname.lower()
1870 f.close()
1871 ignored = False
1872 for k in self._keywords.lower().split():
1873 if k not in t:
1874 result.addIgnore(test, "doesn't match keyword")
1875 ignored = True
1876 break
1877
1878 if ignored:
1879 continue
1880 for _ in xrange(self._runs_per_test):
1881 tests.append(get())
1882
1883 runtests = list(tests)
1884 done = queue.Queue()
1885 running = 0
1886
1887 channels = [""] * self._jobs
1888
1889 def job(test, result):
1890 for n, v in enumerate(channels):
1891 if not v:
1892 channel = n
1893 break
1894 else:
1895 raise ValueError('Could not find output channel')
1896 channels[channel] = "=" + test.name[5:].split(".")[0]
1897 try:
1898 test(result)
1899 done.put(None)
1900 except KeyboardInterrupt:
1901 pass
1902 except: # re-raises
1903 done.put(('!', test, 'run-test raised an error, see traceback'))
1904 raise
1905 finally:
1906 try:
1907 channels[channel] = ''
1908 except IndexError:
1909 pass
1910
1911 def stat():
1912 count = 0
1913 while channels:
1914 d = '\n%03s ' % count
1915 for n, v in enumerate(channels):
1916 if v:
1917 d += v[0]
1918 channels[n] = v[1:] or '.'
1919 else:
1920 d += ' '
1921 d += ' '
1922 with iolock:
1923 sys.stdout.write(d + ' ')
1924 sys.stdout.flush()
1925 for x in xrange(10):
1926 if channels:
1927 time.sleep(.1)
1928 count += 1
1929
1930 stoppedearly = False
1931
1932 if self._showchannels:
1933 statthread = threading.Thread(target=stat, name="stat")
1934 statthread.start()
1935
1936 try:
1937 while tests or running:
1938 if not done.empty() or running == self._jobs or not tests:
1939 try:
1940 done.get(True, 1)
1941 running -= 1
1942 if result and result.shouldStop:
1943 stoppedearly = True
1944 break
1945 except queue.Empty:
1946 continue
1947 if tests and not running == self._jobs:
1948 test = tests.pop(0)
1949 if self._loop:
1950 if getattr(test, 'should_reload', False):
1951 num_tests[0] += 1
1952 tests.append(
1953 self._loadtest(test, num_tests[0]))
1954 else:
1955 tests.append(test)
1956 if self._jobs == 1:
1957 job(test, result)
1958 else:
1959 t = threading.Thread(target=job, name=test.name,
1960 args=(test, result))
1961 t.start()
1962 running += 1
1963
1964 # If we stop early we still need to wait on started tests to
1965 # finish. Otherwise, there is a race between the test completing
1966 # and the test's cleanup code running. This could result in the
1967 # test reporting incorrect.
1968 if stoppedearly:
1969 while running:
1970 try:
1971 done.get(True, 1)
1972 running -= 1
1973 except queue.Empty:
1974 continue
1975 except KeyboardInterrupt:
1976 for test in runtests:
1977 test.abort()
1978
1979 channels = []
1980
1981 return result
1982
1983 # Save the most recent 5 wall-clock runtimes of each test to a
1984 # human-readable text file named .testtimes. Tests are sorted
1985 # alphabetically, while times for each test are listed from oldest to
1986 # newest.
1987
1988 def loadtimes(outputdir):
1989 times = []
855 try: 1990 try:
856 output = proc.fromchild.read() 1991 with open(os.path.join(outputdir, b'.testtimes-')) as fp:
857 except KeyboardInterrupt: 1992 for line in fp:
858 vlog('# Handling keyboard interrupt') 1993 ts = line.split()
859 cleanup() 1994 times.append((ts[0], [float(t) for t in ts[1:]]))
860 raise 1995 except IOError as err:
861 1996 if err.errno != errno.ENOENT:
862 ret = proc.wait() 1997 raise
863 if wifexited(ret): 1998 return times
864 ret = os.WEXITSTATUS(ret) 1999
865 2000 def savetimes(outputdir, result):
866 if proc.timeout: 2001 saved = dict(loadtimes(outputdir))
867 ret = 'timeout' 2002 maxruns = 5
868 2003 skipped = set([str(t[0]) for t in result.skipped])
869 if ret: 2004 for tdata in result.times:
870 killdaemons(env['DAEMON_PIDS']) 2005 test, real = tdata[0], tdata[3]
871 2006 if test not in skipped:
872 if abort: 2007 ts = saved.setdefault(test, [])
873 raise KeyboardInterrupt() 2008 ts.append(real)
874 2009 ts[:] = ts[-maxruns:]
875 for s, r in replacements: 2010
876 output = re.sub(s, r, output) 2011 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
877 return ret, output.splitlines(True) 2012 dir=outputdir, text=True)
878 2013 with os.fdopen(fd, 'w') as fp:
879 def runone(options, test, count): 2014 for name, ts in sorted(saved.items()):
880 '''returns a result element: (code, test, msg)''' 2015 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
881 2016 timepath = os.path.join(outputdir, b'.testtimes')
882 def skip(msg): 2017 try:
883 if options.verbose: 2018 os.unlink(timepath)
884 log("\nSkipping %s: %s" % (testpath, msg)) 2019 except OSError:
885 return 's', test, msg 2020 pass
886 2021 try:
887 def fail(msg, ret): 2022 os.rename(tmpname, timepath)
888 warned = ret is False 2023 except OSError:
889 if not options.nodiff: 2024 pass
890 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', test, msg)) 2025
891 if (not ret and options.interactive 2026 class TextTestRunner(unittest.TextTestRunner):
892 and os.path.exists(testpath + ".err")): 2027 """Custom unittest test runner that uses appropriate settings."""
893 iolock.acquire() 2028
894 print "Accept this change? [n] ", 2029 def __init__(self, runner, *args, **kwargs):
895 answer = sys.stdin.readline().strip() 2030 super(TextTestRunner, self).__init__(*args, **kwargs)
896 iolock.release() 2031
897 if answer.lower() in "y yes".split(): 2032 self._runner = runner
898 if test.endswith(".t"): 2033
899 rename(testpath + ".err", testpath) 2034 def listtests(self, test):
2035 result = TestResult(self._runner.options, self.stream,
2036 self.descriptions, 0)
2037 test = sorted(test, key=lambda t: t.name)
2038 for t in test:
2039 print(t.name)
2040 result.addSuccess(t)
2041
2042 if self._runner.options.xunit:
2043 with open(self._runner.options.xunit, "wb") as xuf:
2044 self._writexunit(result, xuf)
2045
2046 if self._runner.options.json:
2047 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2048 with open(jsonpath, 'w') as fp:
2049 self._writejson(result, fp)
2050
2051 return result
2052
2053 def run(self, test):
2054 result = TestResult(self._runner.options, self.stream,
2055 self.descriptions, self.verbosity)
2056
2057 test(result)
2058
2059 failed = len(result.failures)
2060 skipped = len(result.skipped)
2061 ignored = len(result.ignored)
2062
2063 with iolock:
2064 self.stream.writeln('')
2065
2066 if not self._runner.options.noskips:
2067 for test, msg in result.skipped:
2068 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2069 self.stream.write(highlightmsg(formatted, result.color))
2070 for test, msg in result.failures:
2071 formatted = 'Failed %s: %s\n' % (test.name, msg)
2072 self.stream.write(highlightmsg(formatted, result.color))
2073 for test, msg in result.errors:
2074 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2075
2076 if self._runner.options.xunit:
2077 with open(self._runner.options.xunit, "wb") as xuf:
2078 self._writexunit(result, xuf)
2079
2080 if self._runner.options.json:
2081 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2082 with open(jsonpath, 'w') as fp:
2083 self._writejson(result, fp)
2084
2085 self._runner._checkhglib('Tested')
2086
2087 savetimes(self._runner._outputdir, result)
2088
2089 if failed and self._runner.options.known_good_rev:
2090 self._bisecttests(t for t, m in result.failures)
2091 self.stream.writeln(
2092 '# Ran %d tests, %d skipped, %d failed.'
2093 % (result.testsRun, skipped + ignored, failed))
2094 if failed:
2095 self.stream.writeln('python hash seed: %s' %
2096 os.environ['PYTHONHASHSEED'])
2097 if self._runner.options.time:
2098 self.printtimes(result.times)
2099 self.stream.flush()
2100
2101 return result
2102
2103 def _bisecttests(self, tests):
2104 bisectcmd = ['hg', 'bisect']
2105 bisectrepo = self._runner.options.bisect_repo
2106 if bisectrepo:
2107 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2108 def pread(args):
2109 env = os.environ.copy()
2110 env['HGPLAIN'] = '1'
2111 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2112 stdout=subprocess.PIPE, env=env)
2113 data = p.stdout.read()
2114 p.wait()
2115 return data
2116 for test in tests:
2117 pread(bisectcmd + ['--reset']),
2118 pread(bisectcmd + ['--bad', '.'])
2119 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2120 # TODO: we probably need to forward more options
2121 # that alter hg's behavior inside the tests.
2122 opts = ''
2123 withhg = self._runner.options.with_hg
2124 if withhg:
2125 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2126 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2127 test)
2128 data = pread(bisectcmd + ['--command', rtc])
2129 m = re.search(
2130 (br'\nThe first (?P<goodbad>bad|good) revision '
2131 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2132 br'summary: +(?P<summary>[^\n]+)\n'),
2133 data, (re.MULTILINE | re.DOTALL))
2134 if m is None:
2135 self.stream.writeln(
2136 'Failed to identify failure point for %s' % test)
2137 continue
2138 dat = m.groupdict()
2139 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2140 self.stream.writeln(
2141 '%s %s by %s (%s)' % (
2142 test, verb, dat['node'], dat['summary']))
2143
2144 def printtimes(self, times):
2145 # iolock held by run
2146 self.stream.writeln('# Producing time report')
2147 times.sort(key=lambda t: (t[3]))
2148 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2149 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2150 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2151 for tdata in times:
2152 test = tdata[0]
2153 cuser, csys, real, start, end = tdata[1:6]
2154 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2155
2156 @staticmethod
2157 def _writexunit(result, outf):
2158 # See http://llg.cubic.org/docs/junit/ for a reference.
2159 timesd = dict((t[0], t[3]) for t in result.times)
2160 doc = minidom.Document()
2161 s = doc.createElement('testsuite')
2162 s.setAttribute('name', 'run-tests')
2163 s.setAttribute('tests', str(result.testsRun))
2164 s.setAttribute('errors', "0") # TODO
2165 s.setAttribute('failures', str(len(result.failures)))
2166 s.setAttribute('skipped', str(len(result.skipped) +
2167 len(result.ignored)))
2168 doc.appendChild(s)
2169 for tc in result.successes:
2170 t = doc.createElement('testcase')
2171 t.setAttribute('name', tc.name)
2172 tctime = timesd.get(tc.name)
2173 if tctime is not None:
2174 t.setAttribute('time', '%.3f' % tctime)
2175 s.appendChild(t)
2176 for tc, err in sorted(result.faildata.items()):
2177 t = doc.createElement('testcase')
2178 t.setAttribute('name', tc)
2179 tctime = timesd.get(tc)
2180 if tctime is not None:
2181 t.setAttribute('time', '%.3f' % tctime)
2182 # createCDATASection expects a unicode or it will
2183 # convert using default conversion rules, which will
2184 # fail if string isn't ASCII.
2185 err = cdatasafe(err).decode('utf-8', 'replace')
2186 cd = doc.createCDATASection(err)
2187 # Use 'failure' here instead of 'error' to match errors = 0,
2188 # failures = len(result.failures) in the testsuite element.
2189 failelem = doc.createElement('failure')
2190 failelem.setAttribute('message', 'output changed')
2191 failelem.setAttribute('type', 'output-mismatch')
2192 failelem.appendChild(cd)
2193 t.appendChild(failelem)
2194 s.appendChild(t)
2195 for tc, message in result.skipped:
2196 # According to the schema, 'skipped' has no attributes. So store
2197 # the skip message as a text node instead.
2198 t = doc.createElement('testcase')
2199 t.setAttribute('name', tc.name)
2200 binmessage = message.encode('utf-8')
2201 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2202 cd = doc.createCDATASection(message)
2203 skipelem = doc.createElement('skipped')
2204 skipelem.appendChild(cd)
2205 t.appendChild(skipelem)
2206 s.appendChild(t)
2207 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2208
2209 @staticmethod
2210 def _writejson(result, outf):
2211 timesd = {}
2212 for tdata in result.times:
2213 test = tdata[0]
2214 timesd[test] = tdata[1:]
2215
2216 outcome = {}
2217 groups = [('success', ((tc, None)
2218 for tc in result.successes)),
2219 ('failure', result.failures),
2220 ('skip', result.skipped)]
2221 for res, testcases in groups:
2222 for tc, __ in testcases:
2223 if tc.name in timesd:
2224 diff = result.faildata.get(tc.name, b'')
2225 try:
2226 diff = diff.decode('unicode_escape')
2227 except UnicodeDecodeError as e:
2228 diff = '%r decoding diff, sorry' % e
2229 tres = {'result': res,
2230 'time': ('%0.3f' % timesd[tc.name][2]),
2231 'cuser': ('%0.3f' % timesd[tc.name][0]),
2232 'csys': ('%0.3f' % timesd[tc.name][1]),
2233 'start': ('%0.3f' % timesd[tc.name][3]),
2234 'end': ('%0.3f' % timesd[tc.name][4]),
2235 'diff': diff,
2236 }
900 else: 2237 else:
901 rename(testpath + ".err", testpath + ".out") 2238 # blacklisted test
902 return '.', test, '' 2239 tres = {'result': res}
903 return warned and '~' or '!', test, msg 2240
904 2241 outcome[tc.name] = tres
905 def success(): 2242 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
906 return '.', test, '' 2243 separators=(',', ': '))
907 2244 outf.writelines(("testreport =", jsonout))
908 def ignore(msg): 2245
909 return 'i', test, msg 2246 class TestRunner(object):
910 2247 """Holds context for executing tests.
911 def describe(ret): 2248
912 if ret < 0: 2249 Tests rely on a lot of state. This object holds it for them.
913 return 'killed by signal %d' % -ret 2250 """
914 return 'returned error code %d' % ret 2251
915 2252 # Programs required to run tests.
916 testpath = os.path.join(TESTDIR, test) 2253 REQUIREDTOOLS = [
917 err = os.path.join(TESTDIR, test + ".err") 2254 b'diff',
918 lctest = test.lower() 2255 b'grep',
919 2256 b'unzip',
920 if not os.path.exists(testpath): 2257 b'gunzip',
921 return skip("doesn't exist") 2258 b'bunzip2',
922 2259 b'sed',
923 if not (options.whitelisted and test in options.whitelisted): 2260 ]
924 if options.blacklist and test in options.blacklist: 2261
925 return skip("blacklisted") 2262 # Maps file extensions to test class.
926 2263 TESTTYPES = [
927 if options.retest and not os.path.exists(test + ".err"): 2264 (b'.py', PythonTest),
928 return ignore("not retesting") 2265 (b'.t', TTest),
929 2266 ]
930 if options.keywords: 2267
931 fp = open(test) 2268 def __init__(self):
932 t = fp.read().lower() + test.lower() 2269 self.options = None
933 fp.close() 2270 self._hgroot = None
934 for k in options.keywords.lower().split(): 2271 self._testdir = None
935 if k in t: 2272 self._outputdir = None
2273 self._hgtmp = None
2274 self._installdir = None
2275 self._bindir = None
2276 self._tmpbinddir = None
2277 self._pythondir = None
2278 self._coveragefile = None
2279 self._createdfiles = []
2280 self._hgcommand = None
2281 self._hgpath = None
2282 self._portoffset = 0
2283 self._ports = {}
2284
2285 def run(self, args, parser=None):
2286 """Run the test suite."""
2287 oldmask = os.umask(0o22)
2288 try:
2289 parser = parser or getparser()
2290 options, args = parseargs(args, parser)
2291 # positional arguments are paths to test files to run, so
2292 # we make sure they're all bytestrings
2293 args = [_bytespath(a) for a in args]
2294 if options.test_list is not None:
2295 for listfile in options.test_list:
2296 with open(listfile, 'rb') as f:
2297 args.extend(t for t in f.read().splitlines() if t)
2298 self.options = options
2299
2300 self._checktools()
2301 testdescs = self.findtests(args)
2302 if options.profile_runner:
2303 import statprof
2304 statprof.start()
2305 result = self._run(testdescs)
2306 if options.profile_runner:
2307 statprof.stop()
2308 statprof.display()
2309 return result
2310
2311 finally:
2312 os.umask(oldmask)
2313
2314 def _run(self, testdescs):
2315 if 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
2354 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2355 os, 'getcwdb', os.getcwd)()
2356 if self.options.outputdir:
2357 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2358 else:
2359 self._outputdir = self._testdir
2360
2361 if 'PYTHONHASHSEED' not in os.environ:
2362 # use a random python hash seed all the time
2363 # we do the randomness ourself to know what seed is used
2364 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2365
2366 if self.options.tmpdir:
2367 self.options.keep_tmpdir = True
2368 tmpdir = _bytespath(self.options.tmpdir)
2369 if os.path.exists(tmpdir):
2370 # Meaning of tmpdir has changed since 1.3: we used to create
2371 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2372 # tmpdir already exists.
2373 print("error: temp dir %r already exists" % tmpdir)
2374 return 1
2375
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)
2382 else:
2383 d = None
2384 if os.name == 'nt':
2385 # without this, we get the default temp dir location, but
2386 # in all lowercase, which causes troubles with paths (issue3490)
2387 d = osenvironb.get(b'TMP', None)
2388 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2389
2390 self._hgtmp = osenvironb[b'HGTMP'] = (
2391 os.path.realpath(tmpdir))
2392
2393 if self.options.with_hg:
2394 self._installdir = None
2395 whg = self.options.with_hg
2396 self._bindir = os.path.dirname(os.path.realpath(whg))
2397 assert isinstance(self._bindir, bytes)
2398 self._hgcommand = os.path.basename(whg)
2399 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2400 os.makedirs(self._tmpbindir)
2401
2402 # This looks redundant with how Python initializes sys.path from
2403 # the location of the script being executed. Needed because the
2404 # "hg" specified by --with-hg is not the only Python script
2405 # executed in the test suite that needs to import 'mercurial'
2406 # ... which means it's not really redundant at all.
2407 self._pythondir = self._bindir
2408 else:
2409 self._installdir = os.path.join(self._hgtmp, b"install")
2410 self._bindir = os.path.join(self._installdir, b"bin")
2411 self._hgcommand = b'hg'
2412 self._tmpbindir = self._bindir
2413 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2414
2415 # set CHGHG, then replace "hg" command by "chg"
2416 chgbindir = self._bindir
2417 if self.options.chg or self.options.with_chg:
2418 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2419 else:
2420 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2421 if self.options.chg:
2422 self._hgcommand = b'chg'
2423 elif self.options.with_chg:
2424 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2425 self._hgcommand = os.path.basename(self.options.with_chg)
2426
2427 osenvironb[b"BINDIR"] = self._bindir
2428 osenvironb[b"PYTHON"] = PYTHON
2429
2430 if self.options.with_python3:
2431 osenvironb[b'PYTHON3'] = self.options.with_python3
2432
2433 fileb = _bytespath(__file__)
2434 runtestdir = os.path.abspath(os.path.dirname(fileb))
2435 osenvironb[b'RUNTESTDIR'] = runtestdir
2436 if PYTHON3:
2437 sepb = _bytespath(os.pathsep)
2438 else:
2439 sepb = os.pathsep
2440 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2441 if os.path.islink(__file__):
2442 # test helper will likely be at the end of the symlink
2443 realfile = os.path.realpath(fileb)
2444 realdir = os.path.abspath(os.path.dirname(realfile))
2445 path.insert(2, realdir)
2446 if chgbindir != self._bindir:
2447 path.insert(1, chgbindir)
2448 if self._testdir != runtestdir:
2449 path = [self._testdir] + path
2450 if self._tmpbindir != self._bindir:
2451 path = [self._tmpbindir] + path
2452 osenvironb[b"PATH"] = sepb.join(path)
2453
2454 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2455 # can run .../tests/run-tests.py test-foo where test-foo
2456 # adds an extension to HGRC. Also include run-test.py directory to
2457 # import modules like heredoctest.
2458 pypath = [self._pythondir, self._testdir, runtestdir]
2459 # We have to augment PYTHONPATH, rather than simply replacing
2460 # it, in case external libraries are only available via current
2461 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2462 # are in /opt/subversion.)
2463 oldpypath = osenvironb.get(IMPL_PATH)
2464 if oldpypath:
2465 pypath.append(oldpypath)
2466 osenvironb[IMPL_PATH] = sepb.join(pypath)
2467
2468 if self.options.pure:
2469 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2470 os.environ["HGMODULEPOLICY"] = "py"
2471
2472 if self.options.allow_slow_tests:
2473 os.environ["HGTEST_SLOW"] = "slow"
2474 elif 'HGTEST_SLOW' in os.environ:
2475 del os.environ['HGTEST_SLOW']
2476
2477 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2478
2479 vlog("# Using TESTDIR", self._testdir)
2480 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2481 vlog("# Using HGTMP", self._hgtmp)
2482 vlog("# Using PATH", os.environ["PATH"])
2483 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2484 vlog("# Writing to directory", self._outputdir)
2485
2486 try:
2487 return self._runtests(testdescs) or 0
2488 finally:
2489 time.sleep(.1)
2490 self._cleanup()
2491
2492 def findtests(self, args):
2493 """Finds possible test files from arguments.
2494
2495 If you wish to inject custom tests into the test harness, this would
2496 be a good function to monkeypatch or override in a derived class.
2497 """
2498 if not args:
2499 if self.options.changed:
2500 proc = Popen4('hg st --rev "%s" -man0 .' %
2501 self.options.changed, None, 0)
2502 stdout, stderr = proc.communicate()
2503 args = stdout.strip(b'\0').split(b'\0')
2504 else:
2505 args = os.listdir(b'.')
2506
2507 tests = []
2508 for t in args:
2509 if not (os.path.basename(t).startswith(b'test-')
2510 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2511 continue
2512 if t.endswith(b'.t'):
2513 # .t file may contain multiple test cases
2514 cases = sorted(parsettestcases(t))
2515 if cases:
2516 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2517 else:
2518 tests.append({'path': t})
2519 else:
2520 tests.append({'path': t})
2521 return tests
2522
2523 def _runtests(self, testdescs):
2524 def _reloadtest(test, i):
2525 # convert a test back to its description dict
2526 desc = {'path': test.path}
2527 case = getattr(test, '_case', None)
2528 if case:
2529 desc['case'] = case
2530 return self._gettest(desc, i)
2531
2532 try:
2533 if self.options.restart:
2534 orig = list(testdescs)
2535 while testdescs:
2536 desc = testdescs[0]
2537 # desc['path'] is a relative path
2538 if 'case' in desc:
2539 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2540 else:
2541 errpath = b'%s.err' % desc['path']
2542 errpath = os.path.join(self._outputdir, errpath)
2543 if os.path.exists(errpath):
2544 break
2545 testdescs.pop(0)
2546 if not testdescs:
2547 print("running all tests")
2548 testdescs = orig
2549
2550 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2551
2552 failed = False
2553 kws = self.options.keywords
2554 if kws is not None and PYTHON3:
2555 kws = kws.encode('utf-8')
2556
2557 suite = TestSuite(self._testdir,
2558 jobs=self.options.jobs,
2559 whitelist=self.options.whitelisted,
2560 blacklist=self.options.blacklist,
2561 retest=self.options.retest,
2562 keywords=kws,
2563 loop=self.options.loop,
2564 runs_per_test=self.options.runs_per_test,
2565 showchannels=self.options.showchannels,
2566 tests=tests, loadtest=_reloadtest)
2567 verbosity = 1
2568 if self.options.verbose:
2569 verbosity = 2
2570 runner = TextTestRunner(self, verbosity=verbosity)
2571
2572 if self.options.list_tests:
2573 result = runner.listtests(suite)
2574 else:
2575 if self._installdir:
2576 self._installhg()
2577 self._checkhglib("Testing")
2578 else:
2579 self._usecorrectpython()
2580 if self.options.chg:
2581 assert self._installdir
2582 self._installchg()
2583
2584 result = runner.run(suite)
2585
2586 if result.failures:
2587 failed = True
2588
2589 if self.options.anycoverage:
2590 self._outputcoverage()
2591 except KeyboardInterrupt:
2592 failed = True
2593 print("\ninterrupted!")
2594
2595 if failed:
2596 return 1
2597
2598 def _getport(self, count):
2599 port = self._ports.get(count) # do we have a cached entry?
2600 if port is None:
2601 portneeded = 3
2602 # above 100 tries we just give up and let test reports failure
2603 for tries in xrange(100):
2604 allfree = True
2605 port = self.options.port + self._portoffset
2606 for idx in xrange(portneeded):
2607 if not checkportisavailable(port + idx):
2608 allfree = False
2609 break
2610 self._portoffset += portneeded
2611 if allfree:
936 break 2612 break
2613 self._ports[count] = port
2614 return port
2615
2616 def _gettest(self, testdesc, count):
2617 """Obtain a Test by looking at its filename.
2618
2619 Returns a Test instance. The Test may not be runnable if it doesn't
2620 map to a known type.
2621 """
2622 path = testdesc['path']
2623 lctest = path.lower()
2624 testcls = Test
2625
2626 for ext, cls in self.TESTTYPES:
2627 if lctest.endswith(ext):
2628 testcls = cls
2629 break
2630
2631 refpath = os.path.join(self._testdir, path)
2632 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2633
2634 # extra keyword parameters. 'case' is used by .t tests
2635 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2636
2637 t = testcls(refpath, self._outputdir, tmpdir,
2638 keeptmpdir=self.options.keep_tmpdir,
2639 debug=self.options.debug,
2640 timeout=self.options.timeout,
2641 startport=self._getport(count),
2642 extraconfigopts=self.options.extra_config_opt,
2643 py3kwarnings=self.options.py3k_warnings,
2644 shell=self.options.shell,
2645 hgcommand=self._hgcommand,
2646 usechg=bool(self.options.with_chg or self.options.chg),
2647 useipv6=useipv6, **kwds)
2648 t.should_reload = True
2649 return t
2650
2651 def _cleanup(self):
2652 """Clean up state from this test invocation."""
2653 if self.options.keep_tmpdir:
2654 return
2655
2656 vlog("# Cleaning up HGTMP", self._hgtmp)
2657 shutil.rmtree(self._hgtmp, True)
2658 for f in self._createdfiles:
2659 try:
2660 os.remove(f)
2661 except OSError:
2662 pass
2663
2664 def _usecorrectpython(self):
2665 """Configure the environment to use the appropriate Python in tests."""
2666 # Tests must use the same interpreter as us or bad things will happen.
2667 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2668 if getattr(os, 'symlink', None):
2669 vlog("# Making python executable in test path a symlink to '%s'" %
2670 sys.executable)
2671 mypython = os.path.join(self._tmpbindir, pyexename)
2672 try:
2673 if os.readlink(mypython) == sys.executable:
2674 return
2675 os.unlink(mypython)
2676 except OSError as err:
2677 if err.errno != errno.ENOENT:
2678 raise
2679 if self._findprogram(pyexename) != sys.executable:
2680 try:
2681 os.symlink(sys.executable, mypython)
2682 self._createdfiles.append(mypython)
2683 except OSError as err:
2684 # child processes may race, which is harmless
2685 if err.errno != errno.EEXIST:
2686 raise
2687 else:
2688 exedir, exename = os.path.split(sys.executable)
2689 vlog("# Modifying search path to find %s as %s in '%s'" %
2690 (exename, pyexename, exedir))
2691 path = os.environ['PATH'].split(os.pathsep)
2692 while exedir in path:
2693 path.remove(exedir)
2694 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2695 if not self._findprogram(pyexename):
2696 print("WARNING: Cannot find %s in search path" % pyexename)
2697
2698 def _installhg(self):
2699 """Install hg into the test environment.
2700
2701 This will also configure hg with the appropriate testing settings.
2702 """
2703 vlog("# Performing temporary installation of HG")
2704 installerrs = os.path.join(self._hgtmp, b"install.err")
2705 compiler = ''
2706 if self.options.compiler:
2707 compiler = '--compiler ' + self.options.compiler
2708 if self.options.pure:
2709 pure = b"--pure"
2710 else:
2711 pure = b""
2712
2713 # Run installer in hg root
2714 script = os.path.realpath(sys.argv[0])
2715 exe = sys.executable
2716 if PYTHON3:
2717 compiler = _bytespath(compiler)
2718 script = _bytespath(script)
2719 exe = _bytespath(exe)
2720 hgroot = os.path.dirname(os.path.dirname(script))
2721 self._hgroot = hgroot
2722 os.chdir(hgroot)
2723 nohome = b'--home=""'
2724 if os.name == 'nt':
2725 # The --home="" trick works only on OS where os.sep == '/'
2726 # because of a distutils convert_path() fast-path. Avoid it at
2727 # least on Windows for now, deal with .pydistutils.cfg bugs
2728 # when they happen.
2729 nohome = b''
2730 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2731 b' build %(compiler)s --build-base="%(base)s"'
2732 b' install --force --prefix="%(prefix)s"'
2733 b' --install-lib="%(libdir)s"'
2734 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2735 % {b'exe': exe, b'pure': pure,
2736 b'compiler': compiler,
2737 b'base': os.path.join(self._hgtmp, b"build"),
2738 b'prefix': self._installdir, b'libdir': self._pythondir,
2739 b'bindir': self._bindir,
2740 b'nohome': nohome, b'logfile': installerrs})
2741
2742 # setuptools requires install directories to exist.
2743 def makedirs(p):
2744 try:
2745 os.makedirs(p)
2746 except OSError as e:
2747 if e.errno != errno.EEXIST:
2748 raise
2749 makedirs(self._pythondir)
2750 makedirs(self._bindir)
2751
2752 vlog("# Running", cmd)
2753 if os.system(cmd) == 0:
2754 if not self.options.verbose:
2755 try:
2756 os.remove(installerrs)
2757 except OSError as e:
2758 if e.errno != errno.ENOENT:
2759 raise
2760 else:
2761 f = open(installerrs, 'rb')
2762 for line in f:
2763 if PYTHON3:
2764 sys.stdout.buffer.write(line)
937 else: 2765 else:
938 return ignore("doesn't match keyword") 2766 sys.stdout.write(line)
939 2767 f.close()
940 if not lctest.startswith("test-"): 2768 sys.exit(1)
941 return skip("not a test file") 2769 os.chdir(self._testdir)
942 for ext, func, out in testtypes: 2770
943 if lctest.endswith(ext): 2771 self._usecorrectpython()
944 runner = func 2772
945 ref = os.path.join(TESTDIR, test + out) 2773 if self.options.py3k_warnings and not self.options.anycoverage:
946 break 2774 vlog("# Updating hg command to enable Py3k Warnings switch")
947 else: 2775 f = open(os.path.join(self._bindir, 'hg'), 'rb')
948 return skip("unknown test type") 2776 lines = [line.rstrip() for line in f]
949 2777 lines[0] += ' -3'
950 vlog("# Test", test) 2778 f.close()
951 2779 f = open(os.path.join(self._bindir, 'hg'), 'wb')
952 if os.path.exists(err): 2780 for line in lines:
953 os.remove(err) # Remove any previous output files 2781 f.write(line + '\n')
954 2782 f.close()
955 # Make a tmp subdirectory to work in 2783
956 threadtmp = os.path.join(HGTMP, "child%d" % count) 2784 hgbat = os.path.join(self._bindir, b'hg.bat')
957 testtmp = os.path.join(threadtmp, os.path.basename(test)) 2785 if os.path.isfile(hgbat):
958 os.mkdir(threadtmp) 2786 # hg.bat expects to be put in bin/scripts while run-tests.py
959 os.mkdir(testtmp) 2787 # installation layout put it in bin/ directly. Fix it
960 2788 f = open(hgbat, 'rb')
961 port = options.port + count * 3 2789 data = f.read()
962 replacements = [ 2790 f.close()
963 (r':%s\b' % port, ':$HGPORT'), 2791 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
964 (r':%s\b' % (port + 1), ':$HGPORT1'), 2792 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
965 (r':%s\b' % (port + 2), ':$HGPORT2'), 2793 b'"%~dp0python" "%~dp0hg" %*')
966 ] 2794 f = open(hgbat, 'wb')
967 if os.name == 'nt': 2795 f.write(data)
968 replacements.append( 2796 f.close()
969 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or 2797 else:
970 c in '/\\' and r'[/\\]' or 2798 print('WARNING: cannot fix hg.bat reference to python.exe')
971 c.isdigit() and c or 2799
972 '\\' + c 2800 if self.options.anycoverage:
973 for c in testtmp), '$TESTTMP')) 2801 custom = os.path.join(self._testdir, 'sitecustomize.py')
974 else: 2802 target = os.path.join(self._pythondir, 'sitecustomize.py')
975 replacements.append((re.escape(testtmp), '$TESTTMP')) 2803 vlog('# Installing coverage trigger to %s' % target)
976 2804 shutil.copyfile(custom, target)
977 env = createenv(options, testtmp, threadtmp, port) 2805 rc = os.path.join(self._testdir, '.coveragerc')
978 createhgrc(env['HGRCPATH'], options) 2806 vlog('# Installing coverage rc to %s' % rc)
979 2807 os.environ['COVERAGE_PROCESS_START'] = rc
980 starttime = time.time() 2808 covdir = os.path.join(self._installdir, '..', 'coverage')
2809 try:
2810 os.mkdir(covdir)
2811 except OSError as e:
2812 if e.errno != errno.EEXIST:
2813 raise
2814
2815 os.environ['COVERAGE_DIR'] = covdir
2816
2817 def _checkhglib(self, verb):
2818 """Ensure that the 'mercurial' package imported by python is
2819 the one we expect it to be. If not, print a warning to stderr."""
2820 if ((self._bindir == self._pythondir) and
2821 (self._bindir != self._tmpbindir)):
2822 # The pythondir has been inferred from --with-hg flag.
2823 # We cannot expect anything sensible here.
2824 return
2825 expecthg = os.path.join(self._pythondir, b'mercurial')
2826 actualhg = self._gethgpath()
2827 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2828 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2829 ' (expected %s)\n'
2830 % (verb, actualhg, expecthg))
2831 def _gethgpath(self):
2832 """Return the path to the mercurial package that is actually found by
2833 the current Python interpreter."""
2834 if self._hgpath is not None:
2835 return self._hgpath
2836
2837 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2838 cmd = cmd % PYTHON
2839 if PYTHON3:
2840 cmd = _strpath(cmd)
2841 pipe = os.popen(cmd)
2842 try:
2843 self._hgpath = _bytespath(pipe.read().strip())
2844 finally:
2845 pipe.close()
2846
2847 return self._hgpath
2848
2849 def _installchg(self):
2850 """Install chg into the test environment"""
2851 vlog('# Performing temporary installation of CHG')
2852 assert os.path.dirname(self._bindir) == self._installdir
2853 assert self._hgroot, 'must be called after _installhg()'
2854 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2855 % {b'make': 'make', # TODO: switch by option or environment?
2856 b'prefix': self._installdir})
2857 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2858 vlog("# Running", cmd)
2859 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2860 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2861 stderr=subprocess.STDOUT)
2862 out, _err = proc.communicate()
2863 if proc.returncode != 0:
2864 if PYTHON3:
2865 sys.stdout.buffer.write(out)
2866 else:
2867 sys.stdout.write(out)
2868 sys.exit(1)
2869
2870 def _outputcoverage(self):
2871 """Produce code coverage output."""
2872 import coverage
2873 coverage = coverage.coverage
2874
2875 vlog('# Producing coverage report')
2876 # chdir is the easiest way to get short, relative paths in the
2877 # output.
2878 os.chdir(self._hgroot)
2879 covdir = os.path.join(self._installdir, '..', 'coverage')
2880 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2881
2882 # Map install directory paths back to source directory.
2883 cov.config.paths['srcdir'] = ['.', self._pythondir]
2884
2885 cov.combine()
2886
2887 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2888 cov.report(ignore_errors=True, omit=omit)
2889
2890 if self.options.htmlcov:
2891 htmldir = os.path.join(self._outputdir, 'htmlcov')
2892 cov.html_report(directory=htmldir, omit=omit)
2893 if self.options.annotate:
2894 adir = os.path.join(self._outputdir, 'annotated')
2895 if not os.path.isdir(adir):
2896 os.mkdir(adir)
2897 cov.annotate(directory=adir, omit=omit)
2898
2899 def _findprogram(self, program):
2900 """Search PATH for a executable program"""
2901 dpb = _bytespath(os.defpath)
2902 sepb = _bytespath(os.pathsep)
2903 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2904 name = os.path.join(p, program)
2905 if os.name == 'nt' or os.access(name, os.X_OK):
2906 return name
2907 return None
2908
2909 def _checktools(self):
2910 """Ensure tools required to run tests are present."""
2911 for p in self.REQUIREDTOOLS:
2912 if os.name == 'nt' and not p.endswith('.exe'):
2913 p += '.exe'
2914 found = self._findprogram(p)
2915 if found:
2916 vlog("# Found prerequisite", p, "at", found)
2917 else:
2918 print("WARNING: Did not find prerequisite tool: %s " %
2919 p.decode("utf-8"))
2920
2921 if __name__ == '__main__':
2922 runner = TestRunner()
2923
981 try: 2924 try:
982 ret, out = runner(testpath, testtmp, options, replacements, env) 2925 import msvcrt
983 except KeyboardInterrupt: 2926 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
984 endtime = time.time() 2927 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
985 log('INTERRUPTED: %s (after %d seconds)' % (test, endtime - starttime)) 2928 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
986 raise 2929 except ImportError:
987 endtime = time.time() 2930 pass
988 times.append((test, endtime - starttime)) 2931
989 vlog("# Ret was:", ret) 2932 sys.exit(runner.run(sys.argv[1:]))
990
991 killdaemons(env['DAEMON_PIDS'])
992
993 skipped = (ret == SKIPPED_STATUS)
994
995 # If we're not in --debug mode and reference output file exists,
996 # check test output against it.
997 if options.debug:
998 refout = None # to match "out is None"
999 elif os.path.exists(ref):
1000 f = open(ref, "r")
1001 refout = f.read().splitlines(True)
1002 f.close()
1003 else:
1004 refout = []
1005
1006 if (ret != 0 or out != refout) and not skipped and not options.debug:
1007 # Save errors to a file for diagnosis
1008 f = open(err, "wb")
1009 for line in out:
1010 f.write(line)
1011 f.close()
1012
1013 if skipped:
1014 if out is None: # debug mode: nothing to parse
1015 missing = ['unknown']
1016 failed = None
1017 else:
1018 missing, failed = parsehghaveoutput(out)
1019 if not missing:
1020 missing = ['irrelevant']
1021 if failed:
1022 result = fail("hghave failed checking for %s" % failed[-1], ret)
1023 skipped = False
1024 else:
1025 result = skip(missing[-1])
1026 elif ret == 'timeout':
1027 result = fail("timed out", ret)
1028 elif out != refout:
1029 if not options.nodiff:
1030 iolock.acquire()
1031 if options.view:
1032 os.system("%s %s %s" % (options.view, ref, err))
1033 else:
1034 showdiff(refout, out, ref, err)
1035 iolock.release()
1036 if ret:
1037 result = fail("output changed and " + describe(ret), ret)
1038 else:
1039 result = fail("output changed", ret)
1040 elif ret:
1041 result = fail(describe(ret), ret)
1042 else:
1043 result = success()
1044
1045 if not options.verbose:
1046 iolock.acquire()
1047 sys.stdout.write(result[0])
1048 sys.stdout.flush()
1049 iolock.release()
1050
1051 if not options.keep_tmpdir:
1052 shutil.rmtree(threadtmp, True)
1053 return result
1054
1055 _hgpath = None
1056
1057 def _gethgpath():
1058 """Return the path to the mercurial package that is actually found by
1059 the current Python interpreter."""
1060 global _hgpath
1061 if _hgpath is not None:
1062 return _hgpath
1063
1064 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1065 pipe = os.popen(cmd % PYTHON)
1066 try:
1067 _hgpath = pipe.read().strip()
1068 finally:
1069 pipe.close()
1070 return _hgpath
1071
1072 def _checkhglib(verb):
1073 """Ensure that the 'mercurial' package imported by python is
1074 the one we expect it to be. If not, print a warning to stderr."""
1075 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1076 actualhg = _gethgpath()
1077 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1078 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1079 ' (expected %s)\n'
1080 % (verb, actualhg, expecthg))
1081
1082 results = {'.':[], '!':[], '~': [], 's':[], 'i':[]}
1083 times = []
1084 iolock = threading.Lock()
1085 abort = False
1086
1087 def scheduletests(options, tests):
1088 jobs = options.jobs
1089 done = queue.Queue()
1090 running = 0
1091 count = 0
1092 global abort
1093
1094 def job(test, count):
1095 try:
1096 done.put(runone(options, test, count))
1097 except KeyboardInterrupt:
1098 pass
1099 except: # re-raises
1100 done.put(('!', test, 'run-test raised an error, see traceback'))
1101 raise
1102
1103 try:
1104 while tests or running:
1105 if not done.empty() or running == jobs or not tests:
1106 try:
1107 code, test, msg = done.get(True, 1)
1108 results[code].append((test, msg))
1109 if options.first and code not in '.si':
1110 break
1111 except queue.Empty:
1112 continue
1113 running -= 1
1114 if tests and not running == jobs:
1115 test = tests.pop(0)
1116 if options.loop:
1117 tests.append(test)
1118 t = threading.Thread(target=job, name=test, args=(test, count))
1119 t.start()
1120 running += 1
1121 count += 1
1122 except KeyboardInterrupt:
1123 abort = True
1124
1125 def runtests(options, tests):
1126 try:
1127 if INST:
1128 installhg(options)
1129 _checkhglib("Testing")
1130 else:
1131 usecorrectpython()
1132
1133 if options.restart:
1134 orig = list(tests)
1135 while tests:
1136 if os.path.exists(tests[0] + ".err"):
1137 break
1138 tests.pop(0)
1139 if not tests:
1140 print "running all tests"
1141 tests = orig
1142
1143 scheduletests(options, tests)
1144
1145 failed = len(results['!'])
1146 warned = len(results['~'])
1147 tested = len(results['.']) + failed + warned
1148 skipped = len(results['s'])
1149 ignored = len(results['i'])
1150
1151 print
1152 if not options.noskips:
1153 for s in results['s']:
1154 print "Skipped %s: %s" % s
1155 for s in results['~']:
1156 print "Warned %s: %s" % s
1157 for s in results['!']:
1158 print "Failed %s: %s" % s
1159 _checkhglib("Tested")
1160 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1161 tested, skipped + ignored, warned, failed)
1162 if results['!']:
1163 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1164 if options.time:
1165 outputtimes(options)
1166
1167 if options.anycoverage:
1168 outputcoverage(options)
1169 except KeyboardInterrupt:
1170 failed = True
1171 print "\ninterrupted!"
1172
1173 if failed:
1174 return 1
1175 if warned:
1176 return 80
1177
1178 testtypes = [('.py', pytest, '.out'),
1179 ('.t', tsttest, '')]
1180
1181 def main():
1182 (options, args) = parseargs()
1183 os.umask(022)
1184
1185 checktools()
1186
1187 if len(args) == 0:
1188 args = [t for t in os.listdir(".")
1189 if t.startswith("test-")
1190 and (t.endswith(".py") or t.endswith(".t"))]
1191
1192 tests = args
1193
1194 if options.random:
1195 random.shuffle(tests)
1196 else:
1197 # keywords for slow tests
1198 slow = 'svn gendoc check-code-hg'.split()
1199 def sortkey(f):
1200 # run largest tests first, as they tend to take the longest
1201 try:
1202 val = -os.stat(f).st_size
1203 except OSError, e:
1204 if e.errno != errno.ENOENT:
1205 raise
1206 return -1e9 # file does not exist, tell early
1207 for kw in slow:
1208 if kw in f:
1209 val *= 10
1210 return val
1211 tests.sort(key=sortkey)
1212
1213 if 'PYTHONHASHSEED' not in os.environ:
1214 # use a random python hash seed all the time
1215 # we do the randomness ourself to know what seed is used
1216 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1217
1218 global TESTDIR, HGTMP, INST, BINDIR, TMPBINDIR, PYTHONDIR, COVERAGE_FILE
1219 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1220 if options.tmpdir:
1221 options.keep_tmpdir = True
1222 tmpdir = options.tmpdir
1223 if os.path.exists(tmpdir):
1224 # Meaning of tmpdir has changed since 1.3: we used to create
1225 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1226 # tmpdir already exists.
1227 sys.exit("error: temp dir %r already exists" % tmpdir)
1228
1229 # Automatically removing tmpdir sounds convenient, but could
1230 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1231 # or "--tmpdir=$HOME".
1232 #vlog("# Removing temp dir", tmpdir)
1233 #shutil.rmtree(tmpdir)
1234 os.makedirs(tmpdir)
1235 else:
1236 d = None
1237 if os.name == 'nt':
1238 # without this, we get the default temp dir location, but
1239 # in all lowercase, which causes troubles with paths (issue3490)
1240 d = os.getenv('TMP')
1241 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1242 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1243
1244 if options.with_hg:
1245 INST = None
1246 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1247 TMPBINDIR = os.path.join(HGTMP, 'install', 'bin')
1248 os.makedirs(TMPBINDIR)
1249
1250 # This looks redundant with how Python initializes sys.path from
1251 # the location of the script being executed. Needed because the
1252 # "hg" specified by --with-hg is not the only Python script
1253 # executed in the test suite that needs to import 'mercurial'
1254 # ... which means it's not really redundant at all.
1255 PYTHONDIR = BINDIR
1256 else:
1257 INST = os.path.join(HGTMP, "install")
1258 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1259 TMPBINDIR = BINDIR
1260 PYTHONDIR = os.path.join(INST, "lib", "python")
1261
1262 os.environ["BINDIR"] = BINDIR
1263 os.environ["PYTHON"] = PYTHON
1264
1265 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1266 if TMPBINDIR != BINDIR:
1267 path = [TMPBINDIR] + path
1268 os.environ["PATH"] = os.pathsep.join(path)
1269
1270 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1271 # can run .../tests/run-tests.py test-foo where test-foo
1272 # adds an extension to HGRC. Also include run-test.py directory to import
1273 # modules like heredoctest.
1274 pypath = [PYTHONDIR, TESTDIR, os.path.abspath(os.path.dirname(__file__))]
1275 # We have to augment PYTHONPATH, rather than simply replacing
1276 # it, in case external libraries are only available via current
1277 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1278 # are in /opt/subversion.)
1279 oldpypath = os.environ.get(IMPL_PATH)
1280 if oldpypath:
1281 pypath.append(oldpypath)
1282 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1283
1284 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1285
1286 vlog("# Using TESTDIR", TESTDIR)
1287 vlog("# Using HGTMP", HGTMP)
1288 vlog("# Using PATH", os.environ["PATH"])
1289 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1290
1291 try:
1292 sys.exit(runtests(options, tests) or 0)
1293 finally:
1294 time.sleep(.1)
1295 cleanup(options)
1296
1297 if __name__ == '__main__':
1298 main()