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