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