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