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