##// END OF EJS Templates
run-tests: move checktools into TestRunner.run()
Gregory Szorc -
r21374:592b3d26 default
parent child Browse files
Show More
@@ -1,1468 +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, args):
1018 1018 """Run the test suite."""
1019 self._checktools()
1019 1020 tests = self.findtests(args)
1020 1021 return self._run(tests)
1021 1022
1022 1023 def _run(self, tests):
1023 1024 if self.options.random:
1024 1025 random.shuffle(tests)
1025 1026 else:
1026 1027 # keywords for slow tests
1027 1028 slow = 'svn gendoc check-code-hg'.split()
1028 1029 def sortkey(f):
1029 1030 # run largest tests first, as they tend to take the longest
1030 1031 try:
1031 1032 val = -os.stat(f).st_size
1032 1033 except OSError, e:
1033 1034 if e.errno != errno.ENOENT:
1034 1035 raise
1035 1036 return -1e9 # file does not exist, tell early
1036 1037 for kw in slow:
1037 1038 if kw in f:
1038 1039 val *= 10
1039 1040 return val
1040 1041 tests.sort(key=sortkey)
1041 1042
1042 1043 self.testdir = os.environ['TESTDIR'] = os.getcwd()
1043 1044
1044 1045 if 'PYTHONHASHSEED' not in os.environ:
1045 1046 # use a random python hash seed all the time
1046 1047 # we do the randomness ourself to know what seed is used
1047 1048 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1048 1049
1049 1050 if self.options.tmpdir:
1050 1051 self.options.keep_tmpdir = True
1051 1052 tmpdir = self.options.tmpdir
1052 1053 if os.path.exists(tmpdir):
1053 1054 # Meaning of tmpdir has changed since 1.3: we used to create
1054 1055 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1055 1056 # tmpdir already exists.
1056 1057 print "error: temp dir %r already exists" % tmpdir
1057 1058 return 1
1058 1059
1059 1060 # Automatically removing tmpdir sounds convenient, but could
1060 1061 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1061 1062 # or "--tmpdir=$HOME".
1062 1063 #vlog("# Removing temp dir", tmpdir)
1063 1064 #shutil.rmtree(tmpdir)
1064 1065 os.makedirs(tmpdir)
1065 1066 else:
1066 1067 d = None
1067 1068 if os.name == 'nt':
1068 1069 # without this, we get the default temp dir location, but
1069 1070 # in all lowercase, which causes troubles with paths (issue3490)
1070 1071 d = os.getenv('TMP')
1071 1072 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1072 1073 self.hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1073 1074
1074 1075 if self.options.with_hg:
1075 1076 self.inst = None
1076 1077 self.bindir = os.path.dirname(os.path.realpath(
1077 1078 self.options.with_hg))
1078 1079 self.tmpbindir = os.path.join(self.hgtmp, 'install', 'bin')
1079 1080 os.makedirs(self.tmpbindir)
1080 1081
1081 1082 # This looks redundant with how Python initializes sys.path from
1082 1083 # the location of the script being executed. Needed because the
1083 1084 # "hg" specified by --with-hg is not the only Python script
1084 1085 # executed in the test suite that needs to import 'mercurial'
1085 1086 # ... which means it's not really redundant at all.
1086 1087 self.pythondir = self.bindir
1087 1088 else:
1088 1089 self.inst = os.path.join(self.hgtmp, "install")
1089 1090 self.bindir = os.environ["BINDIR"] = os.path.join(self.inst,
1090 1091 "bin")
1091 1092 self.tmpbindir = self.bindir
1092 1093 self.pythondir = os.path.join(self.inst, "lib", "python")
1093 1094
1094 1095 os.environ["BINDIR"] = self.bindir
1095 1096 os.environ["PYTHON"] = PYTHON
1096 1097
1097 1098 path = [self.bindir] + os.environ["PATH"].split(os.pathsep)
1098 1099 if self.tmpbindir != self.bindir:
1099 1100 path = [self.tmpbindir] + path
1100 1101 os.environ["PATH"] = os.pathsep.join(path)
1101 1102
1102 1103 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1103 1104 # can run .../tests/run-tests.py test-foo where test-foo
1104 1105 # adds an extension to HGRC. Also include run-test.py directory to
1105 1106 # import modules like heredoctest.
1106 1107 pypath = [self.pythondir, self.testdir,
1107 1108 os.path.abspath(os.path.dirname(__file__))]
1108 1109 # We have to augment PYTHONPATH, rather than simply replacing
1109 1110 # it, in case external libraries are only available via current
1110 1111 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1111 1112 # are in /opt/subversion.)
1112 1113 oldpypath = os.environ.get(IMPL_PATH)
1113 1114 if oldpypath:
1114 1115 pypath.append(oldpypath)
1115 1116 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1116 1117
1117 1118 self.coveragefile = os.path.join(self.testdir, '.coverage')
1118 1119
1119 1120 vlog("# Using TESTDIR", self.testdir)
1120 1121 vlog("# Using HGTMP", self.hgtmp)
1121 1122 vlog("# Using PATH", os.environ["PATH"])
1122 1123 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1123 1124
1124 1125 try:
1125 1126 return self._runtests(tests) or 0
1126 1127 finally:
1127 1128 time.sleep(.1)
1128 1129 self._cleanup()
1129 1130
1130 1131 def findtests(self, args):
1131 1132 """Finds possible test files from arguments.
1132 1133
1133 1134 If you wish to inject custom tests into the test harness, this would
1134 1135 be a good function to monkeypatch or override in a derived class.
1135 1136 """
1136 1137 if not args:
1137 1138 if self.options.changed:
1138 1139 proc = Popen4('hg st --rev "%s" -man0 .' %
1139 1140 self.options.changed, None, 0)
1140 1141 stdout, stderr = proc.communicate()
1141 1142 args = stdout.strip('\0').split('\0')
1142 1143 else:
1143 1144 args = os.listdir('.')
1144 1145
1145 1146 return [t for t in args
1146 1147 if os.path.basename(t).startswith('test-')
1147 1148 and (t.endswith('.py') or t.endswith('.t'))]
1148 1149
1149 1150 def _runtests(self, tests):
1150 1151 try:
1151 1152 if self.inst:
1152 1153 self.installhg()
1153 1154 self.checkhglib("Testing")
1154 1155 else:
1155 1156 self.usecorrectpython()
1156 1157
1157 1158 if self.options.restart:
1158 1159 orig = list(tests)
1159 1160 while tests:
1160 1161 if os.path.exists(tests[0] + ".err"):
1161 1162 break
1162 1163 tests.pop(0)
1163 1164 if not tests:
1164 1165 print "running all tests"
1165 1166 tests = orig
1166 1167
1167 1168 self._executetests(tests)
1168 1169
1169 1170 failed = len(self.results['!'])
1170 1171 warned = len(self.results['~'])
1171 1172 tested = len(self.results['.']) + failed + warned
1172 1173 skipped = len(self.results['s'])
1173 1174 ignored = len(self.results['i'])
1174 1175
1175 1176 print
1176 1177 if not self.options.noskips:
1177 1178 for s in self.results['s']:
1178 1179 print "Skipped %s: %s" % s
1179 1180 for s in self.results['~']:
1180 1181 print "Warned %s: %s" % s
1181 1182 for s in self.results['!']:
1182 1183 print "Failed %s: %s" % s
1183 1184 self.checkhglib("Tested")
1184 1185 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1185 1186 tested, skipped + ignored, warned, failed)
1186 1187 if self.results['!']:
1187 1188 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1188 1189 if self.options.time:
1189 1190 self.outputtimes()
1190 1191
1191 1192 if self.options.anycoverage:
1192 1193 self.outputcoverage()
1193 1194 except KeyboardInterrupt:
1194 1195 failed = True
1195 1196 print "\ninterrupted!"
1196 1197
1197 1198 if failed:
1198 1199 return 1
1199 1200 if warned:
1200 1201 return 80
1201 1202
1202 1203 def gettest(self, test, count):
1203 1204 """Obtain a Test by looking at its filename.
1204 1205
1205 1206 Returns a Test instance. The Test may not be runnable if it doesn't
1206 1207 map to a known type.
1207 1208 """
1208 1209 lctest = test.lower()
1209 1210 refpath = os.path.join(self.testdir, test)
1210 1211
1211 1212 testcls = Test
1212 1213
1213 1214 for ext, cls, out in self.TESTTYPES:
1214 1215 if lctest.endswith(ext):
1215 1216 testcls = cls
1216 1217 refpath = os.path.join(self.testdir, test + out)
1217 1218 break
1218 1219
1219 1220 return testcls(self, test, count, refpath)
1220 1221
1221 1222 def _cleanup(self):
1222 1223 """Clean up state from this test invocation."""
1223 1224
1224 1225 if self.options.keep_tmpdir:
1225 1226 return
1226 1227
1227 1228 vlog("# Cleaning up HGTMP", self.hgtmp)
1228 1229 shutil.rmtree(self.hgtmp, True)
1229 1230 for f in self._createdfiles:
1230 1231 try:
1231 1232 os.remove(f)
1232 1233 except OSError:
1233 1234 pass
1234 1235
1235 1236 def usecorrectpython(self):
1236 1237 # Some tests run the Python interpreter. They must use the
1237 1238 # same interpreter or bad things will happen.
1238 1239 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1239 1240 if getattr(os, 'symlink', None):
1240 1241 vlog("# Making python executable in test path a symlink to '%s'" %
1241 1242 sys.executable)
1242 1243 mypython = os.path.join(self.tmpbindir, pyexename)
1243 1244 try:
1244 1245 if os.readlink(mypython) == sys.executable:
1245 1246 return
1246 1247 os.unlink(mypython)
1247 1248 except OSError, err:
1248 1249 if err.errno != errno.ENOENT:
1249 1250 raise
1250 1251 if self._findprogram(pyexename) != sys.executable:
1251 1252 try:
1252 1253 os.symlink(sys.executable, mypython)
1253 1254 self._createdfiles.append(mypython)
1254 1255 except OSError, err:
1255 1256 # child processes may race, which is harmless
1256 1257 if err.errno != errno.EEXIST:
1257 1258 raise
1258 1259 else:
1259 1260 exedir, exename = os.path.split(sys.executable)
1260 1261 vlog("# Modifying search path to find %s as %s in '%s'" %
1261 1262 (exename, pyexename, exedir))
1262 1263 path = os.environ['PATH'].split(os.pathsep)
1263 1264 while exedir in path:
1264 1265 path.remove(exedir)
1265 1266 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1266 1267 if not self._findprogram(pyexename):
1267 1268 print "WARNING: Cannot find %s in search path" % pyexename
1268 1269
1269 1270 def installhg(self):
1270 1271 vlog("# Performing temporary installation of HG")
1271 1272 installerrs = os.path.join("tests", "install.err")
1272 1273 compiler = ''
1273 1274 if self.options.compiler:
1274 1275 compiler = '--compiler ' + self.options.compiler
1275 1276 pure = self.options.pure and "--pure" or ""
1276 1277 py3 = ''
1277 1278 if sys.version_info[0] == 3:
1278 1279 py3 = '--c2to3'
1279 1280
1280 1281 # Run installer in hg root
1281 1282 script = os.path.realpath(sys.argv[0])
1282 1283 hgroot = os.path.dirname(os.path.dirname(script))
1283 1284 os.chdir(hgroot)
1284 1285 nohome = '--home=""'
1285 1286 if os.name == 'nt':
1286 1287 # The --home="" trick works only on OS where os.sep == '/'
1287 1288 # because of a distutils convert_path() fast-path. Avoid it at
1288 1289 # least on Windows for now, deal with .pydistutils.cfg bugs
1289 1290 # when they happen.
1290 1291 nohome = ''
1291 1292 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1292 1293 ' build %(compiler)s --build-base="%(base)s"'
1293 1294 ' install --force --prefix="%(prefix)s"'
1294 1295 ' --install-lib="%(libdir)s"'
1295 1296 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1296 1297 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1297 1298 'compiler': compiler,
1298 1299 'base': os.path.join(self.hgtmp, "build"),
1299 1300 'prefix': self.inst, 'libdir': self.pythondir,
1300 1301 'bindir': self.bindir,
1301 1302 'nohome': nohome, 'logfile': installerrs})
1302 1303 vlog("# Running", cmd)
1303 1304 if os.system(cmd) == 0:
1304 1305 if not self.options.verbose:
1305 1306 os.remove(installerrs)
1306 1307 else:
1307 1308 f = open(installerrs)
1308 1309 for line in f:
1309 1310 print line,
1310 1311 f.close()
1311 1312 sys.exit(1)
1312 1313 os.chdir(self.testdir)
1313 1314
1314 1315 self.usecorrectpython()
1315 1316
1316 1317 if self.options.py3k_warnings and not self.options.anycoverage:
1317 1318 vlog("# Updating hg command to enable Py3k Warnings switch")
1318 1319 f = open(os.path.join(self.bindir, 'hg'), 'r')
1319 1320 lines = [line.rstrip() for line in f]
1320 1321 lines[0] += ' -3'
1321 1322 f.close()
1322 1323 f = open(os.path.join(self.bindir, 'hg'), 'w')
1323 1324 for line in lines:
1324 1325 f.write(line + '\n')
1325 1326 f.close()
1326 1327
1327 1328 hgbat = os.path.join(self.bindir, 'hg.bat')
1328 1329 if os.path.isfile(hgbat):
1329 1330 # hg.bat expects to be put in bin/scripts while run-tests.py
1330 1331 # installation layout put it in bin/ directly. Fix it
1331 1332 f = open(hgbat, 'rb')
1332 1333 data = f.read()
1333 1334 f.close()
1334 1335 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1335 1336 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1336 1337 '"%~dp0python" "%~dp0hg" %*')
1337 1338 f = open(hgbat, 'wb')
1338 1339 f.write(data)
1339 1340 f.close()
1340 1341 else:
1341 1342 print 'WARNING: cannot fix hg.bat reference to python.exe'
1342 1343
1343 1344 if self.options.anycoverage:
1344 1345 custom = os.path.join(self.testdir, 'sitecustomize.py')
1345 1346 target = os.path.join(self.pythondir, 'sitecustomize.py')
1346 1347 vlog('# Installing coverage trigger to %s' % target)
1347 1348 shutil.copyfile(custom, target)
1348 1349 rc = os.path.join(self.testdir, '.coveragerc')
1349 1350 vlog('# Installing coverage rc to %s' % rc)
1350 1351 os.environ['COVERAGE_PROCESS_START'] = rc
1351 1352 fn = os.path.join(self.inst, '..', '.coverage')
1352 1353 os.environ['COVERAGE_FILE'] = fn
1353 1354
1354 1355 def checkhglib(self, verb):
1355 1356 """Ensure that the 'mercurial' package imported by python is
1356 1357 the one we expect it to be. If not, print a warning to stderr."""
1357 1358 expecthg = os.path.join(self.pythondir, 'mercurial')
1358 1359 actualhg = _gethgpath()
1359 1360 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1360 1361 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1361 1362 ' (expected %s)\n'
1362 1363 % (verb, actualhg, expecthg))
1363 1364
1364 1365 def outputtimes(self):
1365 1366 vlog('# Producing time report')
1366 1367 self.times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1367 1368 cols = '%7.3f %s'
1368 1369 print '\n%-7s %s' % ('Time', 'Test')
1369 1370 for test, timetaken in self.times:
1370 1371 print cols % (timetaken, test)
1371 1372
1372 1373 def outputcoverage(self):
1373 1374 vlog('# Producing coverage report')
1374 1375 os.chdir(self.pythondir)
1375 1376
1376 1377 def covrun(*args):
1377 1378 cmd = 'coverage %s' % ' '.join(args)
1378 1379 vlog('# Running: %s' % cmd)
1379 1380 os.system(cmd)
1380 1381
1381 1382 covrun('-c')
1382 1383 omit = ','.join(os.path.join(x, '*') for x in
1383 1384 [self.bindir, self.testdir])
1384 1385 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1385 1386 if self.options.htmlcov:
1386 1387 htmldir = os.path.join(self.testdir, 'htmlcov')
1387 1388 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1388 1389 '"--omit=%s"' % omit)
1389 1390 if self.options.annotate:
1390 1391 adir = os.path.join(self.testdir, 'annotated')
1391 1392 if not os.path.isdir(adir):
1392 1393 os.mkdir(adir)
1393 1394 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1394 1395
1395 1396 def _executetests(self, tests):
1396 1397 jobs = self.options.jobs
1397 1398 done = queue.Queue()
1398 1399 running = 0
1399 1400 count = 0
1400 1401
1401 1402 def job(test, count):
1402 1403 try:
1403 1404 t = self.gettest(test, count)
1404 1405 done.put(t.run())
1405 1406 t.cleanup()
1406 1407 except KeyboardInterrupt:
1407 1408 pass
1408 1409 except: # re-raises
1409 1410 done.put(('!', test, 'run-test raised an error, see traceback'))
1410 1411 raise
1411 1412
1412 1413 try:
1413 1414 while tests or running:
1414 1415 if not done.empty() or running == jobs or not tests:
1415 1416 try:
1416 1417 code, test, msg = done.get(True, 1)
1417 1418 self.results[code].append((test, msg))
1418 1419 if self.options.first and code not in '.si':
1419 1420 break
1420 1421 except queue.Empty:
1421 1422 continue
1422 1423 running -= 1
1423 1424 if tests and not running == jobs:
1424 1425 test = tests.pop(0)
1425 1426 if self.options.loop:
1426 1427 tests.append(test)
1427 1428 t = threading.Thread(target=job, name=test,
1428 1429 args=(test, count))
1429 1430 t.start()
1430 1431 running += 1
1431 1432 count += 1
1432 1433 except KeyboardInterrupt:
1433 1434 self.abort[0] = True
1434 1435
1435 1436 def _findprogram(self, program):
1436 1437 """Search PATH for a executable program"""
1437 1438 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1438 1439 name = os.path.join(p, program)
1439 1440 if os.name == 'nt' or os.access(name, os.X_OK):
1440 1441 return name
1441 1442 return None
1442 1443
1443 def checktools(self):
1444 def _checktools(self):
1444 1445 # Before we go any further, check for pre-requisite tools
1445 1446 # stuff from coreutils (cat, rm, etc) are not tested
1446 1447 for p in self.REQUIREDTOOLS:
1447 1448 if os.name == 'nt' and not p.endswith('.exe'):
1448 1449 p += '.exe'
1449 1450 found = self._findprogram(p)
1450 1451 if found:
1451 1452 vlog("# Found prerequisite", p, "at", found)
1452 1453 else:
1453 1454 print "WARNING: Did not find prerequisite tool: %s " % p
1454 1455
1455 1456 def main(args, runner=None, parser=None):
1456 1457 runner = runner or TestRunner()
1457 1458
1458 1459 parser = parser or getparser()
1459 1460 (options, args) = parseargs(args, parser)
1460 1461 runner.options = options
1461 1462 os.umask(022)
1462 1463
1463 runner.checktools()
1464
1465 1464 return runner.run(args)
1466 1465
1467 1466 if __name__ == '__main__':
1468 1467 sys.exit(main(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now