##// END OF EJS Templates
run-tests: move output difference processing to Test.run()
Gregory Szorc -
r21326:04e04766 default
parent child Browse files
Show More
@@ -1,1430 +1,1434 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 554 def __init__(self, test, path, options, count, refpath, errpath):
555 555 self._test = test
556 556 self._path = path
557 557 self._options = options
558 558 self._count = count
559 559 self._daemonpids = []
560 560 self._refpath = refpath
561 561 self._errpath = errpath
562 562
563 563 # If we're not in --debug mode and reference output file exists,
564 564 # check test output against it.
565 565 if options.debug:
566 566 self._refout = None # to match "out is None"
567 567 elif os.path.exists(refpath):
568 568 f = open(refpath, 'r')
569 569 self._refout = f.read().splitlines(True)
570 570 f.close()
571 571 else:
572 572 self._refout = []
573 573
574 574 self._threadtmp = os.path.join(HGTMP, 'child%d' % count)
575 575 os.mkdir(self._threadtmp)
576 576
577 577 def cleanup(self):
578 578 for entry in self._daemonpids:
579 579 killdaemons(entry)
580 580
581 581 if self._threadtmp and not self._options.keep_tmpdir:
582 582 shutil.rmtree(self._threadtmp, True)
583 583
584 584 def run(self, result):
585 585 # Remove any previous output files.
586 586 if os.path.exists(self._errpath):
587 587 os.remove(self._errpath)
588 588
589 589 testtmp = os.path.join(self._threadtmp, os.path.basename(self._path))
590 590 os.mkdir(testtmp)
591 591 replacements, port = self._getreplacements(testtmp)
592 592 env = self._getenv(testtmp, port)
593 593 self._daemonpids.append(env['DAEMON_PIDS'])
594 594 createhgrc(env['HGRCPATH'], self._options)
595 595
596 596 starttime = time.time()
597 597
598 598 def updateduration():
599 599 result.duration = time.time() - starttime
600 600
601 601 try:
602 602 ret, out = self._run(testtmp, replacements, env)
603 603 updateduration()
604 604 result.ret = ret
605 605 result.out = out
606 606 except KeyboardInterrupt:
607 607 updateduration()
608 608 log('INTERRUPTED: %s (after %d seconds)' % (self._test,
609 609 result.duration))
610 610 raise
611 611 except Exception, e:
612 612 updateduration()
613 613 result.exception = e
614 614
615 615 killdaemons(env['DAEMON_PIDS'])
616 616
617 617 result.refout = self._refout
618 618
619 619 if not self._options.keep_tmpdir:
620 620 shutil.rmtree(testtmp)
621 621
622 def describe(ret):
623 if ret < 0:
624 return 'killed by signal: %d' % -ret
625 return 'returned error code %d' % ret
626
622 627 if ret == SKIPPED_STATUS:
623 628 if out is None: # Debug mode, nothing to parse.
624 629 missing = ['unknown']
625 630 failed = None
626 631 else:
627 632 missing, failed = parsehghaveoutput(out)
628 633
629 634 if not missing:
630 635 missing = ['irrelevant']
631 636
632 637 if failed:
633 638 return self.fail('hg have failed checking for %s' % failed[-1],
634 639 ret)
635 640 else:
636 641 result.skipped = True
637 642 return self.skip(missing[-1])
638 643 elif ret == 'timeout':
639 644 return self.fail('timed out', ret)
645 elif out != self._refout:
646 info = {}
647 if not self._options.nodiff:
648 iolock.acquire()
649 if self._options.view:
650 os.system("%s %s %s" % (self._options.view, self._refpath,
651 self._errpath))
652 else:
653 info = showdiff(self._refout, out, self._refpath,
654 self._errpath)
655 iolock.release()
656 msg = ''
657 if info.get('servefail'):
658 msg += 'serve failed and '
659 if ret:
660 msg += 'output changed and ' + describe(ret)
661 else:
662 msg += 'output changed'
663
664 return self.fail(msg, ret)
640 665
641 666 def _run(self, testtmp, replacements, env):
642 667 raise NotImplemented('Subclasses must implement Test.run()')
643 668
644 669 def _getreplacements(self, testtmp):
645 670 port = self._options.port + self._count * 3
646 671 r = [
647 672 (r':%s\b' % port, ':$HGPORT'),
648 673 (r':%s\b' % (port + 1), ':$HGPORT1'),
649 674 (r':%s\b' % (port + 2), ':$HGPORT2'),
650 675 ]
651 676
652 677 if os.name == 'nt':
653 678 r.append(
654 679 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
655 680 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
656 681 for c in testtmp), '$TESTTMP'))
657 682 else:
658 683 r.append((re.escape(testtmp), '$TESTTMP'))
659 684
660 685 return r, port
661 686
662 687 def _getenv(self, testtmp, port):
663 688 env = os.environ.copy()
664 689 env['TESTTMP'] = testtmp
665 690 env['HOME'] = testtmp
666 691 env["HGPORT"] = str(port)
667 692 env["HGPORT1"] = str(port + 1)
668 693 env["HGPORT2"] = str(port + 2)
669 694 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
670 695 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
671 696 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
672 697 env["HGMERGE"] = "internal:merge"
673 698 env["HGUSER"] = "test"
674 699 env["HGENCODING"] = "ascii"
675 700 env["HGENCODINGMODE"] = "strict"
676 701
677 702 # Reset some environment variables to well-known values so that
678 703 # the tests produce repeatable output.
679 704 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
680 705 env['TZ'] = 'GMT'
681 706 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
682 707 env['COLUMNS'] = '80'
683 708 env['TERM'] = 'xterm'
684 709
685 710 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
686 711 'NO_PROXY').split():
687 712 if k in env:
688 713 del env[k]
689 714
690 715 # unset env related to hooks
691 716 for k in env.keys():
692 717 if k.startswith('HG_'):
693 718 del env[k]
694 719
695 720 return env
696 721
697 722 def success(self):
698 723 return '.', self._test, ''
699 724
700 725 def fail(self, msg, ret):
701 726 warned = ret is False
702 727 if not self._options.nodiff:
703 728 log("\n%s: %s %s" % (warned and 'Warning' or 'ERROR', self._test,
704 729 msg))
705 730 if (not ret and self._options.interactive and
706 731 os.path.exists(self._errpath)):
707 732 iolock.acquire()
708 733 print 'Accept this change? [n] ',
709 734 answer = sys.stdin.readline().strip()
710 735 iolock.release()
711 736 if answer.lower() in ('y', 'yes').split():
712 737 if self._test.endswith('.t'):
713 738 rename(self._errpath, self._testpath)
714 739 else:
715 740 rename(self._errpath, '%s.out' % self._testpath)
716 741
717 742 return '.', self._test, ''
718 743
719 744 return warned and '~' or '!', self._test, msg
720 745
721 746 def skip(self, msg):
722 747 if self._options.verbose:
723 748 log("\nSkipping %s: %s" % (self._path, msg))
724 749
725 750 return 's', self._test, msg
726 751
727 752 class TestResult(object):
728 753 """Holds the result of a test execution."""
729 754
730 755 def __init__(self):
731 756 self.ret = None
732 757 self.out = None
733 758 self.duration = None
734 759 self.exception = None
735 760 self.refout = None
736 761 self.skipped = False
737 762
738 763 class PythonTest(Test):
739 764 """A Python-based test."""
740 765 def _run(self, testtmp, replacements, env):
741 766 py3kswitch = self._options.py3k_warnings and ' -3' or ''
742 767 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self._path)
743 768 vlog("# Running", cmd)
744 769 if os.name == 'nt':
745 770 replacements.append((r'\r\n', '\n'))
746 771 return run(cmd, testtmp, self._options, replacements, env)
747 772
748 773
749 774 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
750 775 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
751 776 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
752 777 escapemap.update({'\\': '\\\\', '\r': r'\r'})
753 778 def escapef(m):
754 779 return escapemap[m.group(0)]
755 780 def stringescape(s):
756 781 return escapesub(escapef, s)
757 782
758 783 class TTest(Test):
759 784 """A "t test" is a test backed by a .t file."""
760 785
761 786 def _run(self, testtmp, replacements, env):
762 787 f = open(self._path)
763 788 lines = f.readlines()
764 789 f.close()
765 790
766 791 salt, script, after, expected = self._parsetest(lines, testtmp)
767 792
768 793 # Write out the generated script.
769 794 fname = '%s.sh' % testtmp
770 795 f = open(fname, 'w')
771 796 for l in script:
772 797 f.write(l)
773 798 f.close()
774 799
775 800 cmd = '%s "%s"' % (self._options.shell, fname)
776 801 vlog("# Running", cmd)
777 802
778 803 exitcode, output = run(cmd, testtmp, self._options, replacements, env)
779 804 # Do not merge output if skipped. Return hghave message instead.
780 805 # Similarly, with --debug, output is None.
781 806 if exitcode == SKIPPED_STATUS or output is None:
782 807 return exitcode, output
783 808
784 809 return self._processoutput(exitcode, output, salt, after, expected)
785 810
786 811 def _hghave(self, reqs, testtmp):
787 812 # TODO do something smarter when all other uses of hghave are gone.
788 813 tdir = TESTDIR.replace('\\', '/')
789 814 proc = Popen4('%s -c "%s/hghave %s"' %
790 815 (self._options.shell, tdir, ' '.join(reqs)),
791 816 testtmp, 0)
792 817 stdout, stderr = proc.communicate()
793 818 ret = proc.wait()
794 819 if wifexited(ret):
795 820 ret = os.WEXITSTATUS(ret)
796 821 if ret == 2:
797 822 print stdout
798 823 sys.exit(1)
799 824
800 825 return ret == 0
801 826
802 827 def _parsetest(self, lines, testtmp):
803 828 # We generate a shell script which outputs unique markers to line
804 829 # up script results with our source. These markers include input
805 830 # line number and the last return code.
806 831 salt = "SALT" + str(time.time())
807 832 def addsalt(line, inpython):
808 833 if inpython:
809 834 script.append('%s %d 0\n' % (salt, line))
810 835 else:
811 836 script.append('echo %s %s $?\n' % (salt, line))
812 837
813 838 script = []
814 839
815 840 # After we run the shell script, we re-unify the script output
816 841 # with non-active parts of the source, with synchronization by our
817 842 # SALT line number markers. The after table contains the non-active
818 843 # components, ordered by line number.
819 844 after = {}
820 845
821 846 # Expected shell script output.
822 847 expected = {}
823 848
824 849 pos = prepos = -1
825 850
826 851 # True or False when in a true or false conditional section
827 852 skipping = None
828 853
829 854 # We keep track of whether or not we're in a Python block so we
830 855 # can generate the surrounding doctest magic.
831 856 inpython = False
832 857
833 858 if self._options.debug:
834 859 script.append('set -x\n')
835 860 if os.getenv('MSYSTEM'):
836 861 script.append('alias pwd="pwd -W"\n')
837 862
838 863 for n, l in enumerate(lines):
839 864 if not l.endswith('\n'):
840 865 l += '\n'
841 866 if l.startswith('#if'):
842 867 lsplit = l.split()
843 868 if len(lsplit) < 2 or lsplit[0] != '#if':
844 869 after.setdefault(pos, []).append(' !!! invalid #if\n')
845 870 if skipping is not None:
846 871 after.setdefault(pos, []).append(' !!! nested #if\n')
847 872 skipping = not self._hghave(lsplit[1:], testtmp)
848 873 after.setdefault(pos, []).append(l)
849 874 elif l.startswith('#else'):
850 875 if skipping is None:
851 876 after.setdefault(pos, []).append(' !!! missing #if\n')
852 877 skipping = not skipping
853 878 after.setdefault(pos, []).append(l)
854 879 elif l.startswith('#endif'):
855 880 if skipping is None:
856 881 after.setdefault(pos, []).append(' !!! missing #if\n')
857 882 skipping = None
858 883 after.setdefault(pos, []).append(l)
859 884 elif skipping:
860 885 after.setdefault(pos, []).append(l)
861 886 elif l.startswith(' >>> '): # python inlines
862 887 after.setdefault(pos, []).append(l)
863 888 prepos = pos
864 889 pos = n
865 890 if not inpython:
866 891 # We've just entered a Python block. Add the header.
867 892 inpython = True
868 893 addsalt(prepos, False) # Make sure we report the exit code.
869 894 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
870 895 addsalt(n, True)
871 896 script.append(l[2:])
872 897 elif l.startswith(' ... '): # python inlines
873 898 after.setdefault(prepos, []).append(l)
874 899 script.append(l[2:])
875 900 elif l.startswith(' $ '): # commands
876 901 if inpython:
877 902 script.append('EOF\n')
878 903 inpython = False
879 904 after.setdefault(pos, []).append(l)
880 905 prepos = pos
881 906 pos = n
882 907 addsalt(n, False)
883 908 cmd = l[4:].split()
884 909 if len(cmd) == 2 and cmd[0] == 'cd':
885 910 l = ' $ cd %s || exit 1\n' % cmd[1]
886 911 script.append(l[4:])
887 912 elif l.startswith(' > '): # continuations
888 913 after.setdefault(prepos, []).append(l)
889 914 script.append(l[4:])
890 915 elif l.startswith(' '): # results
891 916 # Queue up a list of expected results.
892 917 expected.setdefault(pos, []).append(l[2:])
893 918 else:
894 919 if inpython:
895 920 script.append('EOF\n')
896 921 inpython = False
897 922 # Non-command/result. Queue up for merged output.
898 923 after.setdefault(pos, []).append(l)
899 924
900 925 if inpython:
901 926 script.append('EOF\n')
902 927 if skipping is not None:
903 928 after.setdefault(pos, []).append(' !!! missing #endif\n')
904 929 addsalt(n + 1, False)
905 930
906 931 return salt, script, after, expected
907 932
908 933 def _processoutput(self, exitcode, output, salt, after, expected):
909 934 # Merge the script output back into a unified test.
910 935 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
911 936 if exitcode != 0:
912 937 warnonly = 3
913 938
914 939 pos = -1
915 940 postout = []
916 941 for l in output:
917 942 lout, lcmd = l, None
918 943 if salt in l:
919 944 lout, lcmd = l.split(salt, 1)
920 945
921 946 if lout:
922 947 if not lout.endswith('\n'):
923 948 lout += ' (no-eol)\n'
924 949
925 950 # Find the expected output at the current position.
926 951 el = None
927 952 if expected.get(pos, None):
928 953 el = expected[pos].pop(0)
929 954
930 955 r = TTest.linematch(el, lout)
931 956 if isinstance(r, str):
932 957 if r == '+glob':
933 958 lout = el[:-1] + ' (glob)\n'
934 959 r = '' # Warn only this line.
935 960 elif r == '-glob':
936 961 lout = ''.join(el.rsplit(' (glob)', 1))
937 962 r = '' # Warn only this line.
938 963 else:
939 964 log('\ninfo, unknown linematch result: %r\n' % r)
940 965 r = False
941 966 if r:
942 967 postout.append(' ' + el)
943 968 else:
944 969 if needescape(lout):
945 970 lout = stringescape(lout.rstrip('\n')) + ' (esc)\n'
946 971 postout.append(' ' + lout) # Let diff deal with it.
947 972 if r != '': # If line failed.
948 973 warnonly = 3 # for sure not
949 974 elif warnonly == 1: # Is "not yet" and line is warn only.
950 975 warnonly = 2 # Yes do warn.
951 976
952 977 if lcmd:
953 978 # Add on last return code.
954 979 ret = int(lcmd.split()[1])
955 980 if ret != 0:
956 981 postout.append(' [%s]\n' % ret)
957 982 if pos in after:
958 983 # Merge in non-active test bits.
959 984 postout += after.pop(pos)
960 985 pos = int(lcmd.split()[0])
961 986
962 987 if pos in after:
963 988 postout += after.pop(pos)
964 989
965 990 if warnonly == 2:
966 991 exitcode = False # Set exitcode to warned.
967 992
968 993 return exitcode, postout
969 994
970 995 @staticmethod
971 996 def rematch(el, l):
972 997 try:
973 998 # use \Z to ensure that the regex matches to the end of the string
974 999 if os.name == 'nt':
975 1000 return re.match(el + r'\r?\n\Z', l)
976 1001 return re.match(el + r'\n\Z', l)
977 1002 except re.error:
978 1003 # el is an invalid regex
979 1004 return False
980 1005
981 1006 @staticmethod
982 1007 def globmatch(el, l):
983 1008 # The only supported special characters are * and ? plus / which also
984 1009 # matches \ on windows. Escaping of these characters is supported.
985 1010 if el + '\n' == l:
986 1011 if os.altsep:
987 1012 # matching on "/" is not needed for this line
988 1013 return '-glob'
989 1014 return True
990 1015 i, n = 0, len(el)
991 1016 res = ''
992 1017 while i < n:
993 1018 c = el[i]
994 1019 i += 1
995 1020 if c == '\\' and el[i] in '*?\\/':
996 1021 res += el[i - 1:i + 1]
997 1022 i += 1
998 1023 elif c == '*':
999 1024 res += '.*'
1000 1025 elif c == '?':
1001 1026 res += '.'
1002 1027 elif c == '/' and os.altsep:
1003 1028 res += '[/\\\\]'
1004 1029 else:
1005 1030 res += re.escape(c)
1006 1031 return TTest.rematch(res, l)
1007 1032
1008 1033 @staticmethod
1009 1034 def linematch(el, l):
1010 1035 if el == l: # perfect match (fast)
1011 1036 return True
1012 1037 if el:
1013 1038 if el.endswith(" (esc)\n"):
1014 1039 el = el[:-7].decode('string-escape') + '\n'
1015 1040 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
1016 1041 return True
1017 1042 if el.endswith(" (re)\n"):
1018 1043 return TTest.rematch(el[:-6], l)
1019 1044 if el.endswith(" (glob)\n"):
1020 1045 return TTest.globmatch(el[:-8], l)
1021 1046 if os.altsep and l.replace('\\', '/') == el:
1022 1047 return '+glob'
1023 1048 return False
1024 1049
1025 1050 wifexited = getattr(os, "WIFEXITED", lambda x: False)
1026 1051 def run(cmd, wd, options, replacements, env):
1027 1052 """Run command in a sub-process, capturing the output (stdout and stderr).
1028 1053 Return a tuple (exitcode, output). output is None in debug mode."""
1029 1054 # TODO: Use subprocess.Popen if we're running on Python 2.4
1030 1055 if options.debug:
1031 1056 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1032 1057 ret = proc.wait()
1033 1058 return (ret, None)
1034 1059
1035 1060 proc = Popen4(cmd, wd, options.timeout, env)
1036 1061 def cleanup():
1037 1062 terminate(proc)
1038 1063 ret = proc.wait()
1039 1064 if ret == 0:
1040 1065 ret = signal.SIGTERM << 8
1041 1066 killdaemons(env['DAEMON_PIDS'])
1042 1067 return ret
1043 1068
1044 1069 output = ''
1045 1070 proc.tochild.close()
1046 1071
1047 1072 try:
1048 1073 output = proc.fromchild.read()
1049 1074 except KeyboardInterrupt:
1050 1075 vlog('# Handling keyboard interrupt')
1051 1076 cleanup()
1052 1077 raise
1053 1078
1054 1079 ret = proc.wait()
1055 1080 if wifexited(ret):
1056 1081 ret = os.WEXITSTATUS(ret)
1057 1082
1058 1083 if proc.timeout:
1059 1084 ret = 'timeout'
1060 1085
1061 1086 if ret:
1062 1087 killdaemons(env['DAEMON_PIDS'])
1063 1088
1064 1089 if abort:
1065 1090 raise KeyboardInterrupt()
1066 1091
1067 1092 for s, r in replacements:
1068 1093 output = re.sub(s, r, output)
1069 1094 return ret, output.splitlines(True)
1070 1095
1071 1096 def runone(options, test, count):
1072 1097 '''returns a result element: (code, test, msg)'''
1073 1098
1074 1099 def skip(msg):
1075 1100 if options.verbose:
1076 1101 log("\nSkipping %s: %s" % (testpath, msg))
1077 1102 return 's', test, msg
1078 1103
1079 1104 def ignore(msg):
1080 1105 return 'i', test, msg
1081 1106
1082 def describe(ret):
1083 if ret < 0:
1084 return 'killed by signal %d' % -ret
1085 return 'returned error code %d' % ret
1086
1087 1107 testpath = os.path.join(TESTDIR, test)
1088 1108 err = os.path.join(TESTDIR, test + ".err")
1089 1109 lctest = test.lower()
1090 1110
1091 1111 if not os.path.exists(testpath):
1092 1112 return skip("doesn't exist")
1093 1113
1094 1114 if not (options.whitelisted and test in options.whitelisted):
1095 1115 if options.blacklist and test in options.blacklist:
1096 1116 return skip("blacklisted")
1097 1117
1098 1118 if options.retest and not os.path.exists(test + ".err"):
1099 1119 return ignore("not retesting")
1100 1120
1101 1121 if options.keywords:
1102 1122 fp = open(test)
1103 1123 t = fp.read().lower() + test.lower()
1104 1124 fp.close()
1105 1125 for k in options.keywords.lower().split():
1106 1126 if k in t:
1107 1127 break
1108 1128 else:
1109 1129 return ignore("doesn't match keyword")
1110 1130
1111 1131 if not os.path.basename(lctest).startswith("test-"):
1112 1132 return skip("not a test file")
1113 1133 for ext, cls, out in testtypes:
1114 1134 if lctest.endswith(ext):
1115 1135 runner = cls
1116 1136 ref = os.path.join(TESTDIR, test + out)
1117 1137 break
1118 1138 else:
1119 1139 return skip("unknown test type")
1120 1140
1121 1141 vlog("# Test", test)
1122 1142
1123 1143 t = runner(test, testpath, options, count, ref, err)
1124 1144 res = TestResult()
1125 1145 result = t.run(res)
1126 1146
1127 1147 if res.exception:
1128 1148 return t.fail('Exception during execution: %s' % res.exception, 255)
1129 1149
1130 1150 ret = res.ret
1131 1151 out = res.out
1132 1152
1133 1153 times.append((test, res.duration))
1134 1154 vlog("# Ret was:", ret)
1135 1155
1136 1156 skipped = res.skipped
1137 1157 refout = res.refout
1138 1158
1139 1159 if (ret != 0 or out != refout) and not skipped and not options.debug:
1140 1160 # Save errors to a file for diagnosis
1141 1161 f = open(err, "wb")
1142 1162 for line in out:
1143 1163 f.write(line)
1144 1164 f.close()
1145 1165
1146 1166 if result:
1147 1167 pass
1148 elif out != refout:
1149 info = {}
1150 if not options.nodiff:
1151 iolock.acquire()
1152 if options.view:
1153 os.system("%s %s %s" % (options.view, ref, err))
1154 else:
1155 info = showdiff(refout, out, ref, err)
1156 iolock.release()
1157 msg = ""
1158 if info.get('servefail'): msg += "serve failed and "
1159 if ret:
1160 msg += "output changed and " + describe(ret)
1161 else:
1162 msg += "output changed"
1163 result = t.fail(msg, ret)
1164 1168 elif ret:
1165 1169 result = t.fail(describe(ret), ret)
1166 1170 else:
1167 1171 result = t.success()
1168 1172
1169 1173 if not options.verbose:
1170 1174 iolock.acquire()
1171 1175 sys.stdout.write(result[0])
1172 1176 sys.stdout.flush()
1173 1177 iolock.release()
1174 1178
1175 1179 t.cleanup()
1176 1180
1177 1181 return result
1178 1182
1179 1183 _hgpath = None
1180 1184
1181 1185 def _gethgpath():
1182 1186 """Return the path to the mercurial package that is actually found by
1183 1187 the current Python interpreter."""
1184 1188 global _hgpath
1185 1189 if _hgpath is not None:
1186 1190 return _hgpath
1187 1191
1188 1192 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1189 1193 pipe = os.popen(cmd % PYTHON)
1190 1194 try:
1191 1195 _hgpath = pipe.read().strip()
1192 1196 finally:
1193 1197 pipe.close()
1194 1198 return _hgpath
1195 1199
1196 1200 def _checkhglib(verb):
1197 1201 """Ensure that the 'mercurial' package imported by python is
1198 1202 the one we expect it to be. If not, print a warning to stderr."""
1199 1203 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1200 1204 actualhg = _gethgpath()
1201 1205 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1202 1206 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1203 1207 ' (expected %s)\n'
1204 1208 % (verb, actualhg, expecthg))
1205 1209
1206 1210 results = {'.':[], '!':[], '~': [], 's':[], 'i':[]}
1207 1211 times = []
1208 1212 iolock = threading.Lock()
1209 1213 abort = False
1210 1214
1211 1215 def scheduletests(options, tests):
1212 1216 jobs = options.jobs
1213 1217 done = queue.Queue()
1214 1218 running = 0
1215 1219 count = 0
1216 1220 global abort
1217 1221
1218 1222 def job(test, count):
1219 1223 try:
1220 1224 done.put(runone(options, test, count))
1221 1225 except KeyboardInterrupt:
1222 1226 pass
1223 1227 except: # re-raises
1224 1228 done.put(('!', test, 'run-test raised an error, see traceback'))
1225 1229 raise
1226 1230
1227 1231 try:
1228 1232 while tests or running:
1229 1233 if not done.empty() or running == jobs or not tests:
1230 1234 try:
1231 1235 code, test, msg = done.get(True, 1)
1232 1236 results[code].append((test, msg))
1233 1237 if options.first and code not in '.si':
1234 1238 break
1235 1239 except queue.Empty:
1236 1240 continue
1237 1241 running -= 1
1238 1242 if tests and not running == jobs:
1239 1243 test = tests.pop(0)
1240 1244 if options.loop:
1241 1245 tests.append(test)
1242 1246 t = threading.Thread(target=job, name=test, args=(test, count))
1243 1247 t.start()
1244 1248 running += 1
1245 1249 count += 1
1246 1250 except KeyboardInterrupt:
1247 1251 abort = True
1248 1252
1249 1253 def runtests(options, tests):
1250 1254 try:
1251 1255 if INST:
1252 1256 installhg(options)
1253 1257 _checkhglib("Testing")
1254 1258 else:
1255 1259 usecorrectpython()
1256 1260
1257 1261 if options.restart:
1258 1262 orig = list(tests)
1259 1263 while tests:
1260 1264 if os.path.exists(tests[0] + ".err"):
1261 1265 break
1262 1266 tests.pop(0)
1263 1267 if not tests:
1264 1268 print "running all tests"
1265 1269 tests = orig
1266 1270
1267 1271 scheduletests(options, tests)
1268 1272
1269 1273 failed = len(results['!'])
1270 1274 warned = len(results['~'])
1271 1275 tested = len(results['.']) + failed + warned
1272 1276 skipped = len(results['s'])
1273 1277 ignored = len(results['i'])
1274 1278
1275 1279 print
1276 1280 if not options.noskips:
1277 1281 for s in results['s']:
1278 1282 print "Skipped %s: %s" % s
1279 1283 for s in results['~']:
1280 1284 print "Warned %s: %s" % s
1281 1285 for s in results['!']:
1282 1286 print "Failed %s: %s" % s
1283 1287 _checkhglib("Tested")
1284 1288 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1285 1289 tested, skipped + ignored, warned, failed)
1286 1290 if results['!']:
1287 1291 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1288 1292 if options.time:
1289 1293 outputtimes(options)
1290 1294
1291 1295 if options.anycoverage:
1292 1296 outputcoverage(options)
1293 1297 except KeyboardInterrupt:
1294 1298 failed = True
1295 1299 print "\ninterrupted!"
1296 1300
1297 1301 if failed:
1298 1302 return 1
1299 1303 if warned:
1300 1304 return 80
1301 1305
1302 1306 testtypes = [('.py', PythonTest, '.out'),
1303 1307 ('.t', TTest, '')]
1304 1308
1305 1309 def main(args, parser=None):
1306 1310 parser = parser or getparser()
1307 1311 (options, args) = parseargs(args, parser)
1308 1312 os.umask(022)
1309 1313
1310 1314 checktools()
1311 1315
1312 1316 if not args:
1313 1317 if options.changed:
1314 1318 proc = Popen4('hg st --rev "%s" -man0 .' % options.changed,
1315 1319 None, 0)
1316 1320 stdout, stderr = proc.communicate()
1317 1321 args = stdout.strip('\0').split('\0')
1318 1322 else:
1319 1323 args = os.listdir(".")
1320 1324
1321 1325 tests = [t for t in args
1322 1326 if os.path.basename(t).startswith("test-")
1323 1327 and (t.endswith(".py") or t.endswith(".t"))]
1324 1328
1325 1329 if options.random:
1326 1330 random.shuffle(tests)
1327 1331 else:
1328 1332 # keywords for slow tests
1329 1333 slow = 'svn gendoc check-code-hg'.split()
1330 1334 def sortkey(f):
1331 1335 # run largest tests first, as they tend to take the longest
1332 1336 try:
1333 1337 val = -os.stat(f).st_size
1334 1338 except OSError, e:
1335 1339 if e.errno != errno.ENOENT:
1336 1340 raise
1337 1341 return -1e9 # file does not exist, tell early
1338 1342 for kw in slow:
1339 1343 if kw in f:
1340 1344 val *= 10
1341 1345 return val
1342 1346 tests.sort(key=sortkey)
1343 1347
1344 1348 if 'PYTHONHASHSEED' not in os.environ:
1345 1349 # use a random python hash seed all the time
1346 1350 # we do the randomness ourself to know what seed is used
1347 1351 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1348 1352
1349 1353 global TESTDIR, HGTMP, INST, BINDIR, TMPBINDIR, PYTHONDIR, COVERAGE_FILE
1350 1354 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1351 1355 if options.tmpdir:
1352 1356 options.keep_tmpdir = True
1353 1357 tmpdir = options.tmpdir
1354 1358 if os.path.exists(tmpdir):
1355 1359 # Meaning of tmpdir has changed since 1.3: we used to create
1356 1360 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1357 1361 # tmpdir already exists.
1358 1362 print "error: temp dir %r already exists" % tmpdir
1359 1363 return 1
1360 1364
1361 1365 # Automatically removing tmpdir sounds convenient, but could
1362 1366 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1363 1367 # or "--tmpdir=$HOME".
1364 1368 #vlog("# Removing temp dir", tmpdir)
1365 1369 #shutil.rmtree(tmpdir)
1366 1370 os.makedirs(tmpdir)
1367 1371 else:
1368 1372 d = None
1369 1373 if os.name == 'nt':
1370 1374 # without this, we get the default temp dir location, but
1371 1375 # in all lowercase, which causes troubles with paths (issue3490)
1372 1376 d = os.getenv('TMP')
1373 1377 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1374 1378 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1375 1379
1376 1380 if options.with_hg:
1377 1381 INST = None
1378 1382 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1379 1383 TMPBINDIR = os.path.join(HGTMP, 'install', 'bin')
1380 1384 os.makedirs(TMPBINDIR)
1381 1385
1382 1386 # This looks redundant with how Python initializes sys.path from
1383 1387 # the location of the script being executed. Needed because the
1384 1388 # "hg" specified by --with-hg is not the only Python script
1385 1389 # executed in the test suite that needs to import 'mercurial'
1386 1390 # ... which means it's not really redundant at all.
1387 1391 PYTHONDIR = BINDIR
1388 1392 else:
1389 1393 INST = os.path.join(HGTMP, "install")
1390 1394 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1391 1395 TMPBINDIR = BINDIR
1392 1396 PYTHONDIR = os.path.join(INST, "lib", "python")
1393 1397
1394 1398 os.environ["BINDIR"] = BINDIR
1395 1399 os.environ["PYTHON"] = PYTHON
1396 1400
1397 1401 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1398 1402 if TMPBINDIR != BINDIR:
1399 1403 path = [TMPBINDIR] + path
1400 1404 os.environ["PATH"] = os.pathsep.join(path)
1401 1405
1402 1406 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1403 1407 # can run .../tests/run-tests.py test-foo where test-foo
1404 1408 # adds an extension to HGRC. Also include run-test.py directory to import
1405 1409 # modules like heredoctest.
1406 1410 pypath = [PYTHONDIR, TESTDIR, os.path.abspath(os.path.dirname(__file__))]
1407 1411 # We have to augment PYTHONPATH, rather than simply replacing
1408 1412 # it, in case external libraries are only available via current
1409 1413 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1410 1414 # are in /opt/subversion.)
1411 1415 oldpypath = os.environ.get(IMPL_PATH)
1412 1416 if oldpypath:
1413 1417 pypath.append(oldpypath)
1414 1418 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1415 1419
1416 1420 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1417 1421
1418 1422 vlog("# Using TESTDIR", TESTDIR)
1419 1423 vlog("# Using HGTMP", HGTMP)
1420 1424 vlog("# Using PATH", os.environ["PATH"])
1421 1425 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1422 1426
1423 1427 try:
1424 1428 return runtests(options, tests) or 0
1425 1429 finally:
1426 1430 time.sleep(.1)
1427 1431 cleanup(options)
1428 1432
1429 1433 if __name__ == '__main__':
1430 1434 sys.exit(main(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now