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