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