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