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