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