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