##// END OF EJS Templates
run-tests: capture reference output in Test.__init__...
Gregory Szorc -
r21318:6b3d66e4 default
parent child Browse files
Show More
@@ -1,1410 +1,1412 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 #
39 39 # (You could use any subset of the tests: test-s* happens to match
40 40 # enough that it's worth doing parallel runs, few enough that it
41 41 # completes fairly quickly, includes both shell and Python scripts, and
42 42 # includes some scripts that run daemon processes.)
43 43
44 44 from distutils import version
45 45 import difflib
46 46 import errno
47 47 import optparse
48 48 import os
49 49 import shutil
50 50 import subprocess
51 51 import signal
52 52 import sys
53 53 import tempfile
54 54 import time
55 55 import random
56 56 import re
57 57 import threading
58 58 import killdaemons as killmod
59 59 import Queue as queue
60 60
61 61 processlock = threading.Lock()
62 62
63 63 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
64 64 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
65 65 # zombies but it's pretty harmless even if we do.
66 66 if sys.version_info < (2, 5):
67 67 subprocess._cleanup = lambda: None
68 68
69 69 closefds = os.name == 'posix'
70 70 def Popen4(cmd, wd, timeout, env=None):
71 71 processlock.acquire()
72 72 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
73 73 close_fds=closefds,
74 74 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
75 75 stderr=subprocess.STDOUT)
76 76 processlock.release()
77 77
78 78 p.fromchild = p.stdout
79 79 p.tochild = p.stdin
80 80 p.childerr = p.stderr
81 81
82 82 p.timeout = False
83 83 if timeout:
84 84 def t():
85 85 start = time.time()
86 86 while time.time() - start < timeout and p.returncode is None:
87 87 time.sleep(.1)
88 88 p.timeout = True
89 89 if p.returncode is None:
90 90 terminate(p)
91 91 threading.Thread(target=t).start()
92 92
93 93 return p
94 94
95 95 # reserved exit code to skip test (used by hghave)
96 96 SKIPPED_STATUS = 80
97 97 SKIPPED_PREFIX = 'skipped: '
98 98 FAILED_PREFIX = 'hghave check failed: '
99 99 PYTHON = sys.executable.replace('\\', '/')
100 100 IMPL_PATH = 'PYTHONPATH'
101 101 if 'java' in sys.platform:
102 102 IMPL_PATH = 'JYTHONPATH'
103 103
104 104 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
105 105
106 106 requiredtools = [os.path.basename(sys.executable), "diff", "grep", "unzip",
107 107 "gunzip", "bunzip2", "sed"]
108 108 createdfiles = []
109 109
110 110 defaults = {
111 111 'jobs': ('HGTEST_JOBS', 1),
112 112 'timeout': ('HGTEST_TIMEOUT', 180),
113 113 'port': ('HGTEST_PORT', 20059),
114 114 'shell': ('HGTEST_SHELL', 'sh'),
115 115 }
116 116
117 117 def parselistfiles(files, listtype, warn=True):
118 118 entries = dict()
119 119 for filename in files:
120 120 try:
121 121 path = os.path.expanduser(os.path.expandvars(filename))
122 122 f = open(path, "r")
123 123 except IOError, err:
124 124 if err.errno != errno.ENOENT:
125 125 raise
126 126 if warn:
127 127 print "warning: no such %s file: %s" % (listtype, filename)
128 128 continue
129 129
130 130 for line in f.readlines():
131 131 line = line.split('#', 1)[0].strip()
132 132 if line:
133 133 entries[line] = filename
134 134
135 135 f.close()
136 136 return entries
137 137
138 138 def getparser():
139 139 parser = optparse.OptionParser("%prog [options] [tests]")
140 140
141 141 # keep these sorted
142 142 parser.add_option("--blacklist", action="append",
143 143 help="skip tests listed in the specified blacklist file")
144 144 parser.add_option("--whitelist", action="append",
145 145 help="always run tests listed in the specified whitelist file")
146 146 parser.add_option("--changed", type="string",
147 147 help="run tests that are changed in parent rev or working directory")
148 148 parser.add_option("-C", "--annotate", action="store_true",
149 149 help="output files annotated with coverage")
150 150 parser.add_option("-c", "--cover", action="store_true",
151 151 help="print a test coverage report")
152 152 parser.add_option("-d", "--debug", action="store_true",
153 153 help="debug mode: write output of test scripts to console"
154 154 " rather than capturing and diffing it (disables timeout)")
155 155 parser.add_option("-f", "--first", action="store_true",
156 156 help="exit on the first test failure")
157 157 parser.add_option("-H", "--htmlcov", action="store_true",
158 158 help="create an HTML report of the coverage of the files")
159 159 parser.add_option("-i", "--interactive", action="store_true",
160 160 help="prompt to accept changed output")
161 161 parser.add_option("-j", "--jobs", type="int",
162 162 help="number of jobs to run in parallel"
163 163 " (default: $%s or %d)" % defaults['jobs'])
164 164 parser.add_option("--keep-tmpdir", action="store_true",
165 165 help="keep temporary directory after running tests")
166 166 parser.add_option("-k", "--keywords",
167 167 help="run tests matching keywords")
168 168 parser.add_option("-l", "--local", action="store_true",
169 169 help="shortcut for --with-hg=<testdir>/../hg")
170 170 parser.add_option("--loop", action="store_true",
171 171 help="loop tests repeatedly")
172 172 parser.add_option("-n", "--nodiff", action="store_true",
173 173 help="skip showing test changes")
174 174 parser.add_option("-p", "--port", type="int",
175 175 help="port on which servers should listen"
176 176 " (default: $%s or %d)" % defaults['port'])
177 177 parser.add_option("--compiler", type="string",
178 178 help="compiler to build with")
179 179 parser.add_option("--pure", action="store_true",
180 180 help="use pure Python code instead of C extensions")
181 181 parser.add_option("-R", "--restart", action="store_true",
182 182 help="restart at last error")
183 183 parser.add_option("-r", "--retest", action="store_true",
184 184 help="retest failed tests")
185 185 parser.add_option("-S", "--noskips", action="store_true",
186 186 help="don't report skip tests verbosely")
187 187 parser.add_option("--shell", type="string",
188 188 help="shell to use (default: $%s or %s)" % defaults['shell'])
189 189 parser.add_option("-t", "--timeout", type="int",
190 190 help="kill errant tests after TIMEOUT seconds"
191 191 " (default: $%s or %d)" % defaults['timeout'])
192 192 parser.add_option("--time", action="store_true",
193 193 help="time how long each test takes")
194 194 parser.add_option("--tmpdir", type="string",
195 195 help="run tests in the given temporary directory"
196 196 " (implies --keep-tmpdir)")
197 197 parser.add_option("-v", "--verbose", action="store_true",
198 198 help="output verbose messages")
199 199 parser.add_option("--view", type="string",
200 200 help="external diff viewer")
201 201 parser.add_option("--with-hg", type="string",
202 202 metavar="HG",
203 203 help="test using specified hg script rather than a "
204 204 "temporary installation")
205 205 parser.add_option("-3", "--py3k-warnings", action="store_true",
206 206 help="enable Py3k warnings on Python 2.6+")
207 207 parser.add_option('--extra-config-opt', action="append",
208 208 help='set the given config opt in the test hgrc')
209 209 parser.add_option('--random', action="store_true",
210 210 help='run tests in random order')
211 211
212 212 for option, (envvar, default) in defaults.items():
213 213 defaults[option] = type(default)(os.environ.get(envvar, default))
214 214 parser.set_defaults(**defaults)
215 215
216 216 return parser
217 217
218 218 def parseargs(args, parser):
219 219 (options, args) = parser.parse_args(args)
220 220
221 221 # jython is always pure
222 222 if 'java' in sys.platform or '__pypy__' in sys.modules:
223 223 options.pure = True
224 224
225 225 if options.with_hg:
226 226 options.with_hg = os.path.expanduser(options.with_hg)
227 227 if not (os.path.isfile(options.with_hg) and
228 228 os.access(options.with_hg, os.X_OK)):
229 229 parser.error('--with-hg must specify an executable hg script')
230 230 if not os.path.basename(options.with_hg) == 'hg':
231 231 sys.stderr.write('warning: --with-hg should specify an hg script\n')
232 232 if options.local:
233 233 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
234 234 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
235 235 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
236 236 parser.error('--local specified, but %r not found or not executable'
237 237 % hgbin)
238 238 options.with_hg = hgbin
239 239
240 240 options.anycoverage = options.cover or options.annotate or options.htmlcov
241 241 if options.anycoverage:
242 242 try:
243 243 import coverage
244 244 covver = version.StrictVersion(coverage.__version__).version
245 245 if covver < (3, 3):
246 246 parser.error('coverage options require coverage 3.3 or later')
247 247 except ImportError:
248 248 parser.error('coverage options now require the coverage package')
249 249
250 250 if options.anycoverage and options.local:
251 251 # this needs some path mangling somewhere, I guess
252 252 parser.error("sorry, coverage options do not work when --local "
253 253 "is specified")
254 254
255 255 global verbose
256 256 if options.verbose:
257 257 verbose = ''
258 258
259 259 if options.tmpdir:
260 260 options.tmpdir = os.path.expanduser(options.tmpdir)
261 261
262 262 if options.jobs < 1:
263 263 parser.error('--jobs must be positive')
264 264 if options.interactive and options.debug:
265 265 parser.error("-i/--interactive and -d/--debug are incompatible")
266 266 if options.debug:
267 267 if options.timeout != defaults['timeout']:
268 268 sys.stderr.write(
269 269 'warning: --timeout option ignored with --debug\n')
270 270 options.timeout = 0
271 271 if options.py3k_warnings:
272 272 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
273 273 parser.error('--py3k-warnings can only be used on Python 2.6+')
274 274 if options.blacklist:
275 275 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
276 276 if options.whitelist:
277 277 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
278 278 else:
279 279 options.whitelisted = {}
280 280
281 281 return (options, args)
282 282
283 283 def rename(src, dst):
284 284 """Like os.rename(), trade atomicity and opened files friendliness
285 285 for existing destination support.
286 286 """
287 287 shutil.copy(src, dst)
288 288 os.remove(src)
289 289
290 290 def parsehghaveoutput(lines):
291 291 '''Parse hghave log lines.
292 292 Return tuple of lists (missing, failed):
293 293 * the missing/unknown features
294 294 * the features for which existence check failed'''
295 295 missing = []
296 296 failed = []
297 297 for line in lines:
298 298 if line.startswith(SKIPPED_PREFIX):
299 299 line = line.splitlines()[0]
300 300 missing.append(line[len(SKIPPED_PREFIX):])
301 301 elif line.startswith(FAILED_PREFIX):
302 302 line = line.splitlines()[0]
303 303 failed.append(line[len(FAILED_PREFIX):])
304 304
305 305 return missing, failed
306 306
307 307 def showdiff(expected, output, ref, err):
308 308 print
309 309 servefail = False
310 310 for line in difflib.unified_diff(expected, output, ref, err):
311 311 sys.stdout.write(line)
312 312 if not servefail and line.startswith(
313 313 '+ abort: child process failed to start'):
314 314 servefail = True
315 315 return {'servefail': servefail}
316 316
317 317
318 318 verbose = False
319 319 def vlog(*msg):
320 320 if verbose is not False:
321 321 iolock.acquire()
322 322 if verbose:
323 323 print verbose,
324 324 for m in msg:
325 325 print m,
326 326 print
327 327 sys.stdout.flush()
328 328 iolock.release()
329 329
330 330 def log(*msg):
331 331 iolock.acquire()
332 332 if verbose:
333 333 print verbose,
334 334 for m in msg:
335 335 print m,
336 336 print
337 337 sys.stdout.flush()
338 338 iolock.release()
339 339
340 340 def findprogram(program):
341 341 """Search PATH for a executable program"""
342 342 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
343 343 name = os.path.join(p, program)
344 344 if os.name == 'nt' or os.access(name, os.X_OK):
345 345 return name
346 346 return None
347 347
348 348 def createhgrc(path, options):
349 349 # create a fresh hgrc
350 350 hgrc = open(path, 'w')
351 351 hgrc.write('[ui]\n')
352 352 hgrc.write('slash = True\n')
353 353 hgrc.write('interactive = False\n')
354 354 hgrc.write('[defaults]\n')
355 355 hgrc.write('backout = -d "0 0"\n')
356 356 hgrc.write('commit = -d "0 0"\n')
357 357 hgrc.write('shelve = --date "0 0"\n')
358 358 hgrc.write('tag = -d "0 0"\n')
359 359 if options.extra_config_opt:
360 360 for opt in options.extra_config_opt:
361 361 section, key = opt.split('.', 1)
362 362 assert '=' in key, ('extra config opt %s must '
363 363 'have an = for assignment' % opt)
364 364 hgrc.write('[%s]\n%s\n' % (section, key))
365 365 hgrc.close()
366 366
367 367 def checktools():
368 368 # Before we go any further, check for pre-requisite tools
369 369 # stuff from coreutils (cat, rm, etc) are not tested
370 370 for p in requiredtools:
371 371 if os.name == 'nt' and not p.endswith('.exe'):
372 372 p += '.exe'
373 373 found = findprogram(p)
374 374 if found:
375 375 vlog("# Found prerequisite", p, "at", found)
376 376 else:
377 377 print "WARNING: Did not find prerequisite tool: "+p
378 378
379 379 def terminate(proc):
380 380 """Terminate subprocess (with fallback for Python versions < 2.6)"""
381 381 vlog('# Terminating process %d' % proc.pid)
382 382 try:
383 383 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
384 384 except OSError:
385 385 pass
386 386
387 387 def killdaemons(pidfile):
388 388 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
389 389 logfn=vlog)
390 390
391 391 def cleanup(options):
392 392 if not options.keep_tmpdir:
393 393 vlog("# Cleaning up HGTMP", HGTMP)
394 394 shutil.rmtree(HGTMP, True)
395 395 for f in createdfiles:
396 396 try:
397 397 os.remove(f)
398 398 except OSError:
399 399 pass
400 400
401 401 def usecorrectpython():
402 402 # some tests run python interpreter. they must use same
403 403 # interpreter we use or bad things will happen.
404 404 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
405 405 if getattr(os, 'symlink', None):
406 406 vlog("# Making python executable in test path a symlink to '%s'" %
407 407 sys.executable)
408 408 mypython = os.path.join(TMPBINDIR, pyexename)
409 409 try:
410 410 if os.readlink(mypython) == sys.executable:
411 411 return
412 412 os.unlink(mypython)
413 413 except OSError, err:
414 414 if err.errno != errno.ENOENT:
415 415 raise
416 416 if findprogram(pyexename) != sys.executable:
417 417 try:
418 418 os.symlink(sys.executable, mypython)
419 419 createdfiles.append(mypython)
420 420 except OSError, err:
421 421 # child processes may race, which is harmless
422 422 if err.errno != errno.EEXIST:
423 423 raise
424 424 else:
425 425 exedir, exename = os.path.split(sys.executable)
426 426 vlog("# Modifying search path to find %s as %s in '%s'" %
427 427 (exename, pyexename, exedir))
428 428 path = os.environ['PATH'].split(os.pathsep)
429 429 while exedir in path:
430 430 path.remove(exedir)
431 431 os.environ['PATH'] = os.pathsep.join([exedir] + path)
432 432 if not findprogram(pyexename):
433 433 print "WARNING: Cannot find %s in search path" % pyexename
434 434
435 435 def installhg(options):
436 436 vlog("# Performing temporary installation of HG")
437 437 installerrs = os.path.join("tests", "install.err")
438 438 compiler = ''
439 439 if options.compiler:
440 440 compiler = '--compiler ' + options.compiler
441 441 pure = options.pure and "--pure" or ""
442 442 py3 = ''
443 443 if sys.version_info[0] == 3:
444 444 py3 = '--c2to3'
445 445
446 446 # Run installer in hg root
447 447 script = os.path.realpath(sys.argv[0])
448 448 hgroot = os.path.dirname(os.path.dirname(script))
449 449 os.chdir(hgroot)
450 450 nohome = '--home=""'
451 451 if os.name == 'nt':
452 452 # The --home="" trick works only on OS where os.sep == '/'
453 453 # because of a distutils convert_path() fast-path. Avoid it at
454 454 # least on Windows for now, deal with .pydistutils.cfg bugs
455 455 # when they happen.
456 456 nohome = ''
457 457 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
458 458 ' build %(compiler)s --build-base="%(base)s"'
459 459 ' install --force --prefix="%(prefix)s" --install-lib="%(libdir)s"'
460 460 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
461 461 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
462 462 'compiler': compiler, 'base': os.path.join(HGTMP, "build"),
463 463 'prefix': INST, 'libdir': PYTHONDIR, 'bindir': BINDIR,
464 464 'nohome': nohome, 'logfile': installerrs})
465 465 vlog("# Running", cmd)
466 466 if os.system(cmd) == 0:
467 467 if not options.verbose:
468 468 os.remove(installerrs)
469 469 else:
470 470 f = open(installerrs)
471 471 for line in f:
472 472 print line,
473 473 f.close()
474 474 sys.exit(1)
475 475 os.chdir(TESTDIR)
476 476
477 477 usecorrectpython()
478 478
479 479 if options.py3k_warnings and not options.anycoverage:
480 480 vlog("# Updating hg command to enable Py3k Warnings switch")
481 481 f = open(os.path.join(BINDIR, 'hg'), 'r')
482 482 lines = [line.rstrip() for line in f]
483 483 lines[0] += ' -3'
484 484 f.close()
485 485 f = open(os.path.join(BINDIR, 'hg'), 'w')
486 486 for line in lines:
487 487 f.write(line + '\n')
488 488 f.close()
489 489
490 490 hgbat = os.path.join(BINDIR, 'hg.bat')
491 491 if os.path.isfile(hgbat):
492 492 # hg.bat expects to be put in bin/scripts while run-tests.py
493 493 # installation layout put it in bin/ directly. Fix it
494 494 f = open(hgbat, 'rb')
495 495 data = f.read()
496 496 f.close()
497 497 if '"%~dp0..\python" "%~dp0hg" %*' in data:
498 498 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
499 499 '"%~dp0python" "%~dp0hg" %*')
500 500 f = open(hgbat, 'wb')
501 501 f.write(data)
502 502 f.close()
503 503 else:
504 504 print 'WARNING: cannot fix hg.bat reference to python.exe'
505 505
506 506 if options.anycoverage:
507 507 custom = os.path.join(TESTDIR, 'sitecustomize.py')
508 508 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
509 509 vlog('# Installing coverage trigger to %s' % target)
510 510 shutil.copyfile(custom, target)
511 511 rc = os.path.join(TESTDIR, '.coveragerc')
512 512 vlog('# Installing coverage rc to %s' % rc)
513 513 os.environ['COVERAGE_PROCESS_START'] = rc
514 514 fn = os.path.join(INST, '..', '.coverage')
515 515 os.environ['COVERAGE_FILE'] = fn
516 516
517 517 def outputtimes(options):
518 518 vlog('# Producing time report')
519 519 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
520 520 cols = '%7.3f %s'
521 521 print '\n%-7s %s' % ('Time', 'Test')
522 522 for test, timetaken in times:
523 523 print cols % (timetaken, test)
524 524
525 525 def outputcoverage(options):
526 526
527 527 vlog('# Producing coverage report')
528 528 os.chdir(PYTHONDIR)
529 529
530 530 def covrun(*args):
531 531 cmd = 'coverage %s' % ' '.join(args)
532 532 vlog('# Running: %s' % cmd)
533 533 os.system(cmd)
534 534
535 535 covrun('-c')
536 536 omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR])
537 537 covrun('-i', '-r', '"--omit=%s"' % omit) # report
538 538 if options.htmlcov:
539 539 htmldir = os.path.join(TESTDIR, 'htmlcov')
540 540 covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit)
541 541 if options.annotate:
542 542 adir = os.path.join(TESTDIR, 'annotated')
543 543 if not os.path.isdir(adir):
544 544 os.mkdir(adir)
545 545 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
546 546
547 547 class Test(object):
548 548 """Encapsulates a single, runnable test.
549 549
550 550 Test instances can be run multiple times via run(). However, multiple
551 551 runs cannot be run concurrently.
552 552 """
553 553
554 def __init__(self, path, options, count):
554 def __init__(self, path, options, count, refpath):
555 555 self._path = path
556 556 self._options = options
557 557 self._count = count
558 558
559 # If we're not in --debug mode and reference output file exists,
560 # check test output against it.
561 if options.debug:
562 self._refout = None # to match "out is None"
563 elif os.path.exists(refpath):
564 f = open(refpath, 'r')
565 self._refout = f.read().splitlines(True)
566 f.close()
567 else:
568 self._refout = []
569
559 570 self._threadtmp = os.path.join(HGTMP, 'child%d' % count)
560 571 os.mkdir(self._threadtmp)
561 572
562 573 def cleanup(self):
563 574 if self._threadtmp and not self._options.keep_tmpdir:
564 575 shutil.rmtree(self._threadtmp, True)
565 576
566 def run(self, result, refpath):
577 def run(self, result):
567 578 testtmp = os.path.join(self._threadtmp, os.path.basename(self._path))
568 579 os.mkdir(testtmp)
569 580 replacements, port = self._getreplacements(testtmp)
570 581 env = self._getenv(testtmp, port)
571 582 createhgrc(env['HGRCPATH'], self._options)
572 583
573 584 starttime = time.time()
574 585
575 586 def updateduration():
576 587 result.duration = time.time() - starttime
577 588
578 589 try:
579 590 ret, out = self._run(testtmp, replacements, env)
580 591 updateduration()
581 592 result.ret = ret
582 593 result.out = out
583 594 except KeyboardInterrupt:
584 595 updateduration()
585 596 result.interrupted = True
586 597 except Exception, e:
587 598 updateduration()
588 599 result.exception = e
589 600
590 601 killdaemons(env['DAEMON_PIDS'])
591 602
592 # If we're not in --debug mode and reference output file exists,
593 # check test output against it.
594 if self._options.debug:
595 result.refout = None # to match "out is None"
596 elif os.path.exists(refpath):
597 f = open(refpath, 'r')
598 result.refout = f.read().splitlines(True)
599 f.close()
600 else:
601 result.refout = []
603 result.refout = self._refout
602 604
603 605 if not self._options.keep_tmpdir:
604 606 shutil.rmtree(testtmp)
605 607
606 608 def _run(self, testtmp, replacements, env):
607 609 raise NotImplemented('Subclasses must implement Test.run()')
608 610
609 611 def _getreplacements(self, testtmp):
610 612 port = self._options.port + self._count * 3
611 613 r = [
612 614 (r':%s\b' % port, ':$HGPORT'),
613 615 (r':%s\b' % (port + 1), ':$HGPORT1'),
614 616 (r':%s\b' % (port + 2), ':$HGPORT2'),
615 617 ]
616 618
617 619 if os.name == 'nt':
618 620 r.append(
619 621 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
620 622 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
621 623 for c in testtmp), '$TESTTMP'))
622 624 else:
623 625 r.append((re.escape(testtmp), '$TESTTMP'))
624 626
625 627 return r, port
626 628
627 629 def _getenv(self, testtmp, port):
628 630 env = os.environ.copy()
629 631 env['TESTTMP'] = testtmp
630 632 env['HOME'] = testtmp
631 633 env["HGPORT"] = str(port)
632 634 env["HGPORT1"] = str(port + 1)
633 635 env["HGPORT2"] = str(port + 2)
634 636 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
635 637 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
636 638 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
637 639 env["HGMERGE"] = "internal:merge"
638 640 env["HGUSER"] = "test"
639 641 env["HGENCODING"] = "ascii"
640 642 env["HGENCODINGMODE"] = "strict"
641 643
642 644 # Reset some environment variables to well-known values so that
643 645 # the tests produce repeatable output.
644 646 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
645 647 env['TZ'] = 'GMT'
646 648 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
647 649 env['COLUMNS'] = '80'
648 650 env['TERM'] = 'xterm'
649 651
650 652 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
651 653 'NO_PROXY').split():
652 654 if k in env:
653 655 del env[k]
654 656
655 657 # unset env related to hooks
656 658 for k in env.keys():
657 659 if k.startswith('HG_'):
658 660 del env[k]
659 661
660 662 return env
661 663
662 664 class TestResult(object):
663 665 """Holds the result of a test execution."""
664 666
665 667 def __init__(self):
666 668 self.ret = None
667 669 self.out = None
668 670 self.duration = None
669 671 self.interrupted = False
670 672 self.exception = None
671 673 self.refout = None
672 674
673 675 @property
674 676 def skipped(self):
675 677 """Whether the test was skipped."""
676 678 return self.ret == SKIPPED_STATUS
677 679
678 680 class PythonTest(Test):
679 681 """A Python-based test."""
680 682 def _run(self, testtmp, replacements, env):
681 683 py3kswitch = self._options.py3k_warnings and ' -3' or ''
682 684 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self._path)
683 685 vlog("# Running", cmd)
684 686 if os.name == 'nt':
685 687 replacements.append((r'\r\n', '\n'))
686 688 return run(cmd, testtmp, self._options, replacements, env)
687 689
688 690
689 691 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
690 692 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
691 693 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
692 694 escapemap.update({'\\': '\\\\', '\r': r'\r'})
693 695 def escapef(m):
694 696 return escapemap[m.group(0)]
695 697 def stringescape(s):
696 698 return escapesub(escapef, s)
697 699
698 700 class TTest(Test):
699 701 """A "t test" is a test backed by a .t file."""
700 702
701 703 def _run(self, testtmp, replacements, env):
702 704 f = open(self._path)
703 705 lines = f.readlines()
704 706 f.close()
705 707
706 708 salt, script, after, expected = self._parsetest(lines, testtmp)
707 709
708 710 # Write out the generated script.
709 711 fname = '%s.sh' % testtmp
710 712 f = open(fname, 'w')
711 713 for l in script:
712 714 f.write(l)
713 715 f.close()
714 716
715 717 cmd = '%s "%s"' % (self._options.shell, fname)
716 718 vlog("# Running", cmd)
717 719
718 720 exitcode, output = run(cmd, testtmp, self._options, replacements, env)
719 721 # Do not merge output if skipped. Return hghave message instead.
720 722 # Similarly, with --debug, output is None.
721 723 if exitcode == SKIPPED_STATUS or output is None:
722 724 return exitcode, output
723 725
724 726 return self._processoutput(exitcode, output, salt, after, expected)
725 727
726 728 def _hghave(self, reqs, testtmp):
727 729 # TODO do something smarter when all other uses of hghave are gone.
728 730 tdir = TESTDIR.replace('\\', '/')
729 731 proc = Popen4('%s -c "%s/hghave %s"' %
730 732 (self._options.shell, tdir, ' '.join(reqs)),
731 733 testtmp, 0)
732 734 stdout, stderr = proc.communicate()
733 735 ret = proc.wait()
734 736 if wifexited(ret):
735 737 ret = os.WEXITSTATUS(ret)
736 738 if ret == 2:
737 739 print stdout
738 740 sys.exit(1)
739 741
740 742 return ret == 0
741 743
742 744 def _parsetest(self, lines, testtmp):
743 745 # We generate a shell script which outputs unique markers to line
744 746 # up script results with our source. These markers include input
745 747 # line number and the last return code.
746 748 salt = "SALT" + str(time.time())
747 749 def addsalt(line, inpython):
748 750 if inpython:
749 751 script.append('%s %d 0\n' % (salt, line))
750 752 else:
751 753 script.append('echo %s %s $?\n' % (salt, line))
752 754
753 755 script = []
754 756
755 757 # After we run the shell script, we re-unify the script output
756 758 # with non-active parts of the source, with synchronization by our
757 759 # SALT line number markers. The after table contains the non-active
758 760 # components, ordered by line number.
759 761 after = {}
760 762
761 763 # Expected shell script output.
762 764 expected = {}
763 765
764 766 pos = prepos = -1
765 767
766 768 # True or False when in a true or false conditional section
767 769 skipping = None
768 770
769 771 # We keep track of whether or not we're in a Python block so we
770 772 # can generate the surrounding doctest magic.
771 773 inpython = False
772 774
773 775 if self._options.debug:
774 776 script.append('set -x\n')
775 777 if os.getenv('MSYSTEM'):
776 778 script.append('alias pwd="pwd -W"\n')
777 779
778 780 for n, l in enumerate(lines):
779 781 if not l.endswith('\n'):
780 782 l += '\n'
781 783 if l.startswith('#if'):
782 784 lsplit = l.split()
783 785 if len(lsplit) < 2 or lsplit[0] != '#if':
784 786 after.setdefault(pos, []).append(' !!! invalid #if\n')
785 787 if skipping is not None:
786 788 after.setdefault(pos, []).append(' !!! nested #if\n')
787 789 skipping = not self._hghave(lsplit[1:], testtmp)
788 790 after.setdefault(pos, []).append(l)
789 791 elif l.startswith('#else'):
790 792 if skipping is None:
791 793 after.setdefault(pos, []).append(' !!! missing #if\n')
792 794 skipping = not skipping
793 795 after.setdefault(pos, []).append(l)
794 796 elif l.startswith('#endif'):
795 797 if skipping is None:
796 798 after.setdefault(pos, []).append(' !!! missing #if\n')
797 799 skipping = None
798 800 after.setdefault(pos, []).append(l)
799 801 elif skipping:
800 802 after.setdefault(pos, []).append(l)
801 803 elif l.startswith(' >>> '): # python inlines
802 804 after.setdefault(pos, []).append(l)
803 805 prepos = pos
804 806 pos = n
805 807 if not inpython:
806 808 # We've just entered a Python block. Add the header.
807 809 inpython = True
808 810 addsalt(prepos, False) # Make sure we report the exit code.
809 811 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
810 812 addsalt(n, True)
811 813 script.append(l[2:])
812 814 elif l.startswith(' ... '): # python inlines
813 815 after.setdefault(prepos, []).append(l)
814 816 script.append(l[2:])
815 817 elif l.startswith(' $ '): # commands
816 818 if inpython:
817 819 script.append('EOF\n')
818 820 inpython = False
819 821 after.setdefault(pos, []).append(l)
820 822 prepos = pos
821 823 pos = n
822 824 addsalt(n, False)
823 825 cmd = l[4:].split()
824 826 if len(cmd) == 2 and cmd[0] == 'cd':
825 827 l = ' $ cd %s || exit 1\n' % cmd[1]
826 828 script.append(l[4:])
827 829 elif l.startswith(' > '): # continuations
828 830 after.setdefault(prepos, []).append(l)
829 831 script.append(l[4:])
830 832 elif l.startswith(' '): # results
831 833 # Queue up a list of expected results.
832 834 expected.setdefault(pos, []).append(l[2:])
833 835 else:
834 836 if inpython:
835 837 script.append('EOF\n')
836 838 inpython = False
837 839 # Non-command/result. Queue up for merged output.
838 840 after.setdefault(pos, []).append(l)
839 841
840 842 if inpython:
841 843 script.append('EOF\n')
842 844 if skipping is not None:
843 845 after.setdefault(pos, []).append(' !!! missing #endif\n')
844 846 addsalt(n + 1, False)
845 847
846 848 return salt, script, after, expected
847 849
848 850 def _processoutput(self, exitcode, output, salt, after, expected):
849 851 # Merge the script output back into a unified test.
850 852 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
851 853 if exitcode != 0:
852 854 warnonly = 3
853 855
854 856 pos = -1
855 857 postout = []
856 858 for l in output:
857 859 lout, lcmd = l, None
858 860 if salt in l:
859 861 lout, lcmd = l.split(salt, 1)
860 862
861 863 if lout:
862 864 if not lout.endswith('\n'):
863 865 lout += ' (no-eol)\n'
864 866
865 867 # Find the expected output at the current position.
866 868 el = None
867 869 if expected.get(pos, None):
868 870 el = expected[pos].pop(0)
869 871
870 872 r = TTest.linematch(el, lout)
871 873 if isinstance(r, str):
872 874 if r == '+glob':
873 875 lout = el[:-1] + ' (glob)\n'
874 876 r = '' # Warn only this line.
875 877 elif r == '-glob':
876 878 lout = ''.join(el.rsplit(' (glob)', 1))
877 879 r = '' # Warn only this line.
878 880 else:
879 881 log('\ninfo, unknown linematch result: %r\n' % r)
880 882 r = False
881 883 if r:
882 884 postout.append(' ' + el)
883 885 else:
884 886 if needescape(lout):
885 887 lout = stringescape(lout.rstrip('\n')) + ' (esc)\n'
886 888 postout.append(' ' + lout) # Let diff deal with it.
887 889 if r != '': # If line failed.
888 890 warnonly = 3 # for sure not
889 891 elif warnonly == 1: # Is "not yet" and line is warn only.
890 892 warnonly = 2 # Yes do warn.
891 893
892 894 if lcmd:
893 895 # Add on last return code.
894 896 ret = int(lcmd.split()[1])
895 897 if ret != 0:
896 898 postout.append(' [%s]\n' % ret)
897 899 if pos in after:
898 900 # Merge in non-active test bits.
899 901 postout += after.pop(pos)
900 902 pos = int(lcmd.split()[0])
901 903
902 904 if pos in after:
903 905 postout += after.pop(pos)
904 906
905 907 if warnonly == 2:
906 908 exitcode = False # Set exitcode to warned.
907 909
908 910 return exitcode, postout
909 911
910 912 @staticmethod
911 913 def rematch(el, l):
912 914 try:
913 915 # use \Z to ensure that the regex matches to the end of the string
914 916 if os.name == 'nt':
915 917 return re.match(el + r'\r?\n\Z', l)
916 918 return re.match(el + r'\n\Z', l)
917 919 except re.error:
918 920 # el is an invalid regex
919 921 return False
920 922
921 923 @staticmethod
922 924 def globmatch(el, l):
923 925 # The only supported special characters are * and ? plus / which also
924 926 # matches \ on windows. Escaping of these characters is supported.
925 927 if el + '\n' == l:
926 928 if os.altsep:
927 929 # matching on "/" is not needed for this line
928 930 return '-glob'
929 931 return True
930 932 i, n = 0, len(el)
931 933 res = ''
932 934 while i < n:
933 935 c = el[i]
934 936 i += 1
935 937 if c == '\\' and el[i] in '*?\\/':
936 938 res += el[i - 1:i + 1]
937 939 i += 1
938 940 elif c == '*':
939 941 res += '.*'
940 942 elif c == '?':
941 943 res += '.'
942 944 elif c == '/' and os.altsep:
943 945 res += '[/\\\\]'
944 946 else:
945 947 res += re.escape(c)
946 948 return TTest.rematch(res, l)
947 949
948 950 @staticmethod
949 951 def linematch(el, l):
950 952 if el == l: # perfect match (fast)
951 953 return True
952 954 if el:
953 955 if el.endswith(" (esc)\n"):
954 956 el = el[:-7].decode('string-escape') + '\n'
955 957 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
956 958 return True
957 959 if el.endswith(" (re)\n"):
958 960 return TTest.rematch(el[:-6], l)
959 961 if el.endswith(" (glob)\n"):
960 962 return TTest.globmatch(el[:-8], l)
961 963 if os.altsep and l.replace('\\', '/') == el:
962 964 return '+glob'
963 965 return False
964 966
965 967 wifexited = getattr(os, "WIFEXITED", lambda x: False)
966 968 def run(cmd, wd, options, replacements, env):
967 969 """Run command in a sub-process, capturing the output (stdout and stderr).
968 970 Return a tuple (exitcode, output). output is None in debug mode."""
969 971 # TODO: Use subprocess.Popen if we're running on Python 2.4
970 972 if options.debug:
971 973 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
972 974 ret = proc.wait()
973 975 return (ret, None)
974 976
975 977 proc = Popen4(cmd, wd, options.timeout, env)
976 978 def cleanup():
977 979 terminate(proc)
978 980 ret = proc.wait()
979 981 if ret == 0:
980 982 ret = signal.SIGTERM << 8
981 983 killdaemons(env['DAEMON_PIDS'])
982 984 return ret
983 985
984 986 output = ''
985 987 proc.tochild.close()
986 988
987 989 try:
988 990 output = proc.fromchild.read()
989 991 except KeyboardInterrupt:
990 992 vlog('# Handling keyboard interrupt')
991 993 cleanup()
992 994 raise
993 995
994 996 ret = proc.wait()
995 997 if wifexited(ret):
996 998 ret = os.WEXITSTATUS(ret)
997 999
998 1000 if proc.timeout:
999 1001 ret = 'timeout'
1000 1002
1001 1003 if ret:
1002 1004 killdaemons(env['DAEMON_PIDS'])
1003 1005
1004 1006 if abort:
1005 1007 raise KeyboardInterrupt()
1006 1008
1007 1009 for s, r in replacements:
1008 1010 output = re.sub(s, r, output)
1009 1011 return ret, output.splitlines(True)
1010 1012
1011 1013 def runone(options, test, count):
1012 1014 '''returns a result element: (code, test, msg)'''
1013 1015
1014 1016 def skip(msg):
1015 1017 if options.verbose:
1016 1018 log("\nSkipping %s: %s" % (testpath, msg))
1017 1019 return 's', test, msg
1018 1020
1019 1021 def fail(msg, ret):
1020 1022 warned = ret is False
1021 1023 if not options.nodiff:
1022 1024 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', test, msg))
1023 1025 if (not ret and options.interactive
1024 1026 and os.path.exists(testpath + ".err")):
1025 1027 iolock.acquire()
1026 1028 print "Accept this change? [n] ",
1027 1029 answer = sys.stdin.readline().strip()
1028 1030 iolock.release()
1029 1031 if answer.lower() in "y yes".split():
1030 1032 if test.endswith(".t"):
1031 1033 rename(testpath + ".err", testpath)
1032 1034 else:
1033 1035 rename(testpath + ".err", testpath + ".out")
1034 1036 return '.', test, ''
1035 1037 return warned and '~' or '!', test, msg
1036 1038
1037 1039 def success():
1038 1040 return '.', test, ''
1039 1041
1040 1042 def ignore(msg):
1041 1043 return 'i', test, msg
1042 1044
1043 1045 def describe(ret):
1044 1046 if ret < 0:
1045 1047 return 'killed by signal %d' % -ret
1046 1048 return 'returned error code %d' % ret
1047 1049
1048 1050 testpath = os.path.join(TESTDIR, test)
1049 1051 err = os.path.join(TESTDIR, test + ".err")
1050 1052 lctest = test.lower()
1051 1053
1052 1054 if not os.path.exists(testpath):
1053 1055 return skip("doesn't exist")
1054 1056
1055 1057 if not (options.whitelisted and test in options.whitelisted):
1056 1058 if options.blacklist and test in options.blacklist:
1057 1059 return skip("blacklisted")
1058 1060
1059 1061 if options.retest and not os.path.exists(test + ".err"):
1060 1062 return ignore("not retesting")
1061 1063
1062 1064 if options.keywords:
1063 1065 fp = open(test)
1064 1066 t = fp.read().lower() + test.lower()
1065 1067 fp.close()
1066 1068 for k in options.keywords.lower().split():
1067 1069 if k in t:
1068 1070 break
1069 1071 else:
1070 1072 return ignore("doesn't match keyword")
1071 1073
1072 1074 if not os.path.basename(lctest).startswith("test-"):
1073 1075 return skip("not a test file")
1074 1076 for ext, cls, out in testtypes:
1075 1077 if lctest.endswith(ext):
1076 1078 runner = cls
1077 1079 ref = os.path.join(TESTDIR, test + out)
1078 1080 break
1079 1081 else:
1080 1082 return skip("unknown test type")
1081 1083
1082 1084 vlog("# Test", test)
1083 1085
1084 1086 if os.path.exists(err):
1085 1087 os.remove(err) # Remove any previous output files
1086 1088
1087 t = runner(testpath, options, count)
1089 t = runner(testpath, options, count, ref)
1088 1090 res = TestResult()
1089 t.run(res, ref)
1091 t.run(res)
1090 1092 t.cleanup()
1091 1093
1092 1094 if res.interrupted:
1093 1095 log('INTERRUPTED: %s (after %d seconds)' % (test, res.duration))
1094 1096 raise KeyboardInterrupt()
1095 1097
1096 1098 if res.exception:
1097 1099 return fail('Exception during execution: %s' % res.exception, 255)
1098 1100
1099 1101 ret = res.ret
1100 1102 out = res.out
1101 1103
1102 1104 times.append((test, res.duration))
1103 1105 vlog("# Ret was:", ret)
1104 1106
1105 1107 skipped = res.skipped
1106 1108 refout = res.refout
1107 1109
1108 1110 if (ret != 0 or out != refout) and not skipped and not options.debug:
1109 1111 # Save errors to a file for diagnosis
1110 1112 f = open(err, "wb")
1111 1113 for line in out:
1112 1114 f.write(line)
1113 1115 f.close()
1114 1116
1115 1117 if skipped:
1116 1118 if out is None: # debug mode: nothing to parse
1117 1119 missing = ['unknown']
1118 1120 failed = None
1119 1121 else:
1120 1122 missing, failed = parsehghaveoutput(out)
1121 1123 if not missing:
1122 1124 missing = ['irrelevant']
1123 1125 if failed:
1124 1126 result = fail("hghave failed checking for %s" % failed[-1], ret)
1125 1127 skipped = False
1126 1128 else:
1127 1129 result = skip(missing[-1])
1128 1130 elif ret == 'timeout':
1129 1131 result = fail("timed out", ret)
1130 1132 elif out != refout:
1131 1133 info = {}
1132 1134 if not options.nodiff:
1133 1135 iolock.acquire()
1134 1136 if options.view:
1135 1137 os.system("%s %s %s" % (options.view, ref, err))
1136 1138 else:
1137 1139 info = showdiff(refout, out, ref, err)
1138 1140 iolock.release()
1139 1141 msg = ""
1140 1142 if info.get('servefail'): msg += "serve failed and "
1141 1143 if ret:
1142 1144 msg += "output changed and " + describe(ret)
1143 1145 else:
1144 1146 msg += "output changed"
1145 1147 result = fail(msg, ret)
1146 1148 elif ret:
1147 1149 result = fail(describe(ret), ret)
1148 1150 else:
1149 1151 result = success()
1150 1152
1151 1153 if not options.verbose:
1152 1154 iolock.acquire()
1153 1155 sys.stdout.write(result[0])
1154 1156 sys.stdout.flush()
1155 1157 iolock.release()
1156 1158
1157 1159 return result
1158 1160
1159 1161 _hgpath = None
1160 1162
1161 1163 def _gethgpath():
1162 1164 """Return the path to the mercurial package that is actually found by
1163 1165 the current Python interpreter."""
1164 1166 global _hgpath
1165 1167 if _hgpath is not None:
1166 1168 return _hgpath
1167 1169
1168 1170 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1169 1171 pipe = os.popen(cmd % PYTHON)
1170 1172 try:
1171 1173 _hgpath = pipe.read().strip()
1172 1174 finally:
1173 1175 pipe.close()
1174 1176 return _hgpath
1175 1177
1176 1178 def _checkhglib(verb):
1177 1179 """Ensure that the 'mercurial' package imported by python is
1178 1180 the one we expect it to be. If not, print a warning to stderr."""
1179 1181 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1180 1182 actualhg = _gethgpath()
1181 1183 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1182 1184 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1183 1185 ' (expected %s)\n'
1184 1186 % (verb, actualhg, expecthg))
1185 1187
1186 1188 results = {'.':[], '!':[], '~': [], 's':[], 'i':[]}
1187 1189 times = []
1188 1190 iolock = threading.Lock()
1189 1191 abort = False
1190 1192
1191 1193 def scheduletests(options, tests):
1192 1194 jobs = options.jobs
1193 1195 done = queue.Queue()
1194 1196 running = 0
1195 1197 count = 0
1196 1198 global abort
1197 1199
1198 1200 def job(test, count):
1199 1201 try:
1200 1202 done.put(runone(options, test, count))
1201 1203 except KeyboardInterrupt:
1202 1204 pass
1203 1205 except: # re-raises
1204 1206 done.put(('!', test, 'run-test raised an error, see traceback'))
1205 1207 raise
1206 1208
1207 1209 try:
1208 1210 while tests or running:
1209 1211 if not done.empty() or running == jobs or not tests:
1210 1212 try:
1211 1213 code, test, msg = done.get(True, 1)
1212 1214 results[code].append((test, msg))
1213 1215 if options.first and code not in '.si':
1214 1216 break
1215 1217 except queue.Empty:
1216 1218 continue
1217 1219 running -= 1
1218 1220 if tests and not running == jobs:
1219 1221 test = tests.pop(0)
1220 1222 if options.loop:
1221 1223 tests.append(test)
1222 1224 t = threading.Thread(target=job, name=test, args=(test, count))
1223 1225 t.start()
1224 1226 running += 1
1225 1227 count += 1
1226 1228 except KeyboardInterrupt:
1227 1229 abort = True
1228 1230
1229 1231 def runtests(options, tests):
1230 1232 try:
1231 1233 if INST:
1232 1234 installhg(options)
1233 1235 _checkhglib("Testing")
1234 1236 else:
1235 1237 usecorrectpython()
1236 1238
1237 1239 if options.restart:
1238 1240 orig = list(tests)
1239 1241 while tests:
1240 1242 if os.path.exists(tests[0] + ".err"):
1241 1243 break
1242 1244 tests.pop(0)
1243 1245 if not tests:
1244 1246 print "running all tests"
1245 1247 tests = orig
1246 1248
1247 1249 scheduletests(options, tests)
1248 1250
1249 1251 failed = len(results['!'])
1250 1252 warned = len(results['~'])
1251 1253 tested = len(results['.']) + failed + warned
1252 1254 skipped = len(results['s'])
1253 1255 ignored = len(results['i'])
1254 1256
1255 1257 print
1256 1258 if not options.noskips:
1257 1259 for s in results['s']:
1258 1260 print "Skipped %s: %s" % s
1259 1261 for s in results['~']:
1260 1262 print "Warned %s: %s" % s
1261 1263 for s in results['!']:
1262 1264 print "Failed %s: %s" % s
1263 1265 _checkhglib("Tested")
1264 1266 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1265 1267 tested, skipped + ignored, warned, failed)
1266 1268 if results['!']:
1267 1269 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1268 1270 if options.time:
1269 1271 outputtimes(options)
1270 1272
1271 1273 if options.anycoverage:
1272 1274 outputcoverage(options)
1273 1275 except KeyboardInterrupt:
1274 1276 failed = True
1275 1277 print "\ninterrupted!"
1276 1278
1277 1279 if failed:
1278 1280 return 1
1279 1281 if warned:
1280 1282 return 80
1281 1283
1282 1284 testtypes = [('.py', PythonTest, '.out'),
1283 1285 ('.t', TTest, '')]
1284 1286
1285 1287 def main(args, parser=None):
1286 1288 parser = parser or getparser()
1287 1289 (options, args) = parseargs(args, parser)
1288 1290 os.umask(022)
1289 1291
1290 1292 checktools()
1291 1293
1292 1294 if not args:
1293 1295 if options.changed:
1294 1296 proc = Popen4('hg st --rev "%s" -man0 .' % options.changed,
1295 1297 None, 0)
1296 1298 stdout, stderr = proc.communicate()
1297 1299 args = stdout.strip('\0').split('\0')
1298 1300 else:
1299 1301 args = os.listdir(".")
1300 1302
1301 1303 tests = [t for t in args
1302 1304 if os.path.basename(t).startswith("test-")
1303 1305 and (t.endswith(".py") or t.endswith(".t"))]
1304 1306
1305 1307 if options.random:
1306 1308 random.shuffle(tests)
1307 1309 else:
1308 1310 # keywords for slow tests
1309 1311 slow = 'svn gendoc check-code-hg'.split()
1310 1312 def sortkey(f):
1311 1313 # run largest tests first, as they tend to take the longest
1312 1314 try:
1313 1315 val = -os.stat(f).st_size
1314 1316 except OSError, e:
1315 1317 if e.errno != errno.ENOENT:
1316 1318 raise
1317 1319 return -1e9 # file does not exist, tell early
1318 1320 for kw in slow:
1319 1321 if kw in f:
1320 1322 val *= 10
1321 1323 return val
1322 1324 tests.sort(key=sortkey)
1323 1325
1324 1326 if 'PYTHONHASHSEED' not in os.environ:
1325 1327 # use a random python hash seed all the time
1326 1328 # we do the randomness ourself to know what seed is used
1327 1329 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1328 1330
1329 1331 global TESTDIR, HGTMP, INST, BINDIR, TMPBINDIR, PYTHONDIR, COVERAGE_FILE
1330 1332 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1331 1333 if options.tmpdir:
1332 1334 options.keep_tmpdir = True
1333 1335 tmpdir = options.tmpdir
1334 1336 if os.path.exists(tmpdir):
1335 1337 # Meaning of tmpdir has changed since 1.3: we used to create
1336 1338 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1337 1339 # tmpdir already exists.
1338 1340 print "error: temp dir %r already exists" % tmpdir
1339 1341 return 1
1340 1342
1341 1343 # Automatically removing tmpdir sounds convenient, but could
1342 1344 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1343 1345 # or "--tmpdir=$HOME".
1344 1346 #vlog("# Removing temp dir", tmpdir)
1345 1347 #shutil.rmtree(tmpdir)
1346 1348 os.makedirs(tmpdir)
1347 1349 else:
1348 1350 d = None
1349 1351 if os.name == 'nt':
1350 1352 # without this, we get the default temp dir location, but
1351 1353 # in all lowercase, which causes troubles with paths (issue3490)
1352 1354 d = os.getenv('TMP')
1353 1355 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1354 1356 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1355 1357
1356 1358 if options.with_hg:
1357 1359 INST = None
1358 1360 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1359 1361 TMPBINDIR = os.path.join(HGTMP, 'install', 'bin')
1360 1362 os.makedirs(TMPBINDIR)
1361 1363
1362 1364 # This looks redundant with how Python initializes sys.path from
1363 1365 # the location of the script being executed. Needed because the
1364 1366 # "hg" specified by --with-hg is not the only Python script
1365 1367 # executed in the test suite that needs to import 'mercurial'
1366 1368 # ... which means it's not really redundant at all.
1367 1369 PYTHONDIR = BINDIR
1368 1370 else:
1369 1371 INST = os.path.join(HGTMP, "install")
1370 1372 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1371 1373 TMPBINDIR = BINDIR
1372 1374 PYTHONDIR = os.path.join(INST, "lib", "python")
1373 1375
1374 1376 os.environ["BINDIR"] = BINDIR
1375 1377 os.environ["PYTHON"] = PYTHON
1376 1378
1377 1379 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1378 1380 if TMPBINDIR != BINDIR:
1379 1381 path = [TMPBINDIR] + path
1380 1382 os.environ["PATH"] = os.pathsep.join(path)
1381 1383
1382 1384 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1383 1385 # can run .../tests/run-tests.py test-foo where test-foo
1384 1386 # adds an extension to HGRC. Also include run-test.py directory to import
1385 1387 # modules like heredoctest.
1386 1388 pypath = [PYTHONDIR, TESTDIR, os.path.abspath(os.path.dirname(__file__))]
1387 1389 # We have to augment PYTHONPATH, rather than simply replacing
1388 1390 # it, in case external libraries are only available via current
1389 1391 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1390 1392 # are in /opt/subversion.)
1391 1393 oldpypath = os.environ.get(IMPL_PATH)
1392 1394 if oldpypath:
1393 1395 pypath.append(oldpypath)
1394 1396 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1395 1397
1396 1398 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1397 1399
1398 1400 vlog("# Using TESTDIR", TESTDIR)
1399 1401 vlog("# Using HGTMP", HGTMP)
1400 1402 vlog("# Using PATH", os.environ["PATH"])
1401 1403 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1402 1404
1403 1405 try:
1404 1406 return runtests(options, tests) or 0
1405 1407 finally:
1406 1408 time.sleep(.1)
1407 1409 cleanup(options)
1408 1410
1409 1411 if __name__ == '__main__':
1410 1412 sys.exit(main(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now