##// END OF EJS Templates
run-tests: loadable as module
Simon Heimberg -
r13347:ce07defe default
parent child Browse files
Show More
@@ -1,1112 +1,1113 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 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
458 458 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
459 459 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
460 460 escapemap.update({'\\': '\\\\', '\r': r'\r'})
461 461 def escapef(m):
462 462 return escapemap[m.group(0)]
463 463 def stringescape(s):
464 464 return escapesub(escapef, s)
465 465
466 466 def tsttest(test, options, replacements):
467 467 t = open(test)
468 468 out = []
469 469 script = []
470 470 salt = "SALT" + str(time.time())
471 471
472 472 pos = prepos = -1
473 473 after = {}
474 474 expected = {}
475 475 for n, l in enumerate(t):
476 476 if not l.endswith('\n'):
477 477 l += '\n'
478 478 if l.startswith(' $ '): # commands
479 479 after.setdefault(pos, []).append(l)
480 480 prepos = pos
481 481 pos = n
482 482 script.append('echo %s %s $?\n' % (salt, n))
483 483 script.append(l[4:])
484 484 elif l.startswith(' > '): # continuations
485 485 after.setdefault(prepos, []).append(l)
486 486 script.append(l[4:])
487 487 elif l.startswith(' '): # results
488 488 # queue up a list of expected results
489 489 expected.setdefault(pos, []).append(l[2:])
490 490 else:
491 491 # non-command/result - queue up for merged output
492 492 after.setdefault(pos, []).append(l)
493 493
494 494 script.append('echo %s %s $?\n' % (salt, n + 1))
495 495
496 496 fd, name = tempfile.mkstemp(suffix='hg-tst')
497 497
498 498 try:
499 499 for l in script:
500 500 os.write(fd, l)
501 501 os.close(fd)
502 502
503 503 cmd = '/bin/sh "%s"' % name
504 504 vlog("# Running", cmd)
505 505 exitcode, output = run(cmd, options, replacements)
506 506 # do not merge output if skipped, return hghave message instead
507 507 # similarly, with --debug, output is None
508 508 if exitcode == SKIPPED_STATUS or output is None:
509 509 return exitcode, output
510 510 finally:
511 511 os.remove(name)
512 512
513 513 def rematch(el, l):
514 514 try:
515 515 # ensure that the regex matches to the end of the string
516 516 return re.match(el + r'\Z', l)
517 517 except re.error:
518 518 # el is an invalid regex
519 519 return False
520 520
521 521 def globmatch(el, l):
522 522 # The only supported special characters are * and ?. Escaping is
523 523 # supported.
524 524 i, n = 0, len(el)
525 525 res = ''
526 526 while i < n:
527 527 c = el[i]
528 528 i += 1
529 529 if c == '\\' and el[i] in '*?\\':
530 530 res += el[i - 1:i + 1]
531 531 i += 1
532 532 elif c == '*':
533 533 res += '.*'
534 534 elif c == '?':
535 535 res += '.'
536 536 else:
537 537 res += re.escape(c)
538 538 return rematch(res, l)
539 539
540 540 pos = -1
541 541 postout = []
542 542 ret = 0
543 543 for n, l in enumerate(output):
544 544 lout, lcmd = l, None
545 545 if salt in l:
546 546 lout, lcmd = l.split(salt, 1)
547 547
548 548 if lout:
549 549 if lcmd:
550 550 lout += ' (no-eol)\n'
551 551
552 552 el = None
553 553 if pos in expected and expected[pos]:
554 554 el = expected[pos].pop(0)
555 555
556 556 if el == lout: # perfect match (fast)
557 557 postout.append(" " + lout)
558 558 elif (el and
559 559 (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or
560 560 el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout)
561 561 or el.endswith(" (esc)\n") and
562 562 el.decode('string-escape') == l)):
563 563 postout.append(" " + el) # fallback regex/glob/esc match
564 564 else:
565 565 if needescape(lout):
566 566 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
567 567 postout.append(" " + lout) # let diff deal with it
568 568
569 569 if lcmd:
570 570 # add on last return code
571 571 ret = int(lcmd.split()[1])
572 572 if ret != 0:
573 573 postout.append(" [%s]\n" % ret)
574 574 if pos in after:
575 575 postout += after.pop(pos)
576 576 pos = int(lcmd.split()[0])
577 577
578 578 if pos in after:
579 579 postout += after.pop(pos)
580 580
581 581 return exitcode, postout
582 582
583 583 def run(cmd, options, replacements):
584 584 """Run command in a sub-process, capturing the output (stdout and stderr).
585 585 Return a tuple (exitcode, output). output is None in debug mode."""
586 586 # TODO: Use subprocess.Popen if we're running on Python 2.4
587 587 if options.debug:
588 588 proc = subprocess.Popen(cmd, shell=True)
589 589 ret = proc.wait()
590 590 return (ret, None)
591 591
592 592 if os.name == 'nt' or sys.platform.startswith('java'):
593 593 tochild, fromchild = os.popen4(cmd)
594 594 tochild.close()
595 595 output = fromchild.read()
596 596 ret = fromchild.close()
597 597 if ret is None:
598 598 ret = 0
599 599 else:
600 600 proc = Popen4(cmd)
601 601 def cleanup():
602 602 os.kill(proc.pid, signal.SIGTERM)
603 603 ret = proc.wait()
604 604 if ret == 0:
605 605 ret = signal.SIGTERM << 8
606 606 killdaemons()
607 607 return ret
608 608
609 609 try:
610 610 output = ''
611 611 proc.tochild.close()
612 612 output = proc.fromchild.read()
613 613 ret = proc.wait()
614 614 if os.WIFEXITED(ret):
615 615 ret = os.WEXITSTATUS(ret)
616 616 except Timeout:
617 617 vlog('# Process %d timed out - killing it' % proc.pid)
618 618 ret = cleanup()
619 619 output += ("\n### Abort: timeout after %d seconds.\n"
620 620 % options.timeout)
621 621 except KeyboardInterrupt:
622 622 vlog('# Handling keyboard interrupt')
623 623 cleanup()
624 624 raise
625 625
626 626 for s, r in replacements:
627 627 output = re.sub(s, r, output)
628 628 return ret, splitnewlines(output)
629 629
630 630 def runone(options, test, skips, fails):
631 631 '''tristate output:
632 632 None -> skipped
633 633 True -> passed
634 634 False -> failed'''
635 635
636 636 def skip(msg):
637 637 if not options.verbose:
638 638 skips.append((test, msg))
639 639 else:
640 640 print "\nSkipping %s: %s" % (testpath, msg)
641 641 return None
642 642
643 643 def fail(msg):
644 644 fails.append((test, msg))
645 645 if not options.nodiff:
646 646 print "\nERROR: %s %s" % (testpath, msg)
647 647 return None
648 648
649 649 vlog("# Test", test)
650 650
651 651 # create a fresh hgrc
652 652 hgrc = open(HGRCPATH, 'w+')
653 653 hgrc.write('[ui]\n')
654 654 hgrc.write('slash = True\n')
655 655 hgrc.write('[defaults]\n')
656 656 hgrc.write('backout = -d "0 0"\n')
657 657 hgrc.write('commit = -d "0 0"\n')
658 658 hgrc.write('tag = -d "0 0"\n')
659 659 if options.inotify:
660 660 hgrc.write('[extensions]\n')
661 661 hgrc.write('inotify=\n')
662 662 hgrc.write('[inotify]\n')
663 663 hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
664 664 hgrc.write('appendpid=True\n')
665 665 hgrc.close()
666 666
667 667 testpath = os.path.join(TESTDIR, test)
668 668 ref = os.path.join(TESTDIR, test+".out")
669 669 err = os.path.join(TESTDIR, test+".err")
670 670 if os.path.exists(err):
671 671 os.remove(err) # Remove any previous output files
672 672 try:
673 673 tf = open(testpath)
674 674 firstline = tf.readline().rstrip()
675 675 tf.close()
676 676 except:
677 677 firstline = ''
678 678 lctest = test.lower()
679 679
680 680 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
681 681 runner = pytest
682 682 elif lctest.endswith('.t'):
683 683 runner = tsttest
684 684 ref = testpath
685 685 else:
686 686 # do not try to run non-executable programs
687 687 if not os.access(testpath, os.X_OK):
688 688 return skip("not executable")
689 689 runner = shtest
690 690
691 691 # Make a tmp subdirectory to work in
692 692 testtmp = os.environ["TESTTMP"] = os.path.join(HGTMP, test)
693 693 os.mkdir(testtmp)
694 694 os.chdir(testtmp)
695 695
696 696 if options.timeout > 0:
697 697 signal.alarm(options.timeout)
698 698
699 699 ret, out = runner(testpath, options, [
700 700 (re.escape(testtmp), '$TESTTMP'),
701 701 (r':%s\b' % options.port, ':$HGPORT'),
702 702 (r':%s\b' % (options.port + 1), ':$HGPORT1'),
703 703 (r':%s\b' % (options.port + 2), ':$HGPORT2'),
704 704 ])
705 705 vlog("# Ret was:", ret)
706 706
707 707 if options.timeout > 0:
708 708 signal.alarm(0)
709 709
710 710 mark = '.'
711 711
712 712 skipped = (ret == SKIPPED_STATUS)
713 713
714 714 # If we're not in --debug mode and reference output file exists,
715 715 # check test output against it.
716 716 if options.debug:
717 717 refout = None # to match "out is None"
718 718 elif os.path.exists(ref):
719 719 f = open(ref, "r")
720 720 refout = splitnewlines(f.read())
721 721 f.close()
722 722 else:
723 723 refout = []
724 724
725 725 if (ret != 0 or out != refout) and not skipped and not options.debug:
726 726 # Save errors to a file for diagnosis
727 727 f = open(err, "wb")
728 728 for line in out:
729 729 f.write(line)
730 730 f.close()
731 731
732 732 if skipped:
733 733 mark = 's'
734 734 if out is None: # debug mode: nothing to parse
735 735 missing = ['unknown']
736 736 failed = None
737 737 else:
738 738 missing, failed = parsehghaveoutput(out)
739 739 if not missing:
740 740 missing = ['irrelevant']
741 741 if failed:
742 742 fail("hghave failed checking for %s" % failed[-1])
743 743 skipped = False
744 744 else:
745 745 skip(missing[-1])
746 746 elif out != refout:
747 747 mark = '!'
748 748 if ret:
749 749 fail("output changed and returned error code %d" % ret)
750 750 else:
751 751 fail("output changed")
752 752 if not options.nodiff:
753 753 if options.view:
754 754 os.system("%s %s %s" % (options.view, ref, err))
755 755 else:
756 756 showdiff(refout, out, ref, err)
757 757 ret = 1
758 758 elif ret:
759 759 mark = '!'
760 760 fail("returned error code %d" % ret)
761 761
762 762 if not options.verbose:
763 763 sys.stdout.write(mark)
764 764 sys.stdout.flush()
765 765
766 766 killdaemons()
767 767
768 768 os.chdir(TESTDIR)
769 769 if not options.keep_tmpdir:
770 770 shutil.rmtree(testtmp, True)
771 771 if skipped:
772 772 return None
773 773 return ret == 0
774 774
775 775 _hgpath = None
776 776
777 777 def _gethgpath():
778 778 """Return the path to the mercurial package that is actually found by
779 779 the current Python interpreter."""
780 780 global _hgpath
781 781 if _hgpath is not None:
782 782 return _hgpath
783 783
784 784 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
785 785 pipe = os.popen(cmd % PYTHON)
786 786 try:
787 787 _hgpath = pipe.read().strip()
788 788 finally:
789 789 pipe.close()
790 790 return _hgpath
791 791
792 792 def _checkhglib(verb):
793 793 """Ensure that the 'mercurial' package imported by python is
794 794 the one we expect it to be. If not, print a warning to stderr."""
795 795 expecthg = os.path.join(PYTHONDIR, 'mercurial')
796 796 actualhg = _gethgpath()
797 797 if actualhg != expecthg:
798 798 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
799 799 ' (expected %s)\n'
800 800 % (verb, actualhg, expecthg))
801 801
802 802 def runchildren(options, tests):
803 803 if INST:
804 804 installhg(options)
805 805 _checkhglib("Testing")
806 806
807 807 optcopy = dict(options.__dict__)
808 808 optcopy['jobs'] = 1
809 809 del optcopy['blacklist']
810 810 if optcopy['with_hg'] is None:
811 811 optcopy['with_hg'] = os.path.join(BINDIR, "hg")
812 812 optcopy.pop('anycoverage', None)
813 813
814 814 opts = []
815 815 for opt, value in optcopy.iteritems():
816 816 name = '--' + opt.replace('_', '-')
817 817 if value is True:
818 818 opts.append(name)
819 819 elif value is not None:
820 820 opts.append(name + '=' + str(value))
821 821
822 822 tests.reverse()
823 823 jobs = [[] for j in xrange(options.jobs)]
824 824 while tests:
825 825 for job in jobs:
826 826 if not tests:
827 827 break
828 828 job.append(tests.pop())
829 829 fps = {}
830 830
831 831 for j, job in enumerate(jobs):
832 832 if not job:
833 833 continue
834 834 rfd, wfd = os.pipe()
835 835 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
836 836 childtmp = os.path.join(HGTMP, 'child%d' % j)
837 837 childopts += ['--tmpdir', childtmp]
838 838 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
839 839 vlog(' '.join(cmdline))
840 840 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
841 841 os.close(wfd)
842 842 signal.signal(signal.SIGINT, signal.SIG_IGN)
843 843 failures = 0
844 844 tested, skipped, failed = 0, 0, 0
845 845 skips = []
846 846 fails = []
847 847 while fps:
848 848 pid, status = os.wait()
849 849 fp = fps.pop(pid)
850 850 l = fp.read().splitlines()
851 851 try:
852 852 test, skip, fail = map(int, l[:3])
853 853 except ValueError:
854 854 test, skip, fail = 0, 0, 0
855 855 split = -fail or len(l)
856 856 for s in l[3:split]:
857 857 skips.append(s.split(" ", 1))
858 858 for s in l[split:]:
859 859 fails.append(s.split(" ", 1))
860 860 tested += test
861 861 skipped += skip
862 862 failed += fail
863 863 vlog('pid %d exited, status %d' % (pid, status))
864 864 failures |= status
865 865 print
866 866 if not options.noskips:
867 867 for s in skips:
868 868 print "Skipped %s: %s" % (s[0], s[1])
869 869 for s in fails:
870 870 print "Failed %s: %s" % (s[0], s[1])
871 871
872 872 _checkhglib("Tested")
873 873 print "# Ran %d tests, %d skipped, %d failed." % (
874 874 tested, skipped, failed)
875 875
876 876 if options.anycoverage:
877 877 outputcoverage(options)
878 878 sys.exit(failures != 0)
879 879
880 880 def runtests(options, tests):
881 881 global DAEMON_PIDS, HGRCPATH
882 882 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
883 883 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
884 884
885 885 try:
886 886 if INST:
887 887 installhg(options)
888 888 _checkhglib("Testing")
889 889
890 890 if options.timeout > 0:
891 891 try:
892 892 signal.signal(signal.SIGALRM, alarmed)
893 893 vlog('# Running each test with %d second timeout' %
894 894 options.timeout)
895 895 except AttributeError:
896 896 print 'WARNING: cannot run tests with timeouts'
897 897 options.timeout = 0
898 898
899 899 tested = 0
900 900 failed = 0
901 901 skipped = 0
902 902
903 903 if options.restart:
904 904 orig = list(tests)
905 905 while tests:
906 906 if os.path.exists(tests[0] + ".err"):
907 907 break
908 908 tests.pop(0)
909 909 if not tests:
910 910 print "running all tests"
911 911 tests = orig
912 912
913 913 skips = []
914 914 fails = []
915 915
916 916 for test in tests:
917 917 if options.blacklist:
918 918 filename = options.blacklist.get(test)
919 919 if filename is not None:
920 920 skips.append((test, "blacklisted (%s)" % filename))
921 921 skipped += 1
922 922 continue
923 923
924 924 if options.retest and not os.path.exists(test + ".err"):
925 925 skipped += 1
926 926 continue
927 927
928 928 if options.keywords:
929 929 t = open(test).read().lower() + test.lower()
930 930 for k in options.keywords.lower().split():
931 931 if k in t:
932 932 break
933 933 else:
934 934 skipped += 1
935 935 continue
936 936
937 937 ret = runone(options, test, skips, fails)
938 938 if ret is None:
939 939 skipped += 1
940 940 elif not ret:
941 941 if options.interactive:
942 942 print "Accept this change? [n] ",
943 943 answer = sys.stdin.readline().strip()
944 944 if answer.lower() in "y yes".split():
945 945 if test.endswith(".t"):
946 946 rename(test + ".err", test)
947 947 else:
948 948 rename(test + ".err", test + ".out")
949 949 tested += 1
950 950 fails.pop()
951 951 continue
952 952 failed += 1
953 953 if options.first:
954 954 break
955 955 tested += 1
956 956
957 957 if options.child:
958 958 fp = os.fdopen(options.child, 'w')
959 959 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
960 960 for s in skips:
961 961 fp.write("%s %s\n" % s)
962 962 for s in fails:
963 963 fp.write("%s %s\n" % s)
964 964 fp.close()
965 965 else:
966 966 print
967 967 for s in skips:
968 968 print "Skipped %s: %s" % s
969 969 for s in fails:
970 970 print "Failed %s: %s" % s
971 971 _checkhglib("Tested")
972 972 print "# Ran %d tests, %d skipped, %d failed." % (
973 973 tested, skipped, failed)
974 974
975 975 if options.anycoverage:
976 976 outputcoverage(options)
977 977 except KeyboardInterrupt:
978 978 failed = True
979 979 print "\ninterrupted!"
980 980
981 981 if failed:
982 982 sys.exit(1)
983 983
984 984 def main():
985 985 (options, args) = parseargs()
986 986 if not options.child:
987 987 os.umask(022)
988 988
989 989 checktools()
990 990
991 991 if len(args) == 0:
992 992 args = os.listdir(".")
993 993 args.sort()
994 994
995 995 tests = []
996 996 skipped = []
997 997 for test in args:
998 998 if (test.startswith("test-") and '~' not in test and
999 999 ('.' not in test or test.endswith('.py') or
1000 1000 test.endswith('.bat') or test.endswith('.t'))):
1001 1001 if not os.path.exists(test):
1002 1002 skipped.append(test)
1003 1003 else:
1004 1004 tests.append(test)
1005 1005 if not tests:
1006 1006 for test in skipped:
1007 1007 print 'Skipped %s: does not exist' % test
1008 1008 print "# Ran 0 tests, %d skipped, 0 failed." % len(skipped)
1009 1009 return
1010 1010 tests = tests + skipped
1011 1011
1012 1012 # Reset some environment variables to well-known values so that
1013 1013 # the tests produce repeatable output.
1014 1014 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
1015 1015 os.environ['TZ'] = 'GMT'
1016 1016 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1017 1017 os.environ['CDPATH'] = ''
1018 1018 os.environ['COLUMNS'] = '80'
1019 1019 os.environ['GREP_OPTIONS'] = ''
1020 1020 os.environ['http_proxy'] = ''
1021 1021
1022 1022 # unset env related to hooks
1023 1023 for k in os.environ.keys():
1024 1024 if k.startswith('HG_'):
1025 1025 # can't remove on solaris
1026 1026 os.environ[k] = ''
1027 1027 del os.environ[k]
1028 1028
1029 1029 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
1030 1030 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1031 1031 if options.tmpdir:
1032 1032 options.keep_tmpdir = True
1033 1033 tmpdir = options.tmpdir
1034 1034 if os.path.exists(tmpdir):
1035 1035 # Meaning of tmpdir has changed since 1.3: we used to create
1036 1036 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1037 1037 # tmpdir already exists.
1038 1038 sys.exit("error: temp dir %r already exists" % tmpdir)
1039 1039
1040 1040 # Automatically removing tmpdir sounds convenient, but could
1041 1041 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1042 1042 # or "--tmpdir=$HOME".
1043 1043 #vlog("# Removing temp dir", tmpdir)
1044 1044 #shutil.rmtree(tmpdir)
1045 1045 os.makedirs(tmpdir)
1046 1046 else:
1047 1047 tmpdir = tempfile.mkdtemp('', 'hgtests.')
1048 1048 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1049 1049 DAEMON_PIDS = None
1050 1050 HGRCPATH = None
1051 1051
1052 1052 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
1053 1053 os.environ["HGMERGE"] = "internal:merge"
1054 1054 os.environ["HGUSER"] = "test"
1055 1055 os.environ["HGENCODING"] = "ascii"
1056 1056 os.environ["HGENCODINGMODE"] = "strict"
1057 1057 os.environ["HGPORT"] = str(options.port)
1058 1058 os.environ["HGPORT1"] = str(options.port + 1)
1059 1059 os.environ["HGPORT2"] = str(options.port + 2)
1060 1060
1061 1061 if options.with_hg:
1062 1062 INST = None
1063 1063 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1064 1064
1065 1065 # This looks redundant with how Python initializes sys.path from
1066 1066 # the location of the script being executed. Needed because the
1067 1067 # "hg" specified by --with-hg is not the only Python script
1068 1068 # executed in the test suite that needs to import 'mercurial'
1069 1069 # ... which means it's not really redundant at all.
1070 1070 PYTHONDIR = BINDIR
1071 1071 else:
1072 1072 INST = os.path.join(HGTMP, "install")
1073 1073 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1074 1074 PYTHONDIR = os.path.join(INST, "lib", "python")
1075 1075
1076 1076 os.environ["BINDIR"] = BINDIR
1077 1077 os.environ["PYTHON"] = PYTHON
1078 1078
1079 1079 if not options.child:
1080 1080 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1081 1081 os.environ["PATH"] = os.pathsep.join(path)
1082 1082
1083 1083 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1084 1084 # can run .../tests/run-tests.py test-foo where test-foo
1085 1085 # adds an extension to HGRC
1086 1086 pypath = [PYTHONDIR, TESTDIR]
1087 1087 # We have to augment PYTHONPATH, rather than simply replacing
1088 1088 # it, in case external libraries are only available via current
1089 1089 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1090 1090 # are in /opt/subversion.)
1091 1091 oldpypath = os.environ.get(IMPL_PATH)
1092 1092 if oldpypath:
1093 1093 pypath.append(oldpypath)
1094 1094 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1095 1095
1096 1096 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1097 1097
1098 1098 vlog("# Using TESTDIR", TESTDIR)
1099 1099 vlog("# Using HGTMP", HGTMP)
1100 1100 vlog("# Using PATH", os.environ["PATH"])
1101 1101 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1102 1102
1103 1103 try:
1104 1104 if len(tests) > 1 and options.jobs > 1:
1105 1105 runchildren(options, tests)
1106 1106 else:
1107 1107 runtests(options, tests)
1108 1108 finally:
1109 1109 time.sleep(1)
1110 1110 cleanup(options)
1111 1111
1112 if __name__ == '__main__':
1112 1113 main()
General Comments 0
You need to be logged in to leave comments. Login now