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