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