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