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