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