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