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