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