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