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