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