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