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