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