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