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