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