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