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