##// END OF EJS Templates
run-tests: pass an optional TestResult into _executetests()...
Gregory Szorc -
r21438:f647287b default
parent child Browse files
Show More
@@ -1,1596 +1,1607 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 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 'u': [],
1079 1080 }
1080 1081 self.abort = [False]
1081 1082 self._createdfiles = []
1082 1083 self._hgpath = None
1083 1084
1084 1085 def run(self, args, parser=None):
1085 1086 """Run the test suite."""
1086 1087 oldmask = os.umask(022)
1087 1088 try:
1088 1089 parser = parser or getparser()
1089 1090 options, args = parseargs(args, parser)
1090 1091 self.options = options
1091 1092
1092 1093 self._checktools()
1093 1094 tests = self.findtests(args)
1094 1095 return self._run(tests)
1095 1096 finally:
1096 1097 os.umask(oldmask)
1097 1098
1098 1099 def _run(self, tests):
1099 1100 if self.options.random:
1100 1101 random.shuffle(tests)
1101 1102 else:
1102 1103 # keywords for slow tests
1103 1104 slow = 'svn gendoc check-code-hg'.split()
1104 1105 def sortkey(f):
1105 1106 # run largest tests first, as they tend to take the longest
1106 1107 try:
1107 1108 val = -os.stat(f).st_size
1108 1109 except OSError, e:
1109 1110 if e.errno != errno.ENOENT:
1110 1111 raise
1111 1112 return -1e9 # file does not exist, tell early
1112 1113 for kw in slow:
1113 1114 if kw in f:
1114 1115 val *= 10
1115 1116 return val
1116 1117 tests.sort(key=sortkey)
1117 1118
1118 1119 self.testdir = os.environ['TESTDIR'] = os.getcwd()
1119 1120
1120 1121 if 'PYTHONHASHSEED' not in os.environ:
1121 1122 # use a random python hash seed all the time
1122 1123 # we do the randomness ourself to know what seed is used
1123 1124 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1124 1125
1125 1126 if self.options.tmpdir:
1126 1127 self.options.keep_tmpdir = True
1127 1128 tmpdir = self.options.tmpdir
1128 1129 if os.path.exists(tmpdir):
1129 1130 # Meaning of tmpdir has changed since 1.3: we used to create
1130 1131 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1131 1132 # tmpdir already exists.
1132 1133 print "error: temp dir %r already exists" % tmpdir
1133 1134 return 1
1134 1135
1135 1136 # Automatically removing tmpdir sounds convenient, but could
1136 1137 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1137 1138 # or "--tmpdir=$HOME".
1138 1139 #vlog("# Removing temp dir", tmpdir)
1139 1140 #shutil.rmtree(tmpdir)
1140 1141 os.makedirs(tmpdir)
1141 1142 else:
1142 1143 d = None
1143 1144 if os.name == 'nt':
1144 1145 # without this, we get the default temp dir location, but
1145 1146 # in all lowercase, which causes troubles with paths (issue3490)
1146 1147 d = os.getenv('TMP')
1147 1148 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1148 1149 self.hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1149 1150
1150 1151 if self.options.with_hg:
1151 1152 self.inst = None
1152 1153 self.bindir = os.path.dirname(os.path.realpath(
1153 1154 self.options.with_hg))
1154 1155 self.tmpbindir = os.path.join(self.hgtmp, 'install', 'bin')
1155 1156 os.makedirs(self.tmpbindir)
1156 1157
1157 1158 # This looks redundant with how Python initializes sys.path from
1158 1159 # the location of the script being executed. Needed because the
1159 1160 # "hg" specified by --with-hg is not the only Python script
1160 1161 # executed in the test suite that needs to import 'mercurial'
1161 1162 # ... which means it's not really redundant at all.
1162 1163 self.pythondir = self.bindir
1163 1164 else:
1164 1165 self.inst = os.path.join(self.hgtmp, "install")
1165 1166 self.bindir = os.environ["BINDIR"] = os.path.join(self.inst,
1166 1167 "bin")
1167 1168 self.tmpbindir = self.bindir
1168 1169 self.pythondir = os.path.join(self.inst, "lib", "python")
1169 1170
1170 1171 os.environ["BINDIR"] = self.bindir
1171 1172 os.environ["PYTHON"] = PYTHON
1172 1173
1173 1174 path = [self.bindir] + os.environ["PATH"].split(os.pathsep)
1174 1175 if self.tmpbindir != self.bindir:
1175 1176 path = [self.tmpbindir] + path
1176 1177 os.environ["PATH"] = os.pathsep.join(path)
1177 1178
1178 1179 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1179 1180 # can run .../tests/run-tests.py test-foo where test-foo
1180 1181 # adds an extension to HGRC. Also include run-test.py directory to
1181 1182 # import modules like heredoctest.
1182 1183 pypath = [self.pythondir, self.testdir,
1183 1184 os.path.abspath(os.path.dirname(__file__))]
1184 1185 # We have to augment PYTHONPATH, rather than simply replacing
1185 1186 # it, in case external libraries are only available via current
1186 1187 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1187 1188 # are in /opt/subversion.)
1188 1189 oldpypath = os.environ.get(IMPL_PATH)
1189 1190 if oldpypath:
1190 1191 pypath.append(oldpypath)
1191 1192 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1192 1193
1193 1194 self.coveragefile = os.path.join(self.testdir, '.coverage')
1194 1195
1195 1196 vlog("# Using TESTDIR", self.testdir)
1196 1197 vlog("# Using HGTMP", self.hgtmp)
1197 1198 vlog("# Using PATH", os.environ["PATH"])
1198 1199 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1199 1200
1200 1201 try:
1201 1202 return self._runtests(tests) or 0
1202 1203 finally:
1203 1204 time.sleep(.1)
1204 1205 self._cleanup()
1205 1206
1206 1207 def findtests(self, args):
1207 1208 """Finds possible test files from arguments.
1208 1209
1209 1210 If you wish to inject custom tests into the test harness, this would
1210 1211 be a good function to monkeypatch or override in a derived class.
1211 1212 """
1212 1213 if not args:
1213 1214 if self.options.changed:
1214 1215 proc = Popen4('hg st --rev "%s" -man0 .' %
1215 1216 self.options.changed, None, 0)
1216 1217 stdout, stderr = proc.communicate()
1217 1218 args = stdout.strip('\0').split('\0')
1218 1219 else:
1219 1220 args = os.listdir('.')
1220 1221
1221 1222 return [t for t in args
1222 1223 if os.path.basename(t).startswith('test-')
1223 1224 and (t.endswith('.py') or t.endswith('.t'))]
1224 1225
1225 1226 def _runtests(self, tests):
1226 1227 try:
1227 1228 if self.inst:
1228 1229 self._installhg()
1229 1230 self._checkhglib("Testing")
1230 1231 else:
1231 1232 self._usecorrectpython()
1232 1233
1233 1234 if self.options.restart:
1234 1235 orig = list(tests)
1235 1236 while tests:
1236 1237 if os.path.exists(tests[0] + ".err"):
1237 1238 break
1238 1239 tests.pop(0)
1239 1240 if not tests:
1240 1241 print "running all tests"
1241 1242 tests = orig
1242 1243
1243 1244 tests = [self._gettest(t, i, asunit=self.options.unittest)
1244 1245 for i, t in enumerate(tests)]
1245 1246
1246 1247 if self.options.unittest:
1247 1248 suite = unittest.TestSuite(tests=tests)
1248 1249 verbosity = 1
1249 1250 if self.options.verbose:
1250 1251 verbosity = 2
1251 1252 runner = TextTestRunner(verbosity=verbosity)
1252 1253 runner.run(suite)
1253 1254 else:
1254 1255 self._executetests(tests)
1255 1256
1256 1257 failed = len(self.results['!'])
1257 1258 warned = len(self.results['~'])
1258 1259 tested = len(self.results['.']) + failed + warned
1259 1260 skipped = len(self.results['s'])
1260 1261 ignored = len(self.results['i'])
1261 1262
1262 1263 print
1263 1264 if not self.options.noskips:
1264 1265 for s in self.results['s']:
1265 1266 print "Skipped %s: %s" % s
1266 1267 for s in self.results['~']:
1267 1268 print "Warned %s: %s" % s
1268 1269 for s in self.results['!']:
1269 1270 print "Failed %s: %s" % s
1270 1271 self._checkhglib("Tested")
1271 1272 print "# Ran %d tests, %d skipped, %d warned, %d failed." % (
1272 1273 tested, skipped + ignored, warned, failed)
1273 1274 if self.results['!']:
1274 1275 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1275 1276 if self.options.time:
1276 1277 self._outputtimes()
1277 1278
1278 1279 if self.options.anycoverage:
1279 1280 self._outputcoverage()
1280 1281 except KeyboardInterrupt:
1281 1282 failed = True
1282 1283 print "\ninterrupted!"
1283 1284
1284 1285 if failed:
1285 1286 return 1
1286 1287 if warned:
1287 1288 return 80
1288 1289
1289 1290 def _gettest(self, test, count, asunit=False):
1290 1291 """Obtain a Test by looking at its filename.
1291 1292
1292 1293 Returns a Test instance. The Test may not be runnable if it doesn't
1293 1294 map to a known type.
1294 1295 """
1295 1296 lctest = test.lower()
1296 1297 refpath = os.path.join(self.testdir, test)
1297 1298
1298 1299 testcls = Test
1299 1300
1300 1301 for ext, cls, out in self.TESTTYPES:
1301 1302 if lctest.endswith(ext):
1302 1303 testcls = cls
1303 1304 refpath = os.path.join(self.testdir, test + out)
1304 1305 break
1305 1306
1306 1307 t = testcls(self, test, count, refpath, unittest=asunit)
1307 1308
1308 1309 if not asunit:
1309 1310 return t
1310 1311
1311 1312 class MercurialTest(unittest.TestCase):
1312 1313 def __init__(self, name, *args, **kwargs):
1313 1314 super(MercurialTest, self).__init__(*args, **kwargs)
1314 1315 self.name = name
1315 1316
1316 1317 def shortDescription(self):
1317 1318 return self.name
1318 1319
1319 1320 # Need to stash away the TestResult since we do custom things
1320 1321 # with it.
1321 1322 def run(self, result):
1322 1323 self._result = result
1323 1324
1324 1325 return super(MercurialTest, self).run(result)
1325 1326
1326 1327 def runTest(self):
1327 1328 code, tname, msg = t.run()
1328 1329
1329 1330 if code == '!':
1330 1331 self._result.failures.append((self, msg))
1331 1332 elif code == '~':
1332 1333 self._result.addWarn(self, msg)
1333 1334 elif code == '.':
1334 1335 # Success is handled automatically by the built-in run().
1335 1336 pass
1336 1337 elif code == 's':
1337 1338 self._result.addSkip(self, msg)
1338 1339 elif code == 'i':
1339 1340 self._result.addIgnore(self, msg)
1340 1341 else:
1341 1342 self.fail('Unknown test result code: %s' % code)
1342 1343
1343 1344 # We need this proxy until tearDown() is implemented.
1344 1345 def cleanup(self):
1345 1346 return t.cleanup()
1346 1347
1347 1348 return MercurialTest(test)
1348 1349
1349 1350 def _cleanup(self):
1350 1351 """Clean up state from this test invocation."""
1351 1352
1352 1353 if self.options.keep_tmpdir:
1353 1354 return
1354 1355
1355 1356 vlog("# Cleaning up HGTMP", self.hgtmp)
1356 1357 shutil.rmtree(self.hgtmp, True)
1357 1358 for f in self._createdfiles:
1358 1359 try:
1359 1360 os.remove(f)
1360 1361 except OSError:
1361 1362 pass
1362 1363
1363 1364 def _usecorrectpython(self):
1364 1365 # Some tests run the Python interpreter. They must use the
1365 1366 # same interpreter or bad things will happen.
1366 1367 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1367 1368 if getattr(os, 'symlink', None):
1368 1369 vlog("# Making python executable in test path a symlink to '%s'" %
1369 1370 sys.executable)
1370 1371 mypython = os.path.join(self.tmpbindir, pyexename)
1371 1372 try:
1372 1373 if os.readlink(mypython) == sys.executable:
1373 1374 return
1374 1375 os.unlink(mypython)
1375 1376 except OSError, err:
1376 1377 if err.errno != errno.ENOENT:
1377 1378 raise
1378 1379 if self._findprogram(pyexename) != sys.executable:
1379 1380 try:
1380 1381 os.symlink(sys.executable, mypython)
1381 1382 self._createdfiles.append(mypython)
1382 1383 except OSError, err:
1383 1384 # child processes may race, which is harmless
1384 1385 if err.errno != errno.EEXIST:
1385 1386 raise
1386 1387 else:
1387 1388 exedir, exename = os.path.split(sys.executable)
1388 1389 vlog("# Modifying search path to find %s as %s in '%s'" %
1389 1390 (exename, pyexename, exedir))
1390 1391 path = os.environ['PATH'].split(os.pathsep)
1391 1392 while exedir in path:
1392 1393 path.remove(exedir)
1393 1394 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1394 1395 if not self._findprogram(pyexename):
1395 1396 print "WARNING: Cannot find %s in search path" % pyexename
1396 1397
1397 1398 def _installhg(self):
1398 1399 vlog("# Performing temporary installation of HG")
1399 1400 installerrs = os.path.join("tests", "install.err")
1400 1401 compiler = ''
1401 1402 if self.options.compiler:
1402 1403 compiler = '--compiler ' + self.options.compiler
1403 1404 pure = self.options.pure and "--pure" or ""
1404 1405 py3 = ''
1405 1406 if sys.version_info[0] == 3:
1406 1407 py3 = '--c2to3'
1407 1408
1408 1409 # Run installer in hg root
1409 1410 script = os.path.realpath(sys.argv[0])
1410 1411 hgroot = os.path.dirname(os.path.dirname(script))
1411 1412 os.chdir(hgroot)
1412 1413 nohome = '--home=""'
1413 1414 if os.name == 'nt':
1414 1415 # The --home="" trick works only on OS where os.sep == '/'
1415 1416 # because of a distutils convert_path() fast-path. Avoid it at
1416 1417 # least on Windows for now, deal with .pydistutils.cfg bugs
1417 1418 # when they happen.
1418 1419 nohome = ''
1419 1420 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1420 1421 ' build %(compiler)s --build-base="%(base)s"'
1421 1422 ' install --force --prefix="%(prefix)s"'
1422 1423 ' --install-lib="%(libdir)s"'
1423 1424 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1424 1425 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1425 1426 'compiler': compiler,
1426 1427 'base': os.path.join(self.hgtmp, "build"),
1427 1428 'prefix': self.inst, 'libdir': self.pythondir,
1428 1429 'bindir': self.bindir,
1429 1430 'nohome': nohome, 'logfile': installerrs})
1430 1431 vlog("# Running", cmd)
1431 1432 if os.system(cmd) == 0:
1432 1433 if not self.options.verbose:
1433 1434 os.remove(installerrs)
1434 1435 else:
1435 1436 f = open(installerrs)
1436 1437 for line in f:
1437 1438 print line,
1438 1439 f.close()
1439 1440 sys.exit(1)
1440 1441 os.chdir(self.testdir)
1441 1442
1442 1443 self._usecorrectpython()
1443 1444
1444 1445 if self.options.py3k_warnings and not self.options.anycoverage:
1445 1446 vlog("# Updating hg command to enable Py3k Warnings switch")
1446 1447 f = open(os.path.join(self.bindir, 'hg'), 'r')
1447 1448 lines = [line.rstrip() for line in f]
1448 1449 lines[0] += ' -3'
1449 1450 f.close()
1450 1451 f = open(os.path.join(self.bindir, 'hg'), 'w')
1451 1452 for line in lines:
1452 1453 f.write(line + '\n')
1453 1454 f.close()
1454 1455
1455 1456 hgbat = os.path.join(self.bindir, 'hg.bat')
1456 1457 if os.path.isfile(hgbat):
1457 1458 # hg.bat expects to be put in bin/scripts while run-tests.py
1458 1459 # installation layout put it in bin/ directly. Fix it
1459 1460 f = open(hgbat, 'rb')
1460 1461 data = f.read()
1461 1462 f.close()
1462 1463 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1463 1464 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1464 1465 '"%~dp0python" "%~dp0hg" %*')
1465 1466 f = open(hgbat, 'wb')
1466 1467 f.write(data)
1467 1468 f.close()
1468 1469 else:
1469 1470 print 'WARNING: cannot fix hg.bat reference to python.exe'
1470 1471
1471 1472 if self.options.anycoverage:
1472 1473 custom = os.path.join(self.testdir, 'sitecustomize.py')
1473 1474 target = os.path.join(self.pythondir, 'sitecustomize.py')
1474 1475 vlog('# Installing coverage trigger to %s' % target)
1475 1476 shutil.copyfile(custom, target)
1476 1477 rc = os.path.join(self.testdir, '.coveragerc')
1477 1478 vlog('# Installing coverage rc to %s' % rc)
1478 1479 os.environ['COVERAGE_PROCESS_START'] = rc
1479 1480 fn = os.path.join(self.inst, '..', '.coverage')
1480 1481 os.environ['COVERAGE_FILE'] = fn
1481 1482
1482 1483 def _checkhglib(self, verb):
1483 1484 """Ensure that the 'mercurial' package imported by python is
1484 1485 the one we expect it to be. If not, print a warning to stderr."""
1485 1486 expecthg = os.path.join(self.pythondir, 'mercurial')
1486 1487 actualhg = self._gethgpath()
1487 1488 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1488 1489 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1489 1490 ' (expected %s)\n'
1490 1491 % (verb, actualhg, expecthg))
1491 1492 def _gethgpath(self):
1492 1493 """Return the path to the mercurial package that is actually found by
1493 1494 the current Python interpreter."""
1494 1495 if self._hgpath is not None:
1495 1496 return self._hgpath
1496 1497
1497 1498 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1498 1499 pipe = os.popen(cmd % PYTHON)
1499 1500 try:
1500 1501 self._hgpath = pipe.read().strip()
1501 1502 finally:
1502 1503 pipe.close()
1503 1504
1504 1505 return self._hgpath
1505 1506
1506 1507 def _outputtimes(self):
1507 1508 vlog('# Producing time report')
1508 1509 self.times.sort(key=lambda t: (t[1], t[0]), reverse=True)
1509 1510 cols = '%7.3f %s'
1510 1511 print '\n%-7s %s' % ('Time', 'Test')
1511 1512 for test, timetaken in self.times:
1512 1513 print cols % (timetaken, test)
1513 1514
1514 1515 def _outputcoverage(self):
1515 1516 vlog('# Producing coverage report')
1516 1517 os.chdir(self.pythondir)
1517 1518
1518 1519 def covrun(*args):
1519 1520 cmd = 'coverage %s' % ' '.join(args)
1520 1521 vlog('# Running: %s' % cmd)
1521 1522 os.system(cmd)
1522 1523
1523 1524 covrun('-c')
1524 1525 omit = ','.join(os.path.join(x, '*') for x in
1525 1526 [self.bindir, self.testdir])
1526 1527 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1527 1528 if self.options.htmlcov:
1528 1529 htmldir = os.path.join(self.testdir, 'htmlcov')
1529 1530 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1530 1531 '"--omit=%s"' % omit)
1531 1532 if self.options.annotate:
1532 1533 adir = os.path.join(self.testdir, 'annotated')
1533 1534 if not os.path.isdir(adir):
1534 1535 os.mkdir(adir)
1535 1536 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1536 1537
1537 def _executetests(self, tests):
1538 def _executetests(self, tests, result=None):
1539 # We copy because we modify the list.
1540 tests = list(tests)
1541
1538 1542 jobs = self.options.jobs
1539 1543 done = queue.Queue()
1540 1544 running = 0
1541 1545
1542 def job(test):
1546 def job(test, result):
1543 1547 try:
1544 done.put(test.run())
1548 # If in unittest mode.
1549 if result:
1550 test(result)
1551 # We need to put something here to make the logic happy.
1552 # This will get cleaned up later.
1553 done.put(('u', None, None))
1554 else:
1555 done.put(test.run())
1545 1556 test.cleanup()
1546 1557 except KeyboardInterrupt:
1547 1558 pass
1548 1559 except: # re-raises
1549 1560 done.put(('!', test, 'run-test raised an error, see traceback'))
1550 1561 raise
1551 1562
1552 1563 try:
1553 1564 while tests or running:
1554 1565 if not done.empty() or running == jobs or not tests:
1555 1566 try:
1556 1567 code, test, msg = done.get(True, 1)
1557 1568 self.results[code].append((test, msg))
1558 1569 if self.options.first and code not in '.si':
1559 1570 break
1560 1571 except queue.Empty:
1561 1572 continue
1562 1573 running -= 1
1563 1574 if tests and not running == jobs:
1564 1575 test = tests.pop(0)
1565 1576 if self.options.loop:
1566 1577 tests.append(test)
1567 1578 t = threading.Thread(target=job, name=test.name,
1568 args=[test])
1579 args=(test, result))
1569 1580 t.start()
1570 1581 running += 1
1571 1582 except KeyboardInterrupt:
1572 1583 self.abort[0] = True
1573 1584
1574 1585 def _findprogram(self, program):
1575 1586 """Search PATH for a executable program"""
1576 1587 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1577 1588 name = os.path.join(p, program)
1578 1589 if os.name == 'nt' or os.access(name, os.X_OK):
1579 1590 return name
1580 1591 return None
1581 1592
1582 1593 def _checktools(self):
1583 1594 # Before we go any further, check for pre-requisite tools
1584 1595 # stuff from coreutils (cat, rm, etc) are not tested
1585 1596 for p in self.REQUIREDTOOLS:
1586 1597 if os.name == 'nt' and not p.endswith('.exe'):
1587 1598 p += '.exe'
1588 1599 found = self._findprogram(p)
1589 1600 if found:
1590 1601 vlog("# Found prerequisite", p, "at", found)
1591 1602 else:
1592 1603 print "WARNING: Did not find prerequisite tool: %s " % p
1593 1604
1594 1605 if __name__ == '__main__':
1595 1606 runner = TestRunner()
1596 1607 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now