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