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