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