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