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