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