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