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