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