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