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