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