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