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