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