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