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