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