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