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