##// END OF EJS Templates
tests: basic support for unified tests
Matt Mackall -
r11741:431e2bf3 default
parent child Browse files
Show More
@@ -1,981 +1,1051 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 import re
55 56
56 57 closefds = os.name == 'posix'
57 58 def Popen4(cmd, bufsize=-1):
58 59 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
59 60 close_fds=closefds,
60 61 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
61 62 stderr=subprocess.STDOUT)
62 63 p.fromchild = p.stdout
63 64 p.tochild = p.stdin
64 65 p.childerr = p.stderr
65 66 return p
66 67
67 68 # reserved exit code to skip test (used by hghave)
68 69 SKIPPED_STATUS = 80
69 70 SKIPPED_PREFIX = 'skipped: '
70 71 FAILED_PREFIX = 'hghave check failed: '
71 72 PYTHON = sys.executable
72 73 IMPL_PATH = 'PYTHONPATH'
73 74 if 'java' in sys.platform:
74 75 IMPL_PATH = 'JYTHONPATH'
75 76
76 77 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
77 78
78 79 defaults = {
79 80 'jobs': ('HGTEST_JOBS', 1),
80 81 'timeout': ('HGTEST_TIMEOUT', 180),
81 82 'port': ('HGTEST_PORT', 20059),
82 83 }
83 84
84 85 def parseargs():
85 86 parser = optparse.OptionParser("%prog [options] [tests]")
86 87
87 88 # keep these sorted
88 89 parser.add_option("--blacklist", action="append",
89 90 help="skip tests listed in the specified blacklist file")
90 91 parser.add_option("-C", "--annotate", action="store_true",
91 92 help="output files annotated with coverage")
92 93 parser.add_option("--child", type="int",
93 94 help="run as child process, summary to given fd")
94 95 parser.add_option("-c", "--cover", action="store_true",
95 96 help="print a test coverage report")
96 97 parser.add_option("-d", "--debug", action="store_true",
97 98 help="debug mode: write output of test scripts to console"
98 99 " rather than capturing and diff'ing it (disables timeout)")
99 100 parser.add_option("-f", "--first", action="store_true",
100 101 help="exit on the first test failure")
101 102 parser.add_option("--inotify", action="store_true",
102 103 help="enable inotify extension when running tests")
103 104 parser.add_option("-i", "--interactive", action="store_true",
104 105 help="prompt to accept changed output")
105 106 parser.add_option("-j", "--jobs", type="int",
106 107 help="number of jobs to run in parallel"
107 108 " (default: $%s or %d)" % defaults['jobs'])
108 109 parser.add_option("--keep-tmpdir", action="store_true",
109 110 help="keep temporary directory after running tests")
110 111 parser.add_option("-k", "--keywords",
111 112 help="run tests matching keywords")
112 113 parser.add_option("-l", "--local", action="store_true",
113 114 help="shortcut for --with-hg=<testdir>/../hg")
114 115 parser.add_option("-n", "--nodiff", action="store_true",
115 116 help="skip showing test changes")
116 117 parser.add_option("-p", "--port", type="int",
117 118 help="port on which servers should listen"
118 119 " (default: $%s or %d)" % defaults['port'])
119 120 parser.add_option("--pure", action="store_true",
120 121 help="use pure Python code instead of C extensions")
121 122 parser.add_option("-R", "--restart", action="store_true",
122 123 help="restart at last error")
123 124 parser.add_option("-r", "--retest", action="store_true",
124 125 help="retest failed tests")
125 126 parser.add_option("-S", "--noskips", action="store_true",
126 127 help="don't report skip tests verbosely")
127 128 parser.add_option("-t", "--timeout", type="int",
128 129 help="kill errant tests after TIMEOUT seconds"
129 130 " (default: $%s or %d)" % defaults['timeout'])
130 131 parser.add_option("--tmpdir", type="string",
131 132 help="run tests in the given temporary directory"
132 133 " (implies --keep-tmpdir)")
133 134 parser.add_option("-v", "--verbose", action="store_true",
134 135 help="output verbose messages")
135 136 parser.add_option("--view", type="string",
136 137 help="external diff viewer")
137 138 parser.add_option("--with-hg", type="string",
138 139 metavar="HG",
139 140 help="test using specified hg script rather than a "
140 141 "temporary installation")
141 142 parser.add_option("-3", "--py3k-warnings", action="store_true",
142 143 help="enable Py3k warnings on Python 2.6+")
143 144
144 145 for option, default in defaults.items():
145 146 defaults[option] = int(os.environ.get(*default))
146 147 parser.set_defaults(**defaults)
147 148 (options, args) = parser.parse_args()
148 149
149 150 # jython is always pure
150 151 if 'java' in sys.platform or '__pypy__' in sys.modules:
151 152 options.pure = True
152 153
153 154 if options.with_hg:
154 155 if not (os.path.isfile(options.with_hg) and
155 156 os.access(options.with_hg, os.X_OK)):
156 157 parser.error('--with-hg must specify an executable hg script')
157 158 if not os.path.basename(options.with_hg) == 'hg':
158 159 sys.stderr.write('warning: --with-hg should specify an hg script')
159 160 if options.local:
160 161 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
161 162 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
162 163 if not os.access(hgbin, os.X_OK):
163 164 parser.error('--local specified, but %r not found or not executable'
164 165 % hgbin)
165 166 options.with_hg = hgbin
166 167
167 168 options.anycoverage = options.cover or options.annotate
168 169 if options.anycoverage:
169 170 try:
170 171 import coverage
171 172 covver = version.StrictVersion(coverage.__version__).version
172 173 if covver < (3, 3):
173 174 parser.error('coverage options require coverage 3.3 or later')
174 175 except ImportError:
175 176 parser.error('coverage options now require the coverage package')
176 177
177 178 if options.anycoverage and options.local:
178 179 # this needs some path mangling somewhere, I guess
179 180 parser.error("sorry, coverage options do not work when --local "
180 181 "is specified")
181 182
182 183 global vlog
183 184 if options.verbose:
184 185 if options.jobs > 1 or options.child is not None:
185 186 pid = "[%d]" % os.getpid()
186 187 else:
187 188 pid = None
188 189 def vlog(*msg):
189 190 if pid:
190 191 print pid,
191 192 for m in msg:
192 193 print m,
193 194 print
194 195 sys.stdout.flush()
195 196 else:
196 197 vlog = lambda *msg: None
197 198
198 199 if options.tmpdir:
199 200 options.tmpdir = os.path.expanduser(options.tmpdir)
200 201
201 202 if options.jobs < 1:
202 203 parser.error('--jobs must be positive')
203 204 if options.interactive and options.jobs > 1:
204 205 print '(--interactive overrides --jobs)'
205 206 options.jobs = 1
206 207 if options.interactive and options.debug:
207 208 parser.error("-i/--interactive and -d/--debug are incompatible")
208 209 if options.debug:
209 210 if options.timeout != defaults['timeout']:
210 211 sys.stderr.write(
211 212 'warning: --timeout option ignored with --debug\n')
212 213 options.timeout = 0
213 214 if options.py3k_warnings:
214 215 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
215 216 parser.error('--py3k-warnings can only be used on Python 2.6+')
216 217 if options.blacklist:
217 218 blacklist = dict()
218 219 for filename in options.blacklist:
219 220 try:
220 221 path = os.path.expanduser(os.path.expandvars(filename))
221 222 f = open(path, "r")
222 223 except IOError, err:
223 224 if err.errno != errno.ENOENT:
224 225 raise
225 226 print "warning: no such blacklist file: %s" % filename
226 227 continue
227 228
228 229 for line in f.readlines():
229 230 line = line.strip()
230 231 if line and not line.startswith('#'):
231 232 blacklist[line] = filename
232 233
233 234 options.blacklist = blacklist
234 235
235 236 return (options, args)
236 237
237 238 def rename(src, dst):
238 239 """Like os.rename(), trade atomicity and opened files friendliness
239 240 for existing destination support.
240 241 """
241 242 shutil.copy(src, dst)
242 243 os.remove(src)
243 244
244 245 def splitnewlines(text):
245 246 '''like str.splitlines, but only split on newlines.
246 247 keep line endings.'''
247 248 i = 0
248 249 lines = []
249 250 while True:
250 251 n = text.find('\n', i)
251 252 if n == -1:
252 253 last = text[i:]
253 254 if last:
254 255 lines.append(last)
255 256 return lines
256 257 lines.append(text[i:n + 1])
257 258 i = n + 1
258 259
259 260 def parsehghaveoutput(lines):
260 261 '''Parse hghave log lines.
261 262 Return tuple of lists (missing, failed):
262 263 * the missing/unknown features
263 264 * the features for which existence check failed'''
264 265 missing = []
265 266 failed = []
266 267 for line in lines:
267 268 if line.startswith(SKIPPED_PREFIX):
268 269 line = line.splitlines()[0]
269 270 missing.append(line[len(SKIPPED_PREFIX):])
270 271 elif line.startswith(FAILED_PREFIX):
271 272 line = line.splitlines()[0]
272 273 failed.append(line[len(FAILED_PREFIX):])
273 274
274 275 return missing, failed
275 276
276 277 def showdiff(expected, output, ref, err):
277 278 for line in difflib.unified_diff(expected, output, ref, err):
278 279 sys.stdout.write(line)
279 280
280 281 def findprogram(program):
281 282 """Search PATH for a executable program"""
282 283 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
283 284 name = os.path.join(p, program)
284 285 if os.access(name, os.X_OK):
285 286 return name
286 287 return None
287 288
288 289 def checktools():
289 290 # Before we go any further, check for pre-requisite tools
290 291 # stuff from coreutils (cat, rm, etc) are not tested
291 292 for p in requiredtools:
292 293 if os.name == 'nt':
293 294 p += '.exe'
294 295 found = findprogram(p)
295 296 if found:
296 297 vlog("# Found prerequisite", p, "at", found)
297 298 else:
298 299 print "WARNING: Did not find prerequisite tool: "+p
299 300
300 301 def killdaemons():
301 302 # Kill off any leftover daemon processes
302 303 try:
303 304 fp = open(DAEMON_PIDS)
304 305 for line in fp:
305 306 try:
306 307 pid = int(line)
307 308 except ValueError:
308 309 continue
309 310 try:
310 311 os.kill(pid, 0)
311 312 vlog('# Killing daemon process %d' % pid)
312 313 os.kill(pid, signal.SIGTERM)
313 314 time.sleep(0.25)
314 315 os.kill(pid, 0)
315 316 vlog('# Daemon process %d is stuck - really killing it' % pid)
316 317 os.kill(pid, signal.SIGKILL)
317 318 except OSError, err:
318 319 if err.errno != errno.ESRCH:
319 320 raise
320 321 fp.close()
321 322 os.unlink(DAEMON_PIDS)
322 323 except IOError:
323 324 pass
324 325
325 326 def cleanup(options):
326 327 if not options.keep_tmpdir:
327 328 vlog("# Cleaning up HGTMP", HGTMP)
328 329 shutil.rmtree(HGTMP, True)
329 330
330 331 def usecorrectpython():
331 332 # some tests run python interpreter. they must use same
332 333 # interpreter we use or bad things will happen.
333 334 exedir, exename = os.path.split(sys.executable)
334 335 if exename == 'python':
335 336 path = findprogram('python')
336 337 if os.path.dirname(path) == exedir:
337 338 return
338 339 vlog('# Making python executable in test path use correct Python')
339 340 mypython = os.path.join(BINDIR, 'python')
340 341 try:
341 342 os.symlink(sys.executable, mypython)
342 343 except AttributeError:
343 344 # windows fallback
344 345 shutil.copyfile(sys.executable, mypython)
345 346 shutil.copymode(sys.executable, mypython)
346 347
347 348 def installhg(options):
348 349 vlog("# Performing temporary installation of HG")
349 350 installerrs = os.path.join("tests", "install.err")
350 351 pure = options.pure and "--pure" or ""
351 352
352 353 # Run installer in hg root
353 354 script = os.path.realpath(sys.argv[0])
354 355 hgroot = os.path.dirname(os.path.dirname(script))
355 356 os.chdir(hgroot)
356 357 nohome = '--home=""'
357 358 if os.name == 'nt':
358 359 # The --home="" trick works only on OS where os.sep == '/'
359 360 # because of a distutils convert_path() fast-path. Avoid it at
360 361 # least on Windows for now, deal with .pydistutils.cfg bugs
361 362 # when they happen.
362 363 nohome = ''
363 364 cmd = ('%s setup.py %s clean --all'
364 365 ' install --force --prefix="%s" --install-lib="%s"'
365 366 ' --install-scripts="%s" %s >%s 2>&1'
366 367 % (sys.executable, pure, INST, PYTHONDIR, BINDIR, nohome,
367 368 installerrs))
368 369 vlog("# Running", cmd)
369 370 if os.system(cmd) == 0:
370 371 if not options.verbose:
371 372 os.remove(installerrs)
372 373 else:
373 374 f = open(installerrs)
374 375 for line in f:
375 376 print line,
376 377 f.close()
377 378 sys.exit(1)
378 379 os.chdir(TESTDIR)
379 380
380 381 usecorrectpython()
381 382
382 383 vlog("# Installing dummy diffstat")
383 384 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
384 385 f.write('#!' + sys.executable + '\n'
385 386 'import sys\n'
386 387 'files = 0\n'
387 388 'for line in sys.stdin:\n'
388 389 ' if line.startswith("diff "):\n'
389 390 ' files += 1\n'
390 391 'sys.stdout.write("files patched: %d\\n" % files)\n')
391 392 f.close()
392 393 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
393 394
394 395 if options.py3k_warnings and not options.anycoverage:
395 396 vlog("# Updating hg command to enable Py3k Warnings switch")
396 397 f = open(os.path.join(BINDIR, 'hg'), 'r')
397 398 lines = [line.rstrip() for line in f]
398 399 lines[0] += ' -3'
399 400 f.close()
400 401 f = open(os.path.join(BINDIR, 'hg'), 'w')
401 402 for line in lines:
402 403 f.write(line + '\n')
403 404 f.close()
404 405
405 406 if options.anycoverage:
406 407 custom = os.path.join(TESTDIR, 'sitecustomize.py')
407 408 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
408 409 vlog('# Installing coverage trigger to %s' % target)
409 410 shutil.copyfile(custom, target)
410 411 rc = os.path.join(TESTDIR, '.coveragerc')
411 412 vlog('# Installing coverage rc to %s' % rc)
412 413 os.environ['COVERAGE_PROCESS_START'] = rc
413 414 fn = os.path.join(INST, '..', '.coverage')
414 415 os.environ['COVERAGE_FILE'] = fn
415 416
416 417 def outputcoverage(options):
417 418
418 419 vlog('# Producing coverage report')
419 420 os.chdir(PYTHONDIR)
420 421
421 422 def covrun(*args):
422 423 cmd = 'coverage %s' % ' '.join(args)
423 424 vlog('# Running: %s' % cmd)
424 425 os.system(cmd)
425 426
426 427 if options.child:
427 428 return
428 429
429 430 covrun('-c')
430 431 omit = ','.join([BINDIR, TESTDIR])
431 432 covrun('-i', '-r', '"--omit=%s"' % omit) # report
432 433 if options.annotate:
433 434 adir = os.path.join(TESTDIR, 'annotated')
434 435 if not os.path.isdir(adir):
435 436 os.mkdir(adir)
436 437 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
437 438
438 439 class Timeout(Exception):
439 440 pass
440 441
441 442 def alarmed(signum, frame):
442 443 raise Timeout
443 444
444 445 def pytest(test, options):
445 446 py3kswitch = options.py3k_warnings and ' -3' or ''
446 447 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
447 448 vlog("# Running", cmd)
448 449 return run(cmd, options)
449 450
450 451 def shtest(test, options):
451 452 cmd = '"%s"' % test
452 453 vlog("# Running", cmd)
453 454 return run(cmd, options)
454 455
455 456 def battest(test, options):
456 457 # To reliably get the error code from batch files on WinXP,
457 458 # the "cmd /c call" prefix is needed. Grrr
458 459 cmd = 'cmd /c call "%s"' % testpath
459 460 vlog("# Running", cmd)
460 461 return run(cmd, options)
461 462
463 def tsttest(test, options):
464 t = open(test)
465 out = []
466 script = []
467 salt = "SALT" + str(time.time())
468
469 pos = prepos = -1
470 after = {}
471 expected = {}
472 for n, l in enumerate(t):
473 if l.startswith(' $ '): # commands
474 after.setdefault(pos, []).append(l)
475 prepos = pos
476 pos = n
477 script.append('echo %s %s\n' % (salt, n))
478 script.append(l[4:])
479 elif l.startswith(' > '): # continuations
480 after.setdefault(prepos, []).append(l)
481 script.append(l[4:])
482 elif l.startswith(' '): # results
483 # queue up a list of expected results
484 expected.setdefault(pos, []).append(l[2:])
485 else:
486 # non-command/result - queue up for merged output
487 after.setdefault(pos, []).append(l)
488
489 fd, name = tempfile.mkstemp(suffix='hg-tst')
490
491 try:
492 for l in script:
493 os.write(fd, l)
494 os.close(fd)
495
496 cmd = '/bin/sh "%s"' % name
497 vlog("# Running", cmd)
498 exitcode, output = run(cmd, options)
499 finally:
500 os.remove(name)
501
502 pos = -1
503 postout = []
504 for n, l in enumerate(output):
505 if l.startswith(salt):
506 if pos in after:
507 postout += after.pop(pos)
508 pos = int(l.split()[1])
509 else:
510 el = None
511 if pos in expected and expected[pos]:
512 el = expected[pos].pop(0)
513
514 if el == l: # perfect match (fast)
515 postout.append(" " + l)
516 elif el and re.match(el, l): # fallback regex match
517 postout.append(" " + el)
518 else: # mismatch - let diff deal with it
519 postout.append(" " + l)
520
521 if pos in after:
522 postout += after.pop(pos)
523
524 return exitcode, postout
525
462 526 def run(cmd, options):
463 527 """Run command in a sub-process, capturing the output (stdout and stderr).
464 528 Return a tuple (exitcode, output). output is None in debug mode."""
465 529 # TODO: Use subprocess.Popen if we're running on Python 2.4
466 530 if options.debug:
467 531 proc = subprocess.Popen(cmd, shell=True)
468 532 ret = proc.wait()
469 533 return (ret, None)
470 534
471 535 if os.name == 'nt' or sys.platform.startswith('java'):
472 536 tochild, fromchild = os.popen4(cmd)
473 537 tochild.close()
474 538 output = fromchild.read()
475 539 ret = fromchild.close()
476 540 if ret == None:
477 541 ret = 0
478 542 else:
479 543 proc = Popen4(cmd)
480 544 def cleanup():
481 545 os.kill(proc.pid, signal.SIGTERM)
482 546 ret = proc.wait()
483 547 if ret == 0:
484 548 ret = signal.SIGTERM << 8
485 549 killdaemons()
486 550 return ret
487 551
488 552 try:
489 553 output = ''
490 554 proc.tochild.close()
491 555 output = proc.fromchild.read()
492 556 ret = proc.wait()
493 557 if os.WIFEXITED(ret):
494 558 ret = os.WEXITSTATUS(ret)
495 559 except Timeout:
496 560 vlog('# Process %d timed out - killing it' % proc.pid)
497 561 ret = cleanup()
498 562 output += ("\n### Abort: timeout after %d seconds.\n"
499 563 % options.timeout)
500 564 except KeyboardInterrupt:
501 565 vlog('# Handling keyboard interrupt')
502 566 cleanup()
503 567 raise
504 568
505 569 return ret, splitnewlines(output)
506 570
507 571 def runone(options, test, skips, fails):
508 572 '''tristate output:
509 573 None -> skipped
510 574 True -> passed
511 575 False -> failed'''
512 576
513 577 def skip(msg):
514 578 if not options.verbose:
515 579 skips.append((test, msg))
516 580 else:
517 581 print "\nSkipping %s: %s" % (testpath, msg)
518 582 return None
519 583
520 584 def fail(msg):
521 585 fails.append((test, msg))
522 586 if not options.nodiff:
523 587 print "\nERROR: %s %s" % (testpath, msg)
524 588 return None
525 589
526 590 vlog("# Test", test)
527 591
528 592 # create a fresh hgrc
529 593 hgrc = open(HGRCPATH, 'w+')
530 594 hgrc.write('[ui]\n')
531 595 hgrc.write('slash = True\n')
532 596 hgrc.write('[defaults]\n')
533 597 hgrc.write('backout = -d "0 0"\n')
534 598 hgrc.write('commit = -d "0 0"\n')
535 599 hgrc.write('tag = -d "0 0"\n')
536 600 if options.inotify:
537 601 hgrc.write('[extensions]\n')
538 602 hgrc.write('inotify=\n')
539 603 hgrc.write('[inotify]\n')
540 604 hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
541 605 hgrc.write('appendpid=True\n')
542 606 hgrc.close()
543 607
544 608 testpath = os.path.join(TESTDIR, test)
545 609 ref = os.path.join(TESTDIR, test+".out")
546 610 err = os.path.join(TESTDIR, test+".err")
547 611 if os.path.exists(err):
548 612 os.remove(err) # Remove any previous output files
549 613 try:
550 614 tf = open(testpath)
551 615 firstline = tf.readline().rstrip()
552 616 tf.close()
553 617 except:
554 618 firstline = ''
555 619 lctest = test.lower()
556 620
557 621 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
558 622 runner = pytest
559 623 elif lctest.endswith('.bat'):
560 624 # do not run batch scripts on non-windows
561 625 if os.name != 'nt':
562 626 return skip("batch script")
563 627 runner = battest
628 elif lctest.endswith('.t'):
629 runner = tsttest
630 ref = testpath
564 631 else:
565 632 # do not run shell scripts on windows
566 633 if os.name == 'nt':
567 634 return skip("shell script")
568 635 # do not try to run non-executable programs
569 636 if not os.path.exists(testpath):
570 637 return fail("does not exist")
571 638 elif not os.access(testpath, os.X_OK):
572 639 return skip("not executable")
573 640 runner = shtest
574 641
575 642 # Make a tmp subdirectory to work in
576 643 tmpd = os.path.join(HGTMP, test)
577 644 os.mkdir(tmpd)
578 645 os.chdir(tmpd)
579 646
580 647 if options.timeout > 0:
581 648 signal.alarm(options.timeout)
582 649
583 650 ret, out = runner(testpath, options)
584 651 vlog("# Ret was:", ret)
585 652
586 653 if options.timeout > 0:
587 654 signal.alarm(0)
588 655
589 656 mark = '.'
590 657
591 658 skipped = (ret == SKIPPED_STATUS)
592 659
593 660 # If we're not in --debug mode and reference output file exists,
594 661 # check test output against it.
595 662 if options.debug:
596 663 refout = None # to match out == None
597 664 elif os.path.exists(ref):
598 665 f = open(ref, "r")
599 666 refout = splitnewlines(f.read())
600 667 f.close()
601 668 else:
602 669 refout = []
603 670
604 671 if (ret != 0 or out != refout) and not skipped and not options.debug:
605 672 # Save errors to a file for diagnosis
606 673 f = open(err, "wb")
607 674 for line in out:
608 675 f.write(line)
609 676 f.close()
610 677
611 678 if skipped:
612 679 mark = 's'
613 680 if out is None: # debug mode: nothing to parse
614 681 missing = ['unknown']
615 682 failed = None
616 683 else:
617 684 missing, failed = parsehghaveoutput(out)
618 685 if not missing:
619 686 missing = ['irrelevant']
620 687 if failed:
621 688 fail("hghave failed checking for %s" % failed[-1])
622 689 skipped = False
623 690 else:
624 691 skip(missing[-1])
625 692 elif out != refout:
626 693 mark = '!'
627 694 if ret:
628 695 fail("output changed and returned error code %d" % ret)
629 696 else:
630 697 fail("output changed")
631 698 if not options.nodiff:
632 699 if options.view:
633 700 os.system("%s %s %s" % (options.view, ref, err))
634 701 else:
635 702 showdiff(refout, out, ref, err)
636 703 ret = 1
637 704 elif ret:
638 705 mark = '!'
639 706 fail("returned error code %d" % ret)
640 707
641 708 if not options.verbose:
642 709 sys.stdout.write(mark)
643 710 sys.stdout.flush()
644 711
645 712 killdaemons()
646 713
647 714 os.chdir(TESTDIR)
648 715 if not options.keep_tmpdir:
649 716 shutil.rmtree(tmpd, True)
650 717 if skipped:
651 718 return None
652 719 return ret == 0
653 720
654 721 _hgpath = None
655 722
656 723 def _gethgpath():
657 724 """Return the path to the mercurial package that is actually found by
658 725 the current Python interpreter."""
659 726 global _hgpath
660 727 if _hgpath is not None:
661 728 return _hgpath
662 729
663 730 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
664 731 pipe = os.popen(cmd % PYTHON)
665 732 try:
666 733 _hgpath = pipe.read().strip()
667 734 finally:
668 735 pipe.close()
669 736 return _hgpath
670 737
671 738 def _checkhglib(verb):
672 739 """Ensure that the 'mercurial' package imported by python is
673 740 the one we expect it to be. If not, print a warning to stderr."""
674 741 expecthg = os.path.join(PYTHONDIR, 'mercurial')
675 742 actualhg = _gethgpath()
676 743 if actualhg != expecthg:
677 744 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
678 745 ' (expected %s)\n'
679 746 % (verb, actualhg, expecthg))
680 747
681 748 def runchildren(options, tests):
682 749 if INST:
683 750 installhg(options)
684 751 _checkhglib("Testing")
685 752
686 753 optcopy = dict(options.__dict__)
687 754 optcopy['jobs'] = 1
688 755 del optcopy['blacklist']
689 756 if optcopy['with_hg'] is None:
690 757 optcopy['with_hg'] = os.path.join(BINDIR, "hg")
691 758 optcopy.pop('anycoverage', None)
692 759
693 760 opts = []
694 761 for opt, value in optcopy.iteritems():
695 762 name = '--' + opt.replace('_', '-')
696 763 if value is True:
697 764 opts.append(name)
698 765 elif value is not None:
699 766 opts.append(name + '=' + str(value))
700 767
701 768 tests.reverse()
702 769 jobs = [[] for j in xrange(options.jobs)]
703 770 while tests:
704 771 for job in jobs:
705 772 if not tests:
706 773 break
707 774 job.append(tests.pop())
708 775 fps = {}
709 776
710 777 for j, job in enumerate(jobs):
711 778 if not job:
712 779 continue
713 780 rfd, wfd = os.pipe()
714 781 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
715 782 childtmp = os.path.join(HGTMP, 'child%d' % j)
716 783 childopts += ['--tmpdir', childtmp]
717 784 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
718 785 vlog(' '.join(cmdline))
719 786 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
720 787 os.close(wfd)
721 788 signal.signal(signal.SIGINT, signal.SIG_IGN)
722 789 failures = 0
723 790 tested, skipped, failed = 0, 0, 0
724 791 skips = []
725 792 fails = []
726 793 while fps:
727 794 pid, status = os.wait()
728 795 fp = fps.pop(pid)
729 796 l = fp.read().splitlines()
730 797 try:
731 798 test, skip, fail = map(int, l[:3])
732 799 except ValueError:
733 800 test, skip, fail = 0, 0, 0
734 801 split = -fail or len(l)
735 802 for s in l[3:split]:
736 803 skips.append(s.split(" ", 1))
737 804 for s in l[split:]:
738 805 fails.append(s.split(" ", 1))
739 806 tested += test
740 807 skipped += skip
741 808 failed += fail
742 809 vlog('pid %d exited, status %d' % (pid, status))
743 810 failures |= status
744 811 print
745 812 if not options.noskips:
746 813 for s in skips:
747 814 print "Skipped %s: %s" % (s[0], s[1])
748 815 for s in fails:
749 816 print "Failed %s: %s" % (s[0], s[1])
750 817
751 818 _checkhglib("Tested")
752 819 print "# Ran %d tests, %d skipped, %d failed." % (
753 820 tested, skipped, failed)
754 821
755 822 if options.anycoverage:
756 823 outputcoverage(options)
757 824 sys.exit(failures != 0)
758 825
759 826 def runtests(options, tests):
760 827 global DAEMON_PIDS, HGRCPATH
761 828 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
762 829 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
763 830
764 831 try:
765 832 if INST:
766 833 installhg(options)
767 834 _checkhglib("Testing")
768 835
769 836 if options.timeout > 0:
770 837 try:
771 838 signal.signal(signal.SIGALRM, alarmed)
772 839 vlog('# Running each test with %d second timeout' %
773 840 options.timeout)
774 841 except AttributeError:
775 842 print 'WARNING: cannot run tests with timeouts'
776 843 options.timeout = 0
777 844
778 845 tested = 0
779 846 failed = 0
780 847 skipped = 0
781 848
782 849 if options.restart:
783 850 orig = list(tests)
784 851 while tests:
785 852 if os.path.exists(tests[0] + ".err"):
786 853 break
787 854 tests.pop(0)
788 855 if not tests:
789 856 print "running all tests"
790 857 tests = orig
791 858
792 859 skips = []
793 860 fails = []
794 861
795 862 for test in tests:
796 863 if options.blacklist:
797 864 filename = options.blacklist.get(test)
798 865 if filename is not None:
799 866 skips.append((test, "blacklisted (%s)" % filename))
800 867 skipped += 1
801 868 continue
802 869
803 870 if options.retest and not os.path.exists(test + ".err"):
804 871 skipped += 1
805 872 continue
806 873
807 874 if options.keywords:
808 875 t = open(test).read().lower() + test.lower()
809 876 for k in options.keywords.lower().split():
810 877 if k in t:
811 878 break
812 879 else:
813 880 skipped += 1
814 881 continue
815 882
816 883 ret = runone(options, test, skips, fails)
817 884 if ret is None:
818 885 skipped += 1
819 886 elif not ret:
820 887 if options.interactive:
821 888 print "Accept this change? [n] ",
822 889 answer = sys.stdin.readline().strip()
823 890 if answer.lower() in "y yes".split():
891 if test.endswith(".t"):
892 rename(test + ".err", test)
893 else:
824 894 rename(test + ".err", test + ".out")
825 895 tested += 1
826 896 fails.pop()
827 897 continue
828 898 failed += 1
829 899 if options.first:
830 900 break
831 901 tested += 1
832 902
833 903 if options.child:
834 904 fp = os.fdopen(options.child, 'w')
835 905 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
836 906 for s in skips:
837 907 fp.write("%s %s\n" % s)
838 908 for s in fails:
839 909 fp.write("%s %s\n" % s)
840 910 fp.close()
841 911 else:
842 912 print
843 913 for s in skips:
844 914 print "Skipped %s: %s" % s
845 915 for s in fails:
846 916 print "Failed %s: %s" % s
847 917 _checkhglib("Tested")
848 918 print "# Ran %d tests, %d skipped, %d failed." % (
849 919 tested, skipped, failed)
850 920
851 921 if options.anycoverage:
852 922 outputcoverage(options)
853 923 except KeyboardInterrupt:
854 924 failed = True
855 925 print "\ninterrupted!"
856 926
857 927 if failed:
858 928 sys.exit(1)
859 929
860 930 def main():
861 931 (options, args) = parseargs()
862 932 if not options.child:
863 933 os.umask(022)
864 934
865 935 checktools()
866 936
867 937 # Reset some environment variables to well-known values so that
868 938 # the tests produce repeatable output.
869 939 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
870 940 os.environ['TZ'] = 'GMT'
871 941 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
872 942 os.environ['CDPATH'] = ''
873 943 os.environ['COLUMNS'] = '80'
874 944 os.environ['GREP_OPTIONS'] = ''
875 945 os.environ['http_proxy'] = ''
876 946
877 947 # unset env related to hooks
878 948 for k in os.environ.keys():
879 949 if k.startswith('HG_'):
880 950 # can't remove on solaris
881 951 os.environ[k] = ''
882 952 del os.environ[k]
883 953
884 954 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
885 955 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
886 956 if options.tmpdir:
887 957 options.keep_tmpdir = True
888 958 tmpdir = options.tmpdir
889 959 if os.path.exists(tmpdir):
890 960 # Meaning of tmpdir has changed since 1.3: we used to create
891 961 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
892 962 # tmpdir already exists.
893 963 sys.exit("error: temp dir %r already exists" % tmpdir)
894 964
895 965 # Automatically removing tmpdir sounds convenient, but could
896 966 # really annoy anyone in the habit of using "--tmpdir=/tmp"
897 967 # or "--tmpdir=$HOME".
898 968 #vlog("# Removing temp dir", tmpdir)
899 969 #shutil.rmtree(tmpdir)
900 970 os.makedirs(tmpdir)
901 971 else:
902 972 tmpdir = tempfile.mkdtemp('', 'hgtests.')
903 973 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
904 974 DAEMON_PIDS = None
905 975 HGRCPATH = None
906 976
907 977 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
908 978 os.environ["HGMERGE"] = "internal:merge"
909 979 os.environ["HGUSER"] = "test"
910 980 os.environ["HGENCODING"] = "ascii"
911 981 os.environ["HGENCODINGMODE"] = "strict"
912 982 os.environ["HGPORT"] = str(options.port)
913 983 os.environ["HGPORT1"] = str(options.port + 1)
914 984 os.environ["HGPORT2"] = str(options.port + 2)
915 985
916 986 if options.with_hg:
917 987 INST = None
918 988 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
919 989
920 990 # This looks redundant with how Python initializes sys.path from
921 991 # the location of the script being executed. Needed because the
922 992 # "hg" specified by --with-hg is not the only Python script
923 993 # executed in the test suite that needs to import 'mercurial'
924 994 # ... which means it's not really redundant at all.
925 995 PYTHONDIR = BINDIR
926 996 else:
927 997 INST = os.path.join(HGTMP, "install")
928 998 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
929 999 PYTHONDIR = os.path.join(INST, "lib", "python")
930 1000
931 1001 os.environ["BINDIR"] = BINDIR
932 1002 os.environ["PYTHON"] = PYTHON
933 1003
934 1004 if not options.child:
935 1005 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
936 1006 os.environ["PATH"] = os.pathsep.join(path)
937 1007
938 1008 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
939 1009 # can run .../tests/run-tests.py test-foo where test-foo
940 1010 # adds an extension to HGRC
941 1011 pypath = [PYTHONDIR, TESTDIR]
942 1012 # We have to augment PYTHONPATH, rather than simply replacing
943 1013 # it, in case external libraries are only available via current
944 1014 # PYTHONPATH. (In particular, the Subversion bindings on OS X
945 1015 # are in /opt/subversion.)
946 1016 oldpypath = os.environ.get(IMPL_PATH)
947 1017 if oldpypath:
948 1018 pypath.append(oldpypath)
949 1019 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
950 1020
951 1021 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
952 1022
953 1023 if len(args) == 0:
954 1024 args = os.listdir(".")
955 1025 args.sort()
956 1026
957 1027 tests = []
958 1028 for test in args:
959 1029 if (test.startswith("test-") and '~' not in test and
960 1030 ('.' not in test or test.endswith('.py') or
961 test.endswith('.bat'))):
1031 test.endswith('.bat') or test.endswith('.t'))):
962 1032 tests.append(test)
963 1033 if not tests:
964 1034 print "# Ran 0 tests, 0 skipped, 0 failed."
965 1035 return
966 1036
967 1037 vlog("# Using TESTDIR", TESTDIR)
968 1038 vlog("# Using HGTMP", HGTMP)
969 1039 vlog("# Using PATH", os.environ["PATH"])
970 1040 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
971 1041
972 1042 try:
973 1043 if len(tests) > 1 and options.jobs > 1:
974 1044 runchildren(options, tests)
975 1045 else:
976 1046 runtests(options, tests)
977 1047 finally:
978 1048 time.sleep(1)
979 1049 cleanup(options)
980 1050
981 1051 main()
General Comments 0
You need to be logged in to leave comments. Login now