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