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