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