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