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