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