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