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