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